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'  >;  /** | 
