diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-05-30 19:27:21 +0200 | 
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-05-31 23:15:07 +0200 | 
| commit | 782cc0a31a866519fb7c3e18a523b347d3e40238 (patch) | |
| tree | 34fb984e8dc356cd013e5e0d6f203a8c8907b9fe /src/components/molecules | |
| parent | 519b1439dce3f25dd38cd0d0f82fecd10f28dcc8 (diff) | |
chore: replace Checkbox component with a BooleanField component
Checkbox and radio buttons are working the same way so I decided to
group them in a same component.
Diffstat (limited to 'src/components/molecules')
6 files changed, 408 insertions, 7 deletions
| diff --git a/src/components/molecules/forms/labelled-boolean-field.fixture.tsx b/src/components/molecules/forms/labelled-boolean-field.fixture.tsx new file mode 100644 index 0000000..6b06887 --- /dev/null +++ b/src/components/molecules/forms/labelled-boolean-field.fixture.tsx @@ -0,0 +1 @@ +export const label = 'Quas et natus'; diff --git a/src/components/molecules/forms/labelled-boolean-field.module.scss b/src/components/molecules/forms/labelled-boolean-field.module.scss new file mode 100644 index 0000000..10a9eb2 --- /dev/null +++ b/src/components/molecules/forms/labelled-boolean-field.module.scss @@ -0,0 +1,15 @@ +.label { +  &--visible#{&}--left { +    margin-right: var(--spacing-2xs); +  } + +  &--visible#{&}--right { +    margin-left: var(--spacing-2xs); +  } +} + +.wrapper { +  display: inline-flex; +  flex-flow: row wrap; +  align-items: center; +} diff --git a/src/components/molecules/forms/labelled-boolean-field.stories.tsx b/src/components/molecules/forms/labelled-boolean-field.stories.tsx new file mode 100644 index 0000000..643f533 --- /dev/null +++ b/src/components/molecules/forms/labelled-boolean-field.stories.tsx @@ -0,0 +1,253 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { useState } from 'react'; +import LabelledBooleanField from './labelled-boolean-field'; +import { label } from './labelled-boolean-field.fixture'; + +/** + * LabelledBooleanField - Storybook Meta + */ +export default { +  title: 'Molecules/Forms/Boolean', +  component: LabelledBooleanField, +  args: { +    label, +    labelSize: 'small', +    checked: false, +  }, +  argTypes: { +    checked: { +      control: { +        type: 'boolean', +      }, +      description: 'Should the option be checked?', +      type: { +        name: 'boolean', +        required: true, +      }, +    }, +    className: { +      control: { +        type: 'text', +      }, +      description: 'Set additional classnames to the labelled field wrapper.', +      table: { +        category: 'Styles', +      }, +      type: { +        name: 'string', +        required: false, +      }, +    }, +    fieldClassName: { +      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 option id.', +      type: { +        name: 'string', +        required: true, +      }, +    }, +    label: { +      control: { +        type: 'text', +      }, +      description: 'The radio label.', +      type: { +        name: 'string', +        required: true, +      }, +    }, +    labelClassName: { +      control: { +        type: 'text', +      }, +      description: 'Set additional classnames to the label.', +      table: { +        category: 'Styles', +      }, +      type: { +        name: 'string', +        required: false, +      }, +    }, +    labelPosition: { +      control: { +        type: 'select', +      }, +      description: 'Determine the label position.', +      options: ['left', 'right'], +      table: { +        category: 'Options', +        defaultValue: { summary: 'left' }, +      }, +      type: { +        name: 'string', +        required: false, +      }, +    }, +    labelSize: { +      control: { +        type: 'select', +      }, +      description: 'The label size.', +      options: ['medium', 'small'], +      table: { +        category: 'Options', +      }, +      type: { +        name: 'string', +        required: false, +      }, +    }, +    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 LabelledBooleanField>; + +const Template: ComponentStory<typeof LabelledBooleanField> = ({ +  checked, +  onChange: _onChange, +  ...args +}) => { +  const [isChecked, setIsChecked] = useState<boolean>(checked); + +  return ( +    <LabelledBooleanField +      checked={isChecked} +      onChange={() => { +        setIsChecked(!isChecked); +      }} +      {...args} +    /> +  ); +}; + +/** + * Labelled Boolean Field Stories - Checkbox with left label + */ +export const CheckboxLeftLabel = Template.bind({}); +CheckboxLeftLabel.args = { +  id: 'checkbox', +  labelPosition: 'left', +  name: 'checkbox-left-label', +  type: 'checkbox', +  value: 'checkbox', +}; + +/** + * Labelled Boolean Field Stories - Checkbox with right label + */ +export const CheckboxRightLabel = Template.bind({}); +CheckboxRightLabel.args = { +  id: 'checkbox', +  labelPosition: 'right', +  name: 'checkbox-right-label', +  type: 'checkbox', +}; + +/** + * Labelled Boolean Field Stories - Radio button with left label + */ +export const RadioButtonLeftLabel = Template.bind({}); +RadioButtonLeftLabel.args = { +  id: 'radio', +  labelPosition: 'left', +  name: 'radio-left-label', +  type: 'radio', +  value: 'radio', +}; + +/** + * Labelled Boolean Field Stories - Radio button with right label + */ +export const RadioButtonRightLabel = Template.bind({}); +RadioButtonRightLabel.args = { +  id: 'radio', +  labelPosition: 'right', +  name: 'radio-right-label', +  type: 'radio', +  value: 'radio', +}; diff --git a/src/components/molecules/forms/labelled-boolean-field.test.tsx b/src/components/molecules/forms/labelled-boolean-field.test.tsx new file mode 100644 index 0000000..55e04ea --- /dev/null +++ b/src/components/molecules/forms/labelled-boolean-field.test.tsx @@ -0,0 +1,37 @@ +import { render, screen } from '@test-utils'; +import LabelledBooleanField from './labelled-boolean-field'; +import { label } from './labelled-boolean-field.fixture'; + +describe('LabelledBooleanField', () => { +  it('renders a labelled checkbox', () => { +    render( +      <LabelledBooleanField +        checked={true} +        id="jest-checkbox-field" +        label={label} +        name="jest-checkbox-field" +        onChange={() => null} +        type="checkbox" +        value="checkbox" +      /> +    ); +    expect(screen.getByLabelText(label)).toBeInTheDocument(); +    expect(screen.getByRole('checkbox')).toBeChecked(); +  }); + +  it('renders a labelled radio option', () => { +    render( +      <LabelledBooleanField +        checked={true} +        id="jest-radio-field" +        label={label} +        name="jest-radio-field" +        onChange={() => null} +        type="radio" +        value="radio" +      /> +    ); +    expect(screen.getByLabelText(label)).toBeInTheDocument(); +    expect(screen.getByRole('radio')).toBeChecked(); +  }); +}); diff --git a/src/components/molecules/forms/labelled-boolean-field.tsx b/src/components/molecules/forms/labelled-boolean-field.tsx new file mode 100644 index 0000000..46eb080 --- /dev/null +++ b/src/components/molecules/forms/labelled-boolean-field.tsx @@ -0,0 +1,92 @@ +import BooleanField, { +  type BooleanFieldProps, +} from '@components/atoms/forms/boolean-field'; +import Label, { type LabelProps } from '@components/atoms/forms/label'; +import { FC } from 'react'; +import styles from './labelled-boolean-field.module.scss'; + +export type LabelledBooleanFieldProps = Omit< +  BooleanFieldProps, +  'aria-labelledby' | 'className' +> & { +  /** +   * Set additional classnames to the labelled field wrapper. +   */ +  className?: string; +  /** +   * Set additional classnames to the field. +   */ +  fieldClassName?: LabelledBooleanFieldProps['className']; +  /** +   * The field label. +   */ +  label: LabelProps['children']; +  /** +   * Set additional classnames to the label. +   */ +  labelClassName?: LabelProps['className']; +  /** +   * The label position. Default: left. +   */ +  labelPosition?: 'left' | 'right'; +  /** +   * The label size. +   */ +  labelSize?: LabelProps['size']; +}; + +/** + * LabelledBooleanField component + * + * Render a checkbox or radio button with a label. + */ +const LabelledBooleanField: FC<LabelledBooleanFieldProps> = ({ +  className = '', +  fieldClassName, +  hidden, +  id, +  label, +  labelClassName, +  labelPosition = 'left', +  labelSize, +  ...props +}) => { +  const labelHiddenModifier = hidden ? 'label--hidden' : 'label--visible'; +  const labelPositionModifier = `label--${labelPosition}`; + +  return labelPosition === 'left' ? ( +    <span className={`${styles.wrapper} ${className}`}> +      <Label +        className={`${styles[labelPositionModifier]} ${styles[labelHiddenModifier]} ${labelClassName}`} +        htmlFor={id} +        size={labelSize} +      > +        {label} +      </Label> +      <BooleanField +        className={fieldClassName} +        hidden={hidden} +        id={id} +        {...props} +      /> +    </span> +  ) : ( +    <span className={`${styles.wrapper} ${className}`}> +      <BooleanField +        className={fieldClassName} +        hidden={hidden} +        id={id} +        {...props} +      /> +      <Label +        className={`${styles[labelPositionModifier]} ${styles[labelHiddenModifier]} ${labelClassName}`} +        htmlFor={id} +        size={labelSize} +      > +        {label} +      </Label> +    </span> +  ); +}; + +export default LabelledBooleanField; diff --git a/src/components/molecules/forms/toggle.tsx b/src/components/molecules/forms/toggle.tsx index 0fac45c..2f3e778 100644 --- a/src/components/molecules/forms/toggle.tsx +++ b/src/components/molecules/forms/toggle.tsx @@ -1,4 +1,6 @@ -import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox'; +import BooleanField, { +  type BooleanFieldProps, +} from '@components/atoms/forms/boolean-field';  import Label, { type LabelProps } from '@components/atoms/forms/label';  import { FC, ReactNode } from 'react';  import styles from './toggle.module.scss'; @@ -14,7 +16,7 @@ export type ToggleChoices = {    right: ReactNode;  }; -export type ToggleProps = Pick<CheckboxProps, 'id' | 'name'> & { +export type ToggleProps = Pick<BooleanFieldProps, 'id' | 'name'> & {    /**     * The toggle choices.     */ @@ -63,12 +65,13 @@ const Toggle: FC<ToggleProps> = ({  }) => {    return (      <> -      <Checkbox -        name={name} -        id={id} -        value={value} -        setValue={() => setValue(!value)} +      <BooleanField +        checked={value}          className={styles.checkbox} +        id={id} +        name={name} +        onChange={() => setValue(!value)} +        type="checkbox"        />        <Label          size={labelSize} | 
