UNPKG

10.9 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.KeyboardHandler = exports.setTabIndex = exports.handleArrows = void 0;
4const tslib_1 = require("tslib");
5const React = tslib_1.__importStar(require("react"));
6const util_1 = require("./util");
7/**
8 * This function is a helper for handling basic arrow keyboard interactions. If a component already has its own key handler and event start up/tear down, this function may be easier to integrate in over the full component.
9 *
10 * @param {event} event Event triggered by the keyboard
11 * @param {element[]} navigableElements Valid traversable elements of the container
12 * @param {function} isActiveElement Callback to determine if a given element from the navigable elements array is the active element of the page
13 * @param {function} getFocusableElement Callback returning the focusable element of a given element from the navigable elements array
14 * @param {string[]} validSiblingTags Valid sibling tags that horizontal arrow handling will focus
15 * @param {boolean} noVerticalArrowHandling Flag indicating that the included vertical arrow key handling should be ignored
16 * @param {boolean} noHorizontalArrowHandling Flag indicating that the included horizontal arrow key handling should be ignored
17 * @param {boolean} updateTabIndex Flag indicating that the tabIndex of the currently focused element and next focused element should be updated, in the case of using a roving tabIndex
18 * @param {boolean} onlyTraverseSiblings Flag indicating that next focusable element of a horizontal movement will be this element's sibling
19 */
20const handleArrows = (event, navigableElements, isActiveElement = element => document.activeElement.contains(element), getFocusableElement = element => element, validSiblingTags = ['A', 'BUTTON', 'INPUT'], noVerticalArrowHandling = false, noHorizontalArrowHandling = false, updateTabIndex = true, onlyTraverseSiblings = true) => {
21 const activeElement = document.activeElement;
22 const key = event.key;
23 let moveTarget = null;
24 // Handle vertical arrow keys. If noVerticalArrowHandling is passed, skip this block
25 if (!noVerticalArrowHandling) {
26 if (['ArrowUp', 'ArrowDown'].includes(key)) {
27 event.preventDefault();
28 event.stopImmediatePropagation(); // For menus in menus
29 // Traverse navigableElements to find the element which is currently active
30 let currentIndex = -1;
31 // while (currentIndex === -1) {
32 navigableElements.forEach((element, index) => {
33 if (isActiveElement(element)) {
34 // Once found, move up or down the array by 1. Determined by the vertical arrow key direction
35 let increment = 0;
36 // keep increasing the increment until you've tried the whole navigableElement
37 while (!moveTarget && increment < navigableElements.length && increment * -1 < navigableElements.length) {
38 key === 'ArrowUp' ? increment-- : increment++;
39 currentIndex = index + increment;
40 if (currentIndex >= navigableElements.length) {
41 currentIndex = 0;
42 }
43 if (currentIndex < 0) {
44 currentIndex = navigableElements.length - 1;
45 }
46 // Set the next target element (undefined if none found)
47 moveTarget = getFocusableElement(navigableElements[currentIndex]);
48 }
49 }
50 });
51 // }
52 }
53 }
54 // Handle horizontal arrow keys. If noHorizontalArrowHandling is passed, skip this block
55 if (!noHorizontalArrowHandling) {
56 if (['ArrowLeft', 'ArrowRight'].includes(key)) {
57 event.preventDefault();
58 event.stopImmediatePropagation(); // For menus in menus
59 let currentIndex = -1;
60 navigableElements.forEach((element, index) => {
61 if (isActiveElement(element)) {
62 const activeRow = navigableElements[index].querySelectorAll(validSiblingTags.join(',')); // all focusable elements in my row
63 if (!activeRow.length || onlyTraverseSiblings) {
64 let nextSibling = activeElement;
65 // While a sibling exists, check each sibling to determine if it should be focussed
66 while (nextSibling) {
67 // Set the next checked sibling, determined by the horizontal arrow key direction
68 nextSibling = key === 'ArrowLeft' ? nextSibling.previousElementSibling : nextSibling.nextElementSibling;
69 if (nextSibling) {
70 if (validSiblingTags.includes(nextSibling.tagName)) {
71 // If the sibling's tag is included in validSiblingTags, set the next target element and break the loop
72 moveTarget = nextSibling;
73 break;
74 }
75 // If the sibling's tag is not valid, skip to the next sibling if possible
76 }
77 }
78 }
79 else {
80 activeRow.forEach((focusableElement, index) => {
81 if (event.target === focusableElement) {
82 // Once found, move up or down the array by 1. Determined by the vertical arrow key direction
83 const increment = key === 'ArrowLeft' ? -1 : 1;
84 currentIndex = index + increment;
85 if (currentIndex >= activeRow.length) {
86 currentIndex = 0;
87 }
88 if (currentIndex < 0) {
89 currentIndex = activeRow.length - 1;
90 }
91 // Set the next target element
92 moveTarget = activeRow[currentIndex];
93 }
94 });
95 }
96 }
97 });
98 }
99 }
100 if (moveTarget) {
101 // If updateTabIndex is true, set the previously focussed element's tabIndex to -1 and the next focussed element's tabIndex to 0
102 // This updates the tabIndex for a roving tabIndex
103 if (updateTabIndex) {
104 activeElement.tabIndex = -1;
105 moveTarget.tabIndex = 0;
106 }
107 // If a move target has been set by either arrow handler, focus that target
108 moveTarget.focus();
109 }
110};
111exports.handleArrows = handleArrows;
112/**
113 * This function is a helper for setting the initial tabIndexes in a roving tabIndex
114 *
115 * @param {HTMLElement[]} options Array of elements which should have a tabIndex of -1, except for the first element which will have a tabIndex of 0
116 */
117const setTabIndex = (options) => {
118 if (options && options.length > 0) {
119 // Iterate the options and set the tabIndex to -1 on every option
120 options.forEach((option) => {
121 option.tabIndex = -1;
122 });
123 // Manually set the tabIndex of the first option to 0
124 options[0].tabIndex = 0;
125 }
126};
127exports.setTabIndex = setTabIndex;
128class KeyboardHandler extends React.Component {
129 constructor() {
130 super(...arguments);
131 this.keyHandler = (event) => {
132 const { isEventFromContainer } = this.props;
133 // If the passed keyboard event is not from the container, ignore the event by returning
134 if (isEventFromContainer ? !isEventFromContainer(event) : !this._isEventFromContainer(event)) {
135 return;
136 }
137 const { isActiveElement, getFocusableElement, noVerticalArrowHandling, noHorizontalArrowHandling, noEnterHandling, noSpaceHandling, updateTabIndex, validSiblingTags, additionalKeyHandler, createNavigableElements, onlyTraverseSiblings } = this.props;
138 // Pass the event off to be handled by any custom handler
139 additionalKeyHandler && additionalKeyHandler(event);
140 // Initalize navigableElements from the createNavigableElements callback
141 const navigableElements = createNavigableElements();
142 if (!navigableElements) {
143 // eslint-disable-next-line no-console
144 console.warn('No navigable elements have been passed to the KeyboardHandler. Keyboard navigation provided by this component will be ignored.');
145 return;
146 }
147 const key = event.key;
148 // Handle enter key. If noEnterHandling is passed, skip this block
149 if (!noEnterHandling) {
150 if (key === 'Enter') {
151 event.preventDefault();
152 event.stopImmediatePropagation(); // For menus in menus
153 document.activeElement.click();
154 }
155 }
156 // Handle space key. If noSpaceHandling is passed, skip this block
157 if (!noSpaceHandling) {
158 if (key === ' ') {
159 event.preventDefault();
160 event.stopImmediatePropagation(); // For menus in menus
161 document.activeElement.click();
162 }
163 }
164 // Inject helper handler for arrow navigation
165 exports.handleArrows(event, navigableElements, isActiveElement, getFocusableElement, validSiblingTags, noVerticalArrowHandling, noHorizontalArrowHandling, updateTabIndex, onlyTraverseSiblings);
166 };
167 this._isEventFromContainer = (event) => {
168 const { containerRef } = this.props;
169 return containerRef.current && containerRef.current.contains(event.target);
170 };
171 }
172 componentDidMount() {
173 if (util_1.canUseDOM) {
174 window.addEventListener('keydown', this.keyHandler);
175 }
176 }
177 componentWillUnmount() {
178 if (util_1.canUseDOM) {
179 window.removeEventListener('keydown', this.keyHandler);
180 }
181 }
182 render() {
183 return null;
184 }
185}
186exports.KeyboardHandler = KeyboardHandler;
187KeyboardHandler.displayName = 'KeyboardHandler';
188KeyboardHandler.defaultProps = {
189 containerRef: null,
190 createNavigableElements: () => null,
191 isActiveElement: (navigableElement) => document.activeElement === navigableElement,
192 getFocusableElement: (navigableElement) => navigableElement,
193 validSiblingTags: ['BUTTON', 'A'],
194 onlyTraverseSiblings: true,
195 updateTabIndex: true,
196 noHorizontalArrowHandling: false,
197 noVerticalArrowHandling: false,
198 noEnterHandling: false,
199 noSpaceHandling: false
200};
201//# sourceMappingURL=KeyboardHandler.js.map
\No newline at end of file