diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-04-06 23:27:23 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-04-06 23:42:05 +0200 |
| commit | 9bdc49ddf0492177f34bdfe92c9aa8b7999f8cf8 (patch) | |
| tree | 3f79976804d7fcf870ce5528ad58af3804683903 /src/components/atoms | |
| parent | 51889773b12c576dc199fc84d0188f822ac7baae (diff) | |
chore: add a Toggle component
Diffstat (limited to 'src/components/atoms')
| -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; |
