UNPKG

@utahdts/utah-design-system

Version:
96 lines (90 loc) 3.47 kB
import { useEffect, useRef } from 'react'; import { useImmer } from 'use-immer'; import { useAriaMessaging } from '../../../contexts/UtahDesignSystemContext/hooks/useAriaMessaging'; import { ICON_BUTTON_APPEARANCE } from '../../../enums/buttonEnums'; import { joinClassNames } from '../../../util/joinClassNames'; import { IconButton } from '../../buttons/IconButton'; import { getFocusableElements } from '../../../util/getFocusableElements'; import { useHandleEscape } from '../../../hooks/useHandleEscape'; import { useHandleTab } from '../../../hooks/useHandleTab'; /** * @param {object} props * @param {string} props.ariaLabelledBy Must match the id of the title of the modal * @param {import('react').ReactNode} [props.children] * @param {string} [props.className] * @param {string} props.id * @param {import('react').Ref<HTMLDivElement>} [props.innerRef] * @param {import('react').KeyboardEventHandler} [props.onEscape] * @param {import('react').MouseEventHandler} [props.onClose] * @returns {import('react').JSX.Element} */ export function Modal({ ariaLabelledBy, children, className, id, innerRef, onEscape, onClose, }) { const ref = /** @type {typeof useRef<HTMLDialogElement | null>} */ (useRef)(null); const [lastActiveElement] = useImmer(/** @type {HTMLElement | undefined} */(document.activeElement)); const [firstTabElement, setFirstTabElement] = useImmer(/** @type {HTMLElement | undefined} */(undefined)); const [lastTabElement, setLastTabElement] = useImmer(/** @type {HTMLElement | undefined} */(undefined)); const { addAssertiveMessage } = useAriaMessaging(); const handleEscape = useHandleEscape(onEscape); const handleTab = useHandleTab(firstTabElement, lastTabElement); useEffect(() => { if (ref) { // @ts-expect-error const list = getFocusableElements(ref.current); if (list.length) { const firstElement = list[0]; setFirstTabElement(firstElement); const lastElement = list[list.length - 1]; setLastTabElement(lastElement); firstElement?.focus(); } else { // eslint-disable-next-line no-console console.warn('No focusable element found. Make sure to include a way to close the modal.'); } } }, []); useEffect(() => () => { // Unmount component addAssertiveMessage('Closing dialog'); lastActiveElement?.focus(); }, []); return ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions <div className="modal-backdrop backdrop-dark" onClick={onClose} ref={innerRef}> {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} <dialog aria-modal="true" aria-labelledby={ariaLabelledBy} className={joinClassNames('modal__wrapper', className)} id={id} onClick={(e) => e.stopPropagation()} onKeyUp={handleEscape} onKeyDown={handleTab} ref={ref} > {children} { onClose ? ( <IconButton appearance={ICON_BUTTON_APPEARANCE.BORDERLESS} className="modal__close-button" icon={<span className="utds-icon-before-x-icon" aria-hidden="true" />} onClick={onClose} size="small" title="Close modal" /> ) : undefined } </dialog> </div> ); }