UNPKG

@carbon/react

Version:

React components for the Carbon Design System

119 lines (108 loc) 4.6 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. */ 'use strict'; var tabbable = require('tabbable'); var navigation = require('./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 = [], prefix = 'cds') => { if (node instanceof Element && typeof node.closest === 'function') { const allSelectorsFloatingMenus = [`.${prefix}--overflow-menu-options`, `.${prefix}--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, 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(navigation.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(navigation.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.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(); } }; exports.elementOrParentIsFloatingMenu = elementOrParentIsFloatingMenu; exports.wrapFocus = wrapFocus; exports.wrapFocusWithoutSentinels = wrapFocusWithoutSentinels;