278 lines
11 KiB
JavaScript
278 lines
11 KiB
JavaScript
|
"use strict";
|
||
|
|
||
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
||
|
var _typeof = require("@babel/runtime/helpers/typeof");
|
||
|
Object.defineProperty(exports, "__esModule", {
|
||
|
value: true
|
||
|
});
|
||
|
exports.default = useAccessibility;
|
||
|
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
||
|
var React = _interopRequireWildcard(require("react"));
|
||
|
var _KeyCode = _interopRequireDefault(require("rc-util/lib/KeyCode"));
|
||
|
var _raf = _interopRequireDefault(require("rc-util/lib/raf"));
|
||
|
var _focus = require("rc-util/lib/Dom/focus");
|
||
|
var _IdContext = require("../context/IdContext");
|
||
|
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
||
|
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
||
|
// destruct to reduce minify size
|
||
|
var LEFT = _KeyCode.default.LEFT,
|
||
|
RIGHT = _KeyCode.default.RIGHT,
|
||
|
UP = _KeyCode.default.UP,
|
||
|
DOWN = _KeyCode.default.DOWN,
|
||
|
ENTER = _KeyCode.default.ENTER,
|
||
|
ESC = _KeyCode.default.ESC,
|
||
|
HOME = _KeyCode.default.HOME,
|
||
|
END = _KeyCode.default.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 = {}, (0, _defineProperty2.default)(_inline, UP, prev), (0, _defineProperty2.default)(_inline, DOWN, next), _inline);
|
||
|
var horizontal = (_horizontal = {}, (0, _defineProperty2.default)(_horizontal, LEFT, isRtl ? next : prev), (0, _defineProperty2.default)(_horizontal, RIGHT, isRtl ? prev : next), (0, _defineProperty2.default)(_horizontal, DOWN, children), (0, _defineProperty2.default)(_horizontal, ENTER, children), _horizontal);
|
||
|
var vertical = (_vertical = {}, (0, _defineProperty2.default)(_vertical, UP, prev), (0, _defineProperty2.default)(_vertical, DOWN, next), (0, _defineProperty2.default)(_vertical, ENTER, children), (0, _defineProperty2.default)(_vertical, ESC, parent), (0, _defineProperty2.default)(_vertical, LEFT, isRtl ? children : parent), (0, _defineProperty2.default)(_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 = (0, _focus.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];
|
||
|
}
|
||
|
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.default.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((0, _IdContext.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 = (0, _raf.default)(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 = (0, _raf.default)(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);
|
||
|
};
|
||
|
}
|