UNPKG

@mskcc/carbon-react

Version:

Carbon react components for the MSKCC DSM

111 lines (102 loc) 4.73 kB
/** * MSKCC 2021, 2024 */ import findLast from 'lodash.findlast'; import { useEffect } from 'react'; import { DOCUMENT_POSITION_BROAD_PRECEDING, selectorTabbable, DOCUMENT_POSITION_BROAD_FOLLOWING } from './keyboard/navigation.js'; import { tabbable } from 'tabbable'; /** * @param {Node} node A DOM node. * @param {string[]} selectorsFloatingMenus The CSS selectors that matches floating menus. * @returns {boolean} `true` of the given `node` is in a floating menu. */ function elementOrParentIsFloatingMenu(node) { let selectorsFloatingMenus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; if (node && typeof node.closest === 'function') { const allSelectorsFloatingMenus = [`.cds--overflow-menu-options`, `.cds--tooltip`, '.flatpickr-calendar', ...selectorsFloatingMenus]; return allSelectorsFloatingMenus.some(selector => node.closest(selector)); } } /** * Ensures the focus is kept in the given `modalNode`, implementing "focus-wrap" behavior. * @param {object} options The options. * @param {Node|null} options.bodyNode * @param {Node|null} options.startTrapNode The DOM node of the focus sentinel the is placed earlier next to `modalNode`. * @param {Node|null} options.endTrapNode The DOM node of the focus sentinel the is placed next to `modalNode`. * @param {Node} options.currentActiveNode The DOM node that has focus. * @param {Node} options.oldActiveNode The DOM node that previously had focus. * @param {string[]} [options.selectorsFloatingMenus] The CSS selectors that matches floating menus. */ function wrapFocus(_ref) { let { bodyNode, startTrapNode, endTrapNode, currentActiveNode, oldActiveNode, selectorsFloatingMenus } = _ref; if (bodyNode && currentActiveNode && oldActiveNode && !bodyNode.contains(currentActiveNode) && !elementOrParentIsFloatingMenu(currentActiveNode, selectorsFloatingMenus)) { const comparisonResult = oldActiveNode.compareDocumentPosition(currentActiveNode); if (currentActiveNode === startTrapNode || comparisonResult & DOCUMENT_POSITION_BROAD_PRECEDING) { const tabbable = findLast(bodyNode.querySelectorAll(selectorTabbable), elem => Boolean(elem.offsetParent)); if (tabbable) { tabbable.focus(); } else if (bodyNode !== oldActiveNode) { bodyNode.focus(); } } else if (currentActiveNode === endTrapNode || comparisonResult & DOCUMENT_POSITION_BROAD_FOLLOWING) { const tabbable = Array.prototype.find.call(bodyNode.querySelectorAll(selectorTabbable), elem => Boolean(elem.offsetParent)); if (tabbable) { tabbable.focus(); } else if (bodyNode !== oldActiveNode) { bodyNode.focus(); } } } } /** * Ensures the focus is kept in the given `containerNode`, implementing "focus-wrap" behavior. * Note: This must be called *before* focus moves using onKeyDown or similar. * @param {object} options The options. * @param {Node|null} options.containerNode * @param {EventTarget} options.currentActiveNode The DOM node that has focus. * @param {KeyboardEvent} options.event The DOM event */ function wrapFocusWithoutSentinels(_ref2) { let { containerNode, currentActiveNode, event } = _ref2; if (['blur', 'focusout', 'focusin', 'focus'].includes(event.type) && process.env.NODE_ENV !== "production") { // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { throw new Error(`Error: wrapFocusWithoutSentinels(...) called in unsupported ${event.type} event.\n\nCall wrapFocusWithoutSentinels(...) from onKeyDown instead.`); }); } // The reason we're using tabbable is because it returns the tabbable // items *in tab order*, whereas using our `selectorTabbable` only // returns in DOM order const tabbables = tabbable(containerNode); const firstTabbable = tabbables[0]; const lastTabbable = tabbables[tabbables.length - 1]; // console.log(`---------------------------------`); // console.log(containerNode); // console.log(tabbables); // console.log(firstTabbable); // console.log(lastTabbable); // console.log(currentActiveNode); // The shift key is used to determine 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 { wrapFocus as default, elementOrParentIsFloatingMenu, wrapFocusWithoutSentinels };