* adds focusTrap wrapper to preact Modal component * add view specific code for focus trap in admin add nav link * add script to return a focustrap toggle, use in add nav link modal partial * add trap to edit nav link modal * add handlers for sign up modal * update modal controller for admin section, update nav link modals to use * update other admin modals with new data values for trap * remove unneeded erb script file * remove unneeded target * refactor to remove extra unneeded param * remove duplicate code, store getFocusTrapToggle in window * trap focus in comment and bookmark showModal instances for not logged in user * remove need for activator id * clean up id refs no longer needed * remove custom code and re-use focsu-trap lib * update storybook docs * update default export in focusTrap * prevent close button click triggering a modal toggle twice * ensure if user navigates from a modal the trap is deactivated * add jsdoc comments and add dynamic import * ensure admin controller modal traps are cleaned up on disconnect * update sign up modal to use crayons * update modal controller and admin nav links modals to use preact modal * update profile fields modals for new controller * tweak styling of sign up and admin modals to match previous * update listings modal to use crayons modal, adapt focus trap to work with click outside * memoize deactivate callback to ensure modal can be presented on first page load * add missed focustrap changes * fix focus trap issues in onboarding flow * refactor onboarding focus trap, remove getFocusTrapToggle * tweaks for styling and article modal toggle * add click outside tests to modal * add cypress tests for the login modal * update liquid tag tests affected by change * refactors to address review comments * fix issue with login modal presented twice on comment add * change ids to selectors in admin modals * small pr comment refactors * add listings e2e tests * add nav link modal tests * fix issue with help modal * tweak to fix ui bug from merge * remove context from showLoginModal * rename toggleModal * rename state property for clarity Co-authored-by: Nick Taylor <nick@dev.to>
88 lines
2.6 KiB
JavaScript
88 lines
2.6 KiB
JavaScript
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 }) => (
|
|
* <FocusTrap selector=".component-with-focus-trap" onDeactivate={onClose}>
|
|
* <div class="component-with-focus-trap">
|
|
* <button onClick={onClose}>Close</button>
|
|
* </div>
|
|
* </FocusTrap>
|
|
* )
|
|
*
|
|
* @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,
|
|
}) => {
|
|
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,
|
|
});
|
|
|
|
focusTrap.current.activate();
|
|
routeChangeObserver.observe(document.querySelector('body'), {
|
|
childList: true,
|
|
});
|
|
|
|
return () => {
|
|
focusTrap.current.deactivate();
|
|
routeChangeObserver.disconnect();
|
|
};
|
|
}, [clickOutsideDeactivates, selector, deactivate]);
|
|
|
|
const shortcuts = {
|
|
escape: onDeactivate,
|
|
};
|
|
|
|
return (
|
|
<Fragment>
|
|
{children}
|
|
<KeyboardShortcuts shortcuts={shortcuts} />
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
FocusTrap.defaultProps = {
|
|
selector: '.crayons-modal',
|
|
onDeactivate: () => {},
|
|
};
|
|
|
|
FocusTrap.propTypes = {
|
|
selector: PropTypes.string,
|
|
children: defaultChildrenPropTypes.isRequired,
|
|
onDeactivate: PropTypes.func,
|
|
};
|