Skip to content

Commit f40ceb0

Browse files
authored
[react-ui] FocusGrid -> ReactFocusTable + tweaks and fixes (#16806)
1 parent 2f1e8c5 commit f40ceb0

File tree

6 files changed

+482
-283
lines changed

6 files changed

+482
-283
lines changed

packages/react-reconciler/src/ReactFiberScope.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ export function createScopeMethods(
143143
}
144144
return null;
145145
},
146+
getProps(): Object {
147+
const currentFiber = ((instance.fiber: any): Fiber);
148+
return currentFiber.memoizedProps;
149+
},
146150
getScopedNodes(): null | Array<Object> {
147151
const currentFiber = ((instance.fiber: any): Fiber);
148152
const child = currentFiber.child;

packages/react-ui/accessibility/src/FocusGrid.js

Lines changed: 0 additions & 131 deletions
This file was deleted.
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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+
* @flow
8+
*/
9+
10+
import type {ReactScopeMethods} from 'shared/ReactTypes';
11+
import type {KeyboardEvent} from 'react-ui/events/keyboard';
12+
13+
import React from 'react';
14+
import {tabFocusableImpl} from './TabbableScope';
15+
import {useKeyboard} from 'react-ui/events/keyboard';
16+
17+
type FocusCellProps = {
18+
children?: React.Node,
19+
};
20+
21+
type FocusRowProps = {
22+
children: React.Node,
23+
};
24+
25+
type FocusTableProps = {|
26+
children: React.Node,
27+
id?: string,
28+
onKeyboardOut?: (
29+
direction: 'left' | 'right' | 'up' | 'down',
30+
focusTableByID: (id: string) => void,
31+
) => void,
32+
|};
33+
34+
const {useRef} = React;
35+
36+
export function focusFirstCellOnTable(table: ReactScopeMethods): void {
37+
const rows = table.getChildren();
38+
if (rows !== null) {
39+
const firstRow = rows[0];
40+
if (firstRow !== null) {
41+
const cells = firstRow.getChildren();
42+
if (cells !== null) {
43+
const firstCell = cells[0];
44+
if (firstCell !== null) {
45+
const tabbableNodes = firstCell.getScopedNodes();
46+
if (tabbableNodes !== null) {
47+
const firstElem = tabbableNodes[0];
48+
if (firstElem !== null) {
49+
firstElem.focus();
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}
57+
58+
function focusCell(cell: ReactScopeMethods): void {
59+
const tabbableNodes = cell.getScopedNodes();
60+
if (tabbableNodes !== null && tabbableNodes.length > 0) {
61+
tabbableNodes[0].focus();
62+
}
63+
}
64+
65+
function focusCellByRowIndex(row: ReactScopeMethods, rowIndex: number): void {
66+
const cells = row.getChildren();
67+
if (cells !== null) {
68+
const cell = cells[rowIndex];
69+
if (cell) {
70+
focusCell(cell);
71+
}
72+
}
73+
}
74+
75+
function getRowCells(currentCell: ReactScopeMethods) {
76+
const row = currentCell.getParent();
77+
if (row !== null && row.getProps().type === 'row') {
78+
const cells = row.getChildren();
79+
if (cells !== null) {
80+
const rowIndex = cells.indexOf(currentCell);
81+
return [cells, rowIndex];
82+
}
83+
}
84+
return [null, 0];
85+
}
86+
87+
function getRows(currentCell: ReactScopeMethods) {
88+
const row = currentCell.getParent();
89+
if (row !== null && row.getProps().type === 'row') {
90+
const table = row.getParent();
91+
if (table !== null && table.getProps().type === 'table') {
92+
const rows = table.getChildren();
93+
if (rows !== null) {
94+
const columnIndex = rows.indexOf(row);
95+
return [rows, columnIndex];
96+
}
97+
}
98+
}
99+
return [null, 0];
100+
}
101+
102+
function triggerNavigateOut(
103+
currentCell: ReactScopeMethods,
104+
direction: 'left' | 'right' | 'up' | 'down',
105+
): void {
106+
const row = currentCell.getParent();
107+
if (row !== null && row.getProps().type === 'row') {
108+
const table = row.getParent();
109+
if (table !== null) {
110+
const props = table.getProps();
111+
const onKeyboardOut = props.onKeyboardOut;
112+
if (props.type === 'table' && typeof onKeyboardOut === 'function') {
113+
const focusTableByID = (id: string) => {
114+
const topLevelTables = table.getChildrenFromRoot();
115+
if (topLevelTables !== null) {
116+
for (let i = 0; i < topLevelTables.length; i++) {
117+
const topLevelTable = topLevelTables[i];
118+
if (topLevelTable.getProps().id === id) {
119+
focusFirstCellOnTable(topLevelTable);
120+
return;
121+
}
122+
}
123+
}
124+
};
125+
onKeyboardOut(direction, focusTableByID);
126+
}
127+
}
128+
}
129+
}
130+
131+
export function createFocusTable(): Array<React.Component> {
132+
const TableScope = React.unstable_createScope(tabFocusableImpl);
133+
134+
function Table({children, onKeyboardOut, id}): FocusTableProps {
135+
return (
136+
<TableScope type="table" onKeyboardOut={onKeyboardOut} id={id}>
137+
{children}
138+
</TableScope>
139+
);
140+
}
141+
142+
function Row({children}): FocusRowProps {
143+
return <TableScope type="row">{children}</TableScope>;
144+
}
145+
146+
function Cell({children}): FocusCellProps {
147+
const scopeRef = useRef(null);
148+
const keyboard = useKeyboard({
149+
onKeyDown(event: KeyboardEvent): boolean {
150+
const currentCell = scopeRef.current;
151+
switch (event.key) {
152+
case 'UpArrow': {
153+
const [cells, rowIndex] = getRowCells(currentCell);
154+
if (cells !== null) {
155+
const [columns, columnIndex] = getRows(currentCell);
156+
if (columns !== null) {
157+
if (columnIndex > 0) {
158+
const column = columns[columnIndex - 1];
159+
focusCellByRowIndex(column, rowIndex);
160+
} else if (columnIndex === 0) {
161+
triggerNavigateOut(currentCell, 'up');
162+
}
163+
}
164+
}
165+
return false;
166+
}
167+
case 'DownArrow': {
168+
const [cells, rowIndex] = getRowCells(currentCell);
169+
if (cells !== null) {
170+
const [columns, columnIndex] = getRows(currentCell);
171+
if (columns !== null) {
172+
if (columnIndex !== -1) {
173+
if (columnIndex === columns.length - 1) {
174+
triggerNavigateOut(currentCell, 'down');
175+
} else {
176+
const column = columns[columnIndex + 1];
177+
focusCellByRowIndex(column, rowIndex);
178+
}
179+
}
180+
}
181+
}
182+
return false;
183+
}
184+
case 'LeftArrow': {
185+
const [cells, rowIndex] = getRowCells(currentCell);
186+
if (cells !== null) {
187+
if (rowIndex > 0) {
188+
focusCell(cells[rowIndex - 1]);
189+
} else if (rowIndex === 0) {
190+
triggerNavigateOut(currentCell, 'left');
191+
}
192+
}
193+
return false;
194+
}
195+
case 'RightArrow': {
196+
const [cells, rowIndex] = getRowCells(currentCell);
197+
if (cells !== null) {
198+
if (rowIndex !== -1) {
199+
if (rowIndex === cells.length - 1) {
200+
triggerNavigateOut(currentCell, 'right');
201+
} else {
202+
focusCell(cells[rowIndex + 1]);
203+
}
204+
}
205+
}
206+
return false;
207+
}
208+
}
209+
return true;
210+
},
211+
});
212+
return (
213+
<TableScope listeners={keyboard} ref={scopeRef} type="cell">
214+
{children}
215+
</TableScope>
216+
);
217+
}
218+
219+
return [Table, Row, Cell];
220+
}

0 commit comments

Comments
 (0)