Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/@react-aria/collections/src/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ export class BaseNode<T> {
return;
}

if (this._minInvalidChildIndex === child) {
this._minInvalidChildIndex = null;
}

if (child.nextSibling) {
this.invalidateChildIndices(child.nextSibling);
child.nextSibling.previousSibling = child.previousSibling;
Expand Down
13 changes: 7 additions & 6 deletions packages/@react-spectrum/s2/src/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -369,13 +369,13 @@ const centeredWrapper = style({
height: 'full'
});

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

/**
* The body of a `<Table>`, containing the table rows.
*/
export const TableBody = /*#__PURE__*/ (forwardRef as forwardRefType)(function TableBody<T extends object>(props: TableBodyProps<T>, ref: DOMRef<HTMLDivElement>) {
let {items, renderEmptyState, children} = props;
let {items, renderEmptyState, children, dependencies = []} = props;
let domRef = useDOMRef(ref);
let {loadingState, onLoadMore} = useContext(InternalTableContext);
let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
Expand All @@ -402,7 +402,7 @@ export const TableBody = /*#__PURE__*/ (forwardRef as forwardRefType)(function T
if (typeof children === 'function' && items) {
renderer = (
<>
<Collection items={items}>
<Collection items={items} dependencies={dependencies}>
{children}
</Collection>
{loadMoreSpinner}
Expand Down Expand Up @@ -1115,12 +1115,12 @@ const row = style<RowRenderProps & S2TableProps>({
forcedColorAdjust: 'none'
});

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

/**
* A row within a `<Table>`.
*/
export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row<T extends object>({id, columns, children, ...otherProps}: RowProps<T>, ref: DOMRef<HTMLDivElement>) {
export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row<T extends object>({id, columns, children, dependencies = [], ...otherProps}: RowProps<T>, ref: DOMRef<HTMLDivElement>) {
let {selectionBehavior, selectionMode} = useTableOptions();
let tableVisualOptions = useContext(InternalTableContext);
let domRef = useDOMRef(ref);
Expand All @@ -1130,6 +1130,7 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row<T e
// @ts-ignore
ref={domRef}
id={id}
dependencies={[...dependencies, columns]}
className={renderProps => row({
...renderProps,
...tableVisualOptions
Expand All @@ -1140,7 +1141,7 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row<T e
<Checkbox isEmphasized slot="selection" />
</Cell>
)}
<Collection items={columns}>
<Collection items={columns} dependencies={[...dependencies, columns]}>
{children}
</Collection>
</RACRow>
Expand Down
105 changes: 74 additions & 31 deletions packages/@react-spectrum/s2/stories/TableView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,38 @@ const DynamicTable = (args: any) => (
</TableView>
);

export const DynamicColumns = {
render: function DynamicColumnsExample(args: any) {
let [cols, setColumns] = useState(columns);
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 12}}>
<ActionButton onPress={() => setColumns((prev) => prev.length > 3 ? [columns[0]].concat(columns.slice(2, 4)) : columns)}>Toggle columns</ActionButton>
<TableView aria-label="Dynamic table" {...args} styles={style({width: 320, height: 208})}>
<TableHeader columns={cols}>
{(column) => (
<Column width={150} minWidth={150} isRowHeader={column.isRowHeader}>{column.name}</Column>
)}
</TableHeader>
<TableBody items={items} dependencies={[cols]}>
{item => (
<Row id={item.id} columns={cols}>
{(col) => {
return <Cell>{item[col.id]}</Cell>;
}}
</Row>
)}
</TableBody>
</TableView>
</div>
);
},
args: Example.args,
parameters: {
docs: {
disable: true
}
}
};

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


function ColSpanExample(args: any) {
let [hide, setHide] = useState(false);
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 12}}>
<ActionButton onPress={() => setHide(!hide)}>{hide ? 'Show' : 'Hide'}</ActionButton>
<TableView aria-label="Files" {...args} styles={style({width: 320, height: 320})}>
<TableHeader>
<Column isRowHeader>Name</Column>
<Column>Type</Column>
{!hide && <Column>Date Modified</Column>}
<Column>Size</Column>
</TableHeader>
<TableBody>
<Row id="1">
<Cell>Games</Cell>
<Cell>File folder</Cell>
{!hide && <Cell>6/7/2020</Cell>}
<Cell>74 GB</Cell>
</Row>
<Row id="2">
<Cell>Program Files</Cell>
<Cell>File folder</Cell>
{!hide && <Cell>4/7/2021</Cell>}
<Cell>1.2 GB</Cell>
</Row>
<Row id="3">
<Cell>bootmgr</Cell>
<Cell>System file</Cell>
{!hide && <Cell>11/20/2010</Cell>}
<Cell>0.2 GB</Cell>
</Row>
<Row id="4">
<Cell colSpan={hide ? 3 : 4}>Total size: 75.4 GB</Cell>
</Row>
</TableBody>
</TableView>
</div>
);
}


export const ColSpan = {
render: (args: any) => (
<TableView aria-label="Files" {...args} styles={style({width: 320, height: 320})}>
<TableHeader>
<Column isRowHeader>Name</Column>
<Column>Type</Column>
<Column>Date Modified</Column>
<Column>Size</Column>
</TableHeader>
<TableBody>
<Row id="1">
<Cell>Games</Cell>
<Cell>File folder</Cell>
<Cell>6/7/2020</Cell>
<Cell>74 GB</Cell>
</Row>
<Row id="2">
<Cell>Program Files</Cell>
<Cell>File folder</Cell>
<Cell>4/7/2021</Cell>
<Cell>1.2 GB</Cell>
</Row>
<Row id="3">
<Cell>bootmgr</Cell>
<Cell>System file</Cell>
<Cell>11/20/2010</Cell>
<Cell>0.2 GB</Cell>
</Row>
<Row id="4">
<Cell colSpan={4}>Total size: 75.4 GB</Cell>
</Row>
</TableBody>
</TableView>
<ColSpanExample {...args} />
),
parameters: {
docs: {
Expand Down
41 changes: 30 additions & 11 deletions packages/dev/docs/pages/react-aria/collections.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,22 @@ components like action menus where the items are built into the application rath

### Sections

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

```tsx
<Menu>
<Section>
<MenuSection>
<Header>Styles</Header>
<MenuItem>Bold</MenuItem>
<MenuItem>Underline</MenuItem>
</Section>
<Section>
</MenuSection>
<MenuSection>
<Header>Align</Header>
<MenuItem>Left</MenuItem>
<MenuItem>Middle</MenuItem>
<MenuItem>Right</MenuItem>
</Section>
</MenuSection>
</Menu>
```

Expand Down Expand Up @@ -179,9 +179,28 @@ function addAnimal(name) {

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

### Dependencies

As described above, dynamic collections are automatically memoized to improve performance. Rendered item elements are cached based on the object identity
of the list item. If rendering an item depends on additional external state, the `dependencies` prop must be provided. This invalidates rendered elements
similar to dependencies in React's `useMemo` hook.

```tsx
function Example(props) {
return (
<ListBox items={items} dependencies={[props.layout]}>
{item => <MyItem layout={props.layout}>{item.name}</MyItem>}
</ListBox>
);
}
```

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,
update the item object itself to include the information rather than accessing it from external state.

### Sections

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

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

<ListBox items={sections}>
{section =>
<Section id={section.name}>
<ListBoxSection id={section.name}>
<Header>{section.name}</Header>
<Collection items={section.children}>
{item => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>}
</Collection>
</Section>
</ListBoxSection>
}
</ListBox>
```
Expand Down Expand Up @@ -261,12 +280,12 @@ function addPerson(name) {

<ListBox items={tree.items}>
{node =>
<Section id={section.name} items={node.children}>
<ListBoxSection id={section.name} items={node.children}>
<Header>{section.name}</Header>
<Collection items={section.children}>
{item => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>}
</Collection>
</Section>
</ListBoxSection>
}
</ListBox>
```
Expand Down
2 changes: 1 addition & 1 deletion packages/react-aria-components/docs/CheckboxGroup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ interface MyCheckboxGroupProps extends Omit<CheckboxGroupProps, 'children'> {
errorMessage?: string | ((validation: ValidationResult) => string)
}

function MyCheckboxGroup({
export function MyCheckboxGroup({
label,
description,
errorMessage,
Expand Down
73 changes: 52 additions & 21 deletions packages/react-aria-components/docs/Table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import {MyCheckbox} from './Checkbox';
```css hidden
@import './Button.mdx' layer(button);
@import './ToggleButton.mdx' layer(togglebutton);
@import './CheckboxGroup.mdx' layer(checkboxgroup);
@import './Checkbox.mdx' layer(checkbox);
@import './Popover.mdx' layer(popover);
@import './Menu.mdx' layer(menu);
Expand Down Expand Up @@ -431,47 +432,77 @@ Now we can render a table with a default selection column built in.

So far, our examples have shown static collections, where the data is hard coded.
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.
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
only the rows dynamic.
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
and add additional rows. You can also make the columns static and only the rows dynamic.

**Note**: Dynamic collections are automatically memoized to improve performance. Use the `dependencies` prop to invalidate cached elements that depend
on external state (e.g. `columns` in this example). See the [collections](collections.html#dependencies) guide for more details.

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

function FileTable(props: TableProps) {
let [showColumns, setShowColumns] = React.useState(['name', 'type', 'date']);
let columns = [
{name: 'Name', id: 'name', isRowHeader: true},
{name: 'Type', id: 'type'},
{name: 'Date Modified', id: 'date'}
];
].filter(column => showColumns.includes(column.id));

let rows = [
let [rows, setRows] = React.useState([
{id: 1, name: 'Games', date: '6/7/2020', type: 'File folder'},
{id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder'},
{id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file'},
{id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document'}
];
]);

let addRow = () => {
let date = new Date().toLocaleDateString();
setRows(rows => [
...rows,
{id: rows.length + 1, name: 'file.txt', date, type: 'Text Document'}
]);
};

return (
<Table aria-label="Files" {...props}>
<MyTableHeader columns={columns}>
{column => (
<Column isRowHeader={column.isRowHeader}>
{column.name}
</Column>
)}
</MyTableHeader>
<TableBody items={rows}>
{item => (
<MyRow columns={columns}>
{column => <Cell>{item[column.id]}</Cell>}
</MyRow>
)}
</TableBody>
</Table>
<div className="flex-col">
<MyCheckboxGroup aria-label="Show columns" value={showColumns} onChange={setShowColumns} style={{flexDirection: 'row'}}>
<MyCheckbox value="type">Type</MyCheckbox>
<MyCheckbox value="date">Date Modified</MyCheckbox>
</MyCheckboxGroup>
<Table aria-label="Files" {...props}>
<MyTableHeader columns={columns}>
{column => (
<Column isRowHeader={column.isRowHeader}>
{column.name}
</Column>
)}
</MyTableHeader>
<TableBody items={rows} dependencies={[columns]}>
{item => (
<MyRow columns={columns}>
{column => <Cell>{item[column.id]}</Cell>}
</MyRow>
)}
</TableBody>
</Table>
<Button onPress={addRow}>Add row</Button>
</div>
);
}
```

```css hidden
.flex-col {
display: flex;
flex-direction: column;
gap: 8;
align-items: start;
}
```

## Selection

### Single selection
Expand Down
Loading