From 89b9407872028ac7680b12ba39112c43913a262f Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Wed, 29 Oct 2025 11:09:44 +0100 Subject: [PATCH] feat(Field): split label position --- .changeset/tender-gorillas-wonder.md | 5 ++++ .../FieldWrapper/FieldWrapper.stories.tsx | 30 +++++++++++++++++++ .../form/FieldWrapper/FieldWrapper.tsx | 13 ++++++-- .../form/Form/ComplexForm.stories.tsx | 3 ++ src/components/form/Form/Field.stories.tsx | 2 +- src/components/form/Form/Form.docs.mdx | 3 +- src/components/form/Form/Form.stories.tsx | 2 +- src/components/form/Form/Form.tsx | 6 +++- src/components/form/Label.tsx | 1 + src/shared/form.ts | 2 +- 10 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 .changeset/tender-gorillas-wonder.md diff --git a/.changeset/tender-gorillas-wonder.md b/.changeset/tender-gorillas-wonder.md new file mode 100644 index 000000000..f5555b875 --- /dev/null +++ b/.changeset/tender-gorillas-wonder.md @@ -0,0 +1,5 @@ +--- +"@cube-dev/ui-kit": minor +--- + +Add `split` value for `labelPosition` in all field components. diff --git a/src/components/form/FieldWrapper/FieldWrapper.stories.tsx b/src/components/form/FieldWrapper/FieldWrapper.stories.tsx index b5a63cc78..6a47e67de 100644 --- a/src/components/form/FieldWrapper/FieldWrapper.stories.tsx +++ b/src/components/form/FieldWrapper/FieldWrapper.stories.tsx @@ -165,3 +165,33 @@ ErrorMessageOverridesMessage.args = { errorMessage: 'This error message takes precedence', description: 'Description is shown alongside error message', }; + +export const SplitLabel = Template.bind({}); +SplitLabel.args = { + labelPosition: 'split', + styles: { width: '(100vw - 6x)' }, +}; + +export const SplitLabelWithTooltip = Template.bind({}); +SplitLabelWithTooltip.args = { + labelPosition: 'split', + styles: { width: '(100vw - 6x)' }, + tooltip: 'Long description', +}; + +export const SplitLabelWithDescription = Template.bind({}); +SplitLabelWithDescription.args = { + labelPosition: 'split', + styles: { width: '(100vw - 6x)' }, + description: + 'This is a helpful description that explains what this field is for', +}; + +export const SplitLabelWithDescriptionAndErrorMessage = Template.bind({}); +SplitLabelWithDescriptionAndErrorMessage.args = { + labelPosition: 'split', + styles: { width: '(100vw - 6x)' }, + description: + 'This is a helpful description that explains what this field is for', + errorMessage: 'This field has an error that needs to be fixed', +}; diff --git a/src/components/form/FieldWrapper/FieldWrapper.tsx b/src/components/form/FieldWrapper/FieldWrapper.tsx index 869b550db..b3a7ef19e 100644 --- a/src/components/form/FieldWrapper/FieldWrapper.tsx +++ b/src/components/form/FieldWrapper/FieldWrapper.tsx @@ -3,7 +3,6 @@ import { forwardRef } from 'react'; import { InfoCircleIcon } from '../../../icons/index'; import { tasty } from '../../../tasty/index'; import { wrapNodeIfPlain } from '../../../utils/react/index'; -import { Paragraph } from '../../content/Paragraph'; import { Text } from '../../content/Text'; import { Flex } from '../../layout/Flex'; import { Space } from '../../layout/Space'; @@ -20,9 +19,14 @@ const FieldElement = tasty({ gridColumns: { '': 'minmax(0, 1fr)', 'has-sider': '($full-label-width, auto) minmax(0, 1fr)', + 'has-split': 'auto auto', }, gap: 0, placeItems: 'baseline stretch', + placeContent: { + '': 'initial', + 'has-split': 'space-between', + }, '$full-label-width': '($label-width + 1x)', LabelArea: { @@ -34,6 +38,7 @@ const FieldElement = tasty({ margin: { '': '1x bottom', 'has-sider': '1x right', + 'has-split': '1x right', ':empty': '0', }, }, @@ -45,6 +50,7 @@ const FieldElement = tasty({ gridColumn: { '': 'initial', 'has-sider': 2, + 'has-split': 2, }, }, }, @@ -161,12 +167,15 @@ export const FieldWrapper = forwardRef(function FieldWrapper( // Description positioning based on label position const descriptionForLabel = - labelPosition === 'side' ? createDescriptionComponent() : null; + labelPosition === 'side' || labelPosition === 'split' + ? createDescriptionComponent() + : null; const descriptionForInput = labelPosition === 'top' ? createDescriptionComponent() : null; const mods = { 'has-sider': labelPosition === 'side', + 'has-split': labelPosition === 'split', 'has-description': !!description, invalid: validationState === 'invalid', valid: validationState === 'valid', diff --git a/src/components/form/Form/ComplexForm.stories.tsx b/src/components/form/Form/ComplexForm.stories.tsx index 1ac9e8fa7..a7ce41153 100644 --- a/src/components/form/Form/ComplexForm.stories.tsx +++ b/src/components/form/Form/ComplexForm.stories.tsx @@ -374,6 +374,9 @@ export const Default = Template.bind({}); export const ComplexFormSideLabel = Template.bind({}); ComplexFormSideLabel.args = { labelPosition: 'side' }; +export const ComplexFormSplitLabel = Template.bind({}); +ComplexFormSplitLabel.args = { labelPosition: 'split' }; + export const ComplexErrorMessage = ComplexErrorTemplate.bind({}); export const AsyncValidation = AsyncValidationTemplate.bind({}); diff --git a/src/components/form/Form/Field.stories.tsx b/src/components/form/Form/Field.stories.tsx index 690a959b1..4c5f73a41 100644 --- a/src/components/form/Form/Field.stories.tsx +++ b/src/components/form/Form/Field.stories.tsx @@ -53,7 +53,7 @@ export default { /* Presentation */ labelPosition: { - options: ['top', 'side'], + options: ['top', 'side', 'split'], control: { type: 'radio' }, description: 'Position of the field label', table: { diff --git a/src/components/form/Form/Form.docs.mdx b/src/components/form/Form/Form.docs.mdx index fc5a9f40e..7e383e167 100644 --- a/src/components/form/Form/Form.docs.mdx +++ b/src/components/form/Form/Form.docs.mdx @@ -60,7 +60,7 @@ These properties are inherited by all input components within the form: | Property | Type | Description | |----------|------|-------------| -| `labelPosition` | `'top' \| 'side'` | Where to place labels relative to inputs | +| `labelPosition` | `'top' \| 'side' \| 'split'` | Where to place labels relative to inputs. `'top'` places labels above inputs, `'side'` places labels beside inputs with fixed width, `'split'` places labels and inputs on opposite sides without fixed label width | | `requiredMark` | `boolean` | Whether to show required field indicators | | `isRequired` | `boolean` | Whether fields are required by default | | `necessityIndicator` | `'icon' \| 'label'` | Type of necessity indicator | @@ -86,6 +86,7 @@ The `mods` property accepts the following modifiers: | Modifier | Type | Description | |----------|------|-------------| | `has-sider` | `boolean` | Applied when `labelPosition="side"` | +| `has-split` | `boolean` | Applied when `labelPosition="split"` | | `horizontal` | `boolean` | Applied when `orientation="horizontal"` | ## Variants diff --git a/src/components/form/Form/Form.stories.tsx b/src/components/form/Form/Form.stories.tsx index 4279e9994..623148534 100644 --- a/src/components/form/Form/Form.stories.tsx +++ b/src/components/form/Form/Form.stories.tsx @@ -28,7 +28,7 @@ const meta: Meta = { }, labelPosition: { control: { type: 'radio' }, - options: ['top', 'side'], + options: ['top', 'side', 'split'], description: 'Where to place labels relative to inputs', table: { defaultValue: { summary: 'top' }, diff --git a/src/components/form/Form/Form.tsx b/src/components/form/Form/Form.tsx index 30090e7bb..2dbde3bde 100644 --- a/src/components/form/Form/Form.tsx +++ b/src/components/form/Form/Form.tsx @@ -236,7 +236,11 @@ function Form( ref={domRef} noValidate styles={styles} - mods={{ 'has-sider': labelPosition === 'side', horizontal: isHorizontal }} + mods={{ + 'has-sider': labelPosition === 'side', + 'has-split': labelPosition === 'split', + horizontal: isHorizontal, + }} onSubmit={onSubmitCallback} > diff --git a/src/components/form/Label.tsx b/src/components/form/Label.tsx index 48b1adb25..7d5f7b3a8 100644 --- a/src/components/form/Label.tsx +++ b/src/components/form/Label.tsx @@ -68,6 +68,7 @@ export const LABEL_STYLES: Styles = { width: { '': 'initial', side: '($label-width, initial)', + split: 'initial', }, }; diff --git a/src/shared/form.ts b/src/shared/form.ts index bf072d7d8..65dbe3349 100644 --- a/src/shared/form.ts +++ b/src/shared/form.ts @@ -13,7 +13,7 @@ export interface ValidationResult { } /** Where to place label relative to input */ -export type LabelPosition = 'side' | 'top'; +export type LabelPosition = 'side' | 'top' | 'split'; /** The type of necessity indicator */ export type NecessityIndicator = 'icon' | 'label';