UNPKG

@rc-component/menu

Version:
293 lines (274 loc) 9.63 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getFocusableElements = getFocusableElements; exports.refreshElements = void 0; exports.useAccessibility = useAccessibility; var _focus = require("@rc-component/util/lib/Dom/focus"); var _KeyCode = _interopRequireDefault(require("@rc-component/util/lib/KeyCode")); var _raf = _interopRequireDefault(require("@rc-component/util/lib/raf")); var React = _interopRequireWildcard(require("react")); var _IdContext = require("../context/IdContext"); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // destruct to reduce minify size const { LEFT, RIGHT, UP, DOWN, ENTER, ESC, HOME, END } = _KeyCode.default; const ArrowKeys = [UP, DOWN, LEFT, RIGHT]; function getOffset(mode, isRootLevel, isRtl, which) { const prev = 'prev'; const next = 'next'; const children = 'children'; const parent = 'parent'; // Inline enter is special that we use unique operation if (mode === 'inline' && which === ENTER) { return { inlineTrigger: true }; } const inline = { [UP]: prev, [DOWN]: next }; const horizontal = { [LEFT]: isRtl ? next : prev, [RIGHT]: isRtl ? prev : next, [DOWN]: children, [ENTER]: children }; const vertical = { [UP]: prev, [DOWN]: next, [ENTER]: children, [ESC]: parent, [LEFT]: isRtl ? children : parent, [RIGHT]: isRtl ? parent : children }; const offsets = { inline, horizontal, vertical, inlineSub: inline, horizontalSub: vertical, verticalSub: vertical }; const type = offsets[`${mode}${isRootLevel ? '' : 'Sub'}`]?.[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) { let 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) { let 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) { const list = (0, _focus.getFocusNodeList)(container, true); return list.filter(ele => elements.has(ele)); } function getNextFocusElement(parentQueryContainer, elements, focusMenuElement, offset = 1) { // Key on the menu item will not get validate parent container if (!parentQueryContainer) { return null; } // List current level menu item elements const sameLevelFocusableMenuElementList = getFocusableElements(parentQueryContainer, elements); // Find next focus index const count = sameLevelFocusableMenuElementList.length; let focusIndex = sameLevelFocusableMenuElementList.findIndex(ele => 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]; } const refreshElements = (keys, id) => { const elements = new Set(); const key2element = new Map(); const element2key = new Map(); keys.forEach(key => { const element = document.querySelector(`[data-menu-id='${(0, _IdContext.getMenuId)(id, key)}']`); if (element) { elements.add(element); element2key.set(element, key); key2element.set(key, element); } }); return { elements, key2element, element2key }; }; exports.refreshElements = refreshElements; function useAccessibility(mode, activeKey, isRtl, id, containerRef, getKeys, getKeyPath, triggerActiveKey, triggerAccessibilityOpen, originOnKeyDown) { const rafRef = React.useRef(); const activeRef = React.useRef(); activeRef.current = activeKey; const cleanRaf = () => { _raf.default.cancel(rafRef.current); }; React.useEffect(() => () => { cleanRaf(); }, []); return e => { const { which } = e; if ([...ArrowKeys, ENTER, ESC, HOME, END].includes(which)) { const keys = getKeys(); let refreshedElements = refreshElements(keys, id); const { elements, key2element, element2key } = refreshedElements; // First we should find current focused MenuItem/SubMenu element const activeElement = key2element.get(activeKey); const focusMenuElement = getFocusElement(activeElement, elements); const focusMenuKey = element2key.get(focusMenuElement); const 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(); } const tryFocus = menuElement => { if (menuElement) { let focusTargetElement = menuElement; // Focus to link instead of menu item if possible const link = menuElement.querySelector('a'); if (link?.getAttribute('href')) { focusTargetElement = link; } const 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 = (0, _raf.default)(() => { if (activeRef.current === targetKey) { focusTargetElement.focus(); } }); } }; if ([HOME, END].includes(which) || offsetObj.sibling || !focusMenuElement) { // ========================== Sibling ========================== // Find walkable focus menu element container let parentQueryContainer; if (!focusMenuElement || mode === 'inline') { parentQueryContainer = containerRef.current; } else { parentQueryContainer = findContainerUL(focusMenuElement); } // Get next focus element let targetElement; const 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 = (0, _raf.default)(() => { // Async should resync elements refreshedElements = refreshElements(keys, id); const controlId = focusMenuElement.getAttribute('aria-controls'); const subQueryContainer = document.getElementById(controlId); // Get sub focusable menu item const targetElement = getNextFocusElement(subQueryContainer, refreshedElements.elements); // Focus menu item tryFocus(targetElement); }, 5); } else if (offsetObj.offset < 0) { const keyPath = getKeyPath(focusMenuKey, true); const parentKey = keyPath[keyPath.length - 2]; const parentMenuElement = key2element.get(parentKey); // Focus menu item triggerAccessibilityOpen(parentKey, false); tryFocus(parentMenuElement); } } // Pass origin key down event originOnKeyDown?.(e); }; }