import _defineProperty from "@babel/runtime/helpers/esm/defineProperty"; import * as React from 'react'; import KeyCode from "rc-util/es/KeyCode"; import raf from "rc-util/es/raf"; import { getFocusNodeList } from "rc-util/es/Dom/focus"; import { getMenuId } from "../context/IdContext"; // destruct to reduce minify size var LEFT = KeyCode.LEFT, RIGHT = KeyCode.RIGHT, UP = KeyCode.UP, DOWN = KeyCode.DOWN, ENTER = KeyCode.ENTER, ESC = KeyCode.ESC, HOME = KeyCode.HOME, END = KeyCode.END; var ArrowKeys = [UP, DOWN, LEFT, RIGHT]; function getOffset(mode, isRootLevel, isRtl, which) { var _inline, _horizontal, _vertical, _offsets; var prev = 'prev'; var next = 'next'; var children = 'children'; var parent = 'parent'; // Inline enter is special that we use unique operation if (mode === 'inline' && which === ENTER) { return { inlineTrigger: true }; } var inline = (_inline = {}, _defineProperty(_inline, UP, prev), _defineProperty(_inline, DOWN, next), _inline); var horizontal = (_horizontal = {}, _defineProperty(_horizontal, LEFT, isRtl ? next : prev), _defineProperty(_horizontal, RIGHT, isRtl ? prev : next), _defineProperty(_horizontal, DOWN, children), _defineProperty(_horizontal, ENTER, children), _horizontal); var vertical = (_vertical = {}, _defineProperty(_vertical, UP, prev), _defineProperty(_vertical, DOWN, next), _defineProperty(_vertical, ENTER, children), _defineProperty(_vertical, ESC, parent), _defineProperty(_vertical, LEFT, isRtl ? children : parent), _defineProperty(_vertical, RIGHT, isRtl ? parent : children), _vertical); var offsets = { inline: inline, horizontal: horizontal, vertical: vertical, inlineSub: inline, horizontalSub: vertical, verticalSub: vertical }; var type = (_offsets = offsets["".concat(mode).concat(isRootLevel ? '' : 'Sub')]) === null || _offsets === void 0 ? void 0 : _offsets[which]; switch (type) { case prev: return { offset: -1, sibling: true }; case next: return { offset: 1, sibling: true }; case parent: return { offset: -1, sibling: false }; case children: return { offset: 1, sibling: false }; default: return null; } } function findContainerUL(element) { var current = element; while (current) { if (current.getAttribute('data-menu-list')) { return current; } current = current.parentElement; } // Normally should not reach this line /* istanbul ignore next */ return null; } /** * Find focused element within element set provided */ function getFocusElement(activeElement, elements) { var current = activeElement || document.activeElement; while (current) { if (elements.has(current)) { return current; } current = current.parentElement; } return null; } /** * Get focusable elements from the element set under provided container */ function getFocusableElements(container, elements) { var list = getFocusNodeList(container, true); return list.filter(function (ele) { return elements.has(ele); }); } function getNextFocusElement(parentQueryContainer, elements, focusMenuElement) { var offset = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1; // Key on the menu item will not get validate parent container if (!parentQueryContainer) { return null; } // List current level menu item elements var sameLevelFocusableMenuElementList = getFocusableElements(parentQueryContainer, elements); // Find next focus index var count = sameLevelFocusableMenuElementList.length; var focusIndex = sameLevelFocusableMenuElementList.findIndex(function (ele) { return focusMenuElement === ele; }); if (offset < 0) { if (focusIndex === -1) { focusIndex = count - 1; } else { focusIndex -= 1; } } else if (offset > 0) { focusIndex += 1; } focusIndex = (focusIndex + count) % count; // Focus menu item return sameLevelFocusableMenuElementList[focusIndex]; } export default function useAccessibility(mode, activeKey, isRtl, id, containerRef, getKeys, getKeyPath, triggerActiveKey, triggerAccessibilityOpen, originOnKeyDown) { var rafRef = React.useRef(); var activeRef = React.useRef(); activeRef.current = activeKey; var cleanRaf = function cleanRaf() { raf.cancel(rafRef.current); }; React.useEffect(function () { return function () { cleanRaf(); }; }, []); return function (e) { var which = e.which; if ([].concat(ArrowKeys, [ENTER, ESC, HOME, END]).includes(which)) { // Convert key to elements var elements; var key2element; var element2key; // >>> Wrap as function since we use raf for some case var refreshElements = function refreshElements() { elements = new Set(); key2element = new Map(); element2key = new Map(); var keys = getKeys(); keys.forEach(function (key) { var element = document.querySelector("[data-menu-id='".concat(getMenuId(id, key), "']")); if (element) { elements.add(element); element2key.set(element, key); key2element.set(key, element); } }); return elements; }; refreshElements(); // First we should find current focused MenuItem/SubMenu element var activeElement = key2element.get(activeKey); var focusMenuElement = getFocusElement(activeElement, elements); var focusMenuKey = element2key.get(focusMenuElement); var offsetObj = getOffset(mode, getKeyPath(focusMenuKey, true).length === 1, isRtl, which); // Some mode do not have fully arrow operation like inline if (!offsetObj && which !== HOME && which !== END) { return; } // Arrow prevent default to avoid page scroll if (ArrowKeys.includes(which) || [HOME, END].includes(which)) { e.preventDefault(); } var tryFocus = function tryFocus(menuElement) { if (menuElement) { var focusTargetElement = menuElement; // Focus to link instead of menu item if possible var link = menuElement.querySelector('a'); if (link !== null && link !== void 0 && link.getAttribute('href')) { focusTargetElement = link; } var targetKey = element2key.get(menuElement); triggerActiveKey(targetKey); /** * Do not `useEffect` here since `tryFocus` may trigger async * which makes React sync update the `activeKey` * that force render before `useRef` set the next activeKey */ cleanRaf(); rafRef.current = raf(function () { if (activeRef.current === targetKey) { focusTargetElement.focus(); } }); } }; if ([HOME, END].includes(which) || offsetObj.sibling || !focusMenuElement) { // ========================== Sibling ========================== // Find walkable focus menu element container var parentQueryContainer; if (!focusMenuElement || mode === 'inline') { parentQueryContainer = containerRef.current; } else { parentQueryContainer = findContainerUL(focusMenuElement); } // Get next focus element var targetElement; var focusableElements = getFocusableElements(parentQueryContainer, elements); if (which === HOME) { targetElement = focusableElements[0]; } else if (which === END) { targetElement = focusableElements[focusableElements.length - 1]; } else { targetElement = getNextFocusElement(parentQueryContainer, elements, focusMenuElement, offsetObj.offset); } // Focus menu item tryFocus(targetElement); // ======================= InlineTrigger ======================= } else if (offsetObj.inlineTrigger) { // Inline trigger no need switch to sub menu item triggerAccessibilityOpen(focusMenuKey); // =========================== Level =========================== } else if (offsetObj.offset > 0) { triggerAccessibilityOpen(focusMenuKey, true); cleanRaf(); rafRef.current = raf(function () { // Async should resync elements refreshElements(); var controlId = focusMenuElement.getAttribute('aria-controls'); var subQueryContainer = document.getElementById(controlId); // Get sub focusable menu item var targetElement = getNextFocusElement(subQueryContainer, elements); // Focus menu item tryFocus(targetElement); }, 5); } else if (offsetObj.offset < 0) { var keyPath = getKeyPath(focusMenuKey, true); var parentKey = keyPath[keyPath.length - 2]; var parentMenuElement = key2element.get(parentKey); // Focus menu item triggerAccessibilityOpen(parentKey, false); tryFocus(parentMenuElement); } } // Pass origin key down event originOnKeyDown === null || originOnKeyDown === void 0 ? void 0 : originOnKeyDown(e); }; }