docbrown/app/javascript/shared/components/focusTrap.jsx
Suzanne Aitchison 5bed8f56d4
improve keyboard accessibility of modals 12427 10610 (#12511)
* 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>
2021-02-24 16:01:10 +00:00

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,
};