Skip to content

Commit 3445772

Browse files
authored
[react-interactions] Add allowModifiers flag to FocusList + FocusTable (#16971)
1 parent b34f042 commit 3445772

File tree

6 files changed

+172
-71
lines changed

6 files changed

+172
-71
lines changed

packages/react-interactions/accessibility/src/FocusList.js

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type FocusListProps = {|
2424
portrait: boolean,
2525
wrap?: boolean,
2626
tabScope?: ReactScope,
27+
allowModifiers?: boolean,
2728
|};
2829

2930
const {useRef} = React;
@@ -82,6 +83,13 @@ function getListProps(currentCell: ReactScopeMethods): Object {
8283
return {};
8384
}
8485

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+
8593
export function createFocusList(scope: ReactScope): Array<React.Component> {
8694
const TableScope = React.unstable_createScope(scope.fn);
8795

@@ -90,14 +98,16 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
9098
portrait,
9199
wrap,
92100
tabScope: TabScope,
101+
allowModifiers,
93102
}): FocusListProps {
94103
const tabScopeRef = useRef(null);
95104
return (
96105
<TableScope
97106
type="list"
98107
portrait={portrait}
99108
wrap={wrap}
100-
tabScopeRef={tabScopeRef}>
109+
tabScopeRef={tabScopeRef}
110+
allowModifiers={allowModifiers}>
101111
{TabScope ? (
102112
<TabScope ref={tabScopeRef}>{children}</TabScope>
103113
) : (
@@ -117,25 +127,36 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
117127
const listProps = list && list.getProps();
118128
if (list !== null && listProps.type === 'list') {
119129
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);
133143
}
134-
return;
135144
}
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) {
136155
event.continuePropagation();
137156
return;
138157
}
158+
}
159+
switch (key) {
139160
case 'ArrowUp': {
140161
if (portrait) {
141162
const previousListItem = getPreviousListItem(

packages/react-interactions/accessibility/src/FocusTable.js

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type FocusTableProps = {|
3232
) => void,
3333
wrap?: boolean,
3434
tabScope?: ReactScope,
35+
allowModifiers?: boolean,
3536
|};
3637

3738
const {useRef} = React;
@@ -152,6 +153,13 @@ function getTableProps(currentCell: ReactScopeMethods): Object {
152153
return {};
153154
}
154155

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+
155163
export function createFocusTable(scope: ReactScope): Array<React.Component> {
156164
const TableScope = React.unstable_createScope(scope.fn);
157165

@@ -161,6 +169,7 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
161169
id,
162170
wrap,
163171
tabScope: TabScope,
172+
allowModifiers,
164173
}): FocusTableProps {
165174
const tabScopeRef = useRef(null);
166175
return (
@@ -169,7 +178,8 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
169178
onKeyboardOut={onKeyboardOut}
170179
id={id}
171180
wrap={wrap}
172-
tabScopeRef={tabScopeRef}>
181+
tabScopeRef={tabScopeRef}
182+
allowModifiers={allowModifiers}>
173183
{TabScope ? (
174184
<TabScope ref={tabScopeRef}>{children}</TabScope>
175185
) : (
@@ -192,25 +202,35 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
192202
event.continuePropagation();
193203
return;
194204
}
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);
208217
}
209-
return;
210218
}
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) {
211229
event.continuePropagation();
212230
return;
213231
}
232+
}
233+
switch (key) {
214234
case 'ArrowUp': {
215235
const [cells, cellIndex] = getRowCells(currentCell);
216236
if (cells !== null) {

packages/react-interactions/accessibility/src/__tests__/FocusList-test.internal.js

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
* @flow
88
*/
99

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';
1412

1513
let React;
1614
let ReactFeatureFlags;
@@ -46,8 +44,11 @@ describe('FocusList', () => {
4644
function createFocusListComponent() {
4745
const [FocusList, FocusItem] = createFocusList(TabbableScope);
4846

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}>
5152
<ul>
5253
<FocusItem>
5354
<li tabIndex={0}>Item 1</li>
@@ -94,6 +95,12 @@ describe('FocusList', () => {
9495
key: 'ArrowLeft',
9596
});
9697
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');
97104
});
98105

99106
it('handles keyboard arrow operations (landscape)', () => {
@@ -160,6 +167,23 @@ describe('FocusList', () => {
160167
expect(document.activeElement.textContent).toBe('Item 3');
161168
});
162169

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+
163187
it('handles keyboard arrow operations mixed with tabbing', () => {
164188
const [FocusList, FocusItem] = createFocusList(TabbableScope);
165189
const beforeRef = React.createRef();

packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
* @flow
88
*/
99

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';
1412

1513
let React;
1614
let ReactFeatureFlags;
@@ -48,8 +46,12 @@ describe('FocusTable', () => {
4846
TabbableScope,
4947
);
5048

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}>
5355
<table>
5456
<tbody>
5557
<FocusTableRow>
@@ -115,6 +117,20 @@ describe('FocusTable', () => {
115117
);
116118
}
117119

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+
118134
it('handles keyboard arrow operations', () => {
119135
const Test = createFocusTableComponent();
120136

@@ -133,7 +149,7 @@ describe('FocusTable', () => {
133149
});
134150
expect(document.activeElement.textContent).toBe('B2');
135151

136-
const b2 = createEventTarget(document.activeElement);
152+
let b2 = createEventTarget(document.activeElement);
137153
b2.keydown({
138154
key: 'ArrowLeft',
139155
});
@@ -154,6 +170,14 @@ describe('FocusTable', () => {
154170
key: 'ArrowUp',
155171
});
156172
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');
157181
});
158182

159183
it('handles keyboard arrow operations between tables', () => {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -158,33 +158,6 @@ function testWithPointerType(message, testFn) {
158158
});
159159
}
160160

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-
188161
export {
189162
buttonsType,
190163
createEventTarget,
@@ -193,5 +166,4 @@ export {
193166
hasPointerEvent,
194167
setPointerEvent,
195168
testWithPointerType,
196-
emulateBrowserTab,
197169
};

0 commit comments

Comments
 (0)