Add TextField docs

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-04-03 11:34:18 +01:00
parent 0307392e35
commit 8a19b9bc23
7 changed files with 52 additions and 204 deletions
@@ -1,99 +0,0 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
import { FieldSnippet } from '@/snippets/stories-snippets';
import { BaseUI } from '@/components/HeadlessBanners/BaseUI';
import {
fieldRootPropDefs,
fieldLabelPropDefs,
fieldDescriptionPropDefs,
fieldErrorPropDefs,
} from './props';
# Field
A wrapper around `Input` or `Select` component to add label, description and error messages..
<Snippet
align="center"
py={4}
preview={<FieldSnippet story="Default" />}
code={`<Field />`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
<Tabs.Tab>Theming</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Field } from '@backstage/canon';
<Field.Root>
<Field.Label>Name</Field.Label>
<Input placeholder="Enter your name" />
<Field.Description>Visible on your profile</Field.Description>
<Field.Error />
</Field.Root>`}
/>
</Tabs.Panel>
<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={`<Field.Root className='canon-FieldRoot'>
<Field.Label className='canon-FieldLabel' />
<Field.Description className='canon-FieldDescription' />
<Field.Error className='canon-FieldError' />
</Field.Root>`}
/>
</Tabs.Panel>
</Tabs.Root>
## API reference
<BaseUI href="https://base-ui.com/react/components/field" />
### Field.Root
Groups all parts of the field. Renders a `<div>` element.
<PropsTable data={fieldRootPropDefs} />
### Field.Label
An accessible label that is automatically associated with the field control. Renders a `<label>` element.
<PropsTable data={fieldLabelPropDefs} />
### Field.Description
A paragraph with additional information about the field. Renders a `<p>` element.
<PropsTable data={fieldDescriptionPropDefs} />
### Field.Error
An error message displayed if the field control fails validation. Renders a `<div>` element.
<PropsTable data={fieldErrorPropDefs} />
## Examples
### With Label and Description
Here's a simple input with a label and description.
<Snippet
align="center"
py={4}
open
preview={<FieldSnippet story="WithLabelAndDescription" />}
code={`<Field>
<Field.Label>Name</Field.Label>
<Field.Description>Visible on your profile</Field.Description>
<Input placeholder="Enter your name" />
</Field>`}
/>
@@ -1,69 +0,0 @@
import { classNamePropDefs, stylePropDefs } from '../../../../utils/propDefs';
import type { PropDef } from '../../../../utils/propDefs';
export const fieldRootPropDefs: Record<string, PropDef> = {
name: {
type: 'string',
responsive: false,
},
disabled: {
type: 'boolean',
responsive: false,
},
invalid: {
type: 'boolean',
responsive: false,
},
validate: {
type: 'enum',
values: ['(value) => string | string[] | null | Promise'],
responsive: false,
},
validationMode: {
type: 'enum',
values: ['onBlur', 'onChange'],
responsive: false,
},
validationDebounceTime: {
type: 'number',
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const fieldLabelPropDefs: Record<string, PropDef> = {
...classNamePropDefs,
...stylePropDefs,
};
export const fieldDescriptionPropDefs: Record<string, PropDef> = {
...classNamePropDefs,
...stylePropDefs,
};
export const fieldErrorPropDefs: Record<string, PropDef> = {
match: {
type: 'enum',
values: [
'badInput',
'customError',
'patternMismatch',
'rangeOverflow',
'rangeUnderflow',
'stepMismatch',
'tooLong',
'tooShort',
'typeMismatch',
'valid',
'valueMissing',
],
responsive: false,
},
forceShow: {
type: 'boolean',
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -2,19 +2,19 @@ import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
import { InputSnippet } from '@/snippets/stories-snippets';
import { TextFieldSnippet } from '@/snippets/stories-snippets';
import { BaseUI } from '@/components/HeadlessBanners/BaseUI';
import { inputPropDefs } from './props';
# Input
# TextField
A input component tfor your forms.
A text field component for your forms.
<Snippet
align="center"
py={4}
preview={<InputSnippet story="Primary" />}
code={`<Input label="Name" placeholder="Enter your name" />`}
preview={<TextFieldSnippet story="WithLabel" />}
code={`<TextField label="Label" placeholder="Enter a URL" />`}
/>
<Tabs.Root>
@@ -24,9 +24,9 @@ A input component tfor your forms.
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Input } from '@backstage/canon';
code={`import { TextField } from '@backstage/canon';
<Input />
<TextField />
`}
/>
</Tabs.Panel>
@@ -34,30 +34,40 @@ A input component tfor your forms.
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={`<Input className="canon-Input" />`}
code={`<TextField className="canon-TextField" />`}
/>
</Tabs.Panel>
</Tabs.Root>
## API reference
<BaseUI href="https://base-ui.com/react/components/input" />
<PropsTable data={inputPropDefs} />
## Examples
### Sizes
Here's a simple input with a label and description.
We support two different sizes: `small`, `medium`.
<Snippet
align="center"
py={4}
open
preview={<InputSnippet story="Sizes" />}
code={`<Grid>
<Input size="sm" placeholder="Small" />
<Input size="md" placeholder="Medium" />
</Grid>`}
preview={<TextFieldSnippet story="Sizes" />}
code={`<Flex direction="row" gap="4">
<TextField size="small" placeholder="Small" />
<TextField size="medium" placeholder="Medium" />
</Flex>`}
/>
### With description
Here's a simple TextField with a description.
<Snippet
align="center"
py={4}
open
preview={<TextFieldSnippet story="WithDescription" />}
code={`<TextField label="Label" description="Description" placeholder="Enter a URL" />`}
/>
@@ -4,10 +4,20 @@ import type { PropDef } from '../../../../utils/propDefs';
export const inputPropDefs: Record<string, PropDef> = {
size: {
type: 'enum',
values: ['sm', 'md'],
default: 'md',
values: ['small', 'medium'],
default: 'medium',
responsive: false,
},
label: {
type: 'string',
},
description: {
type: 'string',
},
name: {
type: 'string',
required: true,
},
...classNamePropDefs,
...stylePropDefs,
};
+4 -16
View File
@@ -5,12 +5,11 @@ import * as BoxStories from '../../../packages/canon/src/components/Box/Box.stor
import * as ButtonStories from '../../../packages/canon/src/components/Button/Button.stories';
import * as CheckboxStories from '../../../packages/canon/src/components/Checkbox/Checkbox.stories';
import * as ContainerStories from '../../../packages/canon/src/components/Container/Container.stories';
import * as FieldStories from '../../../packages/canon/src/components/Field/Field.stories';
import * as GridStories from '../../../packages/canon/src/components/Grid/Grid.stories';
import * as HeadingStories from '../../../packages/canon/src/components/Heading/Heading.stories';
import * as IconButtonStories from '../../../packages/canon/src/components/IconButton/IconButton.stories';
import * as IconStories from '../../../packages/canon/src/components/Icon/Icon.stories';
import * as InputStories from '../../../packages/canon/src/components/Input/Input.stories';
import * as TextFieldStories from '../../../packages/canon/src/components/TextField/TextField.stories';
import * as TextStories from '../../../packages/canon/src/components/Text/Text.stories';
import * as FlexStories from '../../../packages/canon/src/components/Flex/Flex.stories';
import * as SelectStories from '../../../packages/canon/src/components/Select/Select.stories';
@@ -62,12 +61,12 @@ export const FlexSnippet = ({ story }: { story: keyof typeof FlexStories }) => {
return StoryComponent ? <StoryComponent /> : null;
};
export const FieldSnippet = ({
export const TextFieldSnippet = ({
story,
}: {
story: keyof typeof FieldStories;
story: keyof typeof TextFieldStories;
}) => {
const stories = composeStories(FieldStories);
const stories = composeStories(TextFieldStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
@@ -109,17 +108,6 @@ export const IconSnippet = ({ story }: { story: keyof typeof IconStories }) => {
return StoryComponent ? <StoryComponent /> : null;
};
export const InputSnippet = ({
story,
}: {
story: keyof typeof InputStories;
}) => {
const stories = composeStories(InputStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const TextSnippet = ({ story }: { story: keyof typeof TextStories }) => {
const stories = composeStories(TextStories);
const StoryComponent = stories[story as keyof typeof stories];
@@ -33,6 +33,9 @@ export const Default: Story = {
args: {
name: 'url',
placeholder: 'Enter a URL',
style: {
maxWidth: '300px',
},
},
};
@@ -71,7 +74,7 @@ export const Sizes: Story = {
description: 'Description',
},
render: args => (
<Flex direction="row" gap="4">
<Flex direction="row" gap="4" style={{ width: '100%', maxWidth: '600px' }}>
<TextField {...args} size="small" />
<TextField {...args} size="medium" />
</Flex>
@@ -31,6 +31,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
label,
description,
name,
style,
...rest
} = props;
@@ -38,7 +39,11 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
const responsiveSize = useResponsiveValue(size);
return (
<Field.Root className={clsx('canon-FieldRoot', className)} name={name}>
<Field.Root
className={clsx('canon-FieldRoot', className)}
name={name}
style={style}
>
{label && (
<Field.Label className="canon-FieldLabel">{label}</Field.Label>
)}