@carbon/react
Version:
React components for the Carbon Design System
114 lines (104 loc) • 4.43 kB
JavaScript
/**
* 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 };