Add NumberField component to @backstage/ui (#34264)
* Add NumberField component to @backstage/ui Signed-off-by: James Brooks <jamesbrooks@spotify.com> * Address review feedback on NumberField Signed-off-by: James Brooks <jamesbrooks@spotify.com> * Fix NumberField CSS formatting Signed-off-by: James Brooks <jamesbrooks@spotify.com> * Add increment/decrement buttons to NumberField Signed-off-by: James Brooks <jamesbrooks@spotify.com> * Fix NumberField looking disabled at min/max bounds Signed-off-by: James Brooks <jamesbrooks@spotify.com> --------- Signed-off-by: James Brooks <jamesbrooks@spotify.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { NumberField } from '../../../../../packages/ui/src/components/NumberField/NumberField';
|
||||
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
|
||||
import { RiTimeLine } from '@remixicon/react';
|
||||
|
||||
export const WithLabel = () => {
|
||||
return (
|
||||
<NumberField
|
||||
name="quantity"
|
||||
placeholder="Enter a number"
|
||||
label="Label"
|
||||
style={{ maxWidth: '300px' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Sizes = () => {
|
||||
return (
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="4"
|
||||
style={{ width: '100%', maxWidth: '300px' }}
|
||||
>
|
||||
<NumberField
|
||||
name="quantity"
|
||||
placeholder="Enter a number"
|
||||
size="small"
|
||||
icon={<RiTimeLine />}
|
||||
/>
|
||||
<NumberField
|
||||
name="quantity"
|
||||
placeholder="Enter a number"
|
||||
size="medium"
|
||||
icon={<RiTimeLine />}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithDescription = () => {
|
||||
return (
|
||||
<NumberField
|
||||
name="quantity"
|
||||
placeholder="Enter a number"
|
||||
label="Label"
|
||||
description="Description"
|
||||
style={{ maxWidth: '300px' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Snippet } from '@/components/Snippet';
|
||||
import { numberFieldPropDefs } from './props-definition';
|
||||
import {
|
||||
numberFieldUsageSnippet,
|
||||
withLabelSnippet,
|
||||
sizesSnippet,
|
||||
withDescriptionSnippet,
|
||||
} from './snippets';
|
||||
import { WithLabel, Sizes, WithDescription } from './components';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
import { Theming } from '@/components/Theming';
|
||||
import { NumberFieldDefinition } from '../../../utils/definitions';
|
||||
import { ChangelogComponent } from '@/components/ChangelogComponent';
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
import { ReactAriaLink } from '@/components/ReactAriaLink';
|
||||
|
||||
export const reactAriaUrls = {
|
||||
numberField: 'https://react-aria.adobe.com/NumberField',
|
||||
};
|
||||
|
||||
<PageTitle
|
||||
title="NumberField"
|
||||
description="A numeric input with label, description, icon, and validation support."
|
||||
/>
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
preview={<WithLabel />}
|
||||
code={withLabelSnippet}
|
||||
/>
|
||||
|
||||
## Usage
|
||||
|
||||
<CodeBlock code={numberFieldUsageSnippet} />
|
||||
|
||||
## API reference
|
||||
|
||||
<PropsTable data={numberFieldPropDefs} />
|
||||
|
||||
<ReactAriaLink component="NumberField" href={reactAriaUrls.numberField} />
|
||||
|
||||
## Examples
|
||||
|
||||
### Sizes
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<Sizes />}
|
||||
code={sizesSnippet}
|
||||
layout="side-by-side"
|
||||
/>
|
||||
|
||||
### With description
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<WithDescription />}
|
||||
code={withDescriptionSnippet}
|
||||
layout="side-by-side"
|
||||
/>
|
||||
|
||||
<Theming definition={NumberFieldDefinition} />
|
||||
|
||||
<ChangelogComponent component="number-field" />
|
||||
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
classNamePropDefs,
|
||||
stylePropDefs,
|
||||
type PropDef,
|
||||
} from '@/utils/propDefs';
|
||||
import { Chip } from '@/components/Chip';
|
||||
|
||||
export const numberFieldPropDefs: Record<string, PropDef> = {
|
||||
size: {
|
||||
type: 'enum',
|
||||
values: ['small', 'medium'],
|
||||
default: 'small',
|
||||
responsive: true,
|
||||
description: (
|
||||
<>
|
||||
Visual size of the input. Use <Chip>small</Chip> for dense layouts,{' '}
|
||||
<Chip>medium</Chip> for prominent fields.
|
||||
</>
|
||||
),
|
||||
},
|
||||
label: {
|
||||
type: 'string',
|
||||
description: 'Visible label displayed above the input.',
|
||||
},
|
||||
secondaryLabel: {
|
||||
type: 'string',
|
||||
description: (
|
||||
<>
|
||||
Secondary text shown next to the label. If not provided and isRequired
|
||||
is true, displays <Chip>Required</Chip>.
|
||||
</>
|
||||
),
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Help text displayed below the label.',
|
||||
},
|
||||
icon: {
|
||||
type: 'enum',
|
||||
values: ['ReactNode'],
|
||||
description: 'Icon rendered before the input.',
|
||||
},
|
||||
placeholder: {
|
||||
type: 'string',
|
||||
description: 'Text displayed when the input is empty.',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Form field name for submission.',
|
||||
},
|
||||
minValue: {
|
||||
type: 'number',
|
||||
description: 'Minimum allowed value.',
|
||||
},
|
||||
maxValue: {
|
||||
type: 'number',
|
||||
description: 'Maximum allowed value.',
|
||||
},
|
||||
step: {
|
||||
type: 'number',
|
||||
description: 'Step increment for arrow key changes.',
|
||||
},
|
||||
formatOptions: {
|
||||
type: 'enum',
|
||||
values: ['Intl.NumberFormatOptions'],
|
||||
description: (
|
||||
<>
|
||||
Number formatting options. Defaults to{' '}
|
||||
<Chip>{'useGrouping: false'}</Chip>.
|
||||
</>
|
||||
),
|
||||
},
|
||||
isRequired: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the field is required for form submission.',
|
||||
},
|
||||
isDisabled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the input is disabled.',
|
||||
},
|
||||
isReadOnly: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the input is read-only.',
|
||||
},
|
||||
value: {
|
||||
type: 'number',
|
||||
description: 'Controlled value of the input.',
|
||||
},
|
||||
defaultValue: {
|
||||
type: 'number',
|
||||
description: 'Default value for uncontrolled usage.',
|
||||
},
|
||||
onChange: {
|
||||
type: 'enum',
|
||||
values: ['(value: number) => void'],
|
||||
description: 'Handler called when the input value changes.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
export const numberFieldUsageSnippet = `import { NumberField } from '@backstage/ui';
|
||||
|
||||
<NumberField label="Minutes" minValue={0} maxValue={59} step={1} />`;
|
||||
|
||||
export const withLabelSnippet = `<NumberField
|
||||
name="quantity"
|
||||
placeholder="Enter a number"
|
||||
label="Label"
|
||||
/>`;
|
||||
|
||||
export const sizesSnippet = `<Flex direction="column" gap="4">
|
||||
<NumberField
|
||||
size="small"
|
||||
name="quantity"
|
||||
placeholder="Enter a number"
|
||||
icon={<RiTimeLine />}
|
||||
/>
|
||||
<NumberField
|
||||
size="medium"
|
||||
name="quantity"
|
||||
placeholder="Enter a number"
|
||||
icon={<RiTimeLine />}
|
||||
/>
|
||||
</Flex>`;
|
||||
|
||||
export const withDescriptionSnippet = `<NumberField
|
||||
name="quantity"
|
||||
placeholder="Enter a number"
|
||||
label="Label"
|
||||
description="Description"
|
||||
/>`;
|
||||
@@ -98,6 +98,10 @@ export const components: Page[] = [
|
||||
title: 'Menu',
|
||||
slug: 'menu',
|
||||
},
|
||||
{
|
||||
title: 'NumberField',
|
||||
slug: 'number-field',
|
||||
},
|
||||
{
|
||||
title: 'PasswordField',
|
||||
slug: 'password-field',
|
||||
|
||||
Reference in New Issue
Block a user