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