Skip to content

Commit 1390063

Browse files
fix: Table column updating (#8211)
* fix: Table column updating * fix tests * add docs and examples * cleanup some of this will be added back in daniel's PR but was not important for this fix * update docs * Fix S2 example made a separate story so it isn't included in the docs --------- Co-authored-by: Devon Govett <[email protected]>
1 parent e9bd3a3 commit 1390063

File tree

7 files changed

+324
-70
lines changed

7 files changed

+324
-70
lines changed

packages/@react-aria/collections/src/Document.ts

+4
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,10 @@ export class BaseNode<T> {
182182
return;
183183
}
184184

185+
if (this._minInvalidChildIndex === child) {
186+
this._minInvalidChildIndex = null;
187+
}
188+
185189
if (child.nextSibling) {
186190
this.invalidateChildIndices(child.nextSibling);
187191
child.nextSibling.previousSibling = child.previousSibling;

packages/@react-spectrum/s2/src/TableView.tsx

+7-6
Original file line numberDiff line numberDiff line change
@@ -369,13 +369,13 @@ const centeredWrapper = style({
369369
height: 'full'
370370
});
371371

372-
export interface TableBodyProps<T> extends Omit<RACTableBodyProps<T>, 'style' | 'className' | 'dependencies'> {}
372+
export interface TableBodyProps<T> extends Omit<RACTableBodyProps<T>, 'style' | 'className'> {}
373373

374374
/**
375375
* The body of a `<Table>`, containing the table rows.
376376
*/
377377
export const TableBody = /*#__PURE__*/ (forwardRef as forwardRefType)(function TableBody<T extends object>(props: TableBodyProps<T>, ref: DOMRef<HTMLDivElement>) {
378-
let {items, renderEmptyState, children} = props;
378+
let {items, renderEmptyState, children, dependencies = []} = props;
379379
let domRef = useDOMRef(ref);
380380
let {loadingState, onLoadMore} = useContext(InternalTableContext);
381381
let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
@@ -402,7 +402,7 @@ export const TableBody = /*#__PURE__*/ (forwardRef as forwardRefType)(function T
402402
if (typeof children === 'function' && items) {
403403
renderer = (
404404
<>
405-
<Collection items={items}>
405+
<Collection items={items} dependencies={dependencies}>
406406
{children}
407407
</Collection>
408408
{loadMoreSpinner}
@@ -1115,12 +1115,12 @@ const row = style<RowRenderProps & S2TableProps>({
11151115
forcedColorAdjust: 'none'
11161116
});
11171117

1118-
export interface RowProps<T> extends Pick<RACRowProps<T>, 'id' | 'columns' | 'children' | 'textValue'> {}
1118+
export interface RowProps<T> extends Pick<RACRowProps<T>, 'id' | 'columns' | 'children' | 'textValue' | 'dependencies'> {}
11191119

11201120
/**
11211121
* A row within a `<Table>`.
11221122
*/
1123-
export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row<T extends object>({id, columns, children, ...otherProps}: RowProps<T>, ref: DOMRef<HTMLDivElement>) {
1123+
export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row<T extends object>({id, columns, children, dependencies = [], ...otherProps}: RowProps<T>, ref: DOMRef<HTMLDivElement>) {
11241124
let {selectionBehavior, selectionMode} = useTableOptions();
11251125
let tableVisualOptions = useContext(InternalTableContext);
11261126
let domRef = useDOMRef(ref);
@@ -1130,6 +1130,7 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row<T e
11301130
// @ts-ignore
11311131
ref={domRef}
11321132
id={id}
1133+
dependencies={[...dependencies, columns]}
11331134
className={renderProps => row({
11341135
...renderProps,
11351136
...tableVisualOptions
@@ -1140,7 +1141,7 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row<T e
11401141
<Checkbox isEmphasized slot="selection" />
11411142
</Cell>
11421143
)}
1143-
<Collection items={columns}>
1144+
<Collection items={columns} dependencies={[...dependencies, columns]}>
11441145
{children}
11451146
</Collection>
11461147
</RACRow>

packages/@react-spectrum/s2/stories/TableView.stories.tsx

+74-31
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,38 @@ const DynamicTable = (args: any) => (
153153
</TableView>
154154
);
155155

156+
export const DynamicColumns = {
157+
render: function DynamicColumnsExample(args: any) {
158+
let [cols, setColumns] = useState(columns);
159+
return (
160+
<div style={{display: 'flex', flexDirection: 'column', gap: 12}}>
161+
<ActionButton onPress={() => setColumns((prev) => prev.length > 3 ? [columns[0]].concat(columns.slice(2, 4)) : columns)}>Toggle columns</ActionButton>
162+
<TableView aria-label="Dynamic table" {...args} styles={style({width: 320, height: 208})}>
163+
<TableHeader columns={cols}>
164+
{(column) => (
165+
<Column width={150} minWidth={150} isRowHeader={column.isRowHeader}>{column.name}</Column>
166+
)}
167+
</TableHeader>
168+
<TableBody items={items} dependencies={[cols]}>
169+
{item => (
170+
<Row id={item.id} columns={cols}>
171+
{(col) => {
172+
return <Cell>{item[col.id]}</Cell>;
173+
}}
174+
</Row>
175+
)}
176+
</TableBody>
177+
</TableView>
178+
</div>
179+
);
180+
},
181+
args: Example.args,
182+
parameters: {
183+
docs: {
184+
disable: true
185+
}
186+
}
187+
};
156188

157189
const DynamicTableWithCustomMenus = (args: any) => (
158190
<TableView aria-label="Dynamic table" {...args} styles={style({width: 320, height: 208})}>
@@ -817,39 +849,50 @@ export const FlexWidth = {
817849
};
818850

819851

852+
function ColSpanExample(args: any) {
853+
let [hide, setHide] = useState(false);
854+
return (
855+
<div style={{display: 'flex', flexDirection: 'column', gap: 12}}>
856+
<ActionButton onPress={() => setHide(!hide)}>{hide ? 'Show' : 'Hide'}</ActionButton>
857+
<TableView aria-label="Files" {...args} styles={style({width: 320, height: 320})}>
858+
<TableHeader>
859+
<Column isRowHeader>Name</Column>
860+
<Column>Type</Column>
861+
{!hide && <Column>Date Modified</Column>}
862+
<Column>Size</Column>
863+
</TableHeader>
864+
<TableBody>
865+
<Row id="1">
866+
<Cell>Games</Cell>
867+
<Cell>File folder</Cell>
868+
{!hide && <Cell>6/7/2020</Cell>}
869+
<Cell>74 GB</Cell>
870+
</Row>
871+
<Row id="2">
872+
<Cell>Program Files</Cell>
873+
<Cell>File folder</Cell>
874+
{!hide && <Cell>4/7/2021</Cell>}
875+
<Cell>1.2 GB</Cell>
876+
</Row>
877+
<Row id="3">
878+
<Cell>bootmgr</Cell>
879+
<Cell>System file</Cell>
880+
{!hide && <Cell>11/20/2010</Cell>}
881+
<Cell>0.2 GB</Cell>
882+
</Row>
883+
<Row id="4">
884+
<Cell colSpan={hide ? 3 : 4}>Total size: 75.4 GB</Cell>
885+
</Row>
886+
</TableBody>
887+
</TableView>
888+
</div>
889+
);
890+
}
891+
892+
820893
export const ColSpan = {
821894
render: (args: any) => (
822-
<TableView aria-label="Files" {...args} styles={style({width: 320, height: 320})}>
823-
<TableHeader>
824-
<Column isRowHeader>Name</Column>
825-
<Column>Type</Column>
826-
<Column>Date Modified</Column>
827-
<Column>Size</Column>
828-
</TableHeader>
829-
<TableBody>
830-
<Row id="1">
831-
<Cell>Games</Cell>
832-
<Cell>File folder</Cell>
833-
<Cell>6/7/2020</Cell>
834-
<Cell>74 GB</Cell>
835-
</Row>
836-
<Row id="2">
837-
<Cell>Program Files</Cell>
838-
<Cell>File folder</Cell>
839-
<Cell>4/7/2021</Cell>
840-
<Cell>1.2 GB</Cell>
841-
</Row>
842-
<Row id="3">
843-
<Cell>bootmgr</Cell>
844-
<Cell>System file</Cell>
845-
<Cell>11/20/2010</Cell>
846-
<Cell>0.2 GB</Cell>
847-
</Row>
848-
<Row id="4">
849-
<Cell colSpan={4}>Total size: 75.4 GB</Cell>
850-
</Row>
851-
</TableBody>
852-
</TableView>
895+
<ColSpanExample {...args} />
853896
),
854897
parameters: {
855898
docs: {

packages/dev/docs/pages/react-aria/collections.mdx

+30-11
Original file line numberDiff line numberDiff line change
@@ -55,22 +55,22 @@ components like action menus where the items are built into the application rath
5555

5656
### Sections
5757

58-
Sections or groups of items can be constructed by wrapping the items in a `<Section>` element. A `<Header>` can also be
59-
rendered within a `<Section>` to provide a section title.
58+
Sections or groups of items can be constructed by wrapping the items in a section element. A `<Header>` can also be
59+
rendered within a section to provide a section title.
6060

6161
```tsx
6262
<Menu>
63-
<Section>
63+
<MenuSection>
6464
<Header>Styles</Header>
6565
<MenuItem>Bold</MenuItem>
6666
<MenuItem>Underline</MenuItem>
67-
</Section>
68-
<Section>
67+
</MenuSection>
68+
<MenuSection>
6969
<Header>Align</Header>
7070
<MenuItem>Left</MenuItem>
7171
<MenuItem>Middle</MenuItem>
7272
<MenuItem>Right</MenuItem>
73-
</Section>
73+
</MenuSection>
7474
</Menu>
7575
```
7676

@@ -179,9 +179,28 @@ function addAnimal(name) {
179179

180180
Note that `useListData` is a convenience hook, not a requirement. You can use any state management library to manage collection items.
181181

182+
### Dependencies
183+
184+
As described above, dynamic collections are automatically memoized to improve performance. Rendered item elements are cached based on the object identity
185+
of the list item. If rendering an item depends on additional external state, the `dependencies` prop must be provided. This invalidates rendered elements
186+
similar to dependencies in React's `useMemo` hook.
187+
188+
```tsx
189+
function Example(props) {
190+
return (
191+
<ListBox items={items} dependencies={[props.layout]}>
192+
{item => <MyItem layout={props.layout}>{item.name}</MyItem>}
193+
</ListBox>
194+
);
195+
}
196+
```
197+
198+
Note that adding dependencies will result in the _entire_ list being invalidated when a dependency changes. To avoid this and invalidate only an individual item,
199+
update the item object itself to include the information rather than accessing it from external state.
200+
182201
### Sections
183202

184-
Sections can be built by returning a `<Section>` instead of an item from the top-level item renderer. Sections
203+
Sections can be built by returning a section instead of an item from the top-level item renderer. Sections
185204
also support an `items` prop and a render function for their children. If the section also has a header,
186205
the <TypeLink links={docs.links} type={docs.exports.Collection} /> component can be used to render the child items.
187206

@@ -207,12 +226,12 @@ let [sections, setSections] = useState([
207226

208227
<ListBox items={sections}>
209228
{section =>
210-
<Section id={section.name}>
229+
<ListBoxSection id={section.name}>
211230
<Header>{section.name}</Header>
212231
<Collection items={section.children}>
213232
{item => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>}
214233
</Collection>
215-
</Section>
234+
</ListBoxSection>
216235
}
217236
</ListBox>
218237
```
@@ -261,12 +280,12 @@ function addPerson(name) {
261280

262281
<ListBox items={tree.items}>
263282
{node =>
264-
<Section id={section.name} items={node.children}>
283+
<ListBoxSection id={section.name} items={node.children}>
265284
<Header>{section.name}</Header>
266285
<Collection items={section.children}>
267286
{item => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>}
268287
</Collection>
269-
</Section>
288+
</ListBoxSection>
270289
}
271290
</ListBox>
272291
```

packages/react-aria-components/docs/CheckboxGroup.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ interface MyCheckboxGroupProps extends Omit<CheckboxGroupProps, 'children'> {
185185
errorMessage?: string | ((validation: ValidationResult) => string)
186186
}
187187

188-
function MyCheckboxGroup({
188+
export function MyCheckboxGroup({
189189
label,
190190
description,
191191
errorMessage,

packages/react-aria-components/docs/Table.mdx

+52-21
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ import {MyCheckbox} from './Checkbox';
9393
```css hidden
9494
@import './Button.mdx' layer(button);
9595
@import './ToggleButton.mdx' layer(togglebutton);
96+
@import './CheckboxGroup.mdx' layer(checkboxgroup);
9697
@import './Checkbox.mdx' layer(checkbox);
9798
@import './Popover.mdx' layer(popover);
9899
@import './Menu.mdx' layer(menu);
@@ -431,47 +432,77 @@ Now we can render a table with a default selection column built in.
431432

432433
So far, our examples have shown static collections, where the data is hard coded.
433434
Dynamic collections, as shown below, can be used when the table data comes from an external data source such as an API, or updates over time.
434-
In the example below, both the columns and the rows are provided to the table via a render function. You can also make the columns static and
435-
only the rows dynamic.
435+
In the example below, both the columns and the rows are provided to the table via a render function, enabling the user to hide and show columns
436+
and add additional rows. You can also make the columns static and only the rows dynamic.
437+
438+
**Note**: Dynamic collections are automatically memoized to improve performance. Use the `dependencies` prop to invalidate cached elements that depend
439+
on external state (e.g. `columns` in this example). See the [collections](collections.html#dependencies) guide for more details.
436440

437441
```tsx example export=true
438442
import type {TableProps} from 'react-aria-components';
443+
import {MyCheckboxGroup} from './CheckboxGroup';
444+
import {MyCheckbox} from './Checkbox';
439445

440446
function FileTable(props: TableProps) {
447+
let [showColumns, setShowColumns] = React.useState(['name', 'type', 'date']);
441448
let columns = [
442449
{name: 'Name', id: 'name', isRowHeader: true},
443450
{name: 'Type', id: 'type'},
444451
{name: 'Date Modified', id: 'date'}
445-
];
452+
].filter(column => showColumns.includes(column.id));
446453

447-
let rows = [
454+
let [rows, setRows] = React.useState([
448455
{id: 1, name: 'Games', date: '6/7/2020', type: 'File folder'},
449456
{id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder'},
450457
{id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file'},
451458
{id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document'}
452-
];
459+
]);
460+
461+
let addRow = () => {
462+
let date = new Date().toLocaleDateString();
463+
setRows(rows => [
464+
...rows,
465+
{id: rows.length + 1, name: 'file.txt', date, type: 'Text Document'}
466+
]);
467+
};
453468

454469
return (
455-
<Table aria-label="Files" {...props}>
456-
<MyTableHeader columns={columns}>
457-
{column => (
458-
<Column isRowHeader={column.isRowHeader}>
459-
{column.name}
460-
</Column>
461-
)}
462-
</MyTableHeader>
463-
<TableBody items={rows}>
464-
{item => (
465-
<MyRow columns={columns}>
466-
{column => <Cell>{item[column.id]}</Cell>}
467-
</MyRow>
468-
)}
469-
</TableBody>
470-
</Table>
470+
<div className="flex-col">
471+
<MyCheckboxGroup aria-label="Show columns" value={showColumns} onChange={setShowColumns} style={{flexDirection: 'row'}}>
472+
<MyCheckbox value="type">Type</MyCheckbox>
473+
<MyCheckbox value="date">Date Modified</MyCheckbox>
474+
</MyCheckboxGroup>
475+
<Table aria-label="Files" {...props}>
476+
<MyTableHeader columns={columns}>
477+
{column => (
478+
<Column isRowHeader={column.isRowHeader}>
479+
{column.name}
480+
</Column>
481+
)}
482+
</MyTableHeader>
483+
<TableBody items={rows} dependencies={[columns]}>
484+
{item => (
485+
<MyRow columns={columns}>
486+
{column => <Cell>{item[column.id]}</Cell>}
487+
</MyRow>
488+
)}
489+
</TableBody>
490+
</Table>
491+
<Button onPress={addRow}>Add row</Button>
492+
</div>
471493
);
472494
}
473495
```
474496

497+
```css hidden
498+
.flex-col {
499+
display: flex;
500+
flex-direction: column;
501+
gap: 8;
502+
align-items: start;
503+
}
504+
```
505+
475506
## Selection
476507

477508
### Single selection

0 commit comments

Comments
 (0)