UNPKG

minsky-kit

Version:
94 lines (81 loc) 2.99 kB
const selectors = { trapper: ".js-trap-focus", interactiveElements: "a, button, input, select, textarea", }; const isNull = (value) => value === null; const isVisible = (element) => !isNull(element.offsetParent); const focusFirst = (trapper) => { trapper.querySelector(selectors.interactiveElements).focus(); }; const focusLast = (trapper) => { const interactiveElements = [ ...trapper.querySelectorAll(selectors.interactiveElements), ]; const lastInteractiveElement = interactiveElements .reverse() .find((element) => isVisible(element)); lastInteractiveElement.focus(); }; /** * @function `insertTrapper` * Adds some focus trappers in a container. * 1. Adds the body and the wrapping element to the tab flow. This is necessary to make the focus trap work. * 2. Adds the trapper in the tab flow. * 3. Defines if the trapper is reversed or not (meaning that the last trappers * returns the focus to the first focusable element and vice versa). * 4. If a trapper has to be inserted at the beginning of the wrapper. * 5. If a trapper has to be inserted at the end of the wrapper. * @param {object} dependencies * @property {HTMLElement} wrapper * @property {boolean} before - (default: true) * @property {boolean} after - (default: true) * @property {boolean} reverse - (default: false) * @return {void} */ export const insertTrapper = (dependencies = {}) => { const { wrapper } = dependencies; wrapper.tabIndex = "-1"; /* [1] */ document.body.tabIndex = "-1"; const trap = document.createElement("DIV"); trap.className = selectors.trapper.replace(".", ""); trap.tabIndex = "0"; /* [2] */ const isReversed = "reverse" in dependencies ? dependencies.reverse : false; /* [3] */ if ("before" in dependencies ? dependencies.before : true) { /* [4] */ const trapBefore = trap.cloneNode(true); trapBefore.addEventListener("focus", () => isReversed ? focusLast(wrapper) : focusFirst(wrapper) ); wrapper.prepend(trapBefore); } if ("after" in dependencies ? dependencies.after : true) { /* [5] */ const trapAfter = trap.cloneNode(true); trapAfter.addEventListener("focus", () => isReversed ? focusLast(wrapper) : focusFirst(wrapper) ); wrapper.append(trapAfter); } wrapper.focus({ preventScroll: true, }); }; /** * @function `removeTrapper` * Cleans up the focus trappers of the wrapping element. * Should only be used when the wrapping element has active focus trappers. * * @param {object} dependencies * @property {HTMLElement} wrapper * @property {boolean} lastActiveElement - (default: body) * @return {void} */ export const removeTrapper = (dependencies = {}) => { const { wrapper, lastActiveElement } = dependencies; const trappers = wrapper.querySelectorAll(selectors.trapper); trappers.forEach((trapper) => trapper.remove()); (lastActiveElement || document.body).focus({ preventScroll: true, }); };