Improve TextField
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user