UNPKG

@devseed-ui/form

Version:
931 lines (866 loc) 24.9 kB
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>