Improve input

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-01-13 12:22:39 +00:00
parent 8ab5de7a2f
commit 7eead323ae
11 changed files with 220 additions and 13 deletions
+1
View File
@@ -281,6 +281,7 @@
.canon-input-root {
font-family: var(--canon-font-regular);
flex-direction: column;
width: 100%;
display: flex;
}
@@ -0,0 +1,126 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
import {
InputPreview,
InputLabelAndDescription,
InputError,
} from '@/snippets/input';
import { BaseUI } from '@/components/HeadlessBanners/BaseUI';
# Input
A input component tfor your forms.
<Snippet
align="center"
py={4}
preview={<InputPreview />}
code={`<Input label="Name" placeholder="Enter your name" />`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
<Tabs.Tab>Theming</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Input } from '@backstage/canon';
<Input />
`}
/>
</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={`<Input className="canon-input-root" />`}
/>
</Tabs.Panel>
</Tabs.Root>
## API reference
<BaseUI href="https://base-ui.com/react/components/field" />
<PropsTable
data={{
label: {
type: 'string',
responsive: false,
},
description: {
type: 'string',
responsive: false,
},
size: {
type: ['sm', 'md'],
responsive: false,
},
placeholder: {
type: 'string',
responsive: false,
},
match: {
type: [
'badInput',
'customError',
'patternMismatch',
'rangeOverflow',
'rangeUnderflow',
'stepMismatch',
'tooLong',
'tooShort',
'typeMismatch',
'valid',
'valueMissing',
],
responsive: false,
},
error: {
type: 'string',
responsive: false,
},
required: {
type: 'boolean',
responsive: false,
},
className: {
type: 'string',
responsive: false,
},
style: {
type: 'CSSProperties',
responsive: false,
},
}}
/>
## Examples
### With label and description
Here's a simple input with a label and description.
<Snippet
align="center"
py={4}
open
preview={<InputLabelAndDescription />}
code={`<Input label="Name" description="Visible on your profile" />`}
/>
### Error
Here's a simple input with an error message.
<Snippet
align="center"
py={4}
open
preview={<InputError />}
code={`<Input label="Name" required invalid />`}
/>
@@ -9,6 +9,7 @@ import { ButtonPlayground } from '@/snippets/button';
import { CheckboxPlayground } from '@/snippets/checkbox';
import { HeadingPlayground } from '@/snippets/heading';
import { TextPlayground } from '@/snippets/text';
import { InputPlayground } from '@/snippets/input';
import styles from './styles.module.css';
@@ -66,6 +67,9 @@ const Content = () => {
{selectedComponents.find(c => c === 'text') && (
<Line content={<TextPlayground />} title="Text" />
)}
{selectedComponents.find(c => c === 'input') && (
<Line content={<InputPlayground />} title="Input" />
)}
</Stack>
);
};
+38
View File
@@ -0,0 +1,38 @@
import { Input } from '@backstage/canon';
export const InputPreview = () => {
return (
<div style={{ width: '300px' }}>
<Input label="Name" placeholder="Enter your name" />
</div>
);
};
export const InputLabelAndDescription = () => {
return (
<div style={{ width: '300px' }}>
<Input label="Name" description="Visible on your profile" />
</div>
);
};
export const InputError = () => {
return (
<div style={{ width: '300px' }}>
<Input
label="Name"
errorMessage="Please enter your name"
required
invalid
/>
</div>
);
};
export const InputPlayground = () => {
return (
<div style={{ maxWidth: '300px' }}>
<Input label="Name" description="Visible on your profile" />
</div>
);
};
+7 -2
View File
@@ -70,12 +70,12 @@ export const components: Page[] = [
{
title: 'Button',
slug: 'button',
status: 'inProgress',
status: 'alpha',
},
{
title: 'Checkbox',
slug: 'checkbox',
status: 'inProgress',
status: 'alpha',
},
{
title: 'Heading',
@@ -87,6 +87,11 @@ export const components: Page[] = [
slug: 'icon',
status: 'alpha',
},
{
title: 'Input',
slug: 'input',
status: 'alpha',
},
{
title: 'Table',
slug: 'table',
+8
View File
@@ -6,6 +6,7 @@
/// <reference types="react" />
import type { CSSProperties } from 'react';
import { Field } from '@base-ui-components/react/field';
import { ForwardRefExoticComponent } from 'react';
import { default as React_2 } from 'react';
import * as React_3 from 'react';
@@ -323,6 +324,13 @@ export interface InlineProps extends SpaceProps {
style?: React.CSSProperties;
}
// Warning: (ae-forgotten-export) The symbol "InputProps" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export const Input: React_2.ForwardRefExoticComponent<
InputProps & React_2.RefAttributes<HTMLDivElement>
>;
// @public (undocumented)
export type JustifyContent =
| 'stretch'
@@ -89,13 +89,26 @@ export const Sizes: Story = {
),
};
export const WithError: Story = {
export const WithErrorValidation: Story = {
args: {
invalid: true,
validate: value => (value ? null : 'Please enter your name'),
validate: value =>
value !== 'Charles' ? 'Please enter a different name' : null,
required: true,
label: 'Name',
description: 'Visible on your profile',
error: 'Please enter your name',
validationMode: 'onChange',
errorMatch: 'customError',
},
};
export const WithInvalidProp: Story = {
args: {
invalid: true,
required: true,
label: 'Name',
description: 'Visible on your profile',
validationMode: 'onChange',
errorForceShow: true,
validate: () => 'Stuff',
},
};
+13 -5
View File
@@ -14,6 +14,8 @@
* limitations under the License.
*/
'use client';
import React, { forwardRef } from 'react';
import clsx from 'clsx';
import { Field } from '@base-ui-components/react/field';
@@ -25,12 +27,14 @@ const Input = forwardRef<HTMLDivElement, InputProps>((props, ref) => {
className,
label,
description,
match = 'valueMissing',
errorMatch = 'customError',
errorForceShow = false,
errorMessage = 'An error occurred',
validationMode = 'onChange',
size = 'md',
placeholder,
error = 'Please enter your a value',
required = false,
style,
...rest
} = props;
@@ -38,6 +42,7 @@ const Input = forwardRef<HTMLDivElement, InputProps>((props, ref) => {
<Field.Root
ref={ref}
className={clsx('canon-input-root', className)}
style={style}
validationMode={validationMode}
{...rest}
>
@@ -54,9 +59,12 @@ const Input = forwardRef<HTMLDivElement, InputProps>((props, ref) => {
})}
/>
<Field.Error className="canon-input-error" match={match}>
{error}
</Field.Error>
<Field.Error
className="canon-input-error"
match={errorMatch}
forceShow={errorForceShow}
// children={errorForceShow ? 'An error occurred' : null}
/>
{description && (
<Field.Description className="canon-input-description">
@@ -18,6 +18,7 @@
display: flex;
flex-direction: column;
font-family: var(--canon-font-regular);
width: 100%;
}
.canon-input-label {
+4 -2
View File
@@ -16,13 +16,15 @@
import { Field } from '@base-ui-components/react/field';
/** @public */
export interface InputProps
extends React.ComponentPropsWithoutRef<typeof Field.Root> {
label?: string;
description?: string;
size?: 'sm' | 'md';
placeholder?: string;
match?: Field.Error.Props['match'];
error?: string;
errorMatch?: Field.Error.Props['match'];
errorForceShow?: boolean;
errorMessage?: string;
required?: boolean;
}
+1
View File
@@ -37,6 +37,7 @@ export * from './components/Button';
export * from './components/Icon';
export * from './components/Checkbox';
export * from './components/Table';
export * from './components/Input';
// Types
export * from './types';