Skip to content

Commit e4bc326

Browse files
LFDanLureidbarber
andauthored
Expandable rows TableView (#4641)
* poking around to make Rows process nested rows * cleanup * progress * got nested rows rendering still need to double check node info like index, level, prev/next key and fix keyboard nav * fixing naming and adding way to track rows that have nested rows * fixing expandable row detection and debugging weird TableLayout issue for some reason it is trying to render a child row of a non-expanded row * renaming from treeble to TreeGrid * fix cell values and weird crash with dynamic treeble with subset of keys expanded * update left/right keyboard navigation logic * updating up/down keyboard navigation logic for now made changes to getNext/PrevKey in the collection, see comments about why * fixing typeahead and using this.getChildNodes whereever possible * cleanup and fix getText * fixing index and column assignment to node still need to debug why I cant make the rows in the body 0 indexed * debugging persisted keys * adding basic story to check controlled expanded keys * musings when trying to fix persisted key case * enforcing user places cells before nested rows * making GridNode index track position with respect to parent and adding indexOfType to track posinset * fixing persisted keys by adding index for headerrows and columns * create Table wrapper element to conditionally render Table or TreeGridTable * get rid of extra story * update keyboard delegate to use node.indexOfType we dont actually set indexOfType in TableCollection so use index as fallback * adding tree grid aria for now not including aria row index, will need to test announcement differences * fix crash on collapse it is possible for the collection to be up to date but the removed items still go through a render cycle before being removed * fixes from screen reader testing experiemented with adding aria-rowindex as per wai-aria docs but decided to omit it since it made for a confusing announcement in NVDA where there was two row position announcements * setting up tests, debugging why they dont run not sure why the feature flag isnt being set to true... * add feature flag getter for tests for now * fixing row border styling and adding more tests * TableView expandable rows: rendering + interactions (#4663) * start implementing toggle chevron * expandable-rows-start * add padding based on level * improve padding offset * update level logic * use usePress for expand button * cleanup styles * add keyboard interactions * fix rendering chevron on static example * use expand as row action if none provided * fix chevron rendering * only keyboard toggle if expandable * cleanup * types * skip two focus tests * fix chevron position in RTL * handle cases of controlled & not updating expanded * fixing crash when collapsing rows * Adjusting so chevron appears on first row header cell * fix messed up merge conflict * remove edge case check * fix deps * check for props.childItems instead of link * making chevron focusable by screen readers * Support Enter/press on chevron cell to expand/collapse row seems to interfere with row actions... * update offset logic * update offset logic again * fix crashes when switching between selectionMode/styles and fix tests * fixing indenting * Get rid of cell action since it blocks row actions * fix expansion button rendering * Fix chevron disappearance when selectioncheckboxs are added The columnCount from the collection includes the drag handle and selection columns, messing up the check for if a static row is expandable. Updated to add a counter in the collection that represents the number of columns without the drag handle and selection column so we can easily tell if a static row has nested rows as children without needing to iterate over the children every time * adjusting row header id placement so it doesnt include the chevron label * fix selection extension for nested rows when using shift + click/arrow keys compareNodeOrder didnt handle cases where the compared nodes were of different level and one of the nodes was a top level node OR if one node contained the other node * fixing logic for compareNodeOrders * Adding tests for compareNodeOrder updates + range selection --------- Co-authored-by: Daniel Lu <[email protected]> * fix shared context for base tableview and treegrid tableview moved context up into wrapper since resizer and stuff relies on the the context being the same between the two tables * fixing lint for build * adding more stories * adding more row selection test * make NVDA/Talkback not announce the chevron label when the cell is focused VoiceOver still announces the chevron as part of the cells content but that was true before this change anyways. Change not made in the hooks because we need to differentiate the grid cell content from the chevron but we only have gridCellProps in the hook. Make that update later when we make expandable table rows aria example * add chevron rotation transition and increase hit target some misc followup items from testing as well * add chromatic stories * adding tests for loading state, onAction, and highlight selection * adding persisted key and remaining keyboard nav tests * add expand/collapse tests * (WIP) Simplify TreeGridCollection by building the keymap inside state instead (#4730) * tentative update to treegridstate to minimize usage of treegrid collection reversions of delegate and layout code to come * revert changes to keyboard delegates and other areas now that we are using TableCollection again with a flattened set of rows, reverted uneeded changes where possible. Kept TableLayout changes since it felt non-tree specific and is an overall improvement * partial simplification of TreeGridCollection one snag is that useCollection expects the collection provided as the factory to have a bunch of getters that TreeGridCollection just doesnt need * get rid of TreeGridCollection in favor of function in useTreeGridState TreeGridCollection was only really being used to generate a keymap of tree grid nodes to set aria-posinset and other aria attributes so it was a bit overkill and didnt need all the extra required getters, especially since TableCollection would be used in layout/keyboard delegates. * Refactoring TableView wrapper strategy merges TreeGridTable and TableView logic so there is less duplicated code * renaming tableview files previous commit is for code reviewers for TableView diffing. This commit renames the components to be more appropriate for their actual usage * getting rid of extraneous shouldShowCheckbox now that it is lifted into TableView/TreeGridTableView we dont need it in the base TableView * fixing tests for react-17 not entirely sure what was breaking, but this prevents the base table tests from being run twice when the TreeGridTable tests are run * cleaning up todos * adding alpha to types * Add message for feature flag pointing to docs * Remove zoom transition effect from TableView (#4771) * alternative transition approach 2 * Revert "alternative transition approach 2" This reverts commit ede52f4. * another alterative * trigger build * cleanup * Adding UNSTABLE to expandable rows state hook and prop added so people will know about its alpha status even though it lives in a stable release package * addressing review comments * moving expand/collapse chevron label to intl files * addressing review comments (prop rename and stuff) * hide expandable table props from spectrum docs * removing column node assigment to treegrid node cell as per review, already done in TableCollection, nor do I really use it * expanding type and fixing tests to get rid of .onlys the type change broadens the table children type so Collection builder doesnt complain. I also moved the feature flag test into TableView so I dont need the .onlys. Making a separate file to contain the tableTests function works as well and allows us to run those tests in TreeGridTable instead but this felt more appropriate * fix tests in 17 * Fix click focus and transform overlap for expandable row chevron (#4808) * focusing row when chevron is clicked the chevron doesnt have a tabindex so clicking it doesnt make focus enter the table so we need to manually do so * getting rid of columns array in favor of a counter * fix chevron button hit area doesnt overlap row content when row is narrow and overflow wrap is applied --------- Co-authored-by: Reid Barber <[email protected]>
1 parent c60c872 commit e4bc326

37 files changed

+4332
-1542
lines changed

packages/@adobe/spectrum-css-temp/components/table/index.css

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -219,27 +219,29 @@ svg.spectrum-Table-sortedIcon {
219219
}
220220
}
221221

222-
.spectrum-Table-cell {
223-
box-sizing: border-box;
224-
font-size: var(--spectrum-table-cell-text-size);
225-
font-weight: var(--spectrum-table-cell-text-font-weight);
226-
/* need to subtract 1px here because 14px * 1.5 + 14px + 14px = 49px, and we want 48px */
227-
line-height: calc(calc(var(--spectrum-table-cell-text-size) * var(--spectrum-table-cell-text-line-height)) - 1px);
228-
padding: var(--spectrum-table-cell-regular-padding-y) var(--spectrum-table-cell-padding-x);
222+
.spectrum-Table--regular {
223+
--spectrum-table-cell-padding-y: var(--spectrum-table-cell-regular-padding-y);
229224
}
230-
.spectrum-Table--regular {}
231225

232226
.spectrum-Table--compact .spectrum-Table-cell {
233-
padding-top: var(--spectrum-table-cell-compact-padding-y);
234-
padding-bottom: var(--spectrum-table-cell-compact-padding-y);
227+
--spectrum-table-cell-padding-y: var(--spectrum-table-cell-compact-padding-y);
235228
}
236229

237230
.spectrum-Table--spacious .spectrum-Table-cell {
238-
padding-top: var(--spectrum-table-cell-spacious-padding-y);
239-
padding-bottom: var(--spectrum-table-cell-spacious-padding-y);
231+
--spectrum-table-cell-padding-y: var(--spectrum-table-cell-spacious-padding-y);
232+
}
233+
234+
.spectrum-Table-cell {
235+
box-sizing: border-box;
236+
font-size: var(--spectrum-table-cell-text-size);
237+
font-weight: var(--spectrum-table-cell-text-font-weight);
238+
/* need to subtract 1px here because 14px * 1.5 + 14px + 14px = 49px, and we want 48px */
239+
line-height: calc(calc(var(--spectrum-table-cell-text-size) * var(--spectrum-table-cell-text-line-height)) - 1px);
240+
padding: var(--spectrum-table-cell-padding-y) var(--spectrum-table-cell-padding-x);
240241
}
241242

242243
.spectrum-Table-cellContents {
244+
grid-area: contents;
243245
flex: 1 1 0%;
244246
/* To ensure the flex child only takes up available width, see https://makandracards.com/makandra/66994-css-flex-and-min-width */
245247
min-width: 0px;
@@ -254,6 +256,26 @@ svg.spectrum-Table-sortedIcon {
254256
margin: -4px;
255257
}
256258

259+
.spectrum-Table-expandButton {
260+
grid-area: expand-button;
261+
height: var(--spectrum-global-dimension-size-400);
262+
margin: calc(var(--spectrum-table-cell-padding-y) * -1) 0;
263+
display: flex;
264+
flex-wrap: wrap;
265+
align-content: center;
266+
justify-content: center;
267+
outline: none;
268+
transition: transform ease var(--spectrum-global-animation-duration-100);
269+
&.is-open {
270+
[dir='ltr'] & {
271+
transform: rotate(90deg);
272+
}
273+
[dir='rtl'] & {
274+
transform: rotate(-90deg);
275+
}
276+
}
277+
}
278+
257279
.spectrum-Table-cell--hideHeader {
258280
padding: 0;
259281
justify-content: center;

packages/@react-aria/grid/src/useGridRow.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {DOMAttributes, FocusableElement, Node} from '@react-types/shared';
14-
import {GridCollection} from '@react-types/grid';
13+
import {DOMAttributes, FocusableElement} from '@react-types/shared';
14+
import {GridCollection, GridNode} from '@react-types/grid';
1515
import {gridMap} from './utils';
1616
import {GridState} from '@react-stately/grid';
1717
import {RefObject} from 'react';
1818
import {SelectableItemStates, useSelectableItem} from '@react-aria/selection';
1919

2020
export interface GridRowProps<T> {
2121
/** An object representing the grid row. Contains all the relevant information that makes up the grid row. */
22-
node: Node<T>,
22+
node: GridNode<T>,
2323
/** Whether the grid row is contained in a virtual scroller. */
2424
isVirtualized?: boolean,
2525
/** Whether selection should occur on press up instead of press down. */

packages/@react-aria/table/src/useTable.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {mergeProps, useDescription, useId, useUpdateEffect} from '@react-aria/ut
2020
import {Node} from '@react-types/shared';
2121
import {RefObject, useMemo} from 'react';
2222
import {TableKeyboardDelegate} from './TableKeyboardDelegate';
23-
import {TableState} from '@react-stately/table';
23+
import {TableState, TreeGridState} from '@react-stately/table';
2424
import {useCollator, useLocale, useLocalizedStringFormatter} from '@react-aria/i18n';
2525

2626
export interface AriaTableProps<T> extends GridProps {
@@ -36,7 +36,7 @@ export interface AriaTableProps<T> extends GridProps {
3636
* @param state - State for the table, as returned by `useTableState`.
3737
* @param ref - The ref attached to the table element.
3838
*/
39-
export function useTable<T>(props: AriaTableProps<T>, state: TableState<T>, ref: RefObject<HTMLElement>): GridAria {
39+
export function useTable<T>(props: AriaTableProps<T>, state: TableState<T> | TreeGridState<T>, ref: RefObject<HTMLElement>): GridAria {
4040
let {
4141
keyboardDelegate,
4242
isVirtualized,
@@ -70,6 +70,10 @@ export function useTable<T>(props: AriaTableProps<T>, state: TableState<T>, ref:
7070
gridProps['aria-rowcount'] = state.collection.size + state.collection.headerRows.length;
7171
}
7272

73+
if ('expandedKeys' in state) {
74+
gridProps.role = 'treegrid';
75+
}
76+
7377
let {column, direction: sortDirection} = state.sortDescriptor || {};
7478
let stringFormatter = useLocalizedStringFormatter(intlMessages);
7579
let sortDescription = useMemo(() => {

packages/@react-aria/table/src/useTableHeaderRow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function useTableHeaderRow<T>(props: GridRowProps<T>, state: TableState<T
3232
role: 'row'
3333
};
3434

35-
if (isVirtualized) {
35+
if (isVirtualized && !('expandedKeys' in state)) {
3636
rowProps['aria-rowindex'] = node.index + 1; // aria-rowindex is 1 based
3737
}
3838

packages/@react-aria/table/src/useTableRow.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,71 @@
1111
*/
1212

1313
import {FocusableElement} from '@react-types/shared';
14+
import {getLastItem} from '@react-stately/collections';
1415
import {getRowLabelledBy} from './utils';
16+
import type {GridNode} from '@react-types/grid';
1517
import {GridRowAria, GridRowProps, useGridRow} from '@react-aria/grid';
16-
import {RefObject} from 'react';
18+
import {HTMLAttributes, RefObject} from 'react';
19+
import {mergeProps} from '@react-aria/utils';
1720
import {TableCollection} from '@react-types/table';
18-
import {TableState} from '@react-stately/table';
21+
import {TableState, TreeGridState} from '@react-stately/table';
22+
import {useLocale} from '@react-aria/i18n';
23+
24+
const EXPANSION_KEYS = {
25+
expand: {
26+
ltr: 'ArrowRight',
27+
rtl: 'ArrowLeft'
28+
},
29+
'collapse': {
30+
ltr: 'ArrowLeft',
31+
rtl: 'ArrowRight'
32+
}
33+
};
1934

2035
/**
2136
* Provides the behavior and accessibility implementation for a row in a table.
2237
* @param props - Props for the row.
2338
* @param state - State of the table, as returned by `useTableState`.
2439
*/
25-
export function useTableRow<T>(props: GridRowProps<T>, state: TableState<T>, ref: RefObject<FocusableElement>): GridRowAria {
26-
let {node} = props;
40+
export function useTableRow<T>(props: GridRowProps<T>, state: TableState<T> | TreeGridState<T>, ref: RefObject<FocusableElement>): GridRowAria {
41+
let {node, isVirtualized} = props;
2742
let {rowProps, ...states} = useGridRow<T, TableCollection<T>, TableState<T>>(props, state, ref);
43+
let {direction} = useLocale();
44+
45+
if (isVirtualized && !('expandedKeys' in state)) {
46+
rowProps['aria-rowindex'] = node.index + 1 + state.collection.headerRows.length; // aria-rowindex is 1 based
47+
} else {
48+
delete rowProps['aria-rowindex'];
49+
}
50+
51+
let treeGridRowProps: HTMLAttributes<HTMLElement> = {};
52+
if ('expandedKeys' in state) {
53+
let treeNode = state.keyMap.get(node.key);
54+
if (treeNode != null) {
55+
let hasChildRows = treeNode.props?.UNSTABLE_childItems || treeNode.props?.children?.length > state.userColumnCount;
56+
treeGridRowProps = {
57+
onKeyDown: (e) => {
58+
if ((e.key === EXPANSION_KEYS['expand'][direction]) && state.selectionManager.focusedKey === treeNode.key && hasChildRows && state.expandedKeys !== 'all' && !state.expandedKeys.has(treeNode.key)) {
59+
state.toggleKey(treeNode.key);
60+
e.stopPropagation();
61+
} else if ((e.key === EXPANSION_KEYS['collapse'][direction]) && state.selectionManager.focusedKey === treeNode.key && hasChildRows && (state.expandedKeys === 'all' || state.expandedKeys.has(treeNode.key))) {
62+
state.toggleKey(treeNode.key);
63+
e.stopPropagation();
64+
}
65+
},
66+
'aria-expanded': hasChildRows ? state.expandedKeys === 'all' || state.expandedKeys.has(node.key) : undefined,
67+
'aria-level': treeNode.level,
68+
'aria-posinset': treeNode.indexOfType + 1,
69+
'aria-setsize': treeNode.level > 1 ?
70+
(getLastItem(state.keyMap.get(treeNode?.parentKey).childNodes) as GridNode<T>).indexOfType + 1 :
71+
(getLastItem(state.keyMap.get(state.collection.body.key).childNodes) as GridNode<T>).indexOfType + 1
72+
};
73+
}
74+
}
75+
2876
return {
2977
rowProps: {
30-
...rowProps,
78+
...mergeProps(rowProps, treeGridRowProps),
3179
'aria-labelledby': getRowLabelledBy(state, node.key)
3280
},
3381
...states

0 commit comments

Comments
 (0)