diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-06-01 22:37:56 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-06-01 22:37:56 +0200 |
| commit | 0a33a4658d848fe056715c6da053763407845b2a (patch) | |
| tree | 7c679e54ba4bbadaf0a59bbde780f5742e3b875d /src/components/molecules/forms/fieldset.tsx | |
| parent | 97031a86ca38890e60ecec79828498b7bb13cbfa (diff) | |
| parent | 6be20422494e3806fba3d1c5ad5c3e98bd6e67e5 (diff) | |
chore(a11y): improve website settings accessibility (#17)
The previous switch buttons (using checkbox) was not a11y compliant. So I change my approach to use radio buttons and to clearly separate the two different states. I also convert the Ackee select setting to improve consistency between settings.
Diffstat (limited to 'src/components/molecules/forms/fieldset.tsx')
| -rw-r--r-- | src/components/molecules/forms/fieldset.tsx | 118 |
1 files changed, 118 insertions, 0 deletions
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; |
