import PropTypes from 'prop-types'; import { h, Fragment } from 'preact'; import { useLayoutEffect, useRef, useCallback } from 'preact/hooks'; import { createFocusTrap } from 'focus-trap'; import { defaultChildrenPropTypes } from '../../common-prop-types'; import { KeyboardShortcuts } from './useKeyboardShortcuts'; /** * Wrapper component to create a focus trap within the HTML element found by the given selector * * @example * import { FocusTrap } from "shared/components/FocusTrap"; * * const ExampleComponent = ({ onClose }) => ( * *
* *
*
* ) * * @param {string} selector The CSS selector for the element where focus is to be trapped * @param {Array} children Child element(s) passed via composition * @param {Function} onDeactivate Callback function to be called when the focus trap is deactivated by navigation or Escape press * @param {boolean} clickOutsideDeactivates If true, the focus trap will deactivate when a user clicks outside of the trap area */ export const FocusTrap = ({ selector, children, onDeactivate, clickOutsideDeactivates = false, document = window.document, }) => { const focusTrap = useRef(null); const deactivate = useCallback(() => onDeactivate(), [onDeactivate]); useLayoutEffect(() => { const currentLocationHref = document.location.href; const routeChangeObserver = new MutationObserver((mutations) => { const hasRouteChanged = mutations.some( () => currentLocationHref !== document.location.href, ); // Ensure trap deactivates if user navigates from the page if (hasRouteChanged) { focusTrap.current?.deactivate(); routeChangeObserver.disconnect(); } }); focusTrap.current = createFocusTrap(selector, { escapeDeactivates: false, clickOutsideDeactivates, onDeactivate: deactivate, document, }); focusTrap.current.activate(); routeChangeObserver.observe(document.querySelector('body'), { childList: true, }); return () => { focusTrap.current.deactivate(); routeChangeObserver.disconnect(); }; }, [clickOutsideDeactivates, selector, deactivate, document]); const shortcuts = { escape: onDeactivate, }; return ( {children} ); }; FocusTrap.defaultProps = { selector: '.crayons-modal', onDeactivate: () => {}, }; FocusTrap.propTypes = { selector: PropTypes.string, children: defaultChildrenPropTypes.isRequired, onDeactivate: PropTypes.func, };