From a6ff5eee45215effb3344cb5d631a27a7c0369aa Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Fri, 22 Sep 2023 19:34:01 +0200 Subject: refactor(components): rewrite form components --- src/components/molecules/forms/switch/index.ts | 1 + .../molecules/forms/switch/switch.module.scss | 105 ++++++++++++++++ .../molecules/forms/switch/switch.stories.tsx | 48 ++++++++ .../molecules/forms/switch/switch.test.tsx | 49 ++++++++ src/components/molecules/forms/switch/switch.tsx | 132 +++++++++++++++++++++ 5 files changed, 335 insertions(+) create mode 100644 src/components/molecules/forms/switch/index.ts create mode 100644 src/components/molecules/forms/switch/switch.module.scss create mode 100644 src/components/molecules/forms/switch/switch.stories.tsx create mode 100644 src/components/molecules/forms/switch/switch.test.tsx create mode 100644 src/components/molecules/forms/switch/switch.tsx (limited to 'src/components/molecules/forms/switch') diff --git a/src/components/molecules/forms/switch/index.ts b/src/components/molecules/forms/switch/index.ts new file mode 100644 index 0000000..4dd2256 --- /dev/null +++ b/src/components/molecules/forms/switch/index.ts @@ -0,0 +1 @@ +export * from './switch'; diff --git a/src/components/molecules/forms/switch/switch.module.scss b/src/components/molecules/forms/switch/switch.module.scss new file mode 100644 index 0000000..44244e7 --- /dev/null +++ b/src/components/molecules/forms/switch/switch.module.scss @@ -0,0 +1,105 @@ +@use "../../../../styles/abstracts/functions" as fun; +@use "../../../../styles/abstracts/mixins" as mix; + +.fieldset { + position: relative; +} + +.switch { + display: inline-flex; + flex-flow: row wrap; + align-items: center; + width: fit-content; + background: var(--color-shadow-light); + border: fun.convert-px(2) solid var(--color-primary); + border-radius: fun.convert-px(32); + box-shadow: + inset 0 0 fun.convert-px(1) fun.convert-px(1) var(--color-shadow-dark), + inset 0 0 fun.convert-px(3) fun.convert-px(2) var(--color-shadow); + + &:focus-within { + outline: fun.convert-px(2) solid var(--color-primary-light); + } +} + +.label { + display: flex; + align-items: center; + min-height: 5ex; + padding: fun.convert-px(6) var(--spacing-2xs); + border-top: fun.convert-px(2) solid var(--color-border); + border-bottom: fun.convert-px(2) solid var(--color-border); + transition: all 0.15s linear 0s; + + @include mix.pointer("fine") { + min-height: 3ex; + } +} + +.item:first-of-type { + .label { + border-left: fun.convert-px(2) solid var(--color-border); + border-top-left-radius: fun.convert-px(32); + border-bottom-left-radius: fun.convert-px(32); + } +} + +.item:last-of-type { + .label { + border-right: fun.convert-px(2) solid var(--color-border); + border-top-right-radius: fun.convert-px(32); + border-bottom-right-radius: fun.convert-px(32); + } +} + +.radio { + &:checked + .label { + background: var(--color-primary); + box-shadow: + inset 0 0 fun.convert-px(1) fun.convert-px(2) var(--color-primary-dark), + inset 0 0 fun.convert-px(3) fun.convert-px(2) var(--color-primary-darker); + color: var(--color-fg-inverted); + + svg { + fill: var(--color-fg-inverted); + stroke: var(--color-fg-inverted); + } + } + + &:not(:checked) + .label { + svg { + fill: var(--color-primary-darker); + } + } + + &[disabled] + .label { + opacity: 0.8; + } +} + +.radio:not([disabled]) { + &:checked + .label:hover { + background: var(--color-primary-lighter); + box-shadow: + inset 0 0 fun.convert-px(1) fun.convert-px(2) var(--color-primary-light), + inset 0 0 fun.convert-px(3) fun.convert-px(2) var(--color-primary); + } + + &:not(:checked) + .label:hover { + background: var(--color-shadow-light); + box-shadow: + inset 0 0 0 fun.convert-px(1) var(--color-shadow-dark), + inset 0 0 fun.convert-px(3) fun.convert-px(2) var(--color-shadow); + } + + &:not(:checked):focus + .label { + background: var(--color-shadow-light); + } + + &:checked:focus + .label { + background: var(--color-primary-lighter); + box-shadow: + inset 0 0 fun.convert-px(1) fun.convert-px(2) var(--color-primary-light), + inset 0 0 fun.convert-px(3) fun.convert-px(2) var(--color-primary); + } +} diff --git a/src/components/molecules/forms/switch/switch.stories.tsx b/src/components/molecules/forms/switch/switch.stories.tsx new file mode 100644 index 0000000..eb169ad --- /dev/null +++ b/src/components/molecules/forms/switch/switch.stories.tsx @@ -0,0 +1,48 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Switch as SwitchComponent, SwitchOption } from './switch'; +import { ChangeEventHandler, useCallback, useState } from 'react'; +import { Legend } from '../../../atoms'; + +/** + * Switch - Storybook Meta + */ +export default { + title: 'Molecules/Forms', + component: SwitchComponent, + args: {}, + argTypes: {}, +} as ComponentMeta; + +const Template: ComponentStory = ({ + value, + ...args +}) => { + const [selection, setSelection] = useState(value); + + const handleChange: ChangeEventHandler = useCallback( + (e) => { + setSelection(e.target.value); + }, + [] + ); + + return ( + + ); +}; + +const items: [SwitchOption, SwitchOption] = [ + { id: 'option-1', label: 'Choice 1', value: 'option-1' }, + { id: 'option-2', label: 'Choice 2', value: 'option-2' }, +]; + +/** + * Radio Group Story + */ +export const Switch = Template.bind({}); +Switch.args = { + items, + legend: Choose the best option:, + name: 'example', + value: items[0].value, +}; diff --git a/src/components/molecules/forms/switch/switch.test.tsx b/src/components/molecules/forms/switch/switch.test.tsx new file mode 100644 index 0000000..6ccd525 --- /dev/null +++ b/src/components/molecules/forms/switch/switch.test.tsx @@ -0,0 +1,49 @@ +import { render, screen } from '../../../../../tests/utils'; +import { Legend } from '../../../atoms'; +import { Switch, SwitchOption } from './switch'; + +const doNothing = () => { + /* Do nothing. */ +}; + +const items: [SwitchOption, SwitchOption] = [ + { id: 'item-1', label: 'Option 1', value: 'option-1' }, + { id: 'item-2', label: 'Option 2', value: 'option-2' }, +]; + +describe('Switch', () => { + it('renders a radio group with two choices', () => { + const legend = 'Options:'; + + render( + {legend}} + name="possimus" + onSwitch={doNothing} + value={items[0].value} + /> + ); + + expect( + screen.getByRole('radiogroup', { name: legend }) + ).toBeInTheDocument(); + expect(screen.getAllByRole('radio')).toHaveLength(items.length); + }); + + it('can render a disabled switch', () => { + render( + + ); + + const radios = screen.getAllByRole('radio'); + expect(radios.every((radio) => radio.disabled)).toBe(true); + expect(screen.getByRole('radiogroup')).toBeDisabled(); + }); +}); diff --git a/src/components/molecules/forms/switch/switch.tsx b/src/components/molecules/forms/switch/switch.tsx new file mode 100644 index 0000000..d340a0c --- /dev/null +++ b/src/components/molecules/forms/switch/switch.tsx @@ -0,0 +1,132 @@ +import type { FC, ChangeEventHandler, ReactNode, ReactElement } from 'react'; +import { + Fieldset, + type FieldsetProps, + LabelProps, + RadioProps, + Label, + Radio, +} from '../../../atoms'; +import styles from './switch.module.scss'; +import { TooltipProps } from '../../tooltip'; + +type SwitchItemProps = Omit & + Pick & { + /** + * The item id. + */ + id: string; + /** + * Is the item selected? + */ + isSelected?: boolean; + /** + * The label used to describe the switch item. + */ + label: ReactNode; + /** + * The event handler on value change. + */ + onSwitch: ChangeEventHandler; + /** + * The item value. + */ + value: string; + }; + +/** + * SwitchItem component. + */ +const SwitchItem: FC = ({ + className = '', + id, + isDisabled = false, + isSelected = false, + label, + name, + onSwitch, + value, + ...props +}) => { + const selectedItemClass = isSelected ? styles['item--selected'] : ''; + const disabledItemClass = isDisabled ? styles['item--disabled'] : ''; + const itemClass = `${styles.item} ${selectedItemClass} ${disabledItemClass} ${className}`; + + return ( + + ); +}; + +export type SwitchOption = Pick; + +export type SwitchProps = Omit & { + /** + * The switch items. + */ + items: [SwitchOption, SwitchOption]; + /** + * The switch group name. + */ + name: string; + /** + * A function to handle selection change. + */ + onSwitch: ChangeEventHandler; + /** + * A tooltip to display before switch options. + */ + tooltip?: ReactElement; + /** + * The selected item value. + */ + value: SwitchOption['value']; +}; + +/** + * Switch component. + */ +export const Switch: FC = ({ + className = '', + isDisabled = false, + items, + name, + onSwitch, + tooltip, + value, + ...props +}) => { + return ( +
+ {tooltip} +
+ {items.map((item) => ( + + ))} +
+
+ ); +}; -- cgit v1.2.3