293 lines
9.4 KiB
JavaScript
293 lines
9.4 KiB
JavaScript
"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);
|
|
};
|
|
} |