diff options
Diffstat (limited to 'src/components/molecules/forms')
59 files changed, 846 insertions, 2969 deletions
diff --git a/src/components/molecules/forms/ackee-toggle.fixture.tsx b/src/components/molecules/forms/ackee-toggle.fixture.tsx deleted file mode 100644 index 04602f2..0000000 --- a/src/components/molecules/forms/ackee-toggle.fixture.tsx +++ /dev/null @@ -1 +0,0 @@ -export const storageKey = 'ackee'; diff --git a/src/components/molecules/forms/ackee-toggle.module.scss b/src/components/molecules/forms/ackee-toggle.module.scss deleted file mode 100644 index f238bda..0000000 --- a/src/components/molecules/forms/ackee-toggle.module.scss +++ /dev/null @@ -1,6 +0,0 @@ -.wrapper { - display: flex; - flex-flow: row wrap; - align-items: center; - position: relative; -} diff --git a/src/components/molecules/forms/ackee-toggle.stories.tsx b/src/components/molecules/forms/ackee-toggle.stories.tsx deleted file mode 100644 index 779f49d..0000000 --- a/src/components/molecules/forms/ackee-toggle.stories.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { AckeeToggle } from './ackee-toggle'; -import { storageKey } from './ackee-toggle.fixture'; - -/** - * AckeeToggle - Storybook Meta - */ -export default { - title: 'Molecules/Forms/Toggle', - component: AckeeToggle, - argTypes: { - bodyClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the fieldset body wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - buttonClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the help button.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the toggle wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - defaultValue: { - control: { - type: 'select', - }, - description: 'Set the default value.', - options: ['full', 'partial'], - type: { - name: 'string', - required: true, - }, - }, - groupClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the radio group wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - legendClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the legend.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - storageKey: { - control: { - type: 'text', - }, - description: 'Set local storage key.', - type: { - name: 'string', - required: true, - }, - }, - tooltipClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the tooltip wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - }, -} as ComponentMeta<typeof AckeeToggle>; - -const Template: ComponentStory<typeof AckeeToggle> = (args) => ( - <AckeeToggle {...args} /> -); - -/** - * Toggle Stories - Ackee - */ -export const Ackee = Template.bind({}); -Ackee.args = { - defaultValue: 'full', - storageKey, -}; diff --git a/src/components/molecules/forms/ackee-toggle.test.tsx b/src/components/molecules/forms/ackee-toggle.test.tsx deleted file mode 100644 index 97ebbe5..0000000 --- a/src/components/molecules/forms/ackee-toggle.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { render, screen } from '../../../../tests/utils'; -import { AckeeToggle } from './ackee-toggle'; -import { storageKey } from './ackee-toggle.fixture'; - -describe('AckeeToggle', () => { - // toHaveValue received undefined. Maybe because of localStorage hook... - it('renders a toggle component', () => { - render(<AckeeToggle storageKey={storageKey} defaultValue="full" />); - expect( - screen.getByRole('radiogroup', { - name: /Tracking:/i, - }) - ).toBeInTheDocument(); - }); -}); diff --git a/src/components/molecules/forms/ackee-toggle.tsx b/src/components/molecules/forms/ackee-toggle.tsx deleted file mode 100644 index 32949b2..0000000 --- a/src/components/molecules/forms/ackee-toggle.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { FC } from 'react'; -import { useIntl } from 'react-intl'; -import { - type AckeeOptions, - useLocalStorage, - useUpdateAckeeOptions, -} from '../../../utils/hooks'; -import { - RadioGroup, - type RadioGroupCallback, - type RadioGroupCallbackProps, - type RadioGroupOption, - type RadioGroupProps, -} from './radio-group'; -import { Tooltip, type TooltipProps } from '../modals/tooltip'; - -export type AckeeToggleProps = Pick< - RadioGroupProps, - | 'bodyClassName' - | 'buttonClassName' - | 'groupClassName' - | 'legendClassName' - | 'legendPosition' -> & { - /** - * Set additional classnames to the toggle wrapper. - */ - className?: string; - /** - * True if motion should be reduced by default. - */ - defaultValue: AckeeOptions; - /** - * The local storage key to save preference. - */ - storageKey: string; - /** - * Set additional classnames to the tooltip wrapper. - */ - tooltipClassName?: TooltipProps['className']; -}; - -/** - * AckeeToggle component - * - * Render a Toggle component to set reduce motion. - */ -export const AckeeToggle: FC<AckeeToggleProps> = ({ - defaultValue, - storageKey, - tooltipClassName, - ...props -}) => { - const intl = useIntl(); - const { value, setValue } = useLocalStorage<AckeeOptions>( - storageKey, - defaultValue - ); - useUpdateAckeeOptions(value); - - const ackeeLabel = intl.formatMessage({ - defaultMessage: 'Tracking:', - description: 'AckeeToggle: select label', - id: '0gVlI3', - }); - const tooltipTitle = intl.formatMessage({ - defaultMessage: 'Ackee tracking (analytics)', - description: 'AckeeToggle: tooltip title', - id: 'nGss/j', - }); - const tooltipContent = [ - intl.formatMessage({ - defaultMessage: 'Partial includes only page url, views and duration.', - description: 'AckeeToggle: tooltip message', - id: 'ZB/Aw2', - }), - intl.formatMessage({ - defaultMessage: - 'Full includes all information from partial as well as information about referrer, operating system, device, browser, screen size and language.', - description: 'AckeeToggle: tooltip message', - id: '7zDlQo', - }), - ]; - const partialLabel = intl.formatMessage({ - defaultMessage: 'Partial', - description: 'AckeeToggle: partial option name', - id: 'tIZYpD', - }); - const fullLabel = intl.formatMessage({ - defaultMessage: 'Full', - description: 'AckeeToggle: full option name', - id: '5eD6y2', - }); - - const options: RadioGroupOption[] = [ - { - id: 'ackee-full', - label: fullLabel, - name: 'ackee', - value: 'full', - }, - { - id: 'ackee-partial', - label: partialLabel, - name: 'ackee', - value: 'partial', - }, - ]; - - /** - * Handle change events. - * - * @param {RadioGroupCallbackProps} props - An object with choices. - */ - const handleChange: RadioGroupCallback = ({ - choices, - updateChoice, - }: RadioGroupCallbackProps) => { - let newChoice: AckeeOptions = choices.new as AckeeOptions; - - if (choices.new === choices.prev) { - newChoice = choices.new === 'full' ? 'partial' : 'full'; - updateChoice(newChoice); - } - - setValue(newChoice); - }; - - return ( - <RadioGroup - {...props} - kind="toggle" - initialChoice={value} - legend={ackeeLabel} - onChange={handleChange} - options={options} - Tooltip={ - <Tooltip - title={tooltipTitle} - content={tooltipContent} - icon="?" - className={tooltipClassName} - /> - } - /> - ); -}; diff --git a/src/components/molecules/forms/fieldset.fixture.tsx b/src/components/molecules/forms/fieldset.fixture.tsx deleted file mode 100644 index b94f340..0000000 --- a/src/components/molecules/forms/fieldset.fixture.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { TooltipProps } from '../modals/tooltip'; -import { Help } from '../modals/tooltip.stories'; - -export const body = 'doloribus magni aut'; -export const legend = 'maiores autem est'; -export const Tooltip = <Help {...(Help.args as TooltipProps)} />; diff --git a/src/components/molecules/forms/fieldset.module.scss b/src/components/molecules/forms/fieldset.module.scss deleted file mode 100644 index 38327b4..0000000 --- a/src/components/molecules/forms/fieldset.module.scss +++ /dev/null @@ -1,61 +0,0 @@ -.legend { - float: left; - color: var(--color-primary-darker); - font-size: var(--font-size-md); - font-weight: 600; - - &#{&}--has-tooltip { - margin: 0 var(--spacing-2xs) 0 0; - } -} - -.btn { - margin: 0 var(--spacing-xs) var(--spacing-2xs) 0; - - &--activated { - background: var(--color-primary); - - * { - color: var(--color-fg-inverted); - } - } -} - -.tooltip { - top: calc(100% + var(--spacing-xs)); - transform-origin: top; - transition: all 0.75s ease-in-out 0s; - - &--hidden { - flex: 0 0 0; - opacity: 0; - visibility: hidden; - transform: scale(0); - } - - &--visible { - opacity: 1; - visibility: visible; - transform: scale(1); - } -} - -.wrapper { - display: flex; - flex-flow: row wrap; - align-items: center; - max-width: 100%; - padding: 0; - position: relative; - border: none; - - &--stacked { - .body { - flex: 1 0 100%; - } - } - - .tooltip { - position: absolute; - } -} diff --git a/src/components/molecules/forms/fieldset.stories.tsx b/src/components/molecules/forms/fieldset.stories.tsx deleted file mode 100644 index d53a21a..0000000 --- a/src/components/molecules/forms/fieldset.stories.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Fieldset as FieldsetComponent } from './fieldset'; -import { body, legend, Tooltip } from './fieldset.fixture'; - -/** - * Fieldset - Storybook Meta - */ -export default { - title: 'Molecules/Forms/Fieldset', - component: FieldsetComponent, - args: { - legendPosition: 'stacked', - role: 'group', - }, - argTypes: { - bodyClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the body wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - buttonClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the help button.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - children: { - control: { - type: null, - }, - description: 'The fieldset body.', - type: { - name: 'string', - required: true, - }, - }, - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the fieldset.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - legend: { - control: { - type: 'text', - }, - description: 'The fieldset legend.', - type: { - name: 'string', - required: true, - }, - }, - legendClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the legend.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - legendPosition: { - control: { - type: 'select', - }, - description: 'Determine the legend position.', - options: ['inline', 'stacked'], - table: { - category: 'Options', - defaultValue: { summary: 'inline' }, - }, - type: { - name: 'string', - required: false, - }, - }, - role: { - control: { - type: 'select', - }, - description: 'An accessible role.', - table: { - category: 'Accessibility', - defaultValue: { summary: 'group' }, - }, - options: ['group', 'radiogroup', 'presentation', 'none'], - type: { - name: 'string', - required: false, - }, - }, - Tooltip: { - control: { - type: null, - }, - description: 'Add an optional tooltip.', - table: { - category: 'Options', - }, - type: { - name: 'function', - required: false, - }, - }, - }, -} as ComponentMeta<typeof FieldsetComponent>; - -const Template: ComponentStory<typeof FieldsetComponent> = (args) => ( - <FieldsetComponent {...args} /> -); - -/** - * Fieldset Stories - Stacked legend - */ -export const StackedLegend = Template.bind({}); -StackedLegend.args = { - children: body, - legend: legend, -}; - -/** - * Fieldset Stories - Inlined legend - */ -export const InlinedLegend = Template.bind({}); -InlinedLegend.args = { - children: body, - legend: legend, - legendPosition: 'inline', -}; - -/** - * Fieldset Stories - Stacked legend with tooltip - */ -export const StackedLegendWithTooltip = Template.bind({}); -StackedLegendWithTooltip.args = { - children: body, - legend: legend, - Tooltip, -}; - -/** - * Fieldset Stories - Inlined legend with tooltip - */ -export const InlinedLegendWithTooltip = Template.bind({}); -InlinedLegendWithTooltip.args = { - children: body, - legend: legend, - legendPosition: 'inline', - Tooltip, -}; diff --git a/src/components/molecules/forms/fieldset.test.tsx b/src/components/molecules/forms/fieldset.test.tsx deleted file mode 100644 index f397bcd..0000000 --- a/src/components/molecules/forms/fieldset.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { render, screen } from '../../../../tests/utils'; -import { Fieldset } from './fieldset'; -import { body, legend, Tooltip } from './fieldset.fixture'; - -describe('Fieldset', () => { - // Cannot use toBeInTheDocument because of body is not an HTMLElement. - - it('renders a legend and a body', () => { - render(<Fieldset legend={legend}>{body}</Fieldset>); - expect(screen.findByRole('group', { name: legend })).toBeTruthy(); - expect(screen.findByText(body)).toBeTruthy(); - }); - - it('renders a button to open a tooltip', () => { - render( - <Fieldset legend={legend} Tooltip={Tooltip}> - {body} - </Fieldset> - ); - expect(screen.findByRole('button', { name: /Help/i })).toBeTruthy(); - }); -}); diff --git a/src/components/molecules/forms/fieldset.tsx b/src/components/molecules/forms/fieldset.tsx deleted file mode 100644 index 7564d14..0000000 --- a/src/components/molecules/forms/fieldset.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { - cloneElement, - FC, - ReactComponentElement, - ReactNode, - useRef, - useState, -} from 'react'; -import { useOnClickOutside } from '../../../utils/hooks'; -import { HelpButton } from '../buttons'; -import { Tooltip } from '../modals'; -import styles from './fieldset.module.scss'; - -export type FieldsetProps = { - /** - * Set additional classnames to the body wrapper. - */ - bodyClassName?: string; - /** - * Set additional classnames to the help button. - */ - buttonClassName?: string; - /** - * The fieldset body. - */ - children: ReactNode | ReactNode[]; - /** - * Set additional classnames to the fieldset wrapper. - */ - className?: string; - /** - * The fieldset legend. - */ - legend: string; - /** - * Set additional classnames to the legend. - */ - legendClassName?: string; - /** - * The legend position. - * - * @default 'stacked' - */ - legendPosition?: 'inline' | 'stacked'; - /** - * An accessible role. - * - * @default 'group' - */ - role?: 'group' | 'radiogroup' | 'presentation' | 'none'; - /** - * An optional tooltip component. - */ - Tooltip?: ReactComponentElement<typeof Tooltip>; -}; - -/** - * Fieldset component - * - * Render a fieldset with a legend. - */ -export const Fieldset: FC<FieldsetProps> = ({ - bodyClassName = '', - buttonClassName = '', - children, - className = '', - legend, - legendClassName = '', - legendPosition = 'stacked', - Tooltip: TooltipComponent, - ...props -}) => { - const [isTooltipOpened, setIsTooltipOpened] = useState<boolean>(false); - const buttonRef = useRef<HTMLButtonElement>(null); - const wrapperModifier = `wrapper--${legendPosition}`; - const buttonModifier = isTooltipOpened ? styles['btn--activated'] : ''; - const legendModifier = - TooltipComponent === undefined ? '' : 'legend--has-tooltip'; - const tooltipModifier = isTooltipOpened - ? 'tooltip--visible' - : 'tooltip--hidden'; - - /** - * Close the tooltip if the target is not the button. - * - * @param {Node} target - The event target. - */ - const closeTooltip = (target: Node) => { - if (buttonRef.current && !buttonRef.current.contains(target)) { - setIsTooltipOpened(false); - } - }; - - const tooltipRef = useOnClickOutside<HTMLDivElement>(closeTooltip); - const fieldsetClass = `${styles.wrapper} ${styles[wrapperModifier]} ${className}`; - const legendClass = `${styles.legend} ${styles[legendModifier]} ${legendClassName}`; - - return ( - <fieldset {...props} className={fieldsetClass}> - <legend className={legendClass}>{legend}</legend> - {TooltipComponent && ( - <> - <HelpButton - aria-pressed={isTooltipOpened} - className={`${styles.btn} ${buttonModifier} ${buttonClassName}`} - onClick={() => setIsTooltipOpened(!isTooltipOpened)} - ref={buttonRef} - /> - {cloneElement(TooltipComponent, { - cloneClassName: `${styles.tooltip} ${styles[tooltipModifier]}`, - ref: tooltipRef, - })} - </> - )} - <div className={`${styles.body} ${bodyClassName}`}>{children}</div> - </fieldset> - ); -}; diff --git a/src/components/molecules/forms/flipping-label.module.scss b/src/components/molecules/forms/flipping-label/flipping-label.module.scss index b0452fe..88ef3ec 100644 --- a/src/components/molecules/forms/flipping-label.module.scss +++ b/src/components/molecules/forms/flipping-label/flipping-label.module.scss @@ -1,4 +1,4 @@ -@use "../../../styles/abstracts/functions" as fun; +@use "../../../../styles/abstracts/functions" as fun; .label { display: block; diff --git a/src/components/molecules/forms/flipping-label.stories.tsx b/src/components/molecules/forms/flipping-label/flipping-label.stories.tsx index 7dad3cb..3ad3529 100644 --- a/src/components/molecules/forms/flipping-label.stories.tsx +++ b/src/components/molecules/forms/flipping-label/flipping-label.stories.tsx @@ -1,10 +1,10 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; import { useState } from 'react'; -import { MagnifyingGlass } from '../../atoms'; +import { MagnifyingGlass } from '../../../atoms'; import { FlippingLabel } from './flipping-label'; export default { - title: 'Organisms/Forms/FlippingLabel', + title: 'Molecules/Forms/FlippingLabel', component: FlippingLabel, argTypes: { 'aria-label': { diff --git a/src/components/molecules/forms/flipping-label.test.tsx b/src/components/molecules/forms/flipping-label/flipping-label.test.tsx index 0f5dd30..7813855 100644 --- a/src/components/molecules/forms/flipping-label.test.tsx +++ b/src/components/molecules/forms/flipping-label/flipping-label.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '../../../../tests/utils'; +import { render, screen } from '../../../../../tests/utils'; import { FlippingLabel } from './flipping-label'; describe('FlippingLabel', () => { diff --git a/src/components/molecules/forms/flipping-label.tsx b/src/components/molecules/forms/flipping-label/flipping-label.tsx index c85642b..3e23915 100644 --- a/src/components/molecules/forms/flipping-label.tsx +++ b/src/components/molecules/forms/flipping-label/flipping-label.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { Close, Label, type LabelProps } from '../../atoms'; +import { Close, Label, type LabelProps } from '../../../atoms'; import styles from './flipping-label.module.scss'; export type FlippingLabelProps = Pick< diff --git a/src/components/molecules/forms/flipping-label/index.ts b/src/components/molecules/forms/flipping-label/index.ts new file mode 100644 index 0000000..7b50c75 --- /dev/null +++ b/src/components/molecules/forms/flipping-label/index.ts @@ -0,0 +1 @@ +export * from './flipping-label'; diff --git a/src/components/molecules/forms/index.ts b/src/components/molecules/forms/index.ts index 4d0497d..883a033 100644 --- a/src/components/molecules/forms/index.ts +++ b/src/components/molecules/forms/index.ts @@ -1,10 +1,4 @@ -export * from './ackee-toggle'; -export * from './fieldset'; export * from './flipping-label'; -export * from './labelled-boolean-field'; export * from './labelled-field'; -export * from './labelled-select'; -export * from './motion-toggle'; -export * from './prism-theme-toggle'; export * from './radio-group'; -export * from './theme-toggle'; +export * from './switch'; diff --git a/src/components/molecules/forms/labelled-boolean-field.fixture.tsx b/src/components/molecules/forms/labelled-boolean-field.fixture.tsx deleted file mode 100644 index 6b06887..0000000 --- a/src/components/molecules/forms/labelled-boolean-field.fixture.tsx +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 10a9eb2..0000000 --- a/src/components/molecules/forms/labelled-boolean-field.module.scss +++ /dev/null @@ -1,15 +0,0 @@ -.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 deleted file mode 100644 index b1f8194..0000000 --- a/src/components/molecules/forms/labelled-boolean-field.stories.tsx +++ /dev/null @@ -1,254 +0,0 @@ -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: { - checked: false, - hidden: false, - label, - labelSize: 'small', - }, - argTypes: { - checked: { - control: { - type: null, - }, - 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 deleted file mode 100644 index 6916f95..0000000 --- a/src/components/molecules/forms/labelled-boolean-field.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { render, screen } from '../../../../tests/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 deleted file mode 100644 index d110d45..0000000 --- a/src/components/molecules/forms/labelled-boolean-field.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { FC } from 'react'; -import { - BooleanField, - type BooleanFieldProps, - Label, - type LabelProps, -} from '../../atoms'; -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. - */ -export 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}`; - const labelClass = `${styles[labelPositionModifier]} ${styles[labelHiddenModifier]} ${labelClassName}`; - - return labelPosition === 'left' ? ( - <span className={`${styles.wrapper} ${className}`}> - <Label className={labelClass} htmlFor={id} size={labelSize}> - {label} - </Label> - <BooleanField - {...props} - className={fieldClassName} - hidden={hidden} - id={id} - /> - </span> - ) : ( - <span className={`${styles.wrapper} ${className}`}> - <BooleanField - {...props} - className={fieldClassName} - hidden={hidden} - id={id} - /> - <Label className={labelClass} htmlFor={id} size={labelSize}> - {label} - </Label> - </span> - ); -}; diff --git a/src/components/molecules/forms/labelled-field.module.scss b/src/components/molecules/forms/labelled-field.module.scss deleted file mode 100644 index 64ef3d0..0000000 --- a/src/components/molecules/forms/labelled-field.module.scss +++ /dev/null @@ -1,9 +0,0 @@ -.label { - &--left { - margin-right: var(--spacing-2xs); - } - - &--top { - display: block; - } -} diff --git a/src/components/molecules/forms/labelled-field.stories.tsx b/src/components/molecules/forms/labelled-field.stories.tsx deleted file mode 100644 index 840421b..0000000 --- a/src/components/molecules/forms/labelled-field.stories.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { useState } from 'react'; -import { LabelledField } from './labelled-field'; - -/** - * LabelledField - Storybook Meta - */ -export default { - title: 'Molecules/Forms/Field', - component: LabelledField, - args: { - disabled: false, - hideLabel: false, - labelPosition: 'top', - required: 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, - }, - }, - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the field.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - disabled: { - control: { - type: 'boolean', - }, - description: 'Field state: either enabled or disabled.', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - id: { - control: { - type: 'text', - }, - description: 'Field id.', - type: { - name: 'string', - required: true, - }, - }, - hideLabel: { - control: { - type: 'boolean', - }, - description: 'Visually hide the field label.', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - label: { - control: { - type: 'text', - }, - description: 'Field label.', - type: { - name: 'string', - required: true, - }, - }, - labelPosition: { - control: { - type: 'select', - }, - description: 'The label position.', - options: ['left', 'top'], - table: { - category: 'Options', - defaultValue: { summary: 'top' }, - }, - type: { - name: 'string', - required: false, - }, - }, - max: { - control: { - type: 'number', - }, - description: 'Maximum value.', - table: { - category: 'Options', - }, - type: { - name: 'number', - required: false, - }, - }, - min: { - control: { - type: 'number', - }, - description: 'Minimum value.', - table: { - category: 'Options', - }, - type: { - name: 'number', - required: false, - }, - }, - name: { - control: { - type: 'text', - }, - description: 'Field name.', - type: { - name: 'string', - required: true, - }, - }, - placeholder: { - control: { - type: 'text', - }, - description: 'A placeholder value.', - table: { - category: 'Options', - }, - type: { - name: 'string', - required: false, - }, - }, - required: { - control: { - type: 'boolean', - }, - description: 'Determine if the field is required.', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - setValue: { - control: { - type: null, - }, - description: 'Callback function to set field value.', - table: { - category: 'Events', - }, - type: { - name: 'function', - required: true, - }, - }, - step: { - control: { - type: 'number', - }, - description: 'Field incremental values that are valid.', - table: { - category: 'Options', - }, - type: { - name: 'number', - required: false, - }, - }, - type: { - control: { - type: 'select', - }, - description: 'Field type: input type or textarea.', - options: [ - 'datetime-local', - 'email', - 'number', - 'search', - 'tel', - 'text', - 'textarea', - 'time', - 'url', - ], - type: { - name: 'string', - required: true, - }, - }, - value: { - control: { - type: null, - }, - description: 'Field value.', - type: { - name: 'string', - required: true, - }, - }, - }, -} as ComponentMeta<typeof LabelledField>; - -const Template: ComponentStory<typeof LabelledField> = ({ - value: _value, - setValue: _setValue, - ...args -}) => { - const [value, setValue] = useState<string>(''); - - return <LabelledField value={value} setValue={setValue} {...args} />; -}; - -/** - * Labelled Field Stories - Left - */ -export const Left = Template.bind({}); -Left.args = { - id: 'labelled-field-storybook', - label: 'Labelled field', - labelPosition: 'left', - name: 'labelled-field-storybook', -}; - -/** - * Labelled Field Stories - Top - */ -export const Top = Template.bind({}); -Top.args = { - id: 'labelled-field-storybook', - label: 'Labelled field', - labelPosition: 'top', - name: 'labelled-field-storybook', -}; - -/** - * Labelled Field Stories - Required - */ -export const Required = Template.bind({}); -Required.args = { - id: 'labelled-field-storybook', - label: 'Labelled field', - name: 'labelled-field-storybook', - required: true, -}; - -/** - * Labelled Field Stories - Hidden label - */ -export const HiddenLabel = Template.bind({}); -HiddenLabel.args = { - hideLabel: true, - id: 'labelled-field-storybook', - label: 'Labelled field', - name: 'labelled-field-storybook', -}; - -/** - * Labelled Field Stories - Disabled - */ -export const Disabled = Template.bind({}); -Disabled.args = { - disabled: true, - id: 'labelled-field-storybook', - label: 'Labelled field', - name: 'labelled-field-storybook', -}; diff --git a/src/components/molecules/forms/labelled-field.test.tsx b/src/components/molecules/forms/labelled-field.test.tsx deleted file mode 100644 index e16c129..0000000 --- a/src/components/molecules/forms/labelled-field.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { render, screen } from '../../../../tests/utils'; -import { LabelledField } from './labelled-field'; - -describe('LabelledField', () => { - it('renders a labelled field', () => { - render( - <LabelledField - type="text" - id="jest-text-field" - name="jest-text-field" - label="Jest text field" - value="test" - setValue={() => null} - /> - ); - expect(screen.getByLabelText('Jest text field')).toBeInTheDocument(); - expect(screen.getByRole('textbox')).toHaveValue('test'); - }); -}); diff --git a/src/components/molecules/forms/labelled-field.tsx b/src/components/molecules/forms/labelled-field.tsx deleted file mode 100644 index fca1c54..0000000 --- a/src/components/molecules/forms/labelled-field.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { forwardRef, ForwardRefRenderFunction } from 'react'; -import { Field, type FieldProps, Label } from '../../atoms'; -import styles from './labelled-field.module.scss'; - -export type LabelledFieldProps = FieldProps & { - /** - * Visually hide the field label. Default: false. - */ - hideLabel?: boolean; - /** - * The field label. - */ - label: string; - /** - * The label position. Default: top. - */ - labelPosition?: 'left' | 'top'; -}; - -const LabelledFieldWithRef: ForwardRefRenderFunction< - HTMLInputElement, - LabelledFieldProps -> = ( - { hideLabel = false, id, label, labelPosition = 'top', required, ...props }, - ref -) => { - const positionModifier = `label--${labelPosition}`; - const visibilityClass = hideLabel ? 'screen-reader-text' : ''; - - return ( - <> - <Label - htmlFor={id} - required={required} - className={`${visibilityClass} ${styles[positionModifier]}`} - > - {label} - </Label> - <Field id={id} ref={ref} required={required} {...props} /> - </> - ); -}; - -/** - * LabelledField component - * - * Render a field tied to a label. - */ -export const LabelledField = forwardRef(LabelledFieldWithRef); diff --git a/src/components/molecules/forms/labelled-field/index.ts b/src/components/molecules/forms/labelled-field/index.ts new file mode 100644 index 0000000..b0d9889 --- /dev/null +++ b/src/components/molecules/forms/labelled-field/index.ts @@ -0,0 +1 @@ +export * from './labelled-field'; diff --git a/src/components/molecules/forms/labelled-field/labelled-field.module.scss b/src/components/molecules/forms/labelled-field/labelled-field.module.scss new file mode 100644 index 0000000..bb37dc7 --- /dev/null +++ b/src/components/molecules/forms/labelled-field/labelled-field.module.scss @@ -0,0 +1,22 @@ +.wrapper { + display: flex; + gap: var(--spacing-2xs); + width: fit-content; + + &--inline { + flex-flow: row wrap; + align-items: center; + } + + &--inline#{&}--reverse { + flex-flow: row-reverse wrap; + } + + &--stack { + flex-flow: column wrap; + } + + &--stack#{&}--reverse { + flex-flow: column-reverse wrap; + } +} diff --git a/src/components/molecules/forms/labelled-field/labelled-field.stories.tsx b/src/components/molecules/forms/labelled-field/labelled-field.stories.tsx new file mode 100644 index 0000000..1f29830 --- /dev/null +++ b/src/components/molecules/forms/labelled-field/labelled-field.stories.tsx @@ -0,0 +1,130 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { ChangeEvent, useState } from 'react'; +import { Input, Label } from '../../../atoms'; +import { LabelledField } from './labelled-field'; + +/** + * LabelledField - Storybook Meta + */ +export default { + title: 'Molecules/Forms/Field', + component: LabelledField, + argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the field.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + field: { + control: { + type: null, + }, + description: 'A component: Checkbox, Input, Select, Radio or TextArea.', + type: { + name: 'function', + required: true, + }, + }, + label: { + control: { + type: null, + }, + description: 'A Label component.', + type: { + name: 'function', + required: true, + }, + }, + isInline: { + control: { + type: 'boolean', + }, + description: 'Should the label and the field be inlined?', + table: { + category: 'Options', + defaultValue: { summary: false }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + isReversedOrder: { + control: { + type: 'boolean', + }, + description: 'Should the label and the field be reversed?', + table: { + category: 'Options', + defaultValue: { summary: false }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + }, +} as ComponentMeta<typeof LabelledField>; + +const Template: ComponentStory<typeof LabelledField> = ({ ...args }) => { + const id = 'sunt'; + const [value, setValue] = useState<string>(''); + const updateValue = (e: ChangeEvent<HTMLInputElement>) => { + setValue(e.target.value); + }; + + return ( + <LabelledField + {...args} + field={ + <Input + id={id} + name={id} + onChange={updateValue} + type="text" + value={value} + /> + } + label={<Label htmlFor={id}>A label</Label>} + /> + ); +}; + +/** + * Labelled Field Stories - Left + */ +export const Left = Template.bind({}); +Left.args = { + isInline: true, +}; + +/** + * Labelled Field Stories - Right + */ +export const Right = Template.bind({}); +Right.args = { + isInline: true, + isReversedOrder: true, +}; + +/** + * Labelled Field Stories - Top + */ +export const Top = Template.bind({}); +Top.args = {}; + +/** + * Labelled Field Stories - Bottom + */ +export const Bottom = Template.bind({}); +Bottom.args = { + isReversedOrder: true, +}; diff --git a/src/components/molecules/forms/labelled-field/labelled-field.test.tsx b/src/components/molecules/forms/labelled-field/labelled-field.test.tsx new file mode 100644 index 0000000..9e39e1f --- /dev/null +++ b/src/components/molecules/forms/labelled-field/labelled-field.test.tsx @@ -0,0 +1,32 @@ +import { render, screen } from '../../../../../tests/utils'; +import { Input, Label } from '../../../atoms'; +import { LabelledField } from './labelled-field'; + +const doNothing = () => { + // Do nothing +}; + +describe('LabelledField', () => { + it('renders a labelled field', () => { + const id = 'enim'; + const label = 'eum aliquam culpa'; + const value = 'vitae'; + + render( + <LabelledField + field={ + <Input + id={id} + name="text-field" + onChange={doNothing} + type="text" + value={value} + /> + } + label={<Label htmlFor={id}>{label}</Label>} + /> + ); + expect(screen.getByLabelText(label)).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toHaveValue(value); + }); +}); diff --git a/src/components/molecules/forms/labelled-field/labelled-field.tsx b/src/components/molecules/forms/labelled-field/labelled-field.tsx new file mode 100644 index 0000000..af492b3 --- /dev/null +++ b/src/components/molecules/forms/labelled-field/labelled-field.tsx @@ -0,0 +1,63 @@ +import { FC, HTMLAttributes, ReactElement } from 'react'; +import { + CheckboxProps, + InputProps, + LabelProps, + RadioProps, + SelectProps, + TextAreaProps, +} from '../../../atoms'; +import styles from './labelled-field.module.scss'; + +export type LabelledFieldProps = Omit< + HTMLAttributes<HTMLDivElement>, + 'children' +> & { + /** + * The field. + */ + field: ReactElement< + CheckboxProps | InputProps | RadioProps | SelectProps | TextAreaProps + >; + /** + * Should the label and the field be inlined? + * + * @default false + */ + isInline?: boolean; + /** + * If true, the label is displayed after the field. + * + * @default false + */ + isReversedOrder?: boolean; + /** + * The field label. + */ + label: ReactElement<LabelProps>; +}; + +/** + * LabelledField component + * + * Render a field tied to a label. + */ +export const LabelledField: FC<LabelledFieldProps> = ({ + className = '', + field, + isInline = false, + isReversedOrder = false, + label, + ...props +}) => { + const layoutClass = isInline ? 'wrapper--inline' : 'wrapper--stack'; + const orderClass = isReversedOrder ? 'wrapper--reverse' : ''; + const wrapperClass = `${styles.wrapper} ${styles[layoutClass]} ${styles[orderClass]} ${className}`; + + return ( + <div {...props} className={wrapperClass}> + {label} + {field} + </div> + ); +}; diff --git a/src/components/molecules/forms/labelled-select.module.scss b/src/components/molecules/forms/labelled-select.module.scss deleted file mode 100644 index 64ef3d0..0000000 --- a/src/components/molecules/forms/labelled-select.module.scss +++ /dev/null @@ -1,9 +0,0 @@ -.label { - &--left { - margin-right: var(--spacing-2xs); - } - - &--top { - display: block; - } -} diff --git a/src/components/molecules/forms/labelled-select.stories.tsx b/src/components/molecules/forms/labelled-select.stories.tsx deleted file mode 100644 index d6029a5..0000000 --- a/src/components/molecules/forms/labelled-select.stories.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { useState } from 'react'; -import { LabelledSelect } from './labelled-select'; - -const selectOptions = [ - { id: 'option1', name: 'Option 1', value: 'option1' }, - { id: 'option2', name: 'Option 2', value: 'option2' }, - { id: 'option3', name: 'Option 3', value: 'option3' }, -]; - -/** - * LabelledSelect - Storybook Meta - */ -export default { - title: 'Molecules/Forms/Select', - component: LabelledSelect, - args: { - disabled: false, - labelPosition: 'top', - required: false, - }, - argTypes: { - disabled: { - control: { - type: 'boolean', - }, - description: 'Field state: either enabled or disabled.', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - id: { - control: { - type: 'text', - }, - description: 'Field id.', - type: { - name: 'string', - required: true, - }, - }, - label: { - control: { - type: 'text', - }, - description: 'Field 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: 'The label position.', - options: ['left', 'top'], - table: { - category: 'Options', - defaultValue: { summary: 'top' }, - }, - 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: 'Field name.', - type: { - name: 'string', - required: true, - }, - }, - options: { - control: { - type: null, - }, - description: 'Select options.', - type: { - name: 'array', - required: true, - value: { - name: 'string', - }, - }, - }, - required: { - control: { - type: 'boolean', - }, - description: 'Determine if the field is required.', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - selectClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the select field.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - setValue: { - control: { - type: null, - }, - description: 'Callback function to set field value.', - table: { - category: 'Events', - }, - type: { - name: 'function', - required: true, - }, - }, - value: { - control: { - type: null, - }, - description: 'Field value.', - type: { - name: 'string', - required: true, - }, - }, - }, -} as ComponentMeta<typeof LabelledSelect>; - -const Template: ComponentStory<typeof LabelledSelect> = ({ - value, - setValue: _setValue, - ...args -}) => { - const [selected, setSelected] = useState<string>(value); - - return <LabelledSelect value={selected} setValue={setSelected} {...args} />; -}; - -/** - * Labelled Select Stories - Left - */ -export const Left = Template.bind({}); -Left.args = { - id: 'labelled-select-storybook', - label: 'Labelled select', - labelPosition: 'left', - name: 'labelled-select-storybook', - options: selectOptions, - value: 'option1', -}; - -/** - * Labelled Select Stories - Top - */ -export const Top = Template.bind({}); -Top.args = { - id: 'labelled-select-storybook', - label: 'Labelled select', - labelPosition: 'top', - name: 'labelled-select-storybook', - options: selectOptions, - value: 'option1', -}; - -/** - * Labelled Select Stories - Disabled - */ -export const Disabled = Template.bind({}); -Disabled.args = { - disabled: true, - id: 'labelled-select-storybook', - label: 'Labelled select', - name: 'labelled-select-storybook', - options: selectOptions, - value: 'option1', -}; - -/** - * Labelled Select Stories - Required - */ -export const Required = Template.bind({}); -Required.args = { - id: 'labelled-select-storybook', - label: 'Labelled select', - labelPosition: 'top', - name: 'labelled-select-storybook', - options: selectOptions, - required: true, - value: 'option1', -}; diff --git a/src/components/molecules/forms/labelled-select.test.tsx b/src/components/molecules/forms/labelled-select.test.tsx deleted file mode 100644 index 1ef4a25..0000000 --- a/src/components/molecules/forms/labelled-select.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { render, screen } from '../../../../tests/utils'; -import { LabelledSelect } from './labelled-select'; - -const selectOptions = [ - { id: 'option1', name: 'Option 1', value: 'option1' }, - { id: 'option2', name: 'Option 2', value: 'option2' }, - { id: 'option3', name: 'Option 3', value: 'option3' }, -]; - -describe('LabelledSelect', () => { - it('renders a labelled select', () => { - render( - <LabelledSelect - id="jest-select-field" - name="jest-select-field" - label="Jest select field" - options={selectOptions} - value="option1" - setValue={() => null} - /> - ); - expect(screen.getByLabelText('Jest select field')).toBeInTheDocument(); - expect(screen.getByRole('combobox')).toHaveValue('option1'); - }); -}); diff --git a/src/components/molecules/forms/labelled-select.tsx b/src/components/molecules/forms/labelled-select.tsx deleted file mode 100644 index 300ae8a..0000000 --- a/src/components/molecules/forms/labelled-select.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { FC } from 'react'; -import { Label, type LabelProps, Select, type SelectProps } from '../../atoms'; -import styles from './labelled-select.module.scss'; - -export type LabelledSelectProps = Omit< - SelectProps, - 'aria-labelledby' | 'className' -> & { - /** - * The field label. - */ - label: string; - /** - * Set additional classnames to the label. - */ - labelClassName?: LabelProps['className']; - /** - * The label position. Default: top. - */ - labelPosition?: 'left' | 'top'; - /** - * The label size. - */ - labelSize?: LabelProps['size']; - /** - * Set additional classnames to the select field. - */ - selectClassName?: SelectProps['className']; -}; - -/** - * LabelledSelect component - * - * Render a select with a label. - */ -export const LabelledSelect: FC<LabelledSelectProps> = ({ - id, - label, - labelClassName = '', - labelPosition = 'top', - labelSize, - required, - selectClassName = '', - ...props -}) => { - const positionModifier = `label--${labelPosition}`; - - return ( - <> - <Label - className={`${styles[positionModifier]} ${labelClassName}`} - htmlFor={id} - required={required} - size={labelSize} - > - {label} - </Label> - <Select - {...props} - className={selectClassName} - id={id} - required={required} - /> - </> - ); -}; diff --git a/src/components/molecules/forms/motion-toggle.fixture.tsx b/src/components/molecules/forms/motion-toggle.fixture.tsx deleted file mode 100644 index f13658a..0000000 --- a/src/components/molecules/forms/motion-toggle.fixture.tsx +++ /dev/null @@ -1 +0,0 @@ -export const storageKey = 'reduced-motion'; diff --git a/src/components/molecules/forms/motion-toggle.stories.tsx b/src/components/molecules/forms/motion-toggle.stories.tsx deleted file mode 100644 index bb51e26..0000000 --- a/src/components/molecules/forms/motion-toggle.stories.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { MotionToggle } from './motion-toggle'; -import { storageKey } from './motion-toggle.fixture'; - -/** - * MotionToggle - Storybook Meta - */ -export default { - title: 'Molecules/Forms/Toggle', - component: MotionToggle, - argTypes: { - bodyClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the fieldset body wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - defaultValue: { - control: { - type: 'select', - }, - description: 'Set the default value.', - options: ['on', 'off'], - type: { - name: 'string', - required: true, - }, - }, - groupClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the radio group wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - legendClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the legend.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - storageKey: { - control: { - type: 'text', - }, - description: 'Set local storage key.', - type: { - name: 'string', - required: true, - }, - }, - }, -} as ComponentMeta<typeof MotionToggle>; - -const Template: ComponentStory<typeof MotionToggle> = (args) => ( - <MotionToggle {...args} /> -); - -/** - * Toggle Stories - Motion - */ -export const Motion = Template.bind({}); -Motion.args = { - defaultValue: 'on', - storageKey, -}; diff --git a/src/components/molecules/forms/motion-toggle.test.tsx b/src/components/molecules/forms/motion-toggle.test.tsx deleted file mode 100644 index 93df3a0..0000000 --- a/src/components/molecules/forms/motion-toggle.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { render, screen } from '../../../../tests/utils'; -import { MotionToggle } from './motion-toggle'; -import { storageKey } from './motion-toggle.fixture'; - -describe('MotionToggle', () => { - // toHaveValue received undefined. Maybe because of localStorage hook... - it('renders a toggle component', () => { - render(<MotionToggle storageKey={storageKey} defaultValue="on" />); - expect( - screen.getByRole('radiogroup', { - name: /Animations:/i, - }) - ).toBeInTheDocument(); - }); -}); diff --git a/src/components/molecules/forms/motion-toggle.tsx b/src/components/molecules/forms/motion-toggle.tsx deleted file mode 100644 index c1a55f0..0000000 --- a/src/components/molecules/forms/motion-toggle.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { FC } from 'react'; -import { useIntl } from 'react-intl'; -import { useAttributes, useLocalStorage } from '../../../utils/hooks'; -import { - RadioGroup, - type RadioGroupCallback, - type RadioGroupCallbackProps, - type RadioGroupOption, - type RadioGroupProps, -} from './radio-group'; - -export type MotionToggleValue = 'on' | 'off'; - -export type MotionToggleProps = Pick< - RadioGroupProps, - 'bodyClassName' | 'groupClassName' | 'legendClassName' | 'legendPosition' -> & { - /** - * True if motion should be reduced by default. - */ - defaultValue: 'on' | 'off'; - /** - * The local storage key to save preference. - */ - storageKey: string; -}; - -/** - * MotionToggle component - * - * Render a Toggle component to set reduce motion. - */ -export const MotionToggle: FC<MotionToggleProps> = ({ - defaultValue, - storageKey, - ...props -}) => { - const intl = useIntl(); - const { value: isReduced, setValue: setIsReduced } = useLocalStorage<boolean>( - storageKey, - defaultValue === 'on' ? false : true - ); - useAttributes({ - element: - typeof window !== 'undefined' ? document.documentElement : undefined, - attribute: 'reduced-motion', - value: `${isReduced}`, - }); - - const reduceMotionLabel = intl.formatMessage({ - defaultMessage: 'Animations:', - description: 'MotionToggle: reduce motion label', - id: '/q5csZ', - }); - const onLabel = intl.formatMessage({ - defaultMessage: 'On', - description: 'MotionToggle: activate reduce motion label', - id: 'va65iw', - }); - const offLabel = intl.formatMessage({ - defaultMessage: 'Off', - description: 'MotionToggle: deactivate reduce motion label', - id: 'pWKyyR', - }); - - const options: RadioGroupOption[] = [ - { - id: 'reduced-motion-on', - label: onLabel, - name: 'reduced-motion', - value: 'on', - }, - { - id: 'reduced-motion-off', - label: offLabel, - name: 'reduced-motion', - value: 'off', - }, - ]; - - /** - * Update the current setting. - * - * @param {string} newValue - A boolean as string. - */ - const updateSetting = (newValue: MotionToggleValue) => { - setIsReduced(newValue === 'on' ? false : true); - }; - - /** - * Handle change events. - * - * @param {RadioGroupCallbackProps} props - An object with choices. - */ - const handleChange: RadioGroupCallback = ({ - choices, - updateChoice, - }: RadioGroupCallbackProps) => { - if (choices.new === choices.prev) { - const newChoice = choices.new === 'on' ? 'off' : 'on'; - updateChoice(newChoice); - updateSetting(newChoice); - } else { - updateSetting(choices.new as MotionToggleValue); - } - }; - - return ( - <RadioGroup - {...props} - initialChoice={defaultValue} - kind="toggle" - legend={reduceMotionLabel} - onChange={handleChange} - options={options} - /> - ); -}; diff --git a/src/components/molecules/forms/prism-theme-toggle.stories.tsx b/src/components/molecules/forms/prism-theme-toggle.stories.tsx deleted file mode 100644 index bf0f2df..0000000 --- a/src/components/molecules/forms/prism-theme-toggle.stories.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { PrismThemeToggle } from './prism-theme-toggle'; - -/** - * PrismThemeToggle - Storybook Meta - */ -export default { - title: 'Molecules/Forms/Toggle', - component: PrismThemeToggle, - argTypes: { - bodyClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the fieldset body wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - groupClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the radio group wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - legendClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the legend.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - }, -} as ComponentMeta<typeof PrismThemeToggle>; - -const Template: ComponentStory<typeof PrismThemeToggle> = (args) => ( - <PrismThemeToggle {...args} /> -); - -/** - * Toggle Stories - Prism theme - */ -export const PrismTheme = Template.bind({}); diff --git a/src/components/molecules/forms/prism-theme-toggle.test.tsx b/src/components/molecules/forms/prism-theme-toggle.test.tsx deleted file mode 100644 index bd63762..0000000 --- a/src/components/molecules/forms/prism-theme-toggle.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { render, screen } from '../../../../tests/utils'; -import { PrismThemeToggle } from './prism-theme-toggle'; - -describe('PrismThemeToggle', () => { - it('renders a toggle component', () => { - render(<PrismThemeToggle />); - expect( - screen.getByRole('radiogroup', { - name: /Code blocks:/i, - }) - ).toBeInTheDocument(); - }); -}); diff --git a/src/components/molecules/forms/prism-theme-toggle.tsx b/src/components/molecules/forms/prism-theme-toggle.tsx deleted file mode 100644 index 5427fec..0000000 --- a/src/components/molecules/forms/prism-theme-toggle.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { FC } from 'react'; -import { useIntl } from 'react-intl'; -import { - type PrismTheme, - usePrismTheme, -} from '../../../utils/providers/prism-theme'; -import { Moon, Sun } from '../../atoms'; -import { - RadioGroup, - type RadioGroupCallback, - type RadioGroupCallbackProps, - type RadioGroupOption, - type RadioGroupProps, -} from './radio-group'; - -export type PrismThemeToggleProps = Pick< - RadioGroupProps, - 'bodyClassName' | 'groupClassName' | 'legendClassName' | 'legendPosition' ->; - -/** - * PrismThemeToggle component - * - * Render a Toggle component to set code blocks theme. - */ -export const PrismThemeToggle: FC<PrismThemeToggleProps> = (props) => { - const intl = useIntl(); - const { theme, setTheme, resolvedTheme } = usePrismTheme(); - - /** - * Check if the resolved or chosen theme is dark theme. - * - * @returns {boolean} True if it is dark theme. - */ - const isDarkTheme = (prismTheme?: PrismTheme): boolean => { - if (prismTheme === 'system') return resolvedTheme === 'dark'; - return prismTheme === 'dark'; - }; - - /** - * Update the theme. - * - * @param {string} newTheme - A theme name. - */ - const updateTheme = (newTheme: string) => { - setTheme(newTheme === 'light' ? 'light' : 'dark'); - }; - - /** - * Handle change events. - * - * @param {RadioGroupCallbackProps} props - An object with choices. - */ - const handleChange: RadioGroupCallback = ({ - choices, - updateChoice, - }: RadioGroupCallbackProps) => { - if (choices.new === choices.prev) { - const newTheme = choices.new === 'light' ? 'dark' : 'light'; - updateChoice(newTheme); - updateTheme(newTheme); - } else { - updateTheme(choices.new); - } - }; - - const themeLabel = intl.formatMessage({ - defaultMessage: 'Code blocks:', - description: 'PrismThemeToggle: theme label', - id: 'ftXN+0', - }); - const lightThemeLabel = intl.formatMessage({ - defaultMessage: 'Light theme', - description: 'PrismThemeToggle: light theme label', - id: 'tsWh8x', - }); - const darkThemeLabel = intl.formatMessage({ - defaultMessage: 'Dark theme', - description: 'PrismThemeToggle: dark theme label', - id: 'og/zWL', - }); - - const options: RadioGroupOption[] = [ - { - id: 'code-blocks-light', - label: ( - <> - <span className="screen-reader-text">{lightThemeLabel}</span> - <Sun /> - </> - ), - name: 'code-blocks', - value: 'light', - }, - { - id: 'code-blocks-dark', - label: ( - <> - <span className="screen-reader-text">{darkThemeLabel}</span> - <Moon /> - </> - ), - name: 'code-blocks', - value: 'dark', - }, - ]; - - return ( - <RadioGroup - {...props} - initialChoice={isDarkTheme(theme) ? 'dark' : 'light'} - kind="toggle" - legend={themeLabel} - onChange={handleChange} - options={options} - /> - ); -}; diff --git a/src/components/molecules/forms/radio-group.module.scss b/src/components/molecules/forms/radio-group.module.scss deleted file mode 100644 index cadff46..0000000 --- a/src/components/molecules/forms/radio-group.module.scss +++ /dev/null @@ -1,112 +0,0 @@ -@use "../../../styles/abstracts/functions" as fun; -@use "../../../styles/abstracts/mixins" as mix; - -.wrapper { - &--inline { - .legend { - margin-right: var(--spacing-2xs); - } - } - - &--regular { - .option { - &:not(:last-of-type) { - margin-right: var(--spacing-xs); - } - } - } -} - -.toggle { - display: inline-flex; - flex-flow: row wrap; - align-items: center; - width: fit-content; - position: relative; - 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); - - .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; - } - } - - &:focus-within { - outline: fun.convert-px(2) solid var(--color-primary-light); - } - - .option: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); - } - } - - .option: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); - } - } - - &: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/radio-group.stories.tsx b/src/components/molecules/forms/radio-group.stories.tsx deleted file mode 100644 index b727e28..0000000 --- a/src/components/molecules/forms/radio-group.stories.tsx +++ /dev/null @@ -1,285 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { RadioGroup } from './radio-group'; -import { getOptions, initialChoice, legend } from './radio-group.fixture'; - -/** - * RadioGroup - Storybook Meta - */ -export default { - title: 'Molecules/Forms/RadioGroup', - component: RadioGroup, - args: { - kind: 'regular', - labelSize: 'small', - }, - argTypes: { - bodyClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the fieldset body wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - buttonClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the help button.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the fieldset.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - groupClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the radio group wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - initialChoice: { - control: { - type: 'text', - }, - description: 'The default selected option id.', - type: { - name: 'string', - required: true, - }, - }, - kind: { - control: { - type: 'select', - }, - description: 'The radio group kind.', - options: ['regular', 'toggle'], - table: { - category: 'Options', - defaultValue: { summary: 'regular' }, - }, - 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, - }, - }, - legend: { - control: { - type: 'text', - }, - description: 'The fieldset legend.', - type: { - name: 'string', - required: true, - }, - }, - legendClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the legend.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - legendPosition: { - control: { - type: 'select', - }, - description: 'Determine the legend position.', - options: ['inline', 'stacked'], - table: { - category: 'Options', - defaultValue: { summary: 'inline' }, - }, - type: { - name: 'string', - required: false, - }, - }, - onChange: { - control: { - type: null, - }, - description: 'A callback function to handle selected option change.', - table: { - category: 'Events', - }, - type: { - name: 'function', - required: false, - }, - }, - onClick: { - control: { - type: null, - }, - description: 'A callback function to handle click on a choice.', - table: { - category: 'Events', - }, - type: { - name: 'function', - required: false, - }, - }, - optionClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the option wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - options: { - description: 'An array of radio option object.', - type: { - name: 'object', - required: true, - value: {}, - }, - }, - Tooltip: { - control: { - type: null, - }, - description: 'Add an optional tooltip.', - table: { - category: 'Options', - }, - type: { - name: 'function', - required: false, - }, - }, - }, -} as ComponentMeta<typeof RadioGroup>; - -const Template: ComponentStory<typeof RadioGroup> = (args) => ( - <RadioGroup {...args} /> -); - -/** - * Radio Group Stories - Inlined legend & left label - */ -export const InlinedLegendLeftLabel = Template.bind({}); -InlinedLegendLeftLabel.args = { - initialChoice: initialChoice, - labelPosition: 'left', - legend: legend, - legendPosition: 'inline', - options: getOptions('group1'), -}; - -/** - * Radio Group Stories - Inlined legend & left label - */ -export const InlinedLegendRightLabel = Template.bind({}); -InlinedLegendRightLabel.args = { - initialChoice: initialChoice, - labelPosition: 'right', - legend: legend, - legendPosition: 'inline', - options: getOptions('group2'), -}; - -/** - * Radio Group Stories - Stacked legend & left label - */ -export const StackedLegendLeftLabel = Template.bind({}); -StackedLegendLeftLabel.args = { - initialChoice: initialChoice, - labelPosition: 'left', - legend: legend, - legendPosition: 'stacked', - options: getOptions('group3'), -}; - -/** - * Radio Group Stories - Stacked legend & left label - */ -export const StackedLegendRightLabel = Template.bind({}); -StackedLegendRightLabel.args = { - initialChoice: initialChoice, - labelPosition: 'right', - legend: legend, - legendPosition: 'stacked', - options: getOptions('group4'), -}; - -/** - * Radio Group Stories - Toggle - */ -export const Toggle = Template.bind({}); -Toggle.args = { - initialChoice: initialChoice, - kind: 'toggle', - labelPosition: 'right', - legend: legend, - options: getOptions('group5'), -}; diff --git a/src/components/molecules/forms/radio-group.test.tsx b/src/components/molecules/forms/radio-group.test.tsx deleted file mode 100644 index c4a01e4..0000000 --- a/src/components/molecules/forms/radio-group.test.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { render, screen } from '../../../../tests/utils'; -import { RadioGroup } from './radio-group'; -import { getOptions, initialChoice, legend } from './radio-group.fixture'; - -describe('RadioGroup', () => { - it('renders a legend', () => { - render( - <RadioGroup - initialChoice={initialChoice} - legend={legend} - options={getOptions()} - /> - ); - expect(screen.findByRole('radiogroup', { name: legend })).toBeDefined(); - }); - - it('renders the correct number of radio', () => { - const options = getOptions(); - - render( - <RadioGroup - initialChoice={initialChoice} - legend={legend} - options={options} - /> - ); - const radios = screen.getAllByRole('radio'); - expect(radios).toHaveLength(options.length); - }); -}); diff --git a/src/components/molecules/forms/radio-group.tsx b/src/components/molecules/forms/radio-group.tsx deleted file mode 100644 index a747395..0000000 --- a/src/components/molecules/forms/radio-group.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { ChangeEvent, FC, MouseEvent, SetStateAction } from 'react'; -import { useStateChange } from '../../../utils/hooks'; -import { Fieldset, type FieldsetProps } from './fieldset'; -import { - LabelledBooleanField, - type LabelledBooleanFieldProps, -} from './labelled-boolean-field'; -import styles from './radio-group.module.scss'; - -export type RadioGroupCallbackProps = { - choices: { - new: string; - prev: string; - }; - updateChoice: (value: SetStateAction<string>) => void; -}; - -export type RadioGroupCallback = (props: RadioGroupCallbackProps) => void; - -export type RadioGroupOption = Pick< - LabelledBooleanFieldProps, - 'id' | 'label' | 'name' | 'value' ->; - -export type RadioGroupProps = Pick< - FieldsetProps, - | 'bodyClassName' - | 'buttonClassName' - | 'className' - | 'legend' - | 'legendClassName' - | 'Tooltip' -> & - Pick<LabelledBooleanFieldProps, 'labelPosition' | 'labelSize'> & { - /** - * Set additional classnames to the radio group wrapper when kind is toggle. - */ - groupClassName?: string; - /** - * The default option value. - */ - initialChoice: string; - /** - * The radio group kind. - * - * @default 'regular - */ - kind?: 'regular' | 'toggle'; - /** - * The legend position. - * - * @default 'inline' - */ - legendPosition?: FieldsetProps['legendPosition']; - /** - * A callback function to execute when choice is changed. - */ - onChange?: RadioGroupCallback; - /** - * A callback function to execute when clicking on a choice. - */ - onClick?: RadioGroupCallback; - /** - * Set additional classnames to the labelled field wrapper. - */ - optionClassName?: string; - /** - * The options. - */ - options: RadioGroupOption[]; - }; - -/** - * RadioGroup component - * - * Render a group of labelled radio buttons. - */ -export const RadioGroup: FC<RadioGroupProps> = ({ - className, - groupClassName = '', - initialChoice, - kind = 'regular', - labelPosition, - labelSize, - legendClassName, - legendPosition = 'inline', - onChange, - optionClassName = '', - options, - ...props -}) => { - const [selectedChoice, setSelectedChoice] = - useStateChange<string>(initialChoice); - const isToggle = kind === 'toggle'; - const alignmentModifier = `wrapper--${legendPosition}`; - const toggleModifier = isToggle ? 'wrapper--toggle' : 'wrapper--regular'; - const fieldsetClass = `${styles.wrapper} ${styles[alignmentModifier]} ${styles[toggleModifier]} ${className}`; - - /** - * Update the selected choice on click or change event. - */ - const updateChoice = ( - e: - | ChangeEvent<HTMLInputElement> - | MouseEvent<HTMLInputElement, globalThis.MouseEvent> - ) => { - const input = e.target as HTMLInputElement; - onChange && - onChange({ - choices: { new: input.value, prev: selectedChoice }, - updateChoice: setSelectedChoice, - }); - if (e.type === 'change') setSelectedChoice(input.value); - }; - - /** - * Retrieve an array of radio buttons. - * - * @returns {JSX.Element[]} The radio buttons. - */ - const getOptions = (): JSX.Element[] => { - return options.map((option) => ( - <LabelledBooleanField - {...option} - key={option.id} - checked={selectedChoice === option.value} - className={`${styles.option} ${optionClassName}`} - fieldClassName={styles.radio} - hidden={isToggle} - labelClassName={styles.label} - labelPosition={kind === 'toggle' ? 'right' : labelPosition} - labelSize={labelSize} - onChange={updateChoice} - onClick={updateChoice} - type="radio" - /> - )); - }; - - return ( - <Fieldset - {...props} - className={fieldsetClass} - legendClassName={`${styles.legend} ${legendClassName}`} - legendPosition={legendPosition} - role="radiogroup" - > - {isToggle ? ( - <span className={`${styles.toggle} ${groupClassName}`}> - {getOptions()} - </span> - ) : ( - getOptions() - )} - </Fieldset> - ); -}; diff --git a/src/components/molecules/forms/radio-group/index.ts b/src/components/molecules/forms/radio-group/index.ts new file mode 100644 index 0000000..ed40543 --- /dev/null +++ b/src/components/molecules/forms/radio-group/index.ts @@ -0,0 +1 @@ +export * from './radio-group'; diff --git a/src/components/molecules/forms/radio-group.fixture.tsx b/src/components/molecules/forms/radio-group/radio-group.fixture.tsx index 686467c..f1cbc05 100644 --- a/src/components/molecules/forms/radio-group.fixture.tsx +++ b/src/components/molecules/forms/radio-group/radio-group.fixture.tsx @@ -1,4 +1,4 @@ -import { RadioGroupOption } from './radio-group'; +import { RadioGroupItem } from './radio-group'; export const getOptions = (name: string = 'group1') => { const value1 = 'option1'; @@ -7,34 +7,29 @@ export const getOptions = (name: string = 'group1') => { const value4 = 'option4'; const value5 = 'option5'; - const options: RadioGroupOption[] = [ + const options: RadioGroupItem[] = [ { id: `${name}-${value1}`, - name: name, label: 'Option 1', value: value1, }, { id: `${name}-${value2}`, - name: name, label: 'Option 2', value: value2, }, { id: `${name}-${value3}`, - name: name, label: 'Option 3', value: value3, }, { id: `${name}-${value4}`, - name: name, label: 'Option 4', value: value4, }, { id: `${name}-${value5}`, - name: name, label: 'Option 5', value: value5, }, @@ -44,4 +39,3 @@ export const getOptions = (name: string = 'group1') => { }; export const initialChoice = 'option2'; -export const legend = 'Options:'; diff --git a/src/components/molecules/forms/radio-group/radio-group.module.scss b/src/components/molecules/forms/radio-group/radio-group.module.scss new file mode 100644 index 0000000..ad09c78 --- /dev/null +++ b/src/components/molecules/forms/radio-group/radio-group.module.scss @@ -0,0 +1,9 @@ +.group { + &--inline { + .option { + &:not(:last-of-type) { + margin-right: var(--spacing-xs); + } + } + } +} diff --git a/src/components/molecules/forms/radio-group/radio-group.stories.tsx b/src/components/molecules/forms/radio-group/radio-group.stories.tsx new file mode 100644 index 0000000..8e77c6e --- /dev/null +++ b/src/components/molecules/forms/radio-group/radio-group.stories.tsx @@ -0,0 +1,75 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Legend } from '../../../atoms'; +import { RadioGroup as RadioGroupComponent } from './radio-group'; +import { getOptions, initialChoice } from './radio-group.fixture'; +import { ChangeEventHandler, useCallback, useState } from 'react'; + +/** + * RadioGroup - Storybook Meta + */ +export default { + title: 'Molecules/Forms', + component: RadioGroupComponent, + args: {}, + argTypes: { + onChange: { + control: { + type: null, + }, + description: 'A callback function to handle selected option change.', + table: { + category: 'Events', + }, + type: { + name: 'function', + required: false, + }, + }, + options: { + description: 'An array of radio option object.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + value: { + control: { + type: 'text', + }, + description: 'The default selected option id.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof RadioGroupComponent>; + +const Template: ComponentStory<typeof RadioGroupComponent> = ({ + value, + ...args +}) => { + const [selection, setSelection] = useState(value); + + const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback( + (e) => { + setSelection(e.target.value); + }, + [] + ); + + return ( + <RadioGroupComponent {...args} onSwitch={handleChange} value={selection} /> + ); +}; + +/** + * Radio Group Story + */ +export const RadioGroup = Template.bind({}); +RadioGroup.args = { + legend: <Legend>Options:</Legend>, + options: getOptions('group1'), + value: initialChoice, +}; diff --git a/src/components/molecules/forms/radio-group/radio-group.test.tsx b/src/components/molecules/forms/radio-group/radio-group.test.tsx new file mode 100644 index 0000000..dba1541 --- /dev/null +++ b/src/components/molecules/forms/radio-group/radio-group.test.tsx @@ -0,0 +1,59 @@ +import { render, screen } from '../../../../../tests/utils'; +import { Legend } from '../../../atoms'; +import { RadioGroup } from './radio-group'; +import { getOptions, initialChoice } from './radio-group.fixture'; + +const doNothing = () => { + /* Do nothing. */ +}; + +describe('RadioGroup', () => { + it('renders a legend', () => { + const legend = 'Options:'; + + render( + <RadioGroup + legend={<Legend>{legend}</Legend>} + name="possimus" + onSwitch={doNothing} + options={getOptions()} + value={initialChoice} + /> + ); + + expect( + screen.getByRole('radiogroup', { name: legend }) + ).toBeInTheDocument(); + }); + + it('renders the correct number of radio', () => { + const options = getOptions(); + + render( + <RadioGroup + name="eaque" + onSwitch={doNothing} + options={options} + value={initialChoice} + /> + ); + + expect(screen.getAllByRole('radio')).toHaveLength(options.length); + }); + + it('can render an inlined radio group', () => { + const options = getOptions(); + + render( + <RadioGroup + isInline + name="architecto" + onSwitch={doNothing} + options={options} + value={initialChoice} + /> + ); + + expect(screen.getByRole('radiogroup')).toHaveClass('group--inline'); + }); +}); diff --git a/src/components/molecules/forms/radio-group/radio-group.tsx b/src/components/molecules/forms/radio-group/radio-group.tsx new file mode 100644 index 0000000..0ca4dac --- /dev/null +++ b/src/components/molecules/forms/radio-group/radio-group.tsx @@ -0,0 +1,110 @@ +import { ForwardRefRenderFunction, forwardRef } from 'react'; +import { + Fieldset, + FieldsetProps, + Label, + LabelProps, + Radio, + RadioProps, +} from '../../../atoms'; +import { LabelledField } from '../labelled-field'; +import styles from './radio-group.module.scss'; + +export type RadioGroupItem = { + /** + * The item id. + */ + id: string; + /** + * Should the item be disabled? + */ + isDisabled?: boolean; + /** + * The item label. + */ + label: LabelProps['children']; + /** + * The item value. + */ + value: string; +}; + +export type RadioGroupProps = Omit<FieldsetProps, 'children' | 'role'> & { + /** + * Should we display the radio buttons inlined? + * + * @default false + */ + isInline?: boolean; + /** + * The radio group name. + */ + name: string; + /** + * A function to handle selection change. + */ + onSwitch?: RadioProps['onChange']; + /** + * The options. + */ + options: RadioGroupItem[]; + /** + * The selected value. It should match a RadioGroupItem value or be undefined. + */ + value?: RadioGroupItem['value']; +}; + +const RadioGroupWithRef: ForwardRefRenderFunction< + HTMLFieldSetElement, + RadioGroupProps +> = ( + { + className = '', + isInline = false, + name, + onSwitch, + options, + value, + ...props + }, + ref +) => { + const layoutModifier = isInline ? styles['group--inline'] : ''; + const groupClass = `${layoutModifier} ${className}`; + + return ( + <Fieldset + {...props} + className={groupClass} + isInline={isInline} + ref={ref} + role="radiogroup" + > + {options.map((option) => ( + <LabelledField + className={styles.option} + field={ + <Radio + id={option.id} + isChecked={value === option.value} + name={name} + onChange={onSwitch} + value={option.value} + /> + } + isInline + isReversedOrder + key={option.id} + label={<Label htmlFor={option.id}>{option.label}</Label>} + /> + ))} + </Fieldset> + ); +}; + +/** + * RadioGroup component + * + * Render a group of labelled radio buttons. + */ +export const RadioGroup = forwardRef(RadioGroupWithRef); 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<typeof SwitchComponent>; + +const Template: ComponentStory<typeof SwitchComponent> = ({ + value, + ...args +}) => { + const [selection, setSelection] = useState(value); + + const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback( + (e) => { + setSelection(e.target.value); + }, + [] + ); + + return ( + <SwitchComponent {...args} onSwitch={handleChange} value={selection} /> + ); +}; + +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: <Legend>Choose the best option:</Legend>, + 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( + <Switch + items={items} + legend={<Legend>{legend}</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( + <Switch + isDisabled + items={items} + name="architecto" + onSwitch={doNothing} + value={items[1].value} + /> + ); + + const radios = screen.getAllByRole<HTMLInputElement>('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<LabelProps, 'children' | 'htmlFor' | 'isRequired'> & + Pick<RadioProps, 'isDisabled' | 'name'> & { + /** + * 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<HTMLInputElement>; + /** + * The item value. + */ + value: string; + }; + +/** + * SwitchItem component. + */ +const SwitchItem: FC<SwitchItemProps> = ({ + 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 ( + <Label {...props} className={itemClass} htmlFor={id}> + <Radio + className={styles.radio} + id={id} + isChecked={isSelected} + isDisabled={isDisabled} + isHidden + name={name} + onChange={onSwitch} + value={value} + /> + <span className={styles.label}>{label}</span> + </Label> + ); +}; + +export type SwitchOption = Pick<SwitchItemProps, 'id' | 'label' | 'value'>; + +export type SwitchProps = Omit<FieldsetProps, 'children'> & { + /** + * The switch items. + */ + items: [SwitchOption, SwitchOption]; + /** + * The switch group name. + */ + name: string; + /** + * A function to handle selection change. + */ + onSwitch: ChangeEventHandler<HTMLInputElement>; + /** + * A tooltip to display before switch options. + */ + tooltip?: ReactElement<TooltipProps>; + /** + * The selected item value. + */ + value: SwitchOption['value']; +}; + +/** + * Switch component. + */ +export const Switch: FC<SwitchProps> = ({ + className = '', + isDisabled = false, + items, + name, + onSwitch, + tooltip, + value, + ...props +}) => { + return ( + <Fieldset + {...props} + className={`${styles.fieldset} ${className}`} + isDisabled={isDisabled} + role="radiogroup" + > + {tooltip} + <div className={styles.switch}> + {items.map((item) => ( + <SwitchItem + {...item} + isDisabled={isDisabled} + isSelected={value === item.value} + key={item.id} + name={name} + onSwitch={onSwitch} + /> + ))} + </div> + </Fieldset> + ); +}; diff --git a/src/components/molecules/forms/theme-toggle.stories.tsx b/src/components/molecules/forms/theme-toggle.stories.tsx deleted file mode 100644 index 40f4d8c..0000000 --- a/src/components/molecules/forms/theme-toggle.stories.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { ThemeToggle } from './theme-toggle'; - -/** - * ThemeToggle - Storybook Meta - */ -export default { - title: 'Molecules/Forms/Toggle', - component: ThemeToggle, - argTypes: { - bodyClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the fieldset body wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - groupClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the radio group wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - legendClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the legend.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - }, -} as ComponentMeta<typeof ThemeToggle>; - -const Template: ComponentStory<typeof ThemeToggle> = (args) => ( - <ThemeToggle {...args} /> -); - -/** - * Toggle Stories - Theme - */ -export const Theme = Template.bind({}); diff --git a/src/components/molecules/forms/theme-toggle.test.tsx b/src/components/molecules/forms/theme-toggle.test.tsx deleted file mode 100644 index aae627a..0000000 --- a/src/components/molecules/forms/theme-toggle.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { render, screen } from '../../../../tests/utils'; -import { ThemeToggle } from './theme-toggle'; - -describe('ThemeToggle', () => { - it('renders a toggle component', () => { - render(<ThemeToggle />); - expect( - screen.getByRole('radiogroup', { - name: /Theme:/i, - }) - ).toBeInTheDocument(); - }); -}); diff --git a/src/components/molecules/forms/theme-toggle.tsx b/src/components/molecules/forms/theme-toggle.tsx deleted file mode 100644 index 25e2a39..0000000 --- a/src/components/molecules/forms/theme-toggle.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { useTheme } from 'next-themes'; -import { FC } from 'react'; -import { useIntl } from 'react-intl'; -import { Moon, Sun } from '../../atoms'; -import { - RadioGroup, - type RadioGroupCallback, - type RadioGroupCallbackProps, - type RadioGroupOption, - type RadioGroupProps, -} from './radio-group'; - -export type ThemeToggleProps = Pick< - RadioGroupProps, - 'bodyClassName' | 'groupClassName' | 'legendClassName' | 'legendPosition' ->; - -/** - * ThemeToggle component - * - * Render a Toggle component to set theme. - */ -export const ThemeToggle: FC<ThemeToggleProps> = (props) => { - const intl = useIntl(); - const { resolvedTheme, setTheme } = useTheme(); - const isDarkTheme = resolvedTheme === 'dark'; - - /** - * Update the theme. - * - * @param {string} theme - A theme name. - */ - const updateTheme = (theme: string) => { - setTheme(theme === 'light' ? 'light' : 'dark'); - }; - - /** - * Handle change events. - * - * @param {RadioGroupCallbackProps} props - An object with choices. - */ - const handleChange: RadioGroupCallback = ({ - choices, - updateChoice, - }: RadioGroupCallbackProps) => { - if (choices.new === choices.prev) { - const newTheme = choices.new === 'light' ? 'dark' : 'light'; - updateChoice(newTheme); - updateTheme(newTheme); - } else { - updateTheme(choices.new); - } - }; - - const themeLabel = intl.formatMessage({ - defaultMessage: 'Theme:', - description: 'ThemeToggle: theme label', - id: 'suXOBu', - }); - const lightThemeLabel = intl.formatMessage({ - defaultMessage: 'Light theme', - description: 'ThemeToggle: light theme label', - id: 'Ygea7s', - }); - const darkThemeLabel = intl.formatMessage({ - defaultMessage: 'Dark theme', - description: 'ThemeToggle: dark theme label', - id: '2QwvtS', - }); - - const options: RadioGroupOption[] = [ - { - id: 'theme-light', - label: ( - <> - <span className="screen-reader-text">{lightThemeLabel}</span> - <Sun /> - </> - ), - name: 'theme', - value: 'light', - }, - { - id: 'theme-dark', - label: ( - <> - <span className="screen-reader-text">{darkThemeLabel}</span> - <Moon /> - </> - ), - name: 'theme', - value: 'dark', - }, - ]; - - return ( - <RadioGroup - {...props} - initialChoice={isDarkTheme ? 'dark' : 'light'} - kind="toggle" - legend={themeLabel} - onChange={handleChange} - options={options} - /> - ); -}; |
