1 | ;
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.KeyboardHandler = exports.setTabIndex = exports.handleArrows = void 0;
|
4 | const tslib_1 = require("tslib");
|
5 | const React = tslib_1.__importStar(require("react"));
|
6 | const 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 | */
|
20 | const 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 | };
|
111 | exports.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 | */
|
117 | const 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 | };
|
127 | exports.setTabIndex = setTabIndex;
|
128 | class 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 | }
|
186 | exports.KeyboardHandler = KeyboardHandler;
|
187 | KeyboardHandler.displayName = 'KeyboardHandler';
|
188 | KeyboardHandler.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 |