UNPKG

@carbon/react

Version:

React components for the Carbon Design System

92 lines (90 loc) 4.03 kB
/** * 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 };