UNPKG

8.73 kBJavaScriptView Raw
1import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
2import * as React from 'react';
3import KeyCode from "rc-util/es/KeyCode";
4import raf from "rc-util/es/raf";
5import { getFocusNodeList } from "rc-util/es/Dom/focus";
6import { getMenuId } from '../context/IdContext'; // destruct to reduce minify size
7
8var LEFT = KeyCode.LEFT,
9 RIGHT = KeyCode.RIGHT,
10 UP = KeyCode.UP,
11 DOWN = KeyCode.DOWN,
12 ENTER = KeyCode.ENTER,
13 ESC = KeyCode.ESC;
14var ArrowKeys = [UP, DOWN, LEFT, RIGHT];
15
16function getOffset(mode, isRootLevel, isRtl, which) {
17 var _inline, _horizontal, _vertical, _offsets$;
18
19 var prev = 'prev';
20 var next = 'next';
21 var children = 'children';
22 var parent = 'parent'; // Inline enter is special that we use unique operation
23
24 if (mode === 'inline' && which === ENTER) {
25 return {
26 inlineTrigger: true
27 };
28 }
29
30 var inline = (_inline = {}, _defineProperty(_inline, UP, prev), _defineProperty(_inline, DOWN, next), _inline);
31 var horizontal = (_horizontal = {}, _defineProperty(_horizontal, LEFT, isRtl ? next : prev), _defineProperty(_horizontal, RIGHT, isRtl ? prev : next), _defineProperty(_horizontal, DOWN, children), _defineProperty(_horizontal, ENTER, children), _horizontal);
32 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);
33 var offsets = {
34 inline: inline,
35 horizontal: horizontal,
36 vertical: vertical,
37 inlineSub: inline,
38 horizontalSub: vertical,
39 verticalSub: vertical
40 };
41 var type = (_offsets$ = offsets["".concat(mode).concat(isRootLevel ? '' : 'Sub')]) === null || _offsets$ === void 0 ? void 0 : _offsets$[which];
42
43 switch (type) {
44 case prev:
45 return {
46 offset: -1,
47 sibling: true
48 };
49
50 case next:
51 return {
52 offset: 1,
53 sibling: true
54 };
55
56 case parent:
57 return {
58 offset: -1,
59 sibling: false
60 };
61
62 case children:
63 return {
64 offset: 1,
65 sibling: false
66 };
67
68 default:
69 return null;
70 }
71}
72
73function findContainerUL(element) {
74 var current = element;
75
76 while (current) {
77 if (current.getAttribute('data-menu-list')) {
78 return current;
79 }
80
81 current = current.parentElement;
82 } // Normally should not reach this line
83
84 /* istanbul ignore next */
85
86
87 return null;
88}
89/**
90 * Find focused element within element set provided
91 */
92
93
94function getFocusElement(activeElement, elements) {
95 var current = activeElement || document.activeElement;
96
97 while (current) {
98 if (elements.has(current)) {
99 return current;
100 }
101
102 current = current.parentElement;
103 }
104
105 return null;
106}
107/**
108 * Get focusable elements from the element set under provided container
109 */
110
111
112function getFocusableElements(container, elements) {
113 var list = getFocusNodeList(container, true);
114 return list.filter(function (ele) {
115 return elements.has(ele);
116 });
117}
118
119function getNextFocusElement(parentQueryContainer, elements, focusMenuElement) {
120 var offset = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1;
121
122 // Key on the menu item will not get validate parent container
123 if (!parentQueryContainer) {
124 return null;
125 } // List current level menu item elements
126
127
128 var sameLevelFocusableMenuElementList = getFocusableElements(parentQueryContainer, elements); // Find next focus index
129
130 var count = sameLevelFocusableMenuElementList.length;
131 var focusIndex = sameLevelFocusableMenuElementList.findIndex(function (ele) {
132 return focusMenuElement === ele;
133 });
134
135 if (offset < 0) {
136 if (focusIndex === -1) {
137 focusIndex = count - 1;
138 } else {
139 focusIndex -= 1;
140 }
141 } else if (offset > 0) {
142 focusIndex += 1;
143 }
144
145 focusIndex = (focusIndex + count) % count; // Focus menu item
146
147 return sameLevelFocusableMenuElementList[focusIndex];
148}
149
150export default function useAccessibility(mode, activeKey, isRtl, id, containerRef, getKeys, getKeyPath, triggerActiveKey, triggerAccessibilityOpen, originOnKeyDown) {
151 var rafRef = React.useRef();
152 var activeRef = React.useRef();
153 activeRef.current = activeKey;
154
155 var cleanRaf = function cleanRaf() {
156 raf.cancel(rafRef.current);
157 };
158
159 React.useEffect(function () {
160 return function () {
161 cleanRaf();
162 };
163 }, []);
164 return function (e) {
165 var which = e.which;
166
167 if ([].concat(ArrowKeys, [ENTER, ESC]).includes(which)) {
168 // Convert key to elements
169 var elements;
170 var key2element;
171 var element2key; // >>> Wrap as function since we use raf for some case
172
173 var refreshElements = function refreshElements() {
174 elements = new Set();
175 key2element = new Map();
176 element2key = new Map();
177 var keys = getKeys();
178 keys.forEach(function (key) {
179 var element = document.querySelector("[data-menu-id='".concat(getMenuId(id, key), "']"));
180
181 if (element) {
182 elements.add(element);
183 element2key.set(element, key);
184 key2element.set(key, element);
185 }
186 });
187 return elements;
188 };
189
190 refreshElements(); // First we should find current focused MenuItem/SubMenu element
191
192 var activeElement = key2element.get(activeKey);
193 var focusMenuElement = getFocusElement(activeElement, elements);
194 var focusMenuKey = element2key.get(focusMenuElement);
195 var offsetObj = getOffset(mode, getKeyPath(focusMenuKey, true).length === 1, isRtl, which); // Some mode do not have fully arrow operation like inline
196
197 if (!offsetObj) {
198 return;
199 } // Arrow prevent default to avoid page scroll
200
201
202 if (ArrowKeys.includes(which)) {
203 e.preventDefault();
204 }
205
206 var tryFocus = function tryFocus(menuElement) {
207 if (menuElement) {
208 var focusTargetElement = menuElement; // Focus to link instead of menu item if possible
209
210 var link = menuElement.querySelector('a');
211
212 if (link === null || link === void 0 ? void 0 : link.getAttribute('href')) {
213 focusTargetElement = link;
214 }
215
216 var targetKey = element2key.get(menuElement);
217 triggerActiveKey(targetKey);
218 /**
219 * Do not `useEffect` here since `tryFocus` may trigger async
220 * which makes React sync update the `activeKey`
221 * that force render before `useRef` set the next activeKey
222 */
223
224 cleanRaf();
225 rafRef.current = raf(function () {
226 if (activeRef.current === targetKey) {
227 focusTargetElement.focus();
228 }
229 });
230 }
231 };
232
233 if (offsetObj.sibling || !focusMenuElement) {
234 // ========================== Sibling ==========================
235 // Find walkable focus menu element container
236 var parentQueryContainer;
237
238 if (!focusMenuElement || mode === 'inline') {
239 parentQueryContainer = containerRef.current;
240 } else {
241 parentQueryContainer = findContainerUL(focusMenuElement);
242 } // Get next focus element
243
244
245 var targetElement = getNextFocusElement(parentQueryContainer, elements, focusMenuElement, offsetObj.offset); // Focus menu item
246
247 tryFocus(targetElement); // ======================= InlineTrigger =======================
248 } else if (offsetObj.inlineTrigger) {
249 // Inline trigger no need switch to sub menu item
250 triggerAccessibilityOpen(focusMenuKey); // =========================== Level ===========================
251 } else if (offsetObj.offset > 0) {
252 triggerAccessibilityOpen(focusMenuKey, true);
253 cleanRaf();
254 rafRef.current = raf(function () {
255 // Async should resync elements
256 refreshElements();
257 var controlId = focusMenuElement.getAttribute('aria-controls');
258 var subQueryContainer = document.getElementById(controlId); // Get sub focusable menu item
259
260 var targetElement = getNextFocusElement(subQueryContainer, elements); // Focus menu item
261
262 tryFocus(targetElement);
263 }, 5);
264 } else if (offsetObj.offset < 0) {
265 var keyPath = getKeyPath(focusMenuKey, true);
266 var parentKey = keyPath[keyPath.length - 2];
267 var parentMenuElement = key2element.get(parentKey); // Focus menu item
268
269 triggerAccessibilityOpen(parentKey, false);
270 tryFocus(parentMenuElement);
271 }
272 } // Pass origin key down event
273
274
275 originOnKeyDown === null || originOnKeyDown === void 0 ? void 0 : originOnKeyDown(e);
276 };
277}
\No newline at end of file