diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/atoms/forms/toggle.module.scss | 74 | ||||
| -rw-r--r-- | src/components/atoms/forms/toggle.stories.tsx | 90 | ||||
| -rw-r--r-- | src/components/atoms/forms/toggle.test.tsx | 29 | ||||
| -rw-r--r-- | src/components/atoms/forms/toggle.tsx | 69 | 
4 files changed, 262 insertions, 0 deletions
| diff --git a/src/components/atoms/forms/toggle.module.scss b/src/components/atoms/forms/toggle.module.scss new file mode 100644 index 0000000..00e87a2 --- /dev/null +++ b/src/components/atoms/forms/toggle.module.scss @@ -0,0 +1,74 @@ +@use "@styles/abstracts/functions" as fun; + +.label { +  --toggle-width: #{fun.convert-px(45)}; +  --toggle-height: calc(var(--toggle-width) / 2); + +  display: inline-flex; +  align-items: center; +} + +.title { +  margin-right: var(--spacing-xs); +} + +.toggle { +  display: inline-flex; +  align-items: center; +  width: var(--toggle-width); +  height: var(--toggle-height); +  background: var(--color-shadow-light); +  border: fun.convert-px(1) solid var(--color-primary); +  border-radius: fun.convert-px(32); +  box-shadow: inset 0 0 fun.convert-px(3) 0 var(--color-shadow-dark); +  margin: 0 var(--spacing-2xs); +  position: relative; + +  &::after { +    content: ""; +    display: block; +    width: calc((var(--toggle-width) / 2) - 1px); +    height: calc((var(--toggle-width) / 2) - 1px); +    background: var(--color-primary-light); +    border: fun.convert-px(1) solid var(--color-primary); +    border-radius: 50%; +    box-shadow: inset 0 0 fun.convert-px(1) fun.convert-px(1) +        var(--color-shadow), +      0 0 fun.convert-px(2) fun.convert-px(1) var(--color-shadow-light); +    position: absolute; +    left: fun.convert-px(-2); +    transition: all 0.3s ease-in-out 0s; +  } +} + +.checkbox { +  position: absolute; +  opacity: 0; +  cursor: pointer; + +  &:checked ~ .label { +    .toggle::after { +      position: absolute; +      left: calc(100% - (var(--toggle-width) / 2) + #{fun.convert-px(2)}); +    } +  } + +  &:hover, +  &:focus { +    ~ .label { +      .toggle::after { +        background: var(--color-primary-lighter); +      } +    } +  } + +  &:focus ~ .label { +    .title { +      text-decoration: underline solid var(--color-primary) fun.convert-px(2); +    } + +    .toggle { +      outline: var(--color-border) solid fun.convert-px(5); +    } +  } +} diff --git a/src/components/atoms/forms/toggle.stories.tsx b/src/components/atoms/forms/toggle.stories.tsx new file mode 100644 index 0000000..6e7323b --- /dev/null +++ b/src/components/atoms/forms/toggle.stories.tsx @@ -0,0 +1,90 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { useState } from 'react'; +import ToggleComponent from './toggle'; + +export default { +  title: 'Atoms/Forms', +  component: ToggleComponent, +  argTypes: { +    choices: { +      description: 'The toggle choices.', +      type: { +        name: 'object', +        required: true, +        value: {}, +      }, +    }, +    id: { +      control: { +        type: 'text', +      }, +      description: 'The input id.', +      type: { +        name: 'string', +        required: true, +      }, +    }, +    label: { +      control: { +        type: 'text', +      }, +      description: 'The toggle label.', +      type: { +        name: 'string', +        required: true, +      }, +    }, +    name: { +      control: { +        type: 'text', +      }, +      description: 'The input name.', +      type: { +        name: 'string', +        required: true, +      }, +    }, +    setValue: { +      control: { +        type: null, +      }, +      description: 'A callback function to update the toggle value.', +      type: { +        name: 'function', +        required: true, +      }, +    }, +    value: { +      control: { +        type: null, +      }, +      description: 'The toggle value. True if checked.', +      type: { +        name: 'boolean', +        required: true, +      }, +    }, +  }, +} as ComponentMeta<typeof ToggleComponent>; + +const Template: ComponentStory<typeof ToggleComponent> = ({ +  value: _value, +  setValue: _setValue, +  ...args +}) => { +  const [isChecked, setIsChecked] = useState<boolean>(false); +  return ( +    <ToggleComponent value={isChecked} setValue={setIsChecked} {...args} /> +  ); +}; + +export const Toggle = Template.bind({}); +Toggle.args = { +  choices: { +    left: 'On', +    right: 'Off', +  }, +  id: 'toggle-example', +  label: 'Activate setting:', +  name: 'toggle-example', +}; diff --git a/src/components/atoms/forms/toggle.test.tsx b/src/components/atoms/forms/toggle.test.tsx new file mode 100644 index 0000000..fb97adc --- /dev/null +++ b/src/components/atoms/forms/toggle.test.tsx @@ -0,0 +1,29 @@ +import { render, screen } from '@test-utils'; +import Toggle from './toggle'; + +const choices = { +  left: 'On', +  right: 'Off', +}; + +const label = 'Activate this setting:'; + +describe('Toggle', () => { +  it('renders a checked toggle', () => { +    render( +      <Toggle +        id="toggle-example" +        name="toggle-example" +        choices={choices} +        label={label} +        value={true} +        setValue={(__value) => null} +      /> +    ); +    expect( +      screen.getByRole('checkbox', { +        name: `${label} ${choices.left} ${choices.right}`, +      }) +    ).toBeChecked(); +  }); +}); diff --git a/src/components/atoms/forms/toggle.tsx b/src/components/atoms/forms/toggle.tsx new file mode 100644 index 0000000..e8e8c0f --- /dev/null +++ b/src/components/atoms/forms/toggle.tsx @@ -0,0 +1,69 @@ +import { FC, ReactElement } from 'react'; +import styles from './toggle.module.scss'; + +export type ToggleChoices = { +  left: ReactElement | string; +  right: ReactElement | string; +}; + +export type ToggleProps = { +  /** +   * The toggle choices. +   */ +  choices: ToggleChoices; +  /** +   * The input id. +   */ +  id: string; +  /** +   * The toggle label. +   */ +  label: string; +  /** +   * The input name. +   */ +  name: string; +  /** +   * The toggle value. True if checked. +   */ +  value: boolean; +  /** +   * A callback function to update the toggle value. +   */ +  setValue: (value: boolean) => void; +}; + +/** + * Toggle component + * + * Render a toggle with a label and two choices. + */ +const Toggle: FC<ToggleProps> = ({ +  choices, +  id, +  label, +  name, +  value, +  setValue, +}) => { +  return ( +    <> +      <input +        type="checkbox" +        name={name} +        id={id} +        checked={value} +        onChange={() => setValue(!value)} +        className={styles.checkbox} +      /> +      <label htmlFor={id} className={styles.label}> +        <span className={styles.title}>{label}</span> +        {choices.left} +        <span className={styles.toggle}></span> +        {choices.right} +      </label> +    </> +  ); +}; + +export default Toggle; | 
