-
Notifications
You must be signed in to change notification settings - Fork 112
/
DataGrid.tsx
156 lines (143 loc) · 4.94 KB
/
DataGrid.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import * as React from 'react';
import * as PropTypes from 'prop-types';
import {useUID} from '@twilio-paste/uid-library';
import {useCompositeState, Composite} from '@twilio-paste/reakit-library';
import {Table} from '@twilio-paste/table';
import type {TableProps} from '@twilio-paste/table';
import {DataGridContext} from './DataGridContext';
import {
ensureFocus,
getActiveElement,
getFirstFocusableIn,
getClosestCellFrom,
getClosestGridCellFromCurrentFocus,
isCell,
delayedSetFocusable,
} from './utils';
export interface DataGridProps extends TableProps {
'aria-label': string;
}
/**
* DataGrid wrapper component.
*
* @param {string} aria-label - for screen readers
* @param {boolean} striped - zebra striping on table rows
* @param {string} element - customization element
*/
export const DataGrid = React.forwardRef<HTMLTableElement, DataGridProps>(
({element = 'DATA_GRID', striped = false, ...props}, ref) => {
const dataGridId = `data-grid-${useUID()}`;
const lastFocusedElement = React.useRef<Element | null>(null);
const compositeState = useCompositeState({unstable_virtual: false});
const [actionable, setActionable] = React.useState<boolean>(false);
/**
* Clicking into the DataGrid should unconditionally enable actionable mode
*/
const handleMouseDown = React.useCallback(() => {
setActionable(true);
}, []);
/**
* Keep track of the last focused element. This is needed to restore tabIndex to 0
* when clicking out of the grid, so that we can tab back into it.
*/
const handleFocus = React.useCallback((e: React.FocusEvent) => {
if (e.target != null) {
lastFocusedElement.current = e.target;
}
}, []);
/**
* - Reset DataGrid to navigational on blur
* - Sets the last focused element before blurring to be the active tab stop (line 43)
*/
const handleBlur = React.useCallback(
(event) => {
const isDataGridBlurred = !event.currentTarget.contains(event.relatedTarget);
if (isDataGridBlurred) {
setActionable(false);
if (lastFocusedElement.current != null) {
const closestCell = getClosestCellFrom(lastFocusedElement.current, dataGridId);
if (closestCell) {
// TabIndex isn't resetting to 0 on escape. This makes it reset to 0 after a delay
// Race condition fix vs Composite code
delayedSetFocusable(closestCell);
}
}
}
},
[dataGridId]
);
/**
* Handles Enter and Escape keypresses for swapping between actionable and navigational modes
*/
const handleKeypress = React.useCallback(
(event: React.KeyboardEvent) => {
switch (event.key) {
// Wrapping cases in {} to avoid ESLint error
// https://eslint.org/docs/rules/no-case-declarations
case 'Enter': {
// Set the actionable state
setActionable(true);
const activeElement = getActiveElement() as HTMLElement;
// Only if it's a DataGrid cell
if (isCell(activeElement)) {
// Get the first focusable child
const firstFocusableElement = getFirstFocusableIn(activeElement);
// If there is a focusable child focus it
if (firstFocusableElement) {
ensureFocus(firstFocusableElement as HTMLElement);
// First shift+tab fix upon entering actionable mode
activeElement.tabIndex = actionable ? 0 : -1;
}
}
break;
}
case 'Escape': {
// Set the actionable state
setActionable(false);
// From the current focus target, find the closest parent cell
const closestCell = getClosestGridCellFromCurrentFocus(dataGridId);
// If a cell is found, focus it
if (closestCell) {
ensureFocus(closestCell);
// TabIndex isn't resetting to 0 on escape. This makes it reset to 0 after a delay
delayedSetFocusable(closestCell);
}
break;
}
default:
break;
}
},
[actionable, dataGridId]
);
const dataGridState = {
...compositeState,
actionable,
striped,
};
return (
<DataGridContext.Provider value={dataGridState}>
<Composite
{...props}
{...compositeState}
id={dataGridId}
ref={ref}
as={Table}
element={element}
role="grid"
onKeyDown={handleKeypress}
onMouseDown={handleMouseDown}
onFocus={handleFocus}
onBlur={handleBlur}
isActionable={actionable}
data-actionable={actionable}
/>
</DataGridContext.Provider>
);
}
);
DataGrid.displayName = 'DataGrid';
DataGrid.propTypes = {
'aria-label': PropTypes.string.isRequired,
element: PropTypes.string,
};