[react-interactions] Add allowModifiers flag to FocusList + FocusTable (#16971)
Dominic Gannaway committed
Oct 1, 2019 at 17:59 UTC
34457729a6b78bd319a8082cff032dd55e47c2e8
6 files changed
+172
-71
packages/react-interactions/accessibility/src/FocusList.js
+36
-15
@@ -24,6 +24,7 @@ type FocusListProps = {|
24
portrait: boolean,
25
wrap?: boolean,
26
tabScope?: ReactScope,
27
+ allowModifiers?: boolean,
28
|};
29
30
const {useRef} = React;
@@ -82,6 +83,13 @@ function getListProps(currentCell: ReactScopeMethods): Object {
83
return {};
84
}
85
86
+function hasModifierKey(event: KeyboardEvent): boolean {
87
+ const {altKey, ctrlKey, metaKey, shiftKey} = event;
88
+ return (
89
+ altKey === true || ctrlKey === true || metaKey === true || shiftKey === true
90
+ );
91
+}
92
+
93
export function createFocusList(scope: ReactScope): Array<React.Component> {
94
const TableScope = React.unstable_createScope(scope.fn);
95
@@ -90,6 +98,7 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
98
portrait,
99
wrap,
100
tabScope: TabScope,
101
+ allowModifiers,
102
}): FocusListProps {
103
const tabScopeRef = useRef(null);
104
return (
@@ -97,7 +106,8 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
106
type="list"
107
portrait={portrait}
108
wrap={wrap}
100
- tabScopeRef={tabScopeRef}>
109
+ tabScopeRef={tabScopeRef}
110
+ allowModifiers={allowModifiers}>
111
{TabScope ? (
112
<TabScope ref={tabScopeRef}>{children}</TabScope>
113
) : (
@@ -117,25 +127,36 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
127
const listProps = list && list.getProps();
128
if (list !== null && listProps.type === 'list') {
129
const portrait = listProps.portrait;
120
- switch (event.key) {
121
- case 'Tab': {
122
- const tabScope = getListProps(currentItem).tabScopeRef.current;
123
- if (tabScope) {
124
- const activeNode = document.activeElement;
125
- const nodes = tabScope.getScopedNodes();
126
- for (let i = 0; i < nodes.length; i++) {
127
- const node = nodes[i];
128
- if (node !== activeNode) {
129
- setElementCanTab(node, false);
130
- } else {
131
- setElementCanTab(node, true);
132
- }
130
+ const key = event.key;
131
+
132
+ if (key === 'Tab') {
133
+ const tabScope = getListProps(currentItem).tabScopeRef.current;
134
+ if (tabScope) {
135
+ const activeNode = document.activeElement;
136
+ const nodes = tabScope.getScopedNodes();
137
+ for (let i = 0; i < nodes.length; i++) {
138
+ const node = nodes[i];
139
+ if (node !== activeNode) {
140
+ setElementCanTab(node, false);
141
+ } else {
142
+ setElementCanTab(node, true);
143
}
134
- return;
144
}
145
+ return;
146
+ }
147
+ event.continuePropagation();
148
+ return;
149
+ }
150
+ // Using modifier keys with keyboard arrow events should be no-ops
151
+ // unless an explicit allowModifiers flag is set on the FocusList.
152
+ if (hasModifierKey(event)) {
153
+ const allowModifiers = getListProps(currentItem).allowModifiers;
154
+ if (!allowModifiers) {
155
event.continuePropagation();
156
return;
157
}
158
+ }
159
+ switch (key) {
160
case 'ArrowUp': {
161
if (portrait) {
162
const previousListItem = getPreviousListItem(
packages/react-interactions/accessibility/src/FocusTable.js
+35
-15
@@ -32,6 +32,7 @@ type FocusTableProps = {|
32
) => void,
33
wrap?: boolean,
34
tabScope?: ReactScope,
35
+ allowModifiers?: boolean,
36
|};
37
38
const {useRef} = React;
@@ -152,6 +153,13 @@ function getTableProps(currentCell: ReactScopeMethods): Object {
153
return {};
154
}
155
156
+function hasModifierKey(event: KeyboardEvent): boolean {
157
+ const {altKey, ctrlKey, metaKey, shiftKey} = event;
158
+ return (
159
+ altKey === true || ctrlKey === true || metaKey === true || shiftKey === true
160
+ );
161
+}
162
+
163
export function createFocusTable(scope: ReactScope): Array<React.Component> {
164
const TableScope = React.unstable_createScope(scope.fn);
165
@@ -161,6 +169,7 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
169
id,
170
wrap,
171
tabScope: TabScope,
172
+ allowModifiers,
173
}): FocusTableProps {
174
const tabScopeRef = useRef(null);
175
return (
@@ -169,7 +178,8 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
178
onKeyboardOut={onKeyboardOut}
179
id={id}
180
wrap={wrap}
172
- tabScopeRef={tabScopeRef}>
181
+ tabScopeRef={tabScopeRef}
182
+ allowModifiers={allowModifiers}>
183
{TabScope ? (
184
<TabScope ref={tabScopeRef}>{children}</TabScope>
185
) : (
@@ -192,25 +202,35 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
202
event.continuePropagation();
203
return;
204
}
195
- switch (event.key) {
196
- case 'Tab': {
197
- const tabScope = getTableProps(currentCell).tabScopeRef.current;
198
- if (tabScope) {
199
- const activeNode = document.activeElement;
200
- const nodes = tabScope.getScopedNodes();
201
- for (let i = 0; i < nodes.length; i++) {
202
- const node = nodes[i];
203
- if (node !== activeNode) {
204
- setElementCanTab(node, false);
205
- } else {
206
- setElementCanTab(node, true);
207
- }
205
+ const key = event.key;
206
+ if (key === 'Tab') {
207
+ const tabScope = getTableProps(currentCell).tabScopeRef.current;
208
+ if (tabScope) {
209
+ const activeNode = document.activeElement;
210
+ const nodes = tabScope.getScopedNodes();
211
+ for (let i = 0; i < nodes.length; i++) {
212
+ const node = nodes[i];
213
+ if (node !== activeNode) {
214
+ setElementCanTab(node, false);
215
+ } else {
216
+ setElementCanTab(node, true);
217
}
209
- return;
218
}
219
+ return;
220
+ }
221
+ event.continuePropagation();
222
+ return;
223
+ }
224
+ // Using modifier keys with keyboard arrow events should be no-ops
225
+ // unless an explicit allowModifiers flag is set on the FocusTable.
226
+ if (hasModifierKey(event)) {
227
+ const allowModifiers = getTableProps(currentCell).allowModifiers;
228
+ if (!allowModifiers) {
229
event.continuePropagation();
230
return;
231
}
232
+ }
233
+ switch (key) {
234
case 'ArrowUp': {
235
const [cells, cellIndex] = getRowCells(currentCell);
236
if (cells !== null) {
packages/react-interactions/accessibility/src/__tests__/FocusList-test.internal.js
+30
-6
@@ -7,10 +7,8 @@
7
* @flow
8
*/
9
10
-import {
11
- createEventTarget,
12
- emulateBrowserTab,
13
-} from 'react-interactions/events/src/dom/testing-library';
10
+import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
11
+import {emulateBrowserTab} from '../emulateBrowserTab';
12
13
let React;
14
let ReactFeatureFlags;
@@ -46,8 +44,11 @@ describe('FocusList', () => {
44
function createFocusListComponent() {
45
const [FocusList, FocusItem] = createFocusList(TabbableScope);
46
49
- return ({portrait, wrap}) => (
50
- <FocusList portrait={portrait} wrap={wrap}>
47
+ return ({portrait, wrap, allowModifiers}) => (
48
+ <FocusList
49
+ portrait={portrait}
50
+ wrap={wrap}
51
+ allowModifiers={allowModifiers}>
52
<ul>
53
<FocusItem>
54
<li tabIndex={0}>Item 1</li>
@@ -94,6 +95,12 @@ describe('FocusList', () => {
95
key: 'ArrowLeft',
96
});
97
expect(document.activeElement.textContent).toBe('Item 3');
98
+ // Should be a no-op due to modifier
99
+ thirdListItem.keydown({
100
+ key: 'ArrowUp',
101
+ altKey: true,
102
+ });
103
+ expect(document.activeElement.textContent).toBe('Item 3');
104
});
105
106
it('handles keyboard arrow operations (landscape)', () => {
@@ -160,6 +167,23 @@ describe('FocusList', () => {
167
expect(document.activeElement.textContent).toBe('Item 3');
168
});
169
170
+ it('handles keyboard arrow operations (portrait) with allowModifiers', () => {
171
+ const Test = createFocusListComponent();
172
+
173
+ ReactDOM.render(
174
+ <Test portrait={true} allowModifiers={true} />,
175
+ container,
176
+ );
177
+ const listItems = document.querySelectorAll('li');
178
+ let firstListItem = createEventTarget(listItems[0]);
179
+ firstListItem.focus();
180
+ firstListItem.keydown({
181
+ key: 'ArrowDown',
182
+ altKey: true,
183
+ });
184
+ expect(document.activeElement.textContent).toBe('Item 2');
185
+ });
186
+
187
it('handles keyboard arrow operations mixed with tabbing', () => {
188
const [FocusList, FocusItem] = createFocusList(TabbableScope);
189
const beforeRef = React.createRef();
packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js
+31
-7
@@ -7,10 +7,8 @@
7
* @flow
8
*/
9
10
-import {
11
- createEventTarget,
12
- emulateBrowserTab,
13
-} from 'react-interactions/events/src/dom/testing-library';
10
+import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
11
+import {emulateBrowserTab} from '../emulateBrowserTab';
12
13
let React;
14
let ReactFeatureFlags;
@@ -48,8 +46,12 @@ describe('FocusTable', () => {
46
TabbableScope,
47
);
48
51
- return ({onKeyboardOut, id, wrap}) => (
52
- <FocusTable onKeyboardOut={onKeyboardOut} id={id} wrap={wrap}>
49
+ return ({onKeyboardOut, id, wrap, allowModifiers}) => (
50
+ <FocusTable
51
+ onKeyboardOut={onKeyboardOut}
52
+ id={id}
53
+ wrap={wrap}
54
+ allowModifiers={allowModifiers}>
55
<table>
56
<tbody>
57
<FocusTableRow>
@@ -115,6 +117,20 @@ describe('FocusTable', () => {
117
);
118
}
119
120
+ it('handles keyboard arrow operations with allowModifiers', () => {
121
+ const Test = createFocusTableComponent();
122
+
123
+ ReactDOM.render(<Test allowModifiers={true} />, container);
124
+ const buttons = document.querySelectorAll('button');
125
+ const a1 = createEventTarget(buttons[0]);
126
+ a1.focus();
127
+ a1.keydown({
128
+ key: 'ArrowRight',
129
+ altKey: true,
130
+ });
131
+ expect(document.activeElement.textContent).toBe('A2');
132
+ });
133
+
134
it('handles keyboard arrow operations', () => {
135
const Test = createFocusTableComponent();
136
@@ -133,7 +149,7 @@ describe('FocusTable', () => {
149
});
150
expect(document.activeElement.textContent).toBe('B2');
151
136
- const b2 = createEventTarget(document.activeElement);
152
+ let b2 = createEventTarget(document.activeElement);
153
b2.keydown({
154
key: 'ArrowLeft',
155
});
@@ -154,6 +170,14 @@ describe('FocusTable', () => {
170
key: 'ArrowUp',
171
});
172
expect(document.activeElement.textContent).toBe('B1');
173
+ // Should be a no-op due to modifier
174
+ b2 = createEventTarget(document.activeElement);
175
+ b2.keydown({
176
+ key: 'ArrowUp',
177
+ altKey: true,
178
+ });
179
+ b2 = createEventTarget(document.activeElement);
180
+ expect(document.activeElement.textContent).toBe('B1');
181
});
182
183
it('handles keyboard arrow operations between tables', () => {
packages/react-interactions/accessibility/src/emulateBrowserTab.js
new
+40
@@ -0,0 +1,40 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
13
+
14
+// This function is used by the a11y modules for testing
15
+export function emulateBrowserTab(backwards) {
16
+ const activeElement = document.activeElement;
17
+ const focusedElem = createEventTarget(activeElement);
18
+ let defaultPrevented = false;
19
+ focusedElem.keydown({
20
+ key: 'Tab',
21
+ shiftKey: backwards,
22
+ preventDefault() {
23
+ defaultPrevented = true;
24
+ },
25
+ });
26
+ if (!defaultPrevented) {
27
+ // This is not a full spec compliant version, but should be suffice for this test
28
+ const focusableElems = Array.from(
29
+ document.querySelectorAll(
30
+ 'input, button, select, textarea, a[href], [tabindex], [contenteditable], iframe, object, embed',
31
+ ),
32
+ ).filter(
33
+ elem => elem.tabIndex > -1 && !elem.disabled && !elem.contentEditable,
34
+ );
35
+ const idx = focusableElems.indexOf(activeElement);
36
+ if (idx !== -1) {
37
+ focusableElems[backwards ? idx - 1 : idx + 1].focus();
38
+ }
39
+ }
40
+}
packages/react-interactions/events/src/dom/testing-library/index.js
-28
@@ -158,33 +158,6 @@ function testWithPointerType(message, testFn) {
158
});
159
}
160
161
-function emulateBrowserTab(backwards) {
162
- const activeElement = document.activeElement;
163
- const focusedElem = createEventTarget(activeElement);
164
- let defaultPrevented = false;
165
- focusedElem.keydown({
166
- key: 'Tab',
167
- shiftKey: backwards,
168
- preventDefault() {
169
- defaultPrevented = true;
170
- },
171
- });
172
- if (!defaultPrevented) {
173
- // This is not a full spec compliant version, but should be suffice for this test
174
- const focusableElems = Array.from(
175
- document.querySelectorAll(
176
- 'input, button, select, textarea, a[href], [tabindex], [contenteditable], iframe, object, embed',
177
- ),
178
- ).filter(
179
- elem => elem.tabIndex > -1 && !elem.disabled && !elem.contentEditable,
180
- );
181
- const idx = focusableElems.indexOf(activeElement);
182
- if (idx !== -1) {
183
- focusableElems[backwards ? idx - 1 : idx + 1].focus();
184
- }
185
- }
186
-}
187
-
161
export {
162
buttonsType,
163
createEventTarget,
@@ -193,5 +166,4 @@ export {
166
hasPointerEvent,
167
setPointerEvent,
168
testWithPointerType,
196
- emulateBrowserTab,
169
};