-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
Copy pathindex.js
163 lines (140 loc) · 4.87 KB
/
index.js
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
157
158
159
160
161
162
163
/**
* External dependencies
*/
import { includes } from 'lodash';
/**
* WordPress dependencies
*/
import { focus } from '@wordpress/dom';
import { useCallback } from '@wordpress/element';
import { UP, DOWN, LEFT, RIGHT } from '@wordpress/keycodes';
/**
* Internal dependencies
*/
import RovingTabIndexContainer from './roving-tab-index';
/**
* Return focusables in a row element, excluding those from other branches
* nested within the row.
*
* @param {Element} rowElement The DOM element representing the row.
*
* @return {?Array} The array of focusables in the row.
*/
function getRowFocusables( rowElement ) {
const focusablesInRow = focus.focusable.find( rowElement );
if ( ! focusablesInRow || ! focusablesInRow.length ) {
return;
}
return focusablesInRow.filter( ( focusable ) => {
return focusable.closest( '[role="row"]' ) === rowElement;
} );
}
/**
* Renders both a table and tbody element, used to create a tree hierarchy.
*
* @see https://github.com/WordPress/gutenberg/blob/master/packages/components/src/tree-grid/README.md
*
* @param {Object} props Component props.
* @param {WPElement} props.children Children to be rendered
*/
export default function TreeGrid( { children, ...props } ) {
const onKeyDown = useCallback( ( event ) => {
const { keyCode, metaKey, ctrlKey, altKey, shiftKey } = event;
const hasModifierKeyPressed = metaKey || ctrlKey || altKey || shiftKey;
if (
hasModifierKeyPressed ||
! includes( [ UP, DOWN, LEFT, RIGHT ], keyCode )
) {
return;
}
// The event will be handled, stop propagation.
event.stopPropagation();
const { activeElement } = document;
const { currentTarget: treeGridElement } = event;
if ( ! treeGridElement.contains( activeElement ) ) {
return;
}
// Calculate the columnIndex of the active element.
const activeRow = activeElement.closest( '[role="row"]' );
const focusablesInRow = getRowFocusables( activeRow );
const currentColumnIndex = focusablesInRow.indexOf( activeElement );
if ( includes( [ LEFT, RIGHT ], keyCode ) ) {
// Calculate to the next element.
let nextIndex;
if ( keyCode === LEFT ) {
nextIndex = Math.max( 0, currentColumnIndex - 1 );
} else {
nextIndex = Math.min(
currentColumnIndex + 1,
focusablesInRow.length - 1
);
}
// Focus is either at the left or right edge of the grid. Do nothing.
if ( nextIndex === currentColumnIndex ) {
// Prevent key use for anything else. For example, Voiceover
// will start reading text on continued use of left/right arrow
// keys.
event.preventDefault();
return;
}
// Focus the next element.
focusablesInRow[ nextIndex ].focus();
// Prevent key use for anything else. This ensures Voiceover
// doesn't try to handle key navigation.
event.preventDefault();
} else if ( includes( [ UP, DOWN ], keyCode ) ) {
// Calculate the rowIndex of the next row.
const rows = Array.from(
treeGridElement.querySelectorAll( '[role="row"]' )
);
const currentRowIndex = rows.indexOf( activeRow );
let nextRowIndex;
if ( keyCode === UP ) {
nextRowIndex = Math.max( 0, currentRowIndex - 1 );
} else {
nextRowIndex = Math.min( currentRowIndex + 1, rows.length - 1 );
}
// Focus is either at the top or bottom edge of the grid. Do nothing.
if ( nextRowIndex === currentRowIndex ) {
// Prevent key use for anything else. For example, Voiceover
// will start navigating horizontally when reaching the vertical
// bounds of a table.
event.preventDefault();
return;
}
// Get the focusables in the next row.
const focusablesInNextRow = getRowFocusables(
rows[ nextRowIndex ]
);
// If for some reason there are no focusables in the next row, do nothing.
if ( ! focusablesInNextRow || ! focusablesInNextRow.length ) {
// Prevent key use for anything else. For example, Voiceover
// will still focus text when using arrow keys, while this
// component should limit navigation to focusables.
event.preventDefault();
return;
}
// Try to focus the element in the next row that's at a similar column to the activeElement.
const nextIndex = Math.min(
currentColumnIndex,
focusablesInNextRow.length - 1
);
focusablesInNextRow[ nextIndex ].focus();
// Prevent key use for anything else. This ensures Voiceover
// doesn't try to handle key navigation.
event.preventDefault();
}
}, [] );
return (
<RovingTabIndexContainer>
{ /* Disable reason: A treegrid is implemented using a table element. */ }
{ /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role */ }
<table { ...props } role="treegrid" onKeyDown={ onKeyDown }>
<tbody>{ children }</tbody>
</table>
</RovingTabIndexContainer>
);
}
export { default as TreeGridRow } from './row';
export { default as TreeGridCell } from './cell';
export { default as TreeGridItem } from './item';