Improve TextField

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-06-14 07:08:19 +01:00
parent 0a475d84ac
commit c49e335823
9 changed files with 257 additions and 63 deletions
+5
View File
@@ -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.
+43 -12
View File
@@ -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;
}
+43 -12
View File
@@ -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;
}
+43 -12
View File
@@ -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);
}
+3
View File
@@ -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<HTMLButtonElement>;
secondaryLabel?: string;
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
}
@@ -22,6 +22,14 @@ import { Icon } from '../Icon';
const meta = {
title: 'Components/TextField',
component: TextField,
argTypes: {
secondaryLabel: {
control: 'text',
},
required: {
control: 'boolean',
},
},
} satisfies Meta<typeof TextField>;
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 => (
<Flex direction="row" gap="4" style={{ width: '100%', maxWidth: '600px' }}>
<TextField {...args} labelSize="small" />
<TextField {...args} labelSize="medium" />
</Flex>
),
};
export const HideLabelAndDescription: Story = {
args: {
...WithLabel.args,
hideLabelAndDescription: true,
},
};
export const Disabled: Story = {
args: {
...WithLabel.args,
@@ -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);
}
@@ -29,7 +29,10 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
className,
size = 'small',
label,
labelSize = 'small',
secondaryLabel,
description,
hideLabelAndDescription,
error,
required,
style,
@@ -42,6 +45,9 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
// 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 (
<Field.Root
className={clsx('canon-TextField', className)}
@@ -50,16 +56,32 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
style={style}
ref={ref}
>
{label && (
<Field.Label className="canon-TextFieldLabel">
{label}
{required && (
<span aria-hidden="true" className="canon-TextFieldRequired">
(Required)
</span>
)}
</Field.Label>
)}
<div
className="canon-TextFieldLabelWrapper"
data-hidden={hideLabelAndDescription}
>
{label && (
<Field.Label className="canon-TextFieldLabel" data-size={labelSize}>
{label}
{secondaryLabelText && (
<span
aria-hidden="true"
className="canon-TextFieldSecondaryLabel"
>
({secondaryLabelText})
</span>
)}
</Field.Label>
)}
{description && (
<Field.Description
className="canon-TextFieldDescription"
data-size={labelSize}
>
{description}
</Field.Description>
)}
</div>
<div className="canon-TextFieldInputWrapper" data-size={responsiveSize}>
{icon && (
<div
@@ -85,11 +107,6 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
</button>
)}
</div>
{description && (
<Field.Description className="canon-TextFieldDescription">
{description}
</Field.Description>
)}
{error && (
<Field.Error className="canon-TextFieldError" role="alert" forceShow>
{error}
@@ -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
*/