diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-06-01 22:37:56 +0200 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-06-01 22:37:56 +0200 | 
| commit | 0a33a4658d848fe056715c6da053763407845b2a (patch) | |
| tree | 7c679e54ba4bbadaf0a59bbde780f5742e3b875d /src/components/atoms/forms | |
| parent | 97031a86ca38890e60ecec79828498b7bb13cbfa (diff) | |
| parent | 6be20422494e3806fba3d1c5ad5c3e98bd6e67e5 (diff) | |
chore(a11y): improve website settings accessibility (#17)
The previous switch buttons (using checkbox) was not a11y compliant. So I change my approach to use radio buttons and to clearly separate the two different states. I also convert the Ackee select setting to improve consistency between settings.
Diffstat (limited to 'src/components/atoms/forms')
| -rw-r--r-- | src/components/atoms/forms/boolean-field.module.scss | 5 | ||||
| -rw-r--r-- | src/components/atoms/forms/boolean-field.stories.tsx | 175 | ||||
| -rw-r--r-- | src/components/atoms/forms/boolean-field.test.tsx | 60 | ||||
| -rw-r--r-- | src/components/atoms/forms/boolean-field.tsx | 62 | ||||
| -rw-r--r-- | src/components/atoms/forms/checkbox.stories.tsx | 102 | ||||
| -rw-r--r-- | src/components/atoms/forms/checkbox.test.tsx | 28 | ||||
| -rw-r--r-- | src/components/atoms/forms/checkbox.tsx | 46 | 
7 files changed, 302 insertions, 176 deletions
| diff --git a/src/components/atoms/forms/boolean-field.module.scss b/src/components/atoms/forms/boolean-field.module.scss new file mode 100644 index 0000000..3f0676e --- /dev/null +++ b/src/components/atoms/forms/boolean-field.module.scss @@ -0,0 +1,5 @@ +@use "@styles/abstracts/mixins" as mix; + +.hidden { +  @include mix.visually-hidden; +} diff --git a/src/components/atoms/forms/boolean-field.stories.tsx b/src/components/atoms/forms/boolean-field.stories.tsx new file mode 100644 index 0000000..8b6136b --- /dev/null +++ b/src/components/atoms/forms/boolean-field.stories.tsx @@ -0,0 +1,175 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { useState } from 'react'; +import BooleanFieldComponent from './boolean-field'; + +/** + * BooleanField - Storybook Meta + */ +export default { +  title: 'Atoms/Forms', +  component: BooleanFieldComponent, +  args: { +    hidden: false, +  }, +  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, +      }, +    }, +    checked: { +      control: { +        type: null, +      }, +      description: 'The field state: true if checked.', +      type: { +        name: 'boolean', +        required: true, +      }, +    }, +    className: { +      control: { +        type: 'text', +      }, +      description: 'Set additional classnames to the field.', +      table: { +        category: 'Styles', +      }, +      type: { +        name: 'string', +        required: false, +      }, +    }, +    hidden: { +      control: { +        type: 'boolean', +      }, +      description: 'Define if the field should be visually hidden.', +      table: { +        category: 'Options', +        defaultValue: { summary: false }, +      }, +      type: { +        name: 'boolean', +        required: false, +      }, +    }, +    id: { +      control: { +        type: 'text', +      }, +      description: 'The field id.', +      type: { +        name: 'string', +        required: true, +      }, +    }, +    name: { +      control: { +        type: 'text', +      }, +      description: 'The field name.', +      type: { +        name: 'string', +        required: true, +      }, +    }, +    onChange: { +      control: { +        type: null, +      }, +      description: 'A callback function to handle field state change.', +      table: { +        category: 'Events', +      }, +      type: { +        name: 'function', +        required: true, +      }, +    }, +    onClick: { +      control: { +        type: null, +      }, +      description: 'A callback function to handle click on field.', +      table: { +        category: 'Events', +      }, +      type: { +        name: 'function', +        required: false, +      }, +    }, +    type: { +      control: { +        type: 'select', +      }, +      description: 'The field type. Either checkbox or radio.', +      options: ['checkbox', 'radio'], +      type: { +        name: 'string', +        required: true, +      }, +    }, +    value: { +      control: { +        type: 'text', +      }, +      description: 'The field value.', +      type: { +        name: 'string', +        required: true, +      }, +    }, +  }, +} as ComponentMeta<typeof BooleanFieldComponent>; + +const Template: ComponentStory<typeof BooleanFieldComponent> = ({ +  checked, +  onChange: _onChange, +  ...args +}) => { +  const [isChecked, setIsChecked] = useState<boolean>(checked); + +  return ( +    <BooleanFieldComponent +      checked={isChecked} +      onChange={() => { +        setIsChecked(!isChecked); +      }} +      {...args} +    /> +  ); +}; + +/** + * Checkbox Story + */ +export const Checkbox = Template.bind({}); +Checkbox.args = { +  id: 'checkbox', +  checked: false, +  name: 'checkbox', +  type: 'checkbox', +  value: 'checkbox', +}; + +/** + * Radio Story + */ +export const Radio = Template.bind({}); +Radio.args = { +  id: 'radio', +  checked: false, +  name: 'radio', +  type: 'radio', +  value: 'radio', +}; diff --git a/src/components/atoms/forms/boolean-field.test.tsx b/src/components/atoms/forms/boolean-field.test.tsx new file mode 100644 index 0000000..95ec4b1 --- /dev/null +++ b/src/components/atoms/forms/boolean-field.test.tsx @@ -0,0 +1,60 @@ +import { render, screen } from '@test-utils'; +import BooleanField from './boolean-field'; + +describe('BooleanField', () => { +  it('renders an unchecked checkbox', () => { +    render( +      <BooleanField +        checked={false} +        id="jest-checkbox" +        name="jest-checkbox" +        onChange={() => null} +        type="checkbox" +        value="checkbox" +      /> +    ); +    expect(screen.getByRole('checkbox')).not.toBeChecked(); +  }); + +  it('renders a checked checkbox', () => { +    render( +      <BooleanField +        checked={true} +        id="jest-checkbox" +        name="jest-checkbox" +        onChange={() => null} +        type="checkbox" +        value="checkbox" +      /> +    ); +    expect(screen.getByRole('checkbox')).toBeChecked(); +  }); + +  it('renders an unchecked radio', () => { +    render( +      <BooleanField +        checked={false} +        id="jest-radio" +        name="jest-radio" +        onChange={() => null} +        type="radio" +        value="radio" +      /> +    ); +    expect(screen.getByRole('radio')).not.toBeChecked(); +  }); + +  it('renders a checked radio', () => { +    render( +      <BooleanField +        checked={true} +        id="jest-radio" +        name="jest-radio" +        onChange={() => null} +        type="radio" +        value="radio" +      /> +    ); +    expect(screen.getByRole('radio')).toBeChecked(); +  }); +}); diff --git a/src/components/atoms/forms/boolean-field.tsx b/src/components/atoms/forms/boolean-field.tsx new file mode 100644 index 0000000..946e375 --- /dev/null +++ b/src/components/atoms/forms/boolean-field.tsx @@ -0,0 +1,62 @@ +import { ChangeEventHandler, FC, MouseEventHandler } from 'react'; +import styles from './boolean-field.module.scss'; + +export type BooleanFieldProps = { +  /** +   * One or more ids that refers to the checkbox name. +   */ +  'aria-labelledby'?: string; +  /** +   * True if the field should be checked. +   */ +  checked: boolean; +  /** +   * Add classnames to the checkbox. +   */ +  className?: string; +  /** +   * Field id attribute. +   */ +  id: string; +  /** +   * True if the field should be visually hidden. Default: false. +   */ +  hidden?: boolean; +  /** +   * Field name attribute. +   */ +  name: string; +  /** +   * Callback function to handle state change. +   */ +  onChange: ChangeEventHandler<HTMLInputElement>; +  /** +   * A callback function to handle click. +   */ +  onClick?: MouseEventHandler<HTMLInputElement>; +  /** +   * The input type. +   */ +  type: 'checkbox' | 'radio'; +  /** +   * Field name attribute. +   */ +  value: string; +}; + +/** + * BooleanField component + * + * Render a checkbox or a radio input type. + */ +const BooleanField: FC<BooleanFieldProps> = ({ +  className = '', +  hidden = false, +  ...props +}) => { +  const modifier = hidden ? 'hidden' : ''; + +  return <input className={`${styles[modifier]} ${className}`} {...props} />; +}; + +export default BooleanField; diff --git a/src/components/atoms/forms/checkbox.stories.tsx b/src/components/atoms/forms/checkbox.stories.tsx deleted file mode 100644 index 588fdcc..0000000 --- a/src/components/atoms/forms/checkbox.stories.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { useState } from 'react'; -import CheckboxComponent from './checkbox'; - -/** - * Checkbox - Storybook Meta - */ -export default { -  title: 'Atoms/Forms', -  component: CheckboxComponent, -  argTypes: { -    'aria-labelledby': { -      control: { -        type: 'text', -      }, -      description: 'One or more ids that refers to the checkbox name.', -      table: { -        category: 'Accessibility', -      }, -      type: { -        name: 'string', -        required: false, -      }, -    }, -    className: { -      control: { -        type: 'text', -      }, -      description: 'Set additional classnames to the checkbox.', -      table: { -        category: 'Styles', -      }, -      type: { -        name: 'string', -        required: false, -      }, -    }, -    id: { -      control: { -        type: 'text', -      }, -      description: 'The checkbox id.', -      type: { -        name: 'string', -        required: true, -      }, -    }, -    name: { -      control: { -        type: 'text', -      }, -      description: 'The checkbox name.', -      type: { -        name: 'string', -        required: true, -      }, -    }, -    setValue: { -      control: { -        type: null, -      }, -      description: 'A callback function to handle checkbox state.', -      type: { -        name: 'function', -        required: true, -      }, -    }, -    value: { -      control: { -        type: null, -      }, -      description: -        'The checkbox state: either checked (true) or unchecked (false).', -      type: { -        name: 'boolean', -        required: true, -      }, -    }, -  }, -} as ComponentMeta<typeof CheckboxComponent>; - -const Template: ComponentStory<typeof CheckboxComponent> = ({ -  value, -  setValue: _setValue, -  ...args -}) => { -  const [isChecked, setIsChecked] = useState<boolean>(value); - -  return ( -    <CheckboxComponent value={isChecked} setValue={setIsChecked} {...args} /> -  ); -}; - -/** - * Checkbox Story - */ -export const Checkbox = Template.bind({}); -Checkbox.args = { -  id: 'storybook-checkbox', -  name: 'storybook-checkbox', -  value: false, -}; diff --git a/src/components/atoms/forms/checkbox.test.tsx b/src/components/atoms/forms/checkbox.test.tsx deleted file mode 100644 index 3b54549..0000000 --- a/src/components/atoms/forms/checkbox.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { render, screen } from '@test-utils'; -import Checkbox from './checkbox'; - -describe('Checkbox', () => { -  it('renders an unchecked checkbox', () => { -    render( -      <Checkbox -        id="jest-checkbox" -        name="jest-checkbox" -        value={false} -        setValue={() => null} -      /> -    ); -    expect(screen.getByRole('checkbox')).not.toBeChecked(); -  }); - -  it('renders a checked checkbox', () => { -    render( -      <Checkbox -        id="jest-checkbox" -        name="jest-checkbox" -        value={true} -        setValue={() => null} -      /> -    ); -    expect(screen.getByRole('checkbox')).toBeChecked(); -  }); -}); diff --git a/src/components/atoms/forms/checkbox.tsx b/src/components/atoms/forms/checkbox.tsx deleted file mode 100644 index aec97f0..0000000 --- a/src/components/atoms/forms/checkbox.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { FC, SetStateAction } from 'react'; - -export type CheckboxProps = { -  /** -   * One or more ids that refers to the checkbox name. -   */ -  'aria-labelledby'?: string; -  /** -   * Add classnames to the checkbox. -   */ -  className?: string; -  /** -   * Checkbox id attribute. -   */ -  id: string; -  /** -   * Checkbox name attribute. -   */ -  name: string; -  /** -   * Callback function to set checkbox value. -   */ -  setValue: (value: SetStateAction<boolean>) => void; -  /** -   * Checkbox value. -   */ -  value: boolean; -}; - -/** - * Checkbox component - * - * Render a checkbox type input. - */ -const Checkbox: FC<CheckboxProps> = ({ value, setValue, ...props }) => { -  return ( -    <input -      type="checkbox" -      checked={value} -      onChange={() => setValue(!value)} -      {...props} -    /> -  ); -}; - -export default Checkbox; | 
