UNPKG

@carbon/react

Version:

React components for the Carbon Design System

114 lines (104 loc) 4.43 kB
/** * Copyright IBM Corp. 2016, 2023 * * 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 { tabbable } from 'tabbable'; import { selectorTabbable } from './keyboard/navigation.js'; /** * 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 = []) => { if (node instanceof Element && typeof node.closest === 'function') { const allSelectorsFloatingMenus = ['.cds--overflow-menu-options', '.cds--tooltip', '.flatpickr-calendar', ...selectorsFloatingMenus]; return allSelectorsFloatingMenus.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 }) => { if (bodyNode && currentActiveNode && oldActiveNode && !bodyNode.contains(currentActiveNode) && !elementOrParentIsFloatingMenu(currentActiveNode, selectorsFloatingMenus)) { 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) && process.env.NODE_ENV !== 'production') { throw new Error(`Error: wrapFocusWithoutSentinels(...) called in unsupported ${event.type} event.\n\nCall wrapFocusWithoutSentinels(...) from onKeyDown instead.`); } // Use `tabbable` to get the focusable elements in tab order. // `selectorTabbable` returns elements in DOM order which is why it's not // used. const tabbables = tabbable(containerNode); const firstTabbable = tabbables[0]; const lastTabbable = tabbables[tabbables.length - 1]; // The shift key indicates if focus is moving forwards or backwards. if (currentActiveNode === lastTabbable && !event.shiftKey) { // Cancel the current movement of focus because we're going to place it ourselves event.preventDefault(); firstTabbable.focus(); } if (currentActiveNode === firstTabbable && event.shiftKey) { // Cancel the current movement of focus because we're going to place it ourselves event.preventDefault(); lastTabbable.focus(); } }; export { elementOrParentIsFloatingMenu, wrapFocus, wrapFocusWithoutSentinels };