Merge pull request #29974 from backstage/canon-textfield-updates

Canon - Improve TextField
This commit is contained in:
Charles de Dreuille
2025-05-16 09:44:38 +01:00
committed by GitHub
11 changed files with 93 additions and 38 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/canon': minor
---
The icon prop in TextField now accept a ReactNode instead of an icon name. We also updated the icon sizes for each input sizes.
@@ -33,9 +33,16 @@ A text field component for your forms.
<Tabs.Panel>
We recommend starting with our [global tokens](/theme/theming) to customize the library and align it with
your brand. For additional flexibility, you can use the provided class names for each element listed below.
<CodeBlock
code={`<TextField className="canon-TextField" />`}
/>
- `canon-TextField`
- `canon-TextFieldLabel`
- `canon-TextFieldRequired`
- `canon-TextFieldInputWrapper`
- `canon-TextFieldIcon`
- `canon-TextFieldInput`
- `canon-TextFieldClearButton`
- `canon-TextFieldClearButtonIcon`
- `canon-TextFieldDescription`
- `canon-TextFieldError`
</Tabs.Panel>
</Tabs.Root>
@@ -55,8 +62,8 @@ We support two different sizes: `small`, `medium`.
open
preview={<TextFieldSnippet story="Sizes" />}
code={`<Flex direction="row" gap="4">
<TextField size="small" placeholder="Small" />
<TextField size="medium" placeholder="Medium" />
<TextField size="small" placeholder="Small" icon={<Icon name="sparkling" />} />
<TextField size="medium" placeholder="Medium" icon={<Icon name="sparkling" />} />
</Flex>`}
/>
@@ -5,12 +5,16 @@ export const inputPropDefs: Record<string, PropDef> = {
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'medium',
responsive: false,
default: 'small',
responsive: true,
},
label: {
type: 'string',
},
icon: {
type: 'enum',
values: ['ReactNode'],
},
description: {
type: 'string',
},
+12 -5
View File
@@ -576,13 +576,20 @@
display: flex;
}
.canon-TextFieldInputIcon {
padding-right: var(--canon-space-1);
width: 1.5rem;
height: 1.5rem;
.canon-TextFieldIcon {
margin-right: var(--canon-space-1);
color: var(--canon-fg-primary);
flex-shrink: 0;
display: block;
}
.canon-TextFieldIcon[data-size="small"], .canon-TextFieldIcon[data-size="small"] svg {
width: 1rem;
height: 1rem;
}
.canon-TextFieldIcon[data-size="medium"], .canon-TextFieldIcon[data-size="medium"] svg {
width: 1.25rem;
height: 1.25rem;
}
.canon-TextFieldInput {
+12 -5
View File
@@ -9800,13 +9800,20 @@
display: flex;
}
.canon-TextFieldInputIcon {
padding-right: var(--canon-space-1);
width: 1.5rem;
height: 1.5rem;
.canon-TextFieldIcon {
margin-right: var(--canon-space-1);
color: var(--canon-fg-primary);
flex-shrink: 0;
display: block;
}
.canon-TextFieldIcon[data-size="small"], .canon-TextFieldIcon[data-size="small"] svg {
width: 1rem;
height: 1rem;
}
.canon-TextFieldIcon[data-size="medium"], .canon-TextFieldIcon[data-size="medium"] svg {
width: 1.25rem;
height: 1.25rem;
}
.canon-TextFieldInput {
+12 -5
View File
@@ -43,13 +43,20 @@
display: flex;
}
.canon-TextFieldInputIcon {
padding-right: var(--canon-space-1);
width: 1.5rem;
height: 1.5rem;
.canon-TextFieldIcon {
margin-right: var(--canon-space-1);
color: var(--canon-fg-primary);
flex-shrink: 0;
display: block;
}
.canon-TextFieldIcon[data-size="small"], .canon-TextFieldIcon[data-size="small"] svg {
width: 1rem;
height: 1rem;
}
.canon-TextFieldIcon[data-size="medium"], .canon-TextFieldIcon[data-size="medium"] svg {
width: 1.25rem;
height: 1.25rem;
}
.canon-TextFieldInput {
+3 -2
View File
@@ -15,6 +15,7 @@ import { ForwardRefExoticComponent } from 'react';
import { HTMLAttributes } from 'react';
import { JSX as JSX_2 } from 'react/jsx-runtime';
import { Menu as Menu_2 } from '@base-ui-components/react/menu';
import type { MouseEventHandler } from 'react';
import { ReactElement } from 'react';
import { ReactNode } from 'react';
import { RefAttributes } from 'react';
@@ -1220,10 +1221,10 @@ export interface TextFieldProps
className?: string;
description?: string;
error?: string | null;
icon?: IconNames;
icon?: ReactNode;
label?: string;
name: string;
onClear?: React.MouseEventHandler<HTMLButtonElement>;
onClear?: MouseEventHandler<HTMLButtonElement>;
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
}
@@ -17,6 +17,7 @@
import type { Meta, StoryObj } from '@storybook/react';
import { TextField } from './TextField';
import { Flex } from '../Flex';
import { Icon } from '../Icon';
const meta = {
title: 'Components/TextField',
@@ -79,8 +80,8 @@ export const Sizes: Story = {
},
render: args => (
<Flex direction="row" gap="4" style={{ width: '100%', maxWidth: '600px' }}>
<TextField {...args} size="small" />
<TextField {...args} size="medium" />
<TextField {...args} size="small" icon={<Icon name="sparkling" />} />
<TextField {...args} size="medium" icon={<Icon name="sparkling" />} />
</Flex>
),
};
@@ -114,7 +115,7 @@ export const WithIcon: Story = {
args: {
...WithLabel.args,
placeholder: 'Search...',
icon: 'search',
icon: <Icon name="search" />,
},
};
@@ -59,15 +59,24 @@
background-color: var(--canon-bg-surface-1);
}
.canon-TextFieldInputIcon {
display: block;
padding-right: var(--canon-space-1);
width: 1.5rem;
height: 1.5rem;
.canon-TextFieldIcon {
margin-right: var(--canon-space-1);
color: var(--canon-fg-primary);
flex-shrink: 0;
}
.canon-TextFieldIcon[data-size='small'],
.canon-TextFieldIcon[data-size='small'] svg {
width: 1rem;
height: 1rem;
}
.canon-TextFieldIcon[data-size='medium'],
.canon-TextFieldIcon[data-size='medium'] svg {
width: 1.25rem;
height: 1.25rem;
}
.canon-TextFieldInput {
border: none;
background: none;
@@ -27,7 +27,7 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
(props: TextFieldProps, ref) => {
const {
className,
size = 'medium',
size = 'small',
label,
description,
error,
@@ -61,7 +61,15 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
</Field.Label>
)}
<div className="canon-TextFieldInputWrapper" data-size={responsiveSize}>
{icon && <Icon className="canon-TextFieldInputIcon" name={icon} />}
{icon && (
<div
className="canon-TextFieldIcon"
aria-hidden="true"
data-size={responsiveSize}
>
{icon}
</div>
)}
<Field.Control
className="canon-TextFieldInput"
required={required}
@@ -15,8 +15,7 @@
*/
import type { Breakpoint } from '../../types';
import type { IconNames } from '../Icon';
import type { ReactNode, MouseEventHandler } from 'react';
/** @public */
export interface TextFieldProps
extends Omit<React.ComponentPropsWithoutRef<'input'>, 'size'> {
@@ -54,10 +53,10 @@ export interface TextFieldProps
/**
* An icon to render before the input
*/
icon?: IconNames;
icon?: ReactNode;
/**
* Handler to call when the clear button is pressed
*/
onClear?: React.MouseEventHandler<HTMLButtonElement>;
onClear?: MouseEventHandler<HTMLButtonElement>;
}