minsky-kit
Version:
Kit of components for MINSKY web agency
94 lines (81 loc) • 2.99 kB
JavaScript
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,
});
};