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:
James Brooks
2026-05-27 16:20:27 +01:00
committed by GitHub
parent 58fb313f22
commit b33bb24b5a
15 changed files with 908 additions and 0 deletions
@@ -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"
/>`;
+4
View File
@@ -98,6 +98,10 @@ export const components: Page[] = [
title: 'Menu',
slug: 'menu',
},
{
title: 'NumberField',
slug: 'number-field',
},
{
title: 'PasswordField',
slug: 'password-field',