@carbon/react
Version:
React components for the Carbon Design System
92 lines (90 loc) • 4.03 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { selectorTabbable } from "./keyboard/navigation.js";
import { tabbable } from "tabbable";
//#region src/internal/wrapFocus.ts
/**
* Copyright IBM Corp. 2020, 2025
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* A flag `node.compareDocumentPosition(target)` returns that indicates
* `target` is located earlier than `node` in the document or `target` contains `node`.
*/
const DOCUMENT_POSITION_BROAD_PRECEDING = typeof Node !== "undefined" ? Node.DOCUMENT_POSITION_PRECEDING | Node.DOCUMENT_POSITION_CONTAINS : 0;
/**
* A flag `node.compareDocumentPosition(target)` returns that indicates
* `target` is located later than `node` in the document or `node` contains `target`.
*/
const DOCUMENT_POSITION_BROAD_FOLLOWING = typeof Node !== "undefined" ? Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_CONTAINED_BY : 0;
/**
* Checks whether the given node or one of its ancestors matches any of the
* specified floating menu selectors.
*
* @param {Node} node - A DOM node.
* @param {string[]} selectorsFloatingMenus - Additional CSS selectors that
* match floating menus.
* @returns {boolean} Whether the node or one of its ancestors is in a floating
* menu.
*/
const elementOrParentIsFloatingMenu = (node, selectorsFloatingMenus = [], prefix = "cds") => {
if (node instanceof Element && typeof node.closest === "function") return [
`.${prefix}--overflow-menu-options`,
`.${prefix}--tooltip`,
".flatpickr-calendar",
...selectorsFloatingMenus
].some((selector) => !!node.closest(selector));
return false;
};
/**
* Ensures the focus is kept within the given container by implementing
* "focus-wrap" behavior.
*/
const wrapFocus = ({ bodyNode, startTrapNode, endTrapNode, currentActiveNode, oldActiveNode, selectorsFloatingMenus, prefix = "cds" }) => {
if (bodyNode && currentActiveNode && oldActiveNode && !bodyNode.contains(currentActiveNode) && !elementOrParentIsFloatingMenu(currentActiveNode, selectorsFloatingMenus, prefix)) {
const comparisonResult = oldActiveNode.compareDocumentPosition(currentActiveNode);
if (currentActiveNode === startTrapNode || comparisonResult & DOCUMENT_POSITION_BROAD_PRECEDING) {
const tabbableElement = Array.from(bodyNode.querySelectorAll(selectorTabbable)).reverse().find(({ offsetParent }) => Boolean(offsetParent));
if (tabbableElement) tabbableElement.focus();
else if (bodyNode !== oldActiveNode) bodyNode.focus();
} else if (currentActiveNode === endTrapNode || comparisonResult & DOCUMENT_POSITION_BROAD_FOLLOWING) {
const tabbableElement = Array.from(bodyNode.querySelectorAll(selectorTabbable)).find(({ offsetParent }) => Boolean(offsetParent));
if (tabbableElement) tabbableElement.focus();
else if (bodyNode !== oldActiveNode) bodyNode.focus();
}
}
};
/**
* Ensures the focus is kept in the given container, implementing "focus-wrap"
* behavior.
*
* Note: This must be called *before* focus moves using `onKeyDown` or similar.
*/
const wrapFocusWithoutSentinels = ({ containerNode, currentActiveNode, event }) => {
if (!containerNode) return;
if ([
"blur",
"focusout",
"focusin",
"focus"
].includes(event.type) && true) throw new Error(`Error: wrapFocusWithoutSentinels(...) called in unsupported ${event.type} event.\n\nCall wrapFocusWithoutSentinels(...) from onKeyDown instead.`);
const tabbables = tabbable(containerNode);
const firstTabbable = tabbables[0];
const lastTabbable = tabbables[tabbables.length - 1];
if (currentActiveNode === lastTabbable && !event.shiftKey) {
event.preventDefault();
firstTabbable.focus();
}
if (currentActiveNode === firstTabbable && event.shiftKey) {
event.preventDefault();
lastTabbable.focus();
}
};
//#endregion
export { elementOrParentIsFloatingMenu, wrapFocus, wrapFocusWithoutSentinels };