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 | |
| 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')
24 files changed, 789 insertions, 220 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; 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} diff --git a/src/components/organisms/layout/no-results.test.tsx b/src/components/organisms/layout/no-results.test.tsx index 7f57177..97846b1 100644 --- a/src/components/organisms/layout/no-results.test.tsx +++ b/src/components/organisms/layout/no-results.test.tsx @@ -4,7 +4,7 @@ import NoResults from './no-results'; describe('NoResults', () => { it('renders a no results text', () => { render(<NoResults searchPage="#" />); - expect(screen.getByText(/No results/gi)).toBeInTheDocument(); + expect(screen.getByText(/No results/i)).toBeInTheDocument(); }); it('renders a search form', () => { diff --git a/src/components/organisms/toolbar/main-nav.stories.tsx b/src/components/organisms/toolbar/main-nav.stories.tsx index 831636f..7d6d915 100644 --- a/src/components/organisms/toolbar/main-nav.stories.tsx +++ b/src/components/organisms/toolbar/main-nav.stories.tsx @@ -63,7 +63,15 @@ const Template: ComponentStory<typeof MainNav> = ({ }) => { const [isOpen, setIsOpen] = useState<boolean>(isActive); - return <MainNav isActive={isOpen} setIsActive={setIsOpen} {...args} />; + return ( + <MainNav + isActive={isOpen} + setIsActive={() => { + setIsOpen(!isOpen); + }} + {...args} + /> + ); }; /** diff --git a/src/components/organisms/toolbar/main-nav.tsx b/src/components/organisms/toolbar/main-nav.tsx index d205112..5dd32f7 100644 --- a/src/components/organisms/toolbar/main-nav.tsx +++ b/src/components/organisms/toolbar/main-nav.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 from '@components/atoms/forms/label'; import Hamburger from '@components/atoms/icons/hamburger'; import Nav, { @@ -18,7 +20,7 @@ export type MainNavProps = { /** * The button state. */ - isActive: CheckboxProps['value']; + isActive: BooleanFieldProps['checked']; /** * The main nav items. */ @@ -26,7 +28,7 @@ export type MainNavProps = { /** * A callback function to handle button state. */ - setIsActive: CheckboxProps['setValue']; + setIsActive: BooleanFieldProps['onChange']; }; /** @@ -53,12 +55,14 @@ const MainNav: ForwardRefRenderFunction<HTMLDivElement, MainNavProps> = ( return ( <div className={`${sharedStyles.item} ${mainNavStyles.item}`} ref={ref}> - <Checkbox + <BooleanField + checked={isActive} + className={`${sharedStyles.checkbox} ${mainNavStyles.checkbox}`} id="main-nav-button" name="main-nav-button" - value={isActive} - setValue={setIsActive} - className={`${sharedStyles.checkbox} ${mainNavStyles.checkbox}`} + onChange={setIsActive} + type="checkbox" + value="open" /> <Label htmlFor="main-nav-button" diff --git a/src/components/organisms/toolbar/search.stories.tsx b/src/components/organisms/toolbar/search.stories.tsx index f0f65b4..4baf0bf 100644 --- a/src/components/organisms/toolbar/search.stories.tsx +++ b/src/components/organisms/toolbar/search.stories.tsx @@ -68,7 +68,15 @@ const Template: ComponentStory<typeof Search> = ({ }) => { const [isOpen, setIsOpen] = useState<boolean>(isActive); - return <Search isActive={isOpen} setIsActive={setIsOpen} {...args} />; + return ( + <Search + isActive={isOpen} + setIsActive={() => { + setIsOpen(!isOpen); + }} + {...args} + /> + ); }; /** diff --git a/src/components/organisms/toolbar/search.tsx b/src/components/organisms/toolbar/search.tsx index 6a8af26..2d4b6b5 100644 --- a/src/components/organisms/toolbar/search.tsx +++ b/src/components/organisms/toolbar/search.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 MagnifyingGlass from '@components/atoms/icons/magnifying-glass'; import FlippingLabel from '@components/molecules/forms/flipping-label'; import useInputAutofocus from '@utils/hooks/use-input-autofocus'; @@ -16,7 +18,7 @@ export type SearchProps = { /** * The button state. */ - isActive: CheckboxProps['value']; + isActive: BooleanFieldProps['checked']; /** * A callback function to execute search. */ @@ -24,7 +26,7 @@ export type SearchProps = { /** * A callback function to handle button state. */ - setIsActive: CheckboxProps['setValue']; + setIsActive: BooleanFieldProps['onChange']; }; const Search: ForwardRefRenderFunction<HTMLDivElement, SearchProps> = ( @@ -53,12 +55,14 @@ const Search: ForwardRefRenderFunction<HTMLDivElement, SearchProps> = ( return ( <div className={`${sharedStyles.item} ${searchStyles.item}`} ref={ref}> - <Checkbox + <BooleanField + checked={isActive} + className={`${sharedStyles.checkbox} ${searchStyles.checkbox}`} id="search-button" name="search-button" - value={isActive} - setValue={setIsActive} - className={`${sharedStyles.checkbox} ${searchStyles.checkbox}`} + onChange={setIsActive} + type="checkbox" + value="open" /> <FlippingLabel className={sharedStyles.label} diff --git a/src/components/organisms/toolbar/settings.stories.tsx b/src/components/organisms/toolbar/settings.stories.tsx index 08ec579..20d0b4d 100644 --- a/src/components/organisms/toolbar/settings.stories.tsx +++ b/src/components/organisms/toolbar/settings.stories.tsx @@ -92,7 +92,15 @@ const Template: ComponentStory<typeof Settings> = ({ }) => { const [isOpen, setIsOpen] = useState<boolean>(isActive); - return <Settings isActive={isOpen} setIsActive={setIsOpen} {...args} />; + return ( + <Settings + isActive={isOpen} + setIsActive={() => { + setIsOpen(!isOpen); + }} + {...args} + /> + ); }; /** diff --git a/src/components/organisms/toolbar/settings.tsx b/src/components/organisms/toolbar/settings.tsx index ceb6db4..0e7daa2 100644 --- a/src/components/organisms/toolbar/settings.tsx +++ b/src/components/organisms/toolbar/settings.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 Cog from '@components/atoms/icons/cog'; import FlippingLabel from '@components/molecules/forms/flipping-label'; import { forwardRef, ForwardRefRenderFunction } from 'react'; @@ -13,11 +15,11 @@ export type SettingsProps = SettingsModalProps & { /** * The button state. */ - isActive: CheckboxProps['value']; + isActive: BooleanFieldProps['checked']; /** * A callback function to handle button state. */ - setIsActive: CheckboxProps['setValue']; + setIsActive: BooleanFieldProps['onChange']; }; const Settings: ForwardRefRenderFunction<HTMLDivElement, SettingsProps> = ( @@ -46,12 +48,14 @@ const Settings: ForwardRefRenderFunction<HTMLDivElement, SettingsProps> = ( return ( <div className={`${sharedStyles.item} ${settingsStyles.item}`} ref={ref}> - <Checkbox + <BooleanField + checked={isActive} + className={`${sharedStyles.checkbox} ${settingsStyles.checkbox}`} id="settings-button" name="settings-button" - value={isActive} - setValue={setIsActive} - className={`${sharedStyles.checkbox} ${settingsStyles.checkbox}`} + onChange={setIsActive} + type="checkbox" + value="open" /> <FlippingLabel className={sharedStyles.label} diff --git a/src/components/organisms/toolbar/toolbar.tsx b/src/components/organisms/toolbar/toolbar.tsx index ee61a7b..c18b8ea 100644 --- a/src/components/organisms/toolbar/toolbar.tsx +++ b/src/components/organisms/toolbar/toolbar.tsx @@ -50,14 +50,14 @@ const Toolbar: FC<ToolbarProps> = ({ <MainNav items={nav} isActive={isNavOpened} - setIsActive={setIsNavOpened} + setIsActive={() => setIsNavOpened(!isNavOpened)} className={styles.modal} ref={mainNavRef} /> <Search searchPage={searchPage} isActive={isSearchOpened} - setIsActive={setIsSearchOpened} + setIsActive={() => setIsSearchOpened(!isSearchOpened)} className={`${styles.modal} ${styles['modal--search']}`} ref={searchRef} /> @@ -67,7 +67,7 @@ const Toolbar: FC<ToolbarProps> = ({ isActive={isSettingsOpened} motionStorageKey={motionStorageKey} ref={settingsRef} - setIsActive={setIsSettingsOpened} + setIsActive={() => setIsSettingsOpened(!isSettingsOpened)} tooltipClassName={styles.tooltip} /> </div> diff --git a/src/styles/abstracts/_mixins.scss b/src/styles/abstracts/_mixins.scss index a49c848..da77164 100644 --- a/src/styles/abstracts/_mixins.scss +++ b/src/styles/abstracts/_mixins.scss @@ -1 +1,2 @@ +@forward "./mixins/helpers"; @forward "./mixins/media-queries"; diff --git a/src/styles/abstracts/mixins/_helpers.scss b/src/styles/abstracts/mixins/_helpers.scss new file mode 100644 index 0000000..427ea4b --- /dev/null +++ b/src/styles/abstracts/mixins/_helpers.scss @@ -0,0 +1,13 @@ +@use "../functions" as fun; + +@mixin visually-hidden { + width: fun.convert-px(1); + height: fun.convert-px(1); + padding: 0; + position: absolute !important; + overflow: hidden; + border: 0; + clip: rect(1px, 1px, 1px, 1px); + word-break: normal; + word-wrap: normal !important; /* Many screen reader and browser combinations announce broken words as they would appear visually. */ +} diff --git a/src/styles/base/_helpers.scss b/src/styles/base/_helpers.scss index 3879643..381154f 100644 --- a/src/styles/base/_helpers.scss +++ b/src/styles/base/_helpers.scss @@ -14,15 +14,7 @@ /* Text meant only for screen readers. */ .screen-reader-text { - width: fun.convert-px(1); - height: fun.convert-px(1); - padding: 0; - position: absolute !important; - overflow: hidden; - border: 0; - clip: rect(1px, 1px, 1px, 1px); - word-break: normal; - word-wrap: normal !important; /* Many screen reader and browser combinations announce broken words as they would appear visually. */ + @include mix.visually-hidden; &:focus { display: block; |
