@devseed-ui/form
Version:
devseed UI Kit Form
931 lines (866 loc) • 24.9 kB
text/mdx
import { useState } from 'react';
import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import {
Toolbar,
ToolbarIconButton,
ToolbarLabel,
ToolbarButton,
} from '@devseed-ui/toolbar';
import {
CollecticonTrashBin,
CollecticonXmarkSmall,
CollecticonCircleInformation,
} from '@devseed-ui/collecticons';
import {
Form,
FormGroup,
FormGroupHeader,
FormLabel,
FormGroupBody,
FormInput,
FormTextarea,
FormSelect,
FormCheckable,
FormCheckableGroup,
FormSwitch,
FormHelper,
FormHelperMessage,
FormHelperCounter,
FormFieldset,
FormFieldsetHeader,
FormLegend,
FormFieldsetBody,
FormGroupFooter,
FormGroupStructure,
} from './';
<Meta title="Components/Form" />
# Form
The `form` package provides a series of components that can be used to compose forms.
All the base components are available for usage, but this package also provides more complex components which take care of setting up a recommended structure.
```
yarn add @devseed-ui/form
```
```js
import {
Form,
FormGroup,
FormGroupHeader,
FormLabel,
FormGroupBody,
FormGroupFooter,
FormGroupStructure,
FormHelper,
FormHelperMessage,
FormHelperCounter,
FormFieldset,
FormFieldsetHeader,
FormLegend,
FormFieldsetBody,
FormCheckable,
FormCheckableGroup,
FormInput,
FormSelect,
FormTextarea,
FormSwitch
} from '@devseed-ui/form';
```
## Structure
Each form field should be wrapped in a `FormGroup`. This adds some wrapper elements that help with styling and ensure consistency.
For accessibility purposes it is recommended that every form element has a `FormGroupHeader` with `FormLabel`. The `FormGroupHeader`, can be hidden if needed, via the `isHidden` prop.
<Canvas>
<Story
name="Base structure"
argTypes={{
hideFormHeader: {
name: 'isHidden',
type: { name: 'boolean', required: false },
defaultValue: false,
description: 'Whether or not to visually hide the FormGroupHeader',
table: {
category: 'Form Group Header',
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
control: {
type: 'boolean',
},
},
}}
>
{args => (
<FormGroup>
<FormGroupHeader isHidden={args.hideFormHeader}>
<FormLabel>Form label</FormLabel>
</FormGroupHeader>
<FormGroupBody>Body content where the form field goes</FormGroupBody>
<FormGroupFooter>Footer content</FormGroupFooter>
</FormGroup>
)}
</Story>
</Canvas>
<ArgsTable story="Base structure" />
### Form label
<Canvas>
<Story
name="Form label"
argTypes={{
optional: {
name: 'optional',
type: { name: 'boolean', required: false },
defaultValue: false,
description: 'Display an (optional) hint',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
control: {
type: 'boolean',
},
},
required: {
name: 'required',
type: { name: 'boolean', required: false },
defaultValue: false,
description: 'Display a (required) hint. Overrides the optional hint',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
control: {
type: 'boolean',
},
},
hint: {
name: 'hint',
type: { name: 'node', required: false },
defaultValue: undefined,
description: 'Display a custom hint. Overrides the required hint',
table: {
type: { summary: 'react node' },
defaultValue: { summary: undefined },
},
control: {
type: 'text',
},
},
}}
>
{args => <FormLabel {...args}>Form label</FormLabel>}
</Story>
</Canvas>
<ArgsTable story="Form label" />
## Form Structure component
To facilitate the setup of the structure above, the `FormGroupStructure` wraps all that in a single component
<Canvas>
<Story
name="Form group structure"
argTypes={{
id: {
name: 'id',
type: { name: 'node', required: true },
defaultValue: 'form-structure',
description:
"Value for the label's htmlFor attribute. Prop passed to the `FormLabel`. **Must match the `id` of the input field.**",
table: {
type: { summary: 'text' },
defaultValue: { summary: undefined },
},
control: {
type: 'text',
},
},
label: {
name: 'label',
type: { name: 'node', required: true },
defaultValue: 'Field label',
description: 'Value for the label',
table: {
type: { summary: 'react node' },
defaultValue: { summary: undefined },
},
control: {
type: 'text',
},
},
optional: {
name: 'optional',
type: { name: 'boolean', required: false },
defaultValue: false,
description:
'Display an (optional) hint. Prop passed to the `FormLabel`',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
control: {
type: 'boolean',
},
},
required: {
name: 'required',
type: { name: 'boolean', required: false },
defaultValue: false,
description:
'Display a (required) hint. Overrides the optional hint. Prop passed to the `FormLabel`',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
control: {
type: 'boolean',
},
},
hint: {
name: 'hint',
type: { name: 'node', required: false },
defaultValue: undefined,
description:
'Display a custom hint. Overrides the required hint. Prop passed to the `FormLabel`',
table: {
type: { summary: 'react node' },
defaultValue: { summary: undefined },
},
control: {
type: 'text',
},
},
isHidden: {
name: 'isHidden',
type: { name: 'boolean', required: false },
defaultValue: false,
description:
'Whether or not to visually hide the `FormGroupHeader`. Prop passed to the `FormGroupHeader`',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
control: {
type: 'boolean',
},
},
toolbar: {
name: 'toolbar',
type: { name: 'node', required: false },
defaultValue: undefined,
description:
'Space to render a Toolbar. Will be rendered in the `FormGroupHeader` after `FormLabel`. Should be a `Toolbar` component.',
table: {
type: { summary: 'react node' },
defaultValue: { summary: undefined },
},
control: null,
},
children: {
name: 'children',
type: { name: 'node', required: false },
defaultValue: 'undefined',
description:
'The children of `FormGroupStructure` should be the actual form element',
table: {
type: { summary: 'react node' },
defaultValue: { summary: undefined },
},
control: null,
},
helper: {
name: 'helper',
type: { name: 'node', required: false },
defaultValue: undefined,
description: 'Content rendered inside `FormHelper`. See info below.',
table: {
type: { summary: 'react node' },
defaultValue: { summary: undefined },
},
control: null,
},
footerContent: {
name: 'footerContent',
type: { name: 'node', required: false },
defaultValue: undefined,
description: 'Content for the `FormGroupFooter`',
table: {
type: { summary: 'react node' },
defaultValue: { summary: undefined },
},
control: {
type: 'text',
},
},
}}
>
{args => (
<Form>
<FormGroupStructure {...args}>Form element</FormGroupStructure>
</Form>
)}
</Story>
</Canvas>
<ArgsTable story="Form group structure" />
## Form helper
There are 3 components related to Form helper:
- `FormHelper` - The wrapper where the helper components are rendered. If using `FormGroupStructure` this wrapper is already present when using `helper` prop.
- `FormHelperMessage` - Helper message.
- `FormHelperCounter` - Counter component, shown on the right. Useful for text based inputs. Will change color as the `value` reaches the `max`.
<Canvas>
<Story
name="Form helper"
argTypes={{
invalidMessage: {
name: 'invalid',
type: { name: 'boolean', required: false },
defaultValue: false,
description: 'Whether or not the helper message is showing an error.',
table: {
category: 'Form Helper Message',
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
control: {
type: 'boolean',
},
},
message: {
name: 'children',
type: { name: 'node', required: false },
defaultValue: 'This message provides some help. Hopefully!',
description: 'The message to display',
table: {
category: 'Form Helper Message',
type: { summary: 'react node' },
defaultValue: { summary: false },
},
control: {
type: 'text',
},
},
counterValue: {
name: 'value',
type: { name: 'number', required: true },
defaultValue: 10,
description: 'The current value to show on the counter',
table: {
category: 'Form Helper Counter',
type: { summary: 'number' },
defaultValue: { summary: null },
},
control: {
type: 'number',
},
},
counterMax: {
name: 'max',
type: { name: 'number', required: true },
defaultValue: 20,
description: 'The max value to show on the counter',
table: {
category: 'Form Helper Counter',
type: { summary: 'number' },
defaultValue: { summary: null },
},
control: {
type: 'number',
},
},
counterWarnAt: {
name: 'warnAt',
type: { name: 'number', required: false },
defaultValue: undefined,
description: 'The value at which the counter color changes to warning.',
table: {
category: 'Form Helper Counter',
type: { summary: 'number' },
defaultValue: { summary: '90% of max' },
},
control: {
type: 'number',
},
},
}}
>
{args => (
<FormHelper>
<FormHelperMessage invalid={args.invalidMessage}>
{args.message}
</FormHelperMessage>
<FormHelperCounter
value={args.counterValue}
max={args.counterMax}
warnAt={args.counterWarnAt}
/>
</FormHelper>
)}
</Story>
</Canvas>
<ArgsTable story="Form helper" />
export const invalidArg = {
name: 'invalid',
type: { name: 'boolean', required: false },
defaultValue: false,
description: 'Whether or not the value in this field is invalid',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
control: {
type: 'boolean',
},
};
export const baseInputArgs = {
size: {
name: 'size',
type: { name: 'text', required: false },
defaultValue: 'medium',
description: 'Size of the input field',
options: ['small', 'medium', 'large', 'xlarge'],
table: {
type: { summary: 'string' },
defaultValue: { summary: 'medium' },
},
control: {
type: 'select',
},
},
invalid: invalidArg,
stressed: {
name: 'stressed',
type: { name: 'boolean', required: false },
defaultValue: false,
description:
'A stressed input will animate to call attention. It is useful to notify of errors.',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
control: {
type: 'boolean',
},
},
};
## Form Input
The `FormInput` is a styled-component version on `<input>` therefore the standard html attributes for this element are available.
<Canvas>
<Story name="Form input" argTypes={baseInputArgs}>
{args => (
<FormGroupStructure label="Input for something" id="input-something">
<FormInput
placeholder="Placeholder of the input"
id="input-something"
{...args}
/>
</FormGroupStructure>
)}
</Story>
</Canvas>
<ArgsTable story="Form input" />
## Form Textarea
The `FormTextarea` is a styled-component version on `<textarea>` therefore the standard html attributes for this element are available.
<Canvas>
<Story name="Form textarea" argTypes={baseInputArgs}>
{args => (
<FormGroupStructure label="A longer message" id="input-textarea">
<FormTextarea
placeholder="Placeholder of the textarea"
id="input-textarea"
{...args}
/>
</FormGroupStructure>
)}
</Story>
</Canvas>
<ArgsTable story="Form textarea" />
## Form Select
The `FormSelect` is a styled-component version on `<select>` therefore the standard html attributes for this element are available.
<Canvas>
<Story name="Form select" argTypes={baseInputArgs}>
{args => (
<FormGroupStructure label="A couple of options" id="input-selecting">
<FormSelect id="input-selecting" {...args}>
<option value="option-1">Option 1</option>
<option value="option-2">Option 2</option>
<option value="option-3">Option 3</option>
<option value="option-4">Option 4</option>
</FormSelect>
</FormGroupStructure>
)}
</Story>
</Canvas>
<ArgsTable story="Form select" />
export const checkableBaseArgs = {
children: {
name: 'children',
type: { name: 'node', required: true },
defaultValue: 'Here is some label',
description: 'The label for this checkable',
table: {
type: { summary: 'react node' },
defaultValue: { summary: null },
},
control: {
type: 'text',
},
},
textPlacement: {
name: 'textPlacement',
type: { name: 'string', required: true },
defaultValue: 'right',
description: 'Position of the checkable label',
options: ['right', 'left'],
table: {
type: { summary: 'string' },
defaultValue: { summary: 'right' },
},
control: {
type: 'inline-radio',
},
},
hideText: {
name: 'hideText',
type: { name: 'boolean', required: false },
defaultValue: false,
description: 'Whether or not to hide the checkable label',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
control: {
type: 'boolean',
},
},
};
export const checkableArgs = {
type: {
name: 'type',
type: { name: 'string', required: true },
defaultValue: 'checkbox',
description: 'Type of checkable to use',
options: ['checkbox', 'radio'],
table: {
type: { summary: 'string' },
defaultValue: { summary: 'checkbox' },
},
control: {
type: 'inline-radio',
},
},
invalid: invalidArg,
...checkableBaseArgs,
};
## Form Checkable
Form checkables can be either `checkboxes` or `radio` buttons.
<Canvas>
<Story name="Form checkable" argTypes={checkableArgs}>
{args => (
<FormCheckable
name="example-checkable"
id="example-checkable"
{...args}
/>
)}
</Story>
</Canvas>
<ArgsTable story="Form checkable" />
### Form Checkable Group
When using multiple `FormCheckable` elements, they should be wrapped with `FormCheckableGroup`, for styling purposes.
<Canvas>
<Story name="Form checkable group">
<FormCheckableGroup>
<FormCheckable
textPlacement="right"
checked={undefined}
type="radio"
name="radio-ex-a"
id="radio-ex-a1"
>
Radio A
</FormCheckable>
<FormCheckable
textPlacement="right"
checked={undefined}
type="radio"
name="radio-ex-a"
id="radio-ex-a2"
>
Radio B
</FormCheckable>
<FormCheckable
textPlacement="right"
checked={undefined}
type="radio"
name="radio-ex-a"
id="radio-ex-a3"
>
Radio C
</FormCheckable>
<FormCheckable
textPlacement="right"
checked={undefined}
type="radio"
name="radio-ex-a"
id="radio-ex-a4"
>
Radio D
</FormCheckable>
</FormCheckableGroup>
</Story>
</Canvas>
## Form Switch
A variation on the `FormCheckable` for on/off situations.
<Canvas>
<Story name="Form switch" argTypes={checkableBaseArgs}>
{args => <FormSwitch name="example-switch" id="example-switch" {...args} />}
</Story>
</Canvas>
<ArgsTable story="Form switch" />
## Form Fieldset
A form fieldset is useful to group form elements, and they can be nested.
<Canvas>
<Story name="Form fieldset">
{args => (
<FormFieldset>
<FormFieldsetHeader>
<FormLegend>Form legend</FormLegend>
</FormFieldsetHeader>
<FormFieldsetBody>
<p>A form fieldset is useful to contain other form elements.</p>
<FormFieldset>
<FormFieldsetHeader>
<FormLegend>Form legend</FormLegend>
</FormFieldsetHeader>
<FormFieldsetBody>
<p>A fieldset can also contain another fieldset.</p>
</FormFieldsetBody>
</FormFieldset>
</FormFieldsetBody>
</FormFieldset>
)}
</Story>
</Canvas>
## Example: text input
Example of using a `FormGroupStructure` with helper messages and the inclusion of a toolbar from the Toolbar package.
<Canvas>
<Story
name="Example: text input"
parameters={{
docs: {
source: {
type: 'code',
},
},
}}
>
{args => {
const [value, setValue] = useState('');
return (
<FormGroupStructure
label="First name"
id="first-name"
required
toolbar={
<Toolbar size="small">
<ToolbarIconButton
title="Clear field"
onClick={() => setValue('')}
>
<CollecticonTrashBin title="Clear field" meaningful />
</ToolbarIconButton>
</Toolbar>
}
helper={
<>
<FormHelperMessage invalid={value.length > 10}>
{value.length > 10
? 'Your name must have fewer than 10 characters.'
: 'Come on. Let us know what we should call you.'}
</FormHelperMessage>
<FormHelperCounter value={value.length} max={10} />
</>
}
>
<FormInput
placeholder="Write your name"
id="first-name"
type="text"
value={value}
onChange={e => setValue(e.target.value)}
/>
</FormGroupStructure>
);
}}
</Story>
</Canvas>
## Showcase
<Canvas>
<Story name="Showcase">
<Form>
<FormFieldset>
<FormFieldsetHeader>
<FormLegend>Form legend with a toolbar</FormLegend>
<Toolbar>
<ToolbarIconButton>
<CollecticonXmarkSmall title="Close" meaningful />
</ToolbarIconButton>
</Toolbar>
</FormFieldsetHeader>
<FormFieldsetBody>
<FormGroupStructure
label="Form label"
id="input-text-a"
required
toolbar={
<Toolbar size="small">
<ToolbarIconButton>
<CollecticonCircleInformation
title="View more information"
meaningful
/>
</ToolbarIconButton>
</Toolbar>
}
helper={
<>
<FormHelperMessage invalid>
This is an error message.
</FormHelperMessage>
<FormHelperCounter value={0} max={80} />
</>
}
>
<FormInput
type="text"
size="large"
id="input-text-a"
placeholder="This is a text input"
invalid
/>
</FormGroupStructure>
<FormGroupStructure
label="Form label"
id="checkboxes"
toolbar={
<Toolbar size="small">
<ToolbarLabel>Select</ToolbarLabel>
<ToolbarButton>All</ToolbarButton>
<ToolbarButton>None</ToolbarButton>
</Toolbar>
}
>
<FormCheckableGroup>
<FormCheckable
checked={undefined}
type="checkbox"
name="checkbox-a"
id="checkbox-a"
>
Checkbox A
</FormCheckable>
<FormCheckable
checked={undefined}
type="checkbox"
name="checkbox-b"
id="checkbox-b"
>
Checkbox B
</FormCheckable>
</FormCheckableGroup>
</FormGroupStructure>
<FormGroupStructure label="Form label" id="checkboxes">
<FormCheckableGroup>
<FormCheckable
textPlacement="right"
checked={undefined}
type="radio"
name="radio-a"
id="radio-a1"
>
Radio A
</FormCheckable>
<FormCheckable
textPlacement="right"
checked={undefined}
type="radio"
name="radio-a"
id="radio-a2"
>
Radio B
</FormCheckable>
<FormCheckable
textPlacement="right"
checked={undefined}
type="radio"
name="radio-a"
id="radio-a3"
>
Radio C
</FormCheckable>
<FormCheckable
textPlacement="right"
checked={undefined}
type="radio"
name="radio-a"
id="radio-a4"
>
Radio D
</FormCheckable>
</FormCheckableGroup>
</FormGroupStructure>
<FormFieldset>
<FormFieldsetHeader>
<FormLegend>Form legend</FormLegend>
<Toolbar>
<ToolbarIconButton>
<CollecticonTrashBin
title="Remove fieldset icon"
meaningful
/>
</ToolbarIconButton>
</Toolbar>
</FormFieldsetHeader>
<FormFieldsetBody>
<FormGroupStructure label="Form label" id="textarea-b">
<FormTextarea
size="large"
id="textarea-b"
placeholder="This is a textarea"
/>
</FormGroupStructure>
</FormFieldsetBody>
</FormFieldset>
<FormGroupStructure
label="Form label"
id="select-a"
optional
helper={
<FormHelperMessage>This is some help text.</FormHelperMessage>
}
>
<FormSelect size="large" id="select-a">
<option value="option-1">Option 1</option>
<option value="option-2">Option 2</option>
<option value="option-3">Option 3</option>
<option value="option-4">Option 4</option>
</FormSelect>
</FormGroupStructure>
<FormGroupStructure
label="Form label"
id="textarea-c"
helper={
<FormHelperMessage invalid>
This is an error message.
</FormHelperMessage>
}
>
<FormTextarea
size="large"
id="textarea-c"
placeholder="This is a textarea"
invalid
/>
</FormGroupStructure>
<FormSwitch name="example-switch2" id="example-switch2">
Enable this switch
</FormSwitch>
</FormFieldsetBody>
</FormFieldset>
</Form>
</Story>
</Canvas>