@samitouri / QOS-React-2 / commits / 34457729a6

[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 };