@samitouri / QOS-React-2 / commits / ac8e8b3278

[react-interactions] Add tab handling to FocusList (#16958)

Dominic Gannaway committed Sep 30, 2019 at 17:13 UTC ac8e8b3278c69ddb6267ca0658c49af34cc4e785
7 files changed +153 -38
packages/react-interactions/accessibility/src/FocusList.js
+45 -9
@@ -12,6 +12,7 @@ import type {KeyboardEvent} from 'react-interactions/events/keyboard';
12
13 import React from 'react';
14 import {useKeyboard} from 'react-interactions/events/keyboard';
15 +import {setElementCanTab} from 'react-interactions/accessibility/focus-control';
16
17 type FocusItemProps = {
18 children?: React.Node,
@@ -22,6 +23,7 @@ type FocusListProps = {|
23 children: React.Node,
24 portrait: boolean,
25 wrap?: boolean,
26 + tabScope?: ReactScope,
27 |};
28
29 const {useRef} = React;
@@ -41,7 +43,7 @@ function getPreviousListItem(
43 const items = list.getChildren();
44 if (items !== null) {
45 const currentItemIndex = items.indexOf(currentItem);
44 - const wrap = getListWrapProp(currentItem);
46 + const wrap = getListProps(currentItem).wrap;
47 if (currentItemIndex === 0 && wrap) {
48 return items[items.length - 1] || null;
49 } else if (currentItemIndex > 0) {
@@ -58,7 +60,7 @@ function getNextListItem(
60 const items = list.getChildren();
61 if (items !== null) {
62 const currentItemIndex = items.indexOf(currentItem);
61 - const wrap = getListWrapProp(currentItem);
63 + const wrap = getListProps(currentItem).wrap;
64 const end = currentItemIndex === items.length - 1;
65 if (end && wrap) {
66 return items[0] || null;
@@ -69,22 +71,38 @@ function getNextListItem(
71 return null;
72 }
73
72 -function getListWrapProp(currentItem: ReactScopeMethods): boolean {
73 - const list = currentItem.getParent();
74 +function getListProps(currentCell: ReactScopeMethods): Object {
75 + const list = currentCell.getParent();
76 if (list !== null) {
77 const listProps = list.getProps();
76 - return (listProps.type === 'list' && listProps.wrap) || false;
78 + if (listProps && listProps.type === 'list') {
79 + return listProps;
80 + }
81 }
78 - return false;
82 + return {};
83 }
84
85 export function createFocusList(scope: ReactScope): Array<React.Component> {
86 const TableScope = React.unstable_createScope(scope.fn);
87
84 - function List({children, portrait, wrap}): FocusListProps {
88 + function List({
89 + children,
90 + portrait,
91 + wrap,
92 + tabScope: TabScope,
93 + }): FocusListProps {
94 + const tabScopeRef = useRef(null);
95 return (
86 - <TableScope type="list" portrait={portrait} wrap={wrap}>
87 - {children}
96 + <TableScope
97 + type="list"
98 + portrait={portrait}
99 + wrap={wrap}
100 + tabScopeRef={tabScopeRef}>
101 + {TabScope ? (
102 + <TabScope ref={tabScopeRef}>{children}</TabScope>
103 + ) : (
104 + children
105 + )}
106 </TableScope>
107 );
108 }
@@ -100,6 +118,24 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
118 if (list !== null && listProps.type === 'list') {
119 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 + }
133 + }
134 + return;
135 + }
136 + event.continuePropagation();
137 + return;
138 + }
139 case 'ArrowUp': {
140 if (portrait) {
141 const previousListItem = getPreviousListItem(
packages/react-interactions/accessibility/src/FocusManager.js
+1
@@ -64,6 +64,7 @@ const FocusManager = React.forwardRef(
64 onBlurWithin: function(event) {
65 if (!containFocus) {
66 event.continuePropagation();
67 + return;
68 }
69 const lastNode = event.target;
70 if (lastNode) {
packages/react-interactions/accessibility/src/FocusTable.js
+1
@@ -31,6 +31,7 @@ type FocusTableProps = {|
31 focusTableByID: (id: string) => void,
32 ) => void,
33 wrap?: boolean,
34 + tabScope?: ReactScope,
35 |};
36
37 const {useRef} = React;
packages/react-interactions/accessibility/src/__tests__/FocusList-test.internal.js
+73 -1
@@ -7,7 +7,10 @@
7 * @flow
8 */
9
10 -import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
10 +import {
11 + createEventTarget,
12 + emulateBrowserTab,
13 +} from 'react-interactions/events/src/dom/testing-library';
14
15 let React;
16 let ReactFeatureFlags;
@@ -156,5 +159,74 @@ describe('FocusList', () => {
159 });
160 expect(document.activeElement.textContent).toBe('Item 3');
161 });
162 +
163 + it('handles keyboard arrow operations mixed with tabbing', () => {
164 + const [FocusList, FocusItem] = createFocusList(TabbableScope);
165 + const beforeRef = React.createRef();
166 + const afterRef = React.createRef();
167 +
168 + function Test() {
169 + return (
170 + <>
171 + <input placeholder="Before" ref={beforeRef} />
172 + <FocusList tabScope={TabbableScope} portrait={true}>
173 + <ul>
174 + <FocusItem>
175 + <li>
176 + <input placeholder="A" />
177 + </li>
178 + </FocusItem>
179 + <FocusItem>
180 + <li>
181 + <input placeholder="B" />
182 + </li>
183 + </FocusItem>
184 + <FocusItem>
185 + <li>
186 + <input placeholder="C" />
187 + </li>
188 + </FocusItem>
189 + <FocusItem>
190 + <li>
191 + <input placeholder="D" />
192 + </li>
193 + </FocusItem>
194 + <FocusItem>
195 + <li>
196 + <input placeholder="E" />
197 + </li>
198 + </FocusItem>
199 + <FocusItem>
200 + <li>
201 + <input placeholder="F" />
202 + </li>
203 + </FocusItem>
204 + </ul>
205 + </FocusList>
206 + <input placeholder="After" ref={afterRef} />
207 + </>
208 + );
209 + }
210 +
211 + ReactDOM.render(<Test />, container);
212 + beforeRef.current.focus();
213 +
214 + expect(document.activeElement.placeholder).toBe('Before');
215 + emulateBrowserTab();
216 + expect(document.activeElement.placeholder).toBe('A');
217 + emulateBrowserTab();
218 + expect(document.activeElement.placeholder).toBe('After');
219 + emulateBrowserTab(true);
220 + expect(document.activeElement.placeholder).toBe('A');
221 + const a = createEventTarget(document.activeElement);
222 + a.keydown({
223 + key: 'ArrowDown',
224 + });
225 + expect(document.activeElement.placeholder).toBe('B');
226 + emulateBrowserTab();
227 + expect(document.activeElement.placeholder).toBe('After');
228 + emulateBrowserTab(true);
229 + expect(document.activeElement.placeholder).toBe('B');
230 + });
231 });
232 });
packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js
+4 -28
@@ -7,7 +7,10 @@
7 * @flow
8 */
9
10 -import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
10 +import {
11 + createEventTarget,
12 + emulateBrowserTab,
13 +} from 'react-interactions/events/src/dom/testing-library';
14
15 let React;
16 let ReactFeatureFlags;
@@ -29,33 +32,6 @@ describe('FocusTable', () => {
32 let ReactDOM;
33 let container;
34
32 - function emulateBrowserTab(backwards) {
33 - const activeElement = document.activeElement;
34 - const focusedElem = createEventTarget(activeElement);
35 - let defaultPrevented = false;
36 - focusedElem.keydown({
37 - key: 'Tab',
38 - shiftKey: backwards,
39 - preventDefault() {
40 - defaultPrevented = true;
41 - },
42 - });
43 - if (!defaultPrevented) {
44 - // This is not a full spec compliant version, but should be suffice for this test
45 - const focusableElems = Array.from(
46 - document.querySelectorAll(
47 - 'input, button, select, textarea, a[href], [tabindex], [contenteditable], iframe, object, embed',
48 - ),
49 - ).filter(
50 - elem => elem.tabIndex > -1 && !elem.disabled && !elem.contentEditable,
51 - );
52 - const idx = focusableElems.indexOf(activeElement);
53 - if (idx !== -1) {
54 - focusableElems[backwards ? idx - 1 : idx + 1].focus();
55 - }
56 - }
57 - }
58 -
35 beforeEach(() => {
36 ReactDOM = require('react-dom');
37 container = document.createElement('div');
packages/react-interactions/events/src/dom/testing-library/index.js
+28
@@ -158,6 +158,33 @@ 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 +
188 export {
189 buttonsType,
190 createEventTarget,
@@ -166,4 +193,5 @@ export {
193 hasPointerEvent,
194 setPointerEvent,
195 testWithPointerType,
196 + emulateBrowserTab,
197 };
scripts/rollup/bundles.js
+1
@@ -718,6 +718,7 @@ const bundles = [
718 'react',
719 'react-interactions/events/keyboard',
720 'react-interactions/accessibility/tabbable-scope',
721 + 'react-interactions/accessibility/focus-control',
722 ],
723 },
724 ];