diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-06-01 19:34:43 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-06-01 22:32:16 +0200 |
| commit | 6be20422494e3806fba3d1c5ad5c3e98bd6e67e5 (patch) | |
| tree | 7c679e54ba4bbadaf0a59bbde780f5742e3b875d /src/components/molecules/forms | |
| parent | 8320b1d39ea6402c32e907dbb35082efc6af9f5a (diff) | |
chore: replace the Ackee select by a toggle component
Diffstat (limited to 'src/components/molecules/forms')
24 files changed, 748 insertions, 575 deletions
diff --git a/src/components/molecules/forms/ackee-select.stories.tsx b/src/components/molecules/forms/ackee-select.stories.tsx deleted file mode 100644 index f8d04f6..0000000 --- a/src/components/molecules/forms/ackee-select.stories.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import AckeeSelect from './ackee-select'; -import { storageKey } from './ackee-select.fixture'; - -/** - * AckeeSelect - Storybook Meta - */ -export default { - title: 'Molecules/Forms/Select', - component: AckeeSelect, - argTypes: { - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the select wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - initialValue: { - control: { - type: 'select', - }, - description: 'Initial selected option.', - options: ['full', 'partial'], - type: { - name: 'string', - required: true, - }, - }, - labelClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the label wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - storageKey: { - control: { - type: 'text', - }, - description: 'Set Ackee settings 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 AckeeSelect>; - -const Template: ComponentStory<typeof AckeeSelect> = (args) => ( - <AckeeSelect {...args} /> -); - -/** - * Select Stories - Ackee select - */ -export const Ackee = Template.bind({}); -Ackee.args = { - initialValue: 'full', - storageKey, -}; diff --git a/src/components/molecules/forms/ackee-select.test.tsx b/src/components/molecules/forms/ackee-select.test.tsx deleted file mode 100644 index d255b00..0000000 --- a/src/components/molecules/forms/ackee-select.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import user from '@testing-library/user-event'; -import { act, render, screen } from '@test-utils'; -import AckeeSelect from './ackee-select'; -import { storageKey } from './ackee-select.fixture'; - -describe('Select', () => { - it('should correctly set default option', () => { - render(<AckeeSelect storageKey={storageKey} initialValue="full" />); - expect(screen.getByRole('combobox')).toHaveValue('full'); - expect(screen.queryByRole('combobox')).not.toHaveValue('partial'); - }); - - it('should correctly change value when user choose another option', async () => { - render(<AckeeSelect storageKey={storageKey} initialValue="full" />); - - await act(async () => { - await user.selectOptions( - screen.getByRole('combobox'), - screen.getByRole('option', { name: 'Partial' }) - ); - }); - - expect(screen.getByRole('combobox')).toHaveValue('partial'); - expect(screen.queryByRole('combobox')).not.toHaveValue('full'); - }); -}); diff --git a/src/components/molecules/forms/ackee-select.tsx b/src/components/molecules/forms/ackee-select.tsx deleted file mode 100644 index f00ca74..0000000 --- a/src/components/molecules/forms/ackee-select.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { type SelectOptions } from '@components/atoms/forms/select'; -import useLocalStorage from '@utils/hooks/use-local-storage'; -import useUpdateAckeeOptions, { - type AckeeOptions, -} from '@utils/hooks/use-update-ackee-options'; -import { Dispatch, FC, SetStateAction } from 'react'; -import { useIntl } from 'react-intl'; -import SelectWithTooltip, { - type SelectWithTooltipProps, -} from './select-with-tooltip'; - -export type AckeeSelectProps = Pick< - SelectWithTooltipProps, - 'className' | 'labelClassName' | 'tooltipClassName' -> & { - /** - * A default value for Ackee settings. - */ - initialValue: AckeeOptions; - /** - * The local storage key to save preference. - */ - storageKey: string; -}; - -/** - * AckeeSelect component - * - * Render a select to set Ackee settings. - */ -const AckeeSelect: FC<AckeeSelectProps> = ({ - initialValue, - storageKey, - ...props -}) => { - const intl = useIntl(); - const { value, setValue } = useLocalStorage<AckeeOptions>( - storageKey, - initialValue - ); - useUpdateAckeeOptions(value); - - const ackeeLabel = intl.formatMessage({ - defaultMessage: 'Tracking:', - description: 'AckeeSelect: select label', - id: '2pmylc', - }); - const tooltipTitle = intl.formatMessage({ - defaultMessage: 'Ackee tracking (analytics)', - description: 'AckeeSelect: tooltip title', - id: 'F1EQX3', - }); - const tooltipContent = [ - intl.formatMessage({ - defaultMessage: 'Partial includes only page url, views and duration.', - description: 'AckeeSelect: tooltip message', - id: 'skb4W5', - }), - intl.formatMessage({ - defaultMessage: - 'Full includes all information from partial as well as information about referrer, operating system, device, browser, screen size and language.', - description: 'AckeeSelect: tooltip message', - id: 'Ogccx6', - }), - ]; - const options: SelectOptions[] = [ - { - id: 'partial', - name: intl.formatMessage({ - defaultMessage: 'Partial', - description: 'AckeeSelect: partial option name', - id: 'e/8Kyj', - }), - value: 'partial', - }, - { - id: 'full', - name: intl.formatMessage({ - defaultMessage: 'Full', - description: 'AckeeSelect: full option name', - id: 'PzRpPw', - }), - value: 'full', - }, - ]; - - return ( - <SelectWithTooltip - id="ackee-settings" - name="ackee-settings" - label={ackeeLabel} - labelSize="medium" - options={options} - title={tooltipTitle} - content={tooltipContent} - value={value} - setValue={setValue as Dispatch<SetStateAction<string>>} - {...props} - /> - ); -}; - -export default AckeeSelect; diff --git a/src/components/molecules/forms/ackee-select.fixture.tsx b/src/components/molecules/forms/ackee-toggle.fixture.tsx index 04602f2..04602f2 100644 --- a/src/components/molecules/forms/ackee-select.fixture.tsx +++ b/src/components/molecules/forms/ackee-toggle.fixture.tsx diff --git a/src/components/molecules/forms/ackee-select.module.scss b/src/components/molecules/forms/ackee-toggle.module.scss index 87cd9ee..f238bda 100644 --- a/src/components/molecules/forms/ackee-select.module.scss +++ b/src/components/molecules/forms/ackee-toggle.module.scss @@ -4,8 +4,3 @@ align-items: center; position: relative; } - -.tooltip { - position: absolute; - bottom: -100%; -} diff --git a/src/components/molecules/forms/ackee-toggle.stories.tsx b/src/components/molecules/forms/ackee-toggle.stories.tsx new file mode 100644 index 0000000..bbc6fb4 --- /dev/null +++ b/src/components/molecules/forms/ackee-toggle.stories.tsx @@ -0,0 +1,112 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import AckeeToggleComponent from './ackee-toggle'; +import { storageKey } from './ackee-toggle.fixture'; + +/** + * AckeeToggle - Storybook Meta + */ +export default { + title: 'Molecules/Forms/Toggle', + component: AckeeToggleComponent, + argTypes: { + bodyClassName: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the fieldset body wrapper.', + 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 AckeeToggleComponent>; + +const Template: ComponentStory<typeof AckeeToggleComponent> = (args) => ( + <AckeeToggleComponent {...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 new file mode 100644 index 0000000..8a57ce7 --- /dev/null +++ b/src/components/molecules/forms/ackee-toggle.test.tsx @@ -0,0 +1,15 @@ +import { render, screen } from '@test-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 new file mode 100644 index 0000000..a666731 --- /dev/null +++ b/src/components/molecules/forms/ackee-toggle.tsx @@ -0,0 +1,143 @@ +import useLocalStorage from '@utils/hooks/use-local-storage'; +import useUpdateAckeeOptions, { + type AckeeOptions, +} from '@utils/hooks/use-update-ackee-options'; +import { FC } from 'react'; +import { useIntl } from 'react-intl'; +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' | 'groupClassName' | 'legendClassName' +> & { + /** + * 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. + */ +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 + initialChoice={value} + kind="toggle" + legend={ackeeLabel} + onChange={handleChange} + options={options} + Tooltip={ + <Tooltip + title={tooltipTitle} + content={tooltipContent} + icon="?" + className={tooltipClassName} + /> + } + {...props} + /> + ); +}; + +export default AckeeToggle; diff --git a/src/components/molecules/forms/fieldset.fixture.tsx b/src/components/molecules/forms/fieldset.fixture.tsx new file mode 100644 index 0000000..b94f340 --- /dev/null +++ b/src/components/molecules/forms/fieldset.fixture.tsx @@ -0,0 +1,6 @@ +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/select-with-tooltip.module.scss b/src/components/molecules/forms/fieldset.module.scss index bfadece..3102bf7 100644 --- a/src/components/molecules/forms/select-with-tooltip.module.scss +++ b/src/components/molecules/forms/fieldset.module.scss @@ -1,23 +1,16 @@ -@use "@styles/abstracts/functions" as fun; -@use "@styles/abstracts/mixins" as mix; - -.wrapper { - display: flex; - flex-flow: row wrap; - align-items: center; - position: relative; -} - -.select { - width: auto; - - @include mix.pointer("fine") { - padding: fun.convert-px(3) var(--spacing-xs); +.legend { + float: left; + color: var(--color-primary-darker); + font-size: var(--font-size-md); + font-weight: 600; + + &#{&}--has-tooltip { + padding: 0 var(--spacing-xs) 0 0; } } .btn { - margin-left: var(--spacing-xs); + margin: 0 var(--spacing-2xs) var(--spacing-2xs) 0; &--activated { background: var(--color-primary); @@ -29,12 +22,12 @@ } .tooltip { - position: absolute; 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); @@ -46,3 +39,23 @@ 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 new file mode 100644 index 0000000..0778094 --- /dev/null +++ b/src/components/molecules/forms/fieldset.stories.tsx @@ -0,0 +1,165 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { TooltipProps } from '../modals/tooltip'; +import { Help } from '../modals/tooltip.stories'; +import FieldsetComponent from './fieldset'; +import { body, legend, Tooltip } from './fieldset.fixture'; + +/** + * Fieldset - Storybook Meta + */ +export default { + title: 'Atoms/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, + }, + }, + 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 new file mode 100644 index 0000000..de89e31 --- /dev/null +++ b/src/components/molecules/forms/fieldset.test.tsx @@ -0,0 +1,22 @@ +import { render, screen } from '@test-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 new file mode 100644 index 0000000..9f46247 --- /dev/null +++ b/src/components/molecules/forms/fieldset.tsx @@ -0,0 +1,118 @@ +import useClickOutside from '@utils/hooks/use-click-outside'; +import { + cloneElement, + FC, + ReactComponentElement, + ReactNode, + useRef, + useState, +} from 'react'; +import HelpButton from '../buttons/help-button'; +import Tooltip from '../modals/tooltip'; +import styles from './fieldset.module.scss'; + +export type FieldsetProps = { + /** + * Set additional classnames to the body wrapper. + */ + bodyClassName?: 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. + */ +const Fieldset: FC<FieldsetProps> = ({ + bodyClassName = '', + children, + className = '', + legend, + legendClassName = '', + legendPosition = 'stacked', + Tooltip: TooltipComponent, + ...props +}) => { + const [isTooltipOpened, setIsTooltipOpened] = useState<boolean>(false); + const buttonRef = useRef<HTMLButtonElement>(null); + const tooltipRef = useRef<HTMLDivElement>(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 event target is outside. + * + * @param {EventTarget} target - The event target. + */ + const closeTooltip = (target: EventTarget) => { + if (buttonRef.current && !buttonRef.current.contains(target as Node)) + setIsTooltipOpened(false); + }; + + useClickOutside( + tooltipRef, + (target) => isTooltipOpened && closeTooltip(target) + ); + + return ( + <fieldset + className={`${styles.wrapper} ${styles[wrapperModifier]} ${className}`} + {...props} + > + <legend + className={`${styles.legend} ${styles[legendModifier]} ${legendClassName}`} + > + {legend} + </legend> + {TooltipComponent && ( + <> + <HelpButton + className={`${styles.btn} ${buttonModifier}`} + onClick={() => setIsTooltipOpened(!isTooltipOpened)} + ref={buttonRef} + /> + {cloneElement(TooltipComponent, { + cloneClassName: `${styles.tooltip} ${styles[tooltipModifier]}`, + ref: tooltipRef, + })} + </> + )} + <div className={`${styles.body} ${bodyClassName}`}>{children}</div> + </fieldset> + ); +}; + +export default Fieldset; diff --git a/src/components/molecules/forms/motion-toggle.stories.tsx b/src/components/molecules/forms/motion-toggle.stories.tsx index 5c524a8..541ca8e 100644 --- a/src/components/molecules/forms/motion-toggle.stories.tsx +++ b/src/components/molecules/forms/motion-toggle.stories.tsx @@ -9,6 +9,19 @@ export default { title: 'Molecules/Forms/Toggle', component: MotionToggleComponent, 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', @@ -20,6 +33,19 @@ export default { 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', diff --git a/src/components/molecules/forms/motion-toggle.tsx b/src/components/molecules/forms/motion-toggle.tsx index 6925248..ec2d950 100644 --- a/src/components/molecules/forms/motion-toggle.tsx +++ b/src/components/molecules/forms/motion-toggle.tsx @@ -13,7 +13,7 @@ export type MotionToggleValue = 'on' | 'off'; export type MotionToggleProps = Pick< RadioGroupProps, - 'groupClassName' | 'legendClassName' + 'bodyClassName' | 'groupClassName' | 'legendClassName' > & { /** * True if motion should be reduced by default. diff --git a/src/components/molecules/forms/prism-theme-toggle.stories.tsx b/src/components/molecules/forms/prism-theme-toggle.stories.tsx index 3f57fa5..86f9773 100644 --- a/src/components/molecules/forms/prism-theme-toggle.stories.tsx +++ b/src/components/molecules/forms/prism-theme-toggle.stories.tsx @@ -8,6 +8,32 @@ 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', diff --git a/src/components/molecules/forms/prism-theme-toggle.tsx b/src/components/molecules/forms/prism-theme-toggle.tsx index 66be056..7bf5b7c 100644 --- a/src/components/molecules/forms/prism-theme-toggle.tsx +++ b/src/components/molecules/forms/prism-theme-toggle.tsx @@ -12,7 +12,7 @@ import RadioGroup, { export type PrismThemeToggleProps = Pick< RadioGroupProps, - 'groupClassName' | 'legendClassName' + 'bodyClassName' | 'groupClassName' | 'legendClassName' >; /** diff --git a/src/components/molecules/forms/radio-group.stories.tsx b/src/components/molecules/forms/radio-group.stories.tsx index 3c01af5..ad1bd6d 100644 --- a/src/components/molecules/forms/radio-group.stories.tsx +++ b/src/components/molecules/forms/radio-group.stories.tsx @@ -13,6 +13,19 @@ export default { labelSize: 'small', }, argTypes: { + bodyClassName: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the fieldset body wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, className: { control: { type: 'text', @@ -26,6 +39,19 @@ export default { 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', @@ -131,6 +157,19 @@ export default { 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', @@ -152,6 +191,19 @@ export default { value: {}, }, }, + Tooltip: { + control: { + type: null, + }, + description: 'Add an optional tooltip.', + table: { + category: 'Options', + }, + type: { + name: 'function', + required: false, + }, + }, }, } as ComponentMeta<typeof RadioGroup>; diff --git a/src/components/molecules/forms/radio-group.tsx b/src/components/molecules/forms/radio-group.tsx index 45f585e..64bdaa0 100644 --- a/src/components/molecules/forms/radio-group.tsx +++ b/src/components/molecules/forms/radio-group.tsx @@ -1,4 +1,6 @@ -import Fieldset, { type FieldsetProps } from '@components/atoms/forms/fieldset'; +import Fieldset, { + type FieldsetProps, +} from '@components/molecules/forms/fieldset'; import useStateChange from '@utils/hooks/use-state-change'; import { ChangeEvent, FC, MouseEvent, SetStateAction, useState } from 'react'; import LabelledBooleanField, { @@ -23,7 +25,7 @@ export type RadioGroupOption = Pick< export type RadioGroupProps = Pick< FieldsetProps, - 'className' | 'legend' | 'legendClassName' + 'bodyClassName' | 'className' | 'legend' | 'legendClassName' | 'Tooltip' > & Pick<LabelledBooleanFieldProps, 'labelPosition' | 'labelSize'> & { /** diff --git a/src/components/molecules/forms/select-with-tooltip.stories.tsx b/src/components/molecules/forms/select-with-tooltip.stories.tsx deleted file mode 100644 index d6206a9..0000000 --- a/src/components/molecules/forms/select-with-tooltip.stories.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { useState } from 'react'; -import SelectWithTooltip from './select-with-tooltip'; - -/** - * SelectWithTooltip - Storybook Meta - */ -export default { - title: 'Molecules/Forms/Select', - component: SelectWithTooltip, - argTypes: { - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the select wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - content: { - control: { - type: 'text', - }, - description: 'The tooltip body.', - type: { - name: 'string', - required: true, - }, - }, - 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: 'The select 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, - }, - }, - 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, - }, - }, - title: { - control: { - type: 'text', - }, - description: 'The tooltip title', - type: { - name: 'string', - required: true, - }, - }, - tooltipClassName: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the tooltip.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - value: { - control: { - type: 'text', - }, - description: 'Field value.', - type: { - name: 'string', - required: true, - }, - }, - }, -} as ComponentMeta<typeof SelectWithTooltip>; - -const selectOptions = [ - { id: 'option1', name: 'Option 1', value: 'option1' }, - { id: 'option2', name: 'Option 2', value: 'option2' }, - { id: 'option3', name: 'Option 3', value: 'option3' }, -]; - -const Template: ComponentStory<typeof SelectWithTooltip> = ({ - value: _value, - setValue: _setValue, - ...args -}) => { - const [selected, setSelected] = useState<string>('option1'); - return ( - <SelectWithTooltip value={selected} setValue={setSelected} {...args} /> - ); -}; - -/** - * Select Stories - With tooltip - */ -export const WithTooltip = Template.bind({}); -WithTooltip.args = { - content: 'Illo voluptatibus quia minima placeat sit nostrum excepturi.', - title: 'Possimus quidem dolor', - id: 'storybook-select', - label: 'Officiis:', - name: 'storybook-select', - options: selectOptions, -}; diff --git a/src/components/molecules/forms/select-with-tooltip.test.tsx b/src/components/molecules/forms/select-with-tooltip.test.tsx deleted file mode 100644 index 7a423f5..0000000 --- a/src/components/molecules/forms/select-with-tooltip.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { render, screen } from '@test-utils'; -import SelectWithTooltip from './select-with-tooltip'; - -const selectOptions = [ - { id: 'option1', name: 'Option 1', value: 'option1' }, - { id: 'option2', name: 'Option 2', value: 'option2' }, - { id: 'option3', name: 'Option 3', value: 'option3' }, -]; -const selectLabel = 'Jest select'; -const selectValue = selectOptions[0].value; -const tooltipTitle = 'Jest tooltip'; -const tooltipContent = 'Nesciunt voluptatibus voluptatem omnis at quia libero.'; - -describe('SelectWithTooltip', () => { - it('renders a select', () => { - render( - <SelectWithTooltip - id="jest-select" - name="jest-select" - label={selectLabel} - options={selectOptions} - value={selectValue} - setValue={() => null} - title={tooltipTitle} - content={tooltipContent} - /> - ); - expect(screen.getByRole('combobox', { name: selectLabel })).toHaveValue( - selectValue - ); - }); -}); diff --git a/src/components/molecules/forms/select-with-tooltip.tsx b/src/components/molecules/forms/select-with-tooltip.tsx deleted file mode 100644 index 46075c2..0000000 --- a/src/components/molecules/forms/select-with-tooltip.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import useClickOutside from '@utils/hooks/use-click-outside'; -import { FC, useRef, useState } from 'react'; -import HelpButton from '../buttons/help-button'; -import Tooltip, { type TooltipProps } from '../modals/tooltip'; -import LabelledSelect, { type LabelledSelectProps } from './labelled-select'; -import styles from './select-with-tooltip.module.scss'; - -export type SelectWithTooltipProps = Omit< - LabelledSelectProps, - 'labelPosition' -> & - Pick<TooltipProps, 'title' | 'content'> & { - /** - * Set additional classnames to the select wrapper. - */ - className?: string; - /** - * Set additional classnames to the tooltip wrapper. - */ - tooltipClassName?: TooltipProps['className']; - }; - -/** - * SelectWithTooltip component - * - * Render a select with a button to display a tooltip about options. - */ -const SelectWithTooltip: FC<SelectWithTooltipProps> = ({ - className = '', - content, - id, - title, - tooltipClassName = '', - ...props -}) => { - const [isTooltipOpened, setIsTooltipOpened] = useState<boolean>(false); - const buttonRef = useRef<HTMLButtonElement>(null); - const tooltipRef = useRef<HTMLDivElement>(null); - const buttonModifier = isTooltipOpened ? styles['btn--activated'] : ''; - const tooltipModifier = isTooltipOpened - ? styles['tooltip--visible'] - : styles['tooltip--hidden']; - - const closeTooltip = (target: EventTarget) => { - if (buttonRef.current && !buttonRef.current.contains(target as Node)) - setIsTooltipOpened(false); - }; - - useClickOutside( - tooltipRef, - (target) => isTooltipOpened && closeTooltip(target) - ); - - return ( - <div className={`${styles.wrapper} ${className}`}> - <LabelledSelect - labelPosition="left" - id={id} - labelClassName={styles.label} - {...props} - /> - <HelpButton - className={`${styles.btn} ${buttonModifier}`} - onClick={() => setIsTooltipOpened(!isTooltipOpened)} - ref={buttonRef} - /> - <Tooltip - title={title} - content={content} - icon="?" - className={`${styles.tooltip} ${tooltipModifier} ${tooltipClassName}`} - ref={tooltipRef} - /> - </div> - ); -}; - -export default SelectWithTooltip; diff --git a/src/components/molecules/forms/theme-toggle.stories.tsx b/src/components/molecules/forms/theme-toggle.stories.tsx index cd59d7e..ff1034d 100644 --- a/src/components/molecules/forms/theme-toggle.stories.tsx +++ b/src/components/molecules/forms/theme-toggle.stories.tsx @@ -8,6 +8,32 @@ 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', diff --git a/src/components/molecules/forms/theme-toggle.tsx b/src/components/molecules/forms/theme-toggle.tsx index 30bc55c..b796b27 100644 --- a/src/components/molecules/forms/theme-toggle.tsx +++ b/src/components/molecules/forms/theme-toggle.tsx @@ -12,7 +12,7 @@ import RadioGroup, { export type ThemeToggleProps = Pick< RadioGroupProps, - 'groupClassName' | 'legendClassName' + 'bodyClassName' | 'groupClassName' | 'legendClassName' >; /** |
