From c49e335823207b1ea88f1ca77e9e8efaee1c95b1 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sat, 14 Jun 2025 07:08:19 +0100 Subject: [PATCH] Improve TextField Signed-off-by: Charles de Dreuille --- .changeset/sad-fans-sort.md | 5 ++ packages/canon/css/components.css | 55 +++++++++++++++---- packages/canon/css/styles.css | 55 +++++++++++++++---- packages/canon/css/textfield.css | 55 +++++++++++++++---- packages/canon/report.api.md | 3 + .../TextField/TextField.stories.tsx | 30 ++++++++++ .../components/TextField/TextField.styles.css | 55 +++++++++++++++---- .../src/components/TextField/TextField.tsx | 47 +++++++++++----- .../canon/src/components/TextField/types.ts | 15 +++++ 9 files changed, 257 insertions(+), 63 deletions(-) create mode 100644 .changeset/sad-fans-sort.md diff --git a/.changeset/sad-fans-sort.md b/.changeset/sad-fans-sort.md new file mode 100644 index 0000000000..dcd1e7815c --- /dev/null +++ b/.changeset/sad-fans-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': minor +--- + +TextField in Canon now has multiple label sizes as well as the capacity to hide label and description but still make them available for screen readers. diff --git a/packages/canon/css/components.css b/packages/canon/css/components.css index bcdbb86235..f3be22720c 100644 --- a/packages/canon/css/components.css +++ b/packages/canon/css/components.css @@ -636,27 +636,65 @@ display: flex; } +.canon-TextFieldLabelWrapper { + margin-bottom: var(--canon-space-3); + gap: var(--canon-space-1); + flex-direction: column; + display: flex; +} + +.canon-TextFieldLabelWrapper[data-hidden] { + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + position: absolute; + overflow: hidden; +} + .canon-TextFieldLabel { - font-size: var(--canon-font-size-2); - font-weight: var(--canon-font-weight-regular); color: var(--canon-fg-primary); - margin-bottom: var(--canon-space-1_5); cursor: pointer; margin-right: auto; } +.canon-TextFieldLabel[data-size="small"] { + font-weight: var(--canon-font-weight-regular); + font-size: var(--canon-font-size-2); +} + +.canon-TextFieldLabel[data-size="medium"] { + font-weight: var(--canon-font-weight-bold); + font-size: var(--canon-font-size-3); +} + .canon-TextFieldLabel[data-disabled] { cursor: default; } +.canon-TextFieldSecondaryLabel { + color: var(--canon-fg-secondary); + font-weight: var(--canon-font-weight-regular); + margin-left: var(--canon-space-1); +} + .canon-TextFieldDescription { - font-size: var(--canon-font-size-2); font-weight: var(--canon-font-weight-regular); color: var(--canon-fg-secondary); - padding-top: var(--canon-space-1_5); margin: 0; } +.canon-TextFieldDescription[data-size="small"] { + font-size: var(--canon-font-size-2); +} + +.canon-TextFieldDescription[data-size="medium"] { + font-size: var(--canon-font-size-3); +} + .canon-TextFieldError { font-size: var(--canon-font-size-2); font-weight: var(--canon-font-weight-regular); @@ -769,13 +807,6 @@ height: 2.5rem; } -.canon-TextFieldRequired { - color: var(--canon-fg-secondary); - font-size: var(--canon-font-size-2); - font-weight: var(--canon-font-weight-regular); - margin-left: var(--canon-space-1); -} - .canon-MenuPositioner { outline: 0; } diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css index 43b1cfbc01..8e98996d6d 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -9860,27 +9860,65 @@ display: flex; } +.canon-TextFieldLabelWrapper { + margin-bottom: var(--canon-space-3); + gap: var(--canon-space-1); + flex-direction: column; + display: flex; +} + +.canon-TextFieldLabelWrapper[data-hidden] { + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + position: absolute; + overflow: hidden; +} + .canon-TextFieldLabel { - font-size: var(--canon-font-size-2); - font-weight: var(--canon-font-weight-regular); color: var(--canon-fg-primary); - margin-bottom: var(--canon-space-1_5); cursor: pointer; margin-right: auto; } +.canon-TextFieldLabel[data-size="small"] { + font-weight: var(--canon-font-weight-regular); + font-size: var(--canon-font-size-2); +} + +.canon-TextFieldLabel[data-size="medium"] { + font-weight: var(--canon-font-weight-bold); + font-size: var(--canon-font-size-3); +} + .canon-TextFieldLabel[data-disabled] { cursor: default; } +.canon-TextFieldSecondaryLabel { + color: var(--canon-fg-secondary); + font-weight: var(--canon-font-weight-regular); + margin-left: var(--canon-space-1); +} + .canon-TextFieldDescription { - font-size: var(--canon-font-size-2); font-weight: var(--canon-font-weight-regular); color: var(--canon-fg-secondary); - padding-top: var(--canon-space-1_5); margin: 0; } +.canon-TextFieldDescription[data-size="small"] { + font-size: var(--canon-font-size-2); +} + +.canon-TextFieldDescription[data-size="medium"] { + font-size: var(--canon-font-size-3); +} + .canon-TextFieldError { font-size: var(--canon-font-size-2); font-weight: var(--canon-font-weight-regular); @@ -9993,13 +10031,6 @@ height: 2.5rem; } -.canon-TextFieldRequired { - color: var(--canon-fg-secondary); - font-size: var(--canon-font-size-2); - font-weight: var(--canon-font-weight-regular); - margin-left: var(--canon-space-1); -} - .canon-MenuPositioner { outline: 0; } diff --git a/packages/canon/css/textfield.css b/packages/canon/css/textfield.css index c3c1a88ad6..bdc5a23dbd 100644 --- a/packages/canon/css/textfield.css +++ b/packages/canon/css/textfield.css @@ -5,27 +5,65 @@ display: flex; } +.canon-TextFieldLabelWrapper { + margin-bottom: var(--canon-space-3); + gap: var(--canon-space-1); + flex-direction: column; + display: flex; +} + +.canon-TextFieldLabelWrapper[data-hidden] { + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + position: absolute; + overflow: hidden; +} + .canon-TextFieldLabel { - font-size: var(--canon-font-size-2); - font-weight: var(--canon-font-weight-regular); color: var(--canon-fg-primary); - margin-bottom: var(--canon-space-1_5); cursor: pointer; margin-right: auto; } +.canon-TextFieldLabel[data-size="small"] { + font-weight: var(--canon-font-weight-regular); + font-size: var(--canon-font-size-2); +} + +.canon-TextFieldLabel[data-size="medium"] { + font-weight: var(--canon-font-weight-bold); + font-size: var(--canon-font-size-3); +} + .canon-TextFieldLabel[data-disabled] { cursor: default; } +.canon-TextFieldSecondaryLabel { + color: var(--canon-fg-secondary); + font-weight: var(--canon-font-weight-regular); + margin-left: var(--canon-space-1); +} + .canon-TextFieldDescription { - font-size: var(--canon-font-size-2); font-weight: var(--canon-font-weight-regular); color: var(--canon-fg-secondary); - padding-top: var(--canon-space-1_5); margin: 0; } +.canon-TextFieldDescription[data-size="small"] { + font-size: var(--canon-font-size-2); +} + +.canon-TextFieldDescription[data-size="medium"] { + font-size: var(--canon-font-size-3); +} + .canon-TextFieldError { font-size: var(--canon-font-size-2); font-weight: var(--canon-font-weight-regular); @@ -137,10 +175,3 @@ .canon-TextFieldInputWrapper[data-size="medium"] { height: 2.5rem; } - -.canon-TextFieldRequired { - color: var(--canon-fg-secondary); - font-size: var(--canon-font-size-2); - font-weight: var(--canon-font-weight-regular); - margin-left: var(--canon-space-1); -} diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index b6ce130ecc..ceec381c83 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -1274,10 +1274,13 @@ export interface TextFieldProps className?: string; description?: string; error?: string | null; + hideLabelAndDescription?: boolean; icon?: ReactNode; label?: string; + labelSize?: 'small' | 'medium'; name: string; onClear?: MouseEventHandler; + secondaryLabel?: string; size?: 'small' | 'medium' | Partial>; } diff --git a/packages/canon/src/components/TextField/TextField.stories.tsx b/packages/canon/src/components/TextField/TextField.stories.tsx index 7e625dad24..f052377acb 100644 --- a/packages/canon/src/components/TextField/TextField.stories.tsx +++ b/packages/canon/src/components/TextField/TextField.stories.tsx @@ -22,6 +22,14 @@ import { Icon } from '../Icon'; const meta = { title: 'Components/TextField', component: TextField, + argTypes: { + secondaryLabel: { + control: 'text', + }, + required: { + control: 'boolean', + }, + }, } satisfies Meta; export default meta; @@ -65,6 +73,28 @@ export const Required: Story = { }, }; +export const LabelSizes: Story = { + args: { + ...Default.args, + label: 'Label', + description: 'Description', + required: true, + }, + render: args => ( + + + + + ), +}; + +export const HideLabelAndDescription: Story = { + args: { + ...WithLabel.args, + hideLabelAndDescription: true, + }, +}; + export const Disabled: Story = { args: { ...WithLabel.args, diff --git a/packages/canon/src/components/TextField/TextField.styles.css b/packages/canon/src/components/TextField/TextField.styles.css index be7ec3379c..d7bc147ba1 100644 --- a/packages/canon/src/components/TextField/TextField.styles.css +++ b/packages/canon/src/components/TextField/TextField.styles.css @@ -21,25 +21,63 @@ width: 100%; } +.canon-TextFieldLabelWrapper { + display: flex; + flex-direction: column; + margin-bottom: var(--canon-space-3); + gap: var(--canon-space-1); +} + +.canon-TextFieldLabelWrapper[data-hidden] { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + .canon-TextFieldLabel { - font-size: var(--canon-font-size-2); - font-weight: var(--canon-font-weight-regular); color: var(--canon-fg-primary); - margin-bottom: var(--canon-space-1_5); margin-right: auto; cursor: pointer; } +.canon-TextFieldLabel[data-size='small'] { + font-weight: var(--canon-font-weight-regular); + font-size: var(--canon-font-size-2); +} + +.canon-TextFieldLabel[data-size='medium'] { + font-weight: var(--canon-font-weight-bold); + font-size: var(--canon-font-size-3); +} + .canon-TextFieldLabel[data-disabled] { cursor: default; } +.canon-TextFieldSecondaryLabel { + color: var(--canon-fg-secondary); + font-weight: var(--canon-font-weight-regular); + margin-left: var(--canon-space-1); +} + .canon-TextFieldDescription { - font-size: var(--canon-font-size-2); font-weight: var(--canon-font-weight-regular); color: var(--canon-fg-secondary); margin: 0; - padding-top: var(--canon-space-1_5); +} + +.canon-TextFieldDescription[data-size='small'] { + font-size: var(--canon-font-size-2); +} + +.canon-TextFieldDescription[data-size='medium'] { + font-size: var(--canon-font-size-3); } .canon-TextFieldError { @@ -156,10 +194,3 @@ .canon-TextFieldInputWrapper[data-size='medium'] { height: 2.5rem; } - -.canon-TextFieldRequired { - color: var(--canon-fg-secondary); - font-size: var(--canon-font-size-2); - font-weight: var(--canon-font-weight-regular); - margin-left: var(--canon-space-1); -} diff --git a/packages/canon/src/components/TextField/TextField.tsx b/packages/canon/src/components/TextField/TextField.tsx index e4cf43a711..e534b0952d 100644 --- a/packages/canon/src/components/TextField/TextField.tsx +++ b/packages/canon/src/components/TextField/TextField.tsx @@ -29,7 +29,10 @@ export const TextField = forwardRef( className, size = 'small', label, + labelSize = 'small', + secondaryLabel, description, + hideLabelAndDescription, error, required, style, @@ -42,6 +45,9 @@ export const TextField = forwardRef( // Get the responsive value for the variant const responsiveSize = useResponsiveValue(size); + // If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required. + const secondaryLabelText = secondaryLabel || (required ? 'Required' : null); + return ( ( style={style} ref={ref} > - {label && ( - - {label} - {required && ( - - )} - - )} +
+ {label && ( + + {label} + {secondaryLabelText && ( + + )} + + )} + {description && ( + + {description} + + )} +
{icon && (
( )}
- {description && ( - - {description} - - )} {error && ( {error} diff --git a/packages/canon/src/components/TextField/types.ts b/packages/canon/src/components/TextField/types.ts index 8a37a23efe..8a81492f2f 100644 --- a/packages/canon/src/components/TextField/types.ts +++ b/packages/canon/src/components/TextField/types.ts @@ -35,6 +35,21 @@ export interface TextFieldProps */ label?: string; + /** + * The secondary label of the text field + */ + secondaryLabel?: string; + + /** + * The size of the label and description + */ + labelSize?: 'small' | 'medium'; + + /** + * Hide the label and description but still visible to screen readers + */ + hideLabelAndDescription?: boolean; + /** * The description of the text field */