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

[react-interactions] Add wrapping support to FocusList/FocusTable (#16903)

Dominic Gannaway committed Sep 26, 2019 at 15:15 UTC b9811ed5bc710f6318eed6292ad76e4874ac7f99
4 files changed +160 -34
packages/react-interactions/accessibility/src/FocusList.js
+32 -14
@@ -15,19 +15,22 @@ import {useKeyboard} from 'react-interactions/events/keyboard';
15
16 type FocusItemProps = {
17 children?: React.Node,
18 + onKeyDown?: KeyboardEvent => void,
19 };
20
21 type FocusListProps = {|
22 children: React.Node,
23 portrait: boolean,
24 + wrap?: boolean,
25 |};
26
27 const {useRef} = React;
28
27 -function focusListItem(cell: ReactScopeMethods): void {
29 +function focusListItem(cell: ReactScopeMethods, event: KeyboardEvent): void {
30 const tabbableNodes = cell.getScopedNodes();
31 if (tabbableNodes !== null && tabbableNodes.length > 0) {
32 tabbableNodes[0].focus();
33 + event.preventDefault();
34 }
35 }
36
@@ -38,7 +41,10 @@ function getPreviousListItem(
41 const items = list.getChildren();
42 if (items !== null) {
43 const currentItemIndex = items.indexOf(currentItem);
41 - if (currentItemIndex > 0) {
44 + const wrap = getListWrapProp(currentItem);
45 + if (currentItemIndex === 0 && wrap) {
46 + return items[items.length - 1] || null;
47 + } else if (currentItemIndex > 0) {
48 return items[currentItemIndex - 1] || null;
49 }
50 }
@@ -52,25 +58,38 @@ function getNextListItem(
58 const items = list.getChildren();
59 if (items !== null) {
60 const currentItemIndex = items.indexOf(currentItem);
55 - if (currentItemIndex !== -1 && currentItemIndex !== items.length - 1) {
61 + const wrap = getListWrapProp(currentItem);
62 + const end = currentItemIndex === items.length - 1;
63 + if (end && wrap) {
64 + return items[0] || null;
65 + } else if (currentItemIndex !== -1 && !end) {
66 return items[currentItemIndex + 1] || null;
67 }
68 }
69 return null;
70 }
71
72 +function getListWrapProp(currentItem: ReactScopeMethods): boolean {
73 + const list = currentItem.getParent();
74 + if (list !== null) {
75 + const listProps = list.getProps();
76 + return (listProps.type === 'list' && listProps.wrap) || false;
77 + }
78 + return false;
79 +}
80 +
81 export function createFocusList(scope: ReactScope): Array<React.Component> {
82 const TableScope = React.unstable_createScope(scope.fn);
83
65 - function List({children, portrait}): FocusListProps {
84 + function List({children, portrait, wrap}): FocusListProps {
85 return (
67 - <TableScope type="list" portrait={portrait}>
86 + <TableScope type="list" portrait={portrait} wrap={wrap}>
87 {children}
88 </TableScope>
89 );
90 }
91
73 - function Item({children}): FocusItemProps {
92 + function Item({children, onKeyDown}): FocusItemProps {
93 const scopeRef = useRef(null);
94 const keyboard = useKeyboard({
95 onKeyDown(event: KeyboardEvent): void {
@@ -88,8 +107,7 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
107 currentItem,
108 );
109 if (previousListItem) {
91 - event.preventDefault();
92 - focusListItem(previousListItem);
110 + focusListItem(previousListItem, event);
111 return;
112 }
113 }
@@ -99,8 +117,7 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
117 if (portrait) {
118 const nextListItem = getNextListItem(list, currentItem);
119 if (nextListItem) {
102 - event.preventDefault();
103 - focusListItem(nextListItem);
120 + focusListItem(nextListItem, event);
121 return;
122 }
123 }
@@ -113,8 +130,7 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
130 currentItem,
131 );
132 if (previousListItem) {
116 - event.preventDefault();
117 - focusListItem(previousListItem);
133 + focusListItem(previousListItem, event);
134 return;
135 }
136 }
@@ -124,8 +140,7 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
140 if (!portrait) {
141 const nextListItem = getNextListItem(list, currentItem);
142 if (nextListItem) {
127 - event.preventDefault();
128 - focusListItem(nextListItem);
143 + focusListItem(nextListItem, event);
144 return;
145 }
146 }
@@ -134,6 +149,9 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
149 }
150 }
151 }
152 + if (onKeyDown) {
153 + onKeyDown(event);
154 + }
155 event.continuePropagation();
156 },
157 });
packages/react-interactions/accessibility/src/FocusTable.js
+62 -16
@@ -15,6 +15,7 @@ import {useKeyboard} from 'react-interactions/events/keyboard';
15
16 type FocusCellProps = {
17 children?: React.Node,
18 + onKeyDown?: KeyboardEvent => void,
19 };
20
21 type FocusRowProps = {
@@ -28,6 +29,7 @@ type FocusTableProps = {|
29 direction: 'left' | 'right' | 'up' | 'down',
30 focusTableByID: (id: string) => void,
31 ) => void,
32 + wrap?: boolean,
33 |};
34
35 const {useRef} = React;
@@ -54,19 +56,26 @@ export function focusFirstCellOnTable(table: ReactScopeMethods): void {
56 }
57 }
58
57 -function focusCell(cell: ReactScopeMethods): void {
59 +function focusCell(cell: ReactScopeMethods, event?: KeyboardEvent): void {
60 const tabbableNodes = cell.getScopedNodes();
61 if (tabbableNodes !== null && tabbableNodes.length > 0) {
62 tabbableNodes[0].focus();
63 + if (event) {
64 + event.preventDefault();
65 + }
66 }
67 }
68
64 -function focusCellByIndex(row: ReactScopeMethods, cellIndex: number): void {
69 +function focusCellByIndex(
70 + row: ReactScopeMethods,
71 + cellIndex: number,
72 + event?: KeyboardEvent,
73 +): void {
74 const cells = row.getChildren();
75 if (cells !== null) {
76 const cell = cells[cellIndex];
77 if (cell) {
69 - focusCell(cell);
78 + focusCell(cell, event);
79 }
80 }
81 }
@@ -130,12 +139,27 @@ function triggerNavigateOut(
139 event.continuePropagation();
140 }
141
142 +function getTableWrapProp(currentCell: ReactScopeMethods): boolean {
143 + const row = currentCell.getParent();
144 + if (row !== null && row.getProps().type === 'row') {
145 + const table = row.getParent();
146 + if (table !== null) {
147 + return table.getProps().wrap || false;
148 + }
149 + }
150 + return false;
151 +}
152 +
153 export function createFocusTable(scope: ReactScope): Array<React.Component> {
154 const TableScope = React.unstable_createScope(scope.fn);
155
136 - function Table({children, onKeyboardOut, id}): FocusTableProps {
156 + function Table({children, onKeyboardOut, id, wrap}): FocusTableProps {
157 return (
138 - <TableScope type="table" onKeyboardOut={onKeyboardOut} id={id}>
158 + <TableScope
159 + type="table"
160 + onKeyboardOut={onKeyboardOut}
161 + id={id}
162 + wrap={wrap}>
163 {children}
164 </TableScope>
165 );
@@ -145,7 +169,7 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
169 return <TableScope type="row">{children}</TableScope>;
170 }
171
148 - function Cell({children}): FocusCellProps {
172 + function Cell({children, onKeyDown}): FocusCellProps {
173 const scopeRef = useRef(null);
174 const keyboard = useKeyboard({
175 onKeyDown(event: KeyboardEvent): void {
@@ -162,10 +186,15 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
186 if (rows !== null) {
187 if (rowIndex > 0) {
188 const row = rows[rowIndex - 1];
165 - focusCellByIndex(row, cellIndex);
166 - event.preventDefault();
189 + focusCellByIndex(row, cellIndex, event);
190 } else if (rowIndex === 0) {
168 - triggerNavigateOut(currentCell, 'up', event);
191 + const wrap = getTableWrapProp(currentCell);
192 + if (wrap) {
193 + const row = rows[rows.length - 1];
194 + focusCellByIndex(row, cellIndex, event);
195 + } else {
196 + triggerNavigateOut(currentCell, 'up', event);
197 + }
198 }
199 }
200 }
@@ -178,11 +207,16 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
207 if (rows !== null) {
208 if (rowIndex !== -1) {
209 if (rowIndex === rows.length - 1) {
181 - triggerNavigateOut(currentCell, 'down', event);
210 + const wrap = getTableWrapProp(currentCell);
211 + if (wrap) {
212 + const row = rows[0];
213 + focusCellByIndex(row, cellIndex, event);
214 + } else {
215 + triggerNavigateOut(currentCell, 'down', event);
216 + }
217 } else {
218 const row = rows[rowIndex + 1];
184 - focusCellByIndex(row, cellIndex);
185 - event.preventDefault();
219 + focusCellByIndex(row, cellIndex, event);
220 }
221 }
222 }
@@ -196,7 +230,12 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
230 focusCell(cells[rowIndex - 1]);
231 event.preventDefault();
232 } else if (rowIndex === 0) {
199 - triggerNavigateOut(currentCell, 'left', event);
233 + const wrap = getTableWrapProp(currentCell);
234 + if (wrap) {
235 + focusCell(cells[cells.length - 1], event);
236 + } else {
237 + triggerNavigateOut(currentCell, 'left', event);
238 + }
239 }
240 }
241 return;
@@ -206,16 +245,23 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
245 if (cells !== null) {
246 if (rowIndex !== -1) {
247 if (rowIndex === cells.length - 1) {
209 - triggerNavigateOut(currentCell, 'right', event);
248 + const wrap = getTableWrapProp(currentCell);
249 + if (wrap) {
250 + focusCell(cells[0], event);
251 + } else {
252 + triggerNavigateOut(currentCell, 'right', event);
253 + }
254 } else {
211 - focusCell(cells[rowIndex + 1]);
212 - event.preventDefault();
255 + focusCell(cells[rowIndex + 1], event);
256 }
257 }
258 }
259 return;
260 }
261 }
262 + if (onKeyDown) {
263 + onKeyDown(event);
264 + }
265 },
266 });
267 return (
packages/react-interactions/accessibility/src/__tests__/FocusList-test.internal.js
+33 -2
@@ -43,8 +43,8 @@ describe('FocusList', () => {
43 function createFocusListComponent() {
44 const [FocusList, FocusItem] = createFocusList(TabbableScope);
45
46 - return ({portrait}) => (
47 - <FocusList portrait={portrait}>
46 + return ({portrait, wrap}) => (
47 + <FocusList portrait={portrait} wrap={wrap}>
48 <ul>
49 <FocusItem>
50 <li tabIndex={0}>Item 1</li>
@@ -125,5 +125,36 @@ describe('FocusList', () => {
125 });
126 expect(document.activeElement.textContent).toBe('Item 3');
127 });
128 +
129 + it('handles keyboard arrow operations (portrait) with wrapping enabled', () => {
130 + const Test = createFocusListComponent();
131 +
132 + ReactDOM.render(<Test portrait={true} wrap={true} />, container);
133 + const listItems = document.querySelectorAll('li');
134 + let firstListItem = createEventTarget(listItems[0]);
135 + firstListItem.focus();
136 + firstListItem.keydown({
137 + key: 'ArrowDown',
138 + });
139 + expect(document.activeElement.textContent).toBe('Item 2');
140 +
141 + const secondListItem = createEventTarget(document.activeElement);
142 + secondListItem.keydown({
143 + key: 'ArrowDown',
144 + });
145 + expect(document.activeElement.textContent).toBe('Item 3');
146 +
147 + const thirdListItem = createEventTarget(document.activeElement);
148 + thirdListItem.keydown({
149 + key: 'ArrowDown',
150 + });
151 + expect(document.activeElement.textContent).toBe('Item 1');
152 +
153 + firstListItem = createEventTarget(document.activeElement);
154 + firstListItem.keydown({
155 + key: 'ArrowUp',
156 + });
157 + expect(document.activeElement.textContent).toBe('Item 3');
158 + });
159 });
160 });
packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js
+33 -2
@@ -45,8 +45,8 @@ describe('FocusTable', () => {
45 TabbableScope,
46 );
47
48 - return ({onKeyboardOut, id}) => (
49 - <FocusTable onKeyboardOut={onKeyboardOut} id={id}>
48 + return ({onKeyboardOut, id, wrap}) => (
49 + <FocusTable onKeyboardOut={onKeyboardOut} id={id} wrap={wrap}>
50 <table>
51 <tbody>
52 <FocusTableRow>
@@ -326,5 +326,36 @@ describe('FocusTable', () => {
326 });
327 expect(document.activeElement.placeholder).toBe('B1');
328 });
329 +
330 + it('handles keyboard arrow operations with wrapping enabled', () => {
331 + const Test = createFocusTableComponent();
332 +
333 + ReactDOM.render(<Test wrap={true} />, container);
334 + const buttons = document.querySelectorAll('button');
335 + let a1 = createEventTarget(buttons[0]);
336 + a1.focus();
337 + a1.keydown({
338 + key: 'ArrowRight',
339 + });
340 + expect(document.activeElement.textContent).toBe('A2');
341 +
342 + const a2 = createEventTarget(document.activeElement);
343 + a2.keydown({
344 + key: 'ArrowRight',
345 + });
346 + expect(document.activeElement.textContent).toBe('A3');
347 +
348 + const a3 = createEventTarget(document.activeElement);
349 + a3.keydown({
350 + key: 'ArrowRight',
351 + });
352 + expect(document.activeElement.textContent).toBe('A1');
353 +
354 + a1 = createEventTarget(document.activeElement);
355 + a1.keydown({
356 + key: 'ArrowLeft',
357 + });
358 + expect(document.activeElement.textContent).toBe('A3');
359 + });
360 });
361 });