diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-04-08 22:36:24 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-04-08 23:31:58 +0200 |
| commit | 0b3146f7278929c4d1b33dd8f94f34e351e5e5a9 (patch) | |
| tree | 6a784b197a283a7da07c2e1df80a29fee8b3790a /src/components/atoms/forms | |
| parent | 61278678ea8a8febee0574cd0f6006492d7b15cb (diff) | |
chore: add a Settings modal component
Diffstat (limited to 'src/components/atoms/forms')
| -rw-r--r-- | src/components/atoms/forms/field.stories.tsx | 45 | ||||
| -rw-r--r-- | src/components/atoms/forms/field.tsx | 21 | ||||
| -rw-r--r-- | src/components/atoms/forms/form.test.tsx | 9 | ||||
| -rw-r--r-- | src/components/atoms/forms/form.tsx | 73 | ||||
| -rw-r--r-- | src/components/atoms/forms/forms.module.scss | 23 | ||||
| -rw-r--r-- | src/components/atoms/forms/label.module.scss | 17 | ||||
| -rw-r--r-- | src/components/atoms/forms/label.stories.tsx | 42 | ||||
| -rw-r--r-- | src/components/atoms/forms/label.test.tsx | 2 | ||||
| -rw-r--r-- | src/components/atoms/forms/label.tsx | 32 | ||||
| -rw-r--r-- | src/components/atoms/forms/select.stories.tsx | 44 | ||||
| -rw-r--r-- | src/components/atoms/forms/select.tsx | 25 | ||||
| -rw-r--r-- | src/components/atoms/forms/toggle.module.scss | 2 | ||||
| -rw-r--r-- | src/components/atoms/forms/toggle.tsx | 7 |
13 files changed, 287 insertions, 55 deletions
diff --git a/src/components/atoms/forms/field.stories.tsx b/src/components/atoms/forms/field.stories.tsx index 02681e7..ec81922 100644 --- a/src/components/atoms/forms/field.stories.tsx +++ b/src/components/atoms/forms/field.stories.tsx @@ -1,4 +1,5 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { useState } from 'react'; import FieldComponent from './field'; export default { @@ -7,11 +8,35 @@ export default { args: { disabled: false, required: false, - setValue: () => null, type: 'text', - value: '', }, argTypes: { + 'aria-labelledby': { + control: { + type: 'text', + }, + description: 'One or more ids that refers to the field name.', + table: { + category: 'Accessibility', + }, + type: { + name: 'string', + required: false, + }, + }, + className: { + control: { + type: 'text', + }, + description: 'Add classnames to the field.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, disabled: { control: { type: 'boolean', @@ -148,7 +173,7 @@ export default { }, value: { control: { - type: 'text', + type: null, }, description: 'Field value.', type: { @@ -159,14 +184,18 @@ export default { }, } as ComponentMeta<typeof FieldComponent>; -const Template: ComponentStory<typeof FieldComponent> = (args) => ( - <FieldComponent {...args} /> -); +const Template: ComponentStory<typeof FieldComponent> = ({ + value: _value, + setValue: _setValue, + ...args +}) => { + const [value, setValue] = useState<string>(''); + + return <FieldComponent value={value} setValue={setValue} {...args} />; +}; export const Field = Template.bind({}); Field.args = { id: 'field-storybook', name: 'field-storybook', - setValue: () => null, - value: '', }; diff --git a/src/components/atoms/forms/field.tsx b/src/components/atoms/forms/field.tsx index 513d2ba..2e75d0f 100644 --- a/src/components/atoms/forms/field.tsx +++ b/src/components/atoms/forms/field.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, FC, SetStateAction } from 'react'; +import { ChangeEvent, SetStateAction, VFC } from 'react'; import styles from './forms.module.scss'; export type FieldType = @@ -14,6 +14,14 @@ export type FieldType = export type FieldProps = { /** + * One or more ids that refers to the field name. + */ + 'aria-labelledby'?: string; + /** + * Add classnames to the field. + */ + className?: string; + /** * Field state. Either enabled (false) or disabled (true). */ disabled?: boolean; @@ -64,7 +72,12 @@ export type FieldProps = { * * Render either an input or a textarea. */ -const Field: FC<FieldProps> = ({ setValue, type, ...props }) => { +const Field: VFC<FieldProps> = ({ + className = '', + setValue, + type, + ...props +}) => { /** * Update select value when an option is selected. * @param e - The option change event. @@ -78,14 +91,14 @@ const Field: FC<FieldProps> = ({ setValue, type, ...props }) => { return type === 'textarea' ? ( <textarea onChange={updateValue} - className={`${styles.field} ${styles['field--textarea']}`} + className={`${styles.field} ${styles['field--textarea']} ${className}`} {...props} /> ) : ( <input type={type} onChange={updateValue} - className={styles.field} + className={`${styles.field} ${className}`} {...props} /> ); diff --git a/src/components/atoms/forms/form.test.tsx b/src/components/atoms/forms/form.test.tsx new file mode 100644 index 0000000..9cd3c58 --- /dev/null +++ b/src/components/atoms/forms/form.test.tsx @@ -0,0 +1,9 @@ +import { render, screen } from '@test-utils'; +import Form from './form'; + +describe('Form', () => { + it('renders a form', () => { + render(<Form aria-label="Jest form" onSubmit={() => null}></Form>); + expect(screen.getByRole('form', { name: 'Jest form' })).toBeInTheDocument(); + }); +}); diff --git a/src/components/atoms/forms/form.tsx b/src/components/atoms/forms/form.tsx new file mode 100644 index 0000000..8e80930 --- /dev/null +++ b/src/components/atoms/forms/form.tsx @@ -0,0 +1,73 @@ +import { Children, FC, FormEvent, Fragment } from 'react'; +import styles from './forms.module.scss'; + +export type FormProps = { + /** + * An accessible name. + */ + 'aria-label'?: string; + /** + * One or more ids that refers to the form name. + */ + 'aria-labelledby'?: string; + /** + * Set additional classnames to the form wrapper. + */ + className?: string; + /** + * Wrap each items with a div. Default: true. + */ + grouped?: boolean; + /** + * A callback function to execute on submit. + */ + onSubmit: () => void; +}; + +/** + * Form component. + * + * Render children wrapped in a form element. + */ +const Form: FC<FormProps> = ({ + children, + className = '', + grouped = true, + onSubmit, + ...props +}) => { + const arrayChildren = Children.toArray(children); + + /** + * Get the form items. + * @returns {JSX.Element[]} An array of child elements wrapped in a div. + */ + const getFormItems = (): JSX.Element[] => { + return arrayChildren.map((child, index) => + grouped ? ( + <div key={`item-${index}`} className={styles.item}> + {child} + </div> + ) : ( + <Fragment key={`item-${index}`}>{child}</Fragment> + ) + ); + }; + + /** + * Handle form submit. + * @param {FormEvent} e - The form event. + */ + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + onSubmit(); + }; + + return ( + <form onSubmit={handleSubmit} className={className} {...props}> + {getFormItems()} + </form> + ); +}; + +export default Form; diff --git a/src/components/atoms/forms/forms.module.scss b/src/components/atoms/forms/forms.module.scss index 689a318..279c185 100644 --- a/src/components/atoms/forms/forms.module.scss +++ b/src/components/atoms/forms/forms.module.scss @@ -1,7 +1,12 @@ @use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; + +.item { + margin: var(--spacing-xs) 0; + max-width: 45ch; +} .field { - width: 100%; padding: var(--spacing-2xs) var(--spacing-xs); background: var(--color-bg-tertiary); border: fun.convert-px(2) solid var(--color-border); @@ -10,6 +15,10 @@ &--select { cursor: pointer; + + @include mix.pointer("fine") { + padding: fun.convert-px(3) var(--spacing-xs); + } } &--textarea { @@ -41,15 +50,3 @@ } } } - -.label { - display: block; - color: var(--color-primary-darker); - font-size: var(--font-size-sm); - font-variant: small-caps; - font-weight: 600; -} - -.required { - color: var(--color-secondary); -} diff --git a/src/components/atoms/forms/label.module.scss b/src/components/atoms/forms/label.module.scss new file mode 100644 index 0000000..f900925 --- /dev/null +++ b/src/components/atoms/forms/label.module.scss @@ -0,0 +1,17 @@ +.label { + color: var(--color-primary-darker); + font-weight: 600; + + &--small { + font-size: var(--font-size-sm); + font-variant: small-caps; + } + + &--medium { + font-size: var(--font-size-md); + } +} + +.required { + color: var(--color-secondary); +} diff --git a/src/components/atoms/forms/label.stories.tsx b/src/components/atoms/forms/label.stories.tsx index 06e8eb9..463e8ac 100644 --- a/src/components/atoms/forms/label.stories.tsx +++ b/src/components/atoms/forms/label.stories.tsx @@ -4,7 +4,24 @@ import LabelComponent from './label'; export default { title: 'Atoms/Forms', component: LabelComponent, + args: { + required: false, + size: 'small', + }, argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Add classnames to the label.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, children: { control: { type: 'text', @@ -32,22 +49,37 @@ export default { description: 'Set to true if the field is required.', table: { category: 'Options', + defaultValue: { summary: false }, }, type: { name: 'boolean', required: false, }, }, + size: { + control: { + type: 'select', + }, + description: 'The label size.', + options: ['medium', 'small'], + table: { + category: 'Options', + defaultValue: { summary: 'small' }, + }, + type: { + name: 'string', + required: false, + }, + }, }, } as ComponentMeta<typeof LabelComponent>; -const Template: ComponentStory<typeof LabelComponent> = (args) => { - const { children, ...props } = args; - return <LabelComponent {...props}>{children}</LabelComponent>; -}; +const Template: ComponentStory<typeof LabelComponent> = ({ + children, + ...args +}) => <LabelComponent {...args}>{children}</LabelComponent>; export const Label = Template.bind({}); Label.args = { children: 'A label', - htmlFor: 'a-field-id', }; diff --git a/src/components/atoms/forms/label.test.tsx b/src/components/atoms/forms/label.test.tsx index fcf1731..14257c3 100644 --- a/src/components/atoms/forms/label.test.tsx +++ b/src/components/atoms/forms/label.test.tsx @@ -3,7 +3,7 @@ import Label from './label'; describe('Label', () => { it('renders a field label', () => { - render(<Label htmlFor="a-field-id">A label</Label>); + render(<Label>A label</Label>); expect(screen.getByText('A label')).toBeDefined(); }); }); diff --git a/src/components/atoms/forms/label.tsx b/src/components/atoms/forms/label.tsx index 860cd73..8d57ee2 100644 --- a/src/components/atoms/forms/label.tsx +++ b/src/components/atoms/forms/label.tsx @@ -1,9 +1,23 @@ import { FC } from 'react'; -import styles from './forms.module.scss'; +import styles from './label.module.scss'; -type LabelProps = { - htmlFor: string; +export type LabelProps = { + /** + * Add classnames to the label. + */ + className?: string; + /** + * The field id. + */ + htmlFor?: string; + /** + * Is the field required? Default: false. + */ required?: boolean; + /** + * The label size. Default: small. + */ + size?: 'medium' | 'small'; }; /** @@ -11,9 +25,17 @@ type LabelProps = { * * Render a HTML label element. */ -const Label: FC<LabelProps> = ({ children, required = false, ...props }) => { +const Label: FC<LabelProps> = ({ + children, + className = '', + required = false, + size = 'small', + ...props +}) => { + const sizeClass = styles[`label--${size}`]; + return ( - <label className={styles.label} {...props}> + <label className={`${styles.label} ${sizeClass} ${className}`} {...props}> {children} {required && <span className={styles.required}> *</span>} </label> diff --git a/src/components/atoms/forms/select.stories.tsx b/src/components/atoms/forms/select.stories.tsx index c7bb253..c2fb8c6 100644 --- a/src/components/atoms/forms/select.stories.tsx +++ b/src/components/atoms/forms/select.stories.tsx @@ -1,4 +1,5 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { useState } from 'react'; import SelectComponent from './select'; const selectOptions = [ @@ -10,14 +11,31 @@ const selectOptions = [ export default { title: 'Atoms/Forms', component: SelectComponent, + args: { + disabled: false, + required: false, + }, argTypes: { - classes: { + 'aria-labelledby': { control: { type: 'text', }, - description: 'Set additional classes', + description: 'One or more ids that refers to the select field name.', table: { - category: 'Options', + category: 'Accessibility', + }, + type: { + name: 'string', + required: false, + }, + }, + className: { + control: { + type: 'text', + }, + description: 'Add classnames to the select field.', + table: { + category: 'Styles', }, type: { name: 'string', @@ -59,9 +77,6 @@ export default { }, }, options: { - control: { - type: null, - }, description: 'Select options.', type: { name: 'array', @@ -100,7 +115,7 @@ export default { }, value: { control: { - type: 'text', + type: null, }, description: 'Field value.', type: { @@ -111,13 +126,20 @@ export default { }, } as ComponentMeta<typeof SelectComponent>; -const Template: ComponentStory<typeof SelectComponent> = (args) => ( - <SelectComponent {...args} /> -); +const Template: ComponentStory<typeof SelectComponent> = ({ + value, + setValue: _setValue, + ...args +}) => { + const [selected, setSelected] = useState<string>(value); + + return <SelectComponent value={selected} setValue={setSelected} {...args} />; +}; export const Select = Template.bind({}); Select.args = { + id: 'storybook-select', + name: 'storybook-select', options: selectOptions, - setValue: () => null, value: 'option2', }; diff --git a/src/components/atoms/forms/select.tsx b/src/components/atoms/forms/select.tsx index a42dbda..25e86e0 100644 --- a/src/components/atoms/forms/select.tsx +++ b/src/components/atoms/forms/select.tsx @@ -1,17 +1,30 @@ -import { ChangeEvent, FC, SetStateAction } from 'react'; +import { ChangeEvent, SetStateAction, VFC } from 'react'; import styles from './forms.module.scss'; export type SelectOptions = { + /** + * The option id. + */ id: string; + /** + * The option name. + */ name: string; + /** + * The option value. + */ value: string; }; export type SelectProps = { /** - * Set additional classes. + * One or more ids that refers to the select field name. + */ + 'aria-labelledby'?: string; + /** + * Add classnames to the select field. */ - classes?: string; + className?: string; /** * Field state. Either enabled (false) or disabled (true). */ @@ -47,8 +60,8 @@ export type SelectProps = { * * Render a HTML select element. */ -const Select: FC<SelectProps> = ({ - classes = '', +const Select: VFC<SelectProps> = ({ + className = '', options, setValue, ...props @@ -74,7 +87,7 @@ const Select: FC<SelectProps> = ({ return ( <select - className={`${styles.field} ${styles['field--select']} ${classes}`} + className={`${styles.field} ${styles['field--select']} ${className}`} onChange={updateValue} {...props} > diff --git a/src/components/atoms/forms/toggle.module.scss b/src/components/atoms/forms/toggle.module.scss index 24b867e..2e8a49f 100644 --- a/src/components/atoms/forms/toggle.module.scss +++ b/src/components/atoms/forms/toggle.module.scss @@ -10,7 +10,7 @@ } .title { - margin-right: auto; + margin-right: var(--spacing-2xs); } .toggle { diff --git a/src/components/atoms/forms/toggle.tsx b/src/components/atoms/forms/toggle.tsx index 7ef40ed..c3bc09d 100644 --- a/src/components/atoms/forms/toggle.tsx +++ b/src/components/atoms/forms/toggle.tsx @@ -27,6 +27,10 @@ export type ToggleProps = { */ label: string; /** + * Set additional classnames to the label. + */ + labelClassName?: string; + /** * The label size. */ labelSize?: LabelProps['size']; @@ -53,6 +57,7 @@ const Toggle: VFC<ToggleProps> = ({ choices, id, label, + labelClassName = '', labelSize, name, setValue, @@ -69,7 +74,7 @@ const Toggle: VFC<ToggleProps> = ({ className={styles.checkbox} /> <Label size={labelSize} htmlFor={id} className={styles.label}> - <span className={styles.title}>{label}</span> + <span className={`${styles.title} ${labelClassName}`}>{label}</span> {choices.left} <span className={styles.toggle}></span> {choices.right} |
