diff --git a/Makefile b/Makefile index 684fd286e04..f5de6668ec7 100644 --- a/Makefile +++ b/Makefile @@ -143,6 +143,8 @@ s2-api-diff: node scripts/api-diff.js --skip-same --skip-style-props s2-docs: + node packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs + node packages/dev/s2-docs/scripts/generateOGImages.mjs node scripts/extractStarter.mjs REGISTRY_URL=https://reactspectrum.blob.core.windows.net/reactspectrum/$$(git rev-parse HEAD)/s2-docs/registry node scripts/buildRegistry.mjs REGISTRY_URL=https://reactspectrum.blob.core.windows.net/reactspectrum/$$(git rev-parse HEAD)/s2-docs/registry yarn build:s2-docs --public-url /reactspectrum/$$(git rev-parse HEAD)/s2-docs/ diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 27c8cb292ac..a425c90713b 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -507,6 +507,7 @@ const columnStyles = style({ }); export interface ColumnProps extends Omit { + // TODO: this prop doesn't seem to work, is it supposed to be supported? /** Whether the column should render a divider between it and the next column. */ showDivider?: boolean, /** Whether the column allows resizing. */ diff --git a/packages/dev/s2-docs/pages/react-aria/Table.mdx b/packages/dev/s2-docs/pages/react-aria/Table.mdx index b1ca44cb569..778037d7df4 100644 --- a/packages/dev/s2-docs/pages/react-aria/Table.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Table.mdx @@ -121,7 +121,7 @@ import {Table, TableHeader, TableBody, Column, Row, Cell, Button, Checkbox, Resi ## Content -`Table` follows the **Collection Components API**, accepting both static and dynamic collections. +Table follows the **Collection Components API**, accepting both static and dynamic collections. In this example, 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. ```tsx render @@ -290,7 +290,7 @@ function AsyncSortTable() { ### Links -Use the `href` prop on a `` to create a link. See the **client side routing guide** to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the [selection guide](selection.html) for more details. +Use the `href` prop on a Row to create a link. See the **client side routing guide** to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the [selection guide](selection.html) for more details. ```tsx render docs={docs.exports.ListBox} links={docs.links} props={['selectionBehavior']} initialProps={{'aria-label': 'Bookmarks', selectionMode: 'multiple'}} wide "use client"; @@ -408,7 +408,7 @@ function Example(props) { ## Sorting -Set the `allowsSorting` prop on a `` to make it sortable. When the column header is pressed, the table will call `onSortChange` with a including the sorted column and direction (ascending or descending). Use this to sort the data accordingly, and pass the `sortDescriptor` prop to the `` to display the sorted column. +Set the `allowsSorting` prop on a Column to make it sortable. When the column header is pressed, the table will call `onSortChange` with a including the sorted column and direction (ascending or descending). Use this to sort the data accordingly, and pass the `sortDescriptor` prop to the `
` to display the sorted column. ```tsx render "use client"; @@ -441,7 +441,7 @@ function SortableTable() { } return ( -
` with a ``, and add a `` to each column to make it resizable. Use the `defaultWidth`, `width`, `minWidth`, and `maxWidth` props on a `` to control resizing behavior. These accept pixels, percentages, or fractional values (the [fr](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Basic_Concepts_of_Grid_Layout#the_fr_unit) unit). The default column width is `1fr`. +Wrap the `
` with a ``, and add a `` to each column to make it resizable. Use the `defaultWidth`, `width`, `minWidth`, and `maxWidth` props on a Column to control resizing behavior. These accept pixels, percentages, or fractional values (the [fr](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Basic_Concepts_of_Grid_Layout#the_fr_unit) unit). The default column width is `1fr`. ```tsx render "use client"; @@ -519,7 +519,7 @@ const rows = [ ### Resize events -The ResizableTableContainer's `onResize` event is called when a column resizer is moved by the user. The `onResizeEnd` event is called when the user finishes resizing. These receive a `Map` containing the widths of all columns in the Table. This example persists the column widths in `localStorage`. +The `ResizableTableContainer`'s `onResize` event is called when a column resizer is moved by the user. The `onResizeEnd` event is called when the user finishes resizing. These receive a `Map` containing the widths of all columns in the Table. This example persists the column widths in `localStorage`. ```tsx render "use client"; @@ -562,14 +562,14 @@ export default function ResizableTable() {
{column => ( -
- File Name + {column.name}
diff --git a/packages/dev/s2-docs/pages/s2/TableView.mdx b/packages/dev/s2-docs/pages/s2/TableView.mdx new file mode 100644 index 00000000000..9472eb4d47c --- /dev/null +++ b/packages/dev/s2-docs/pages/s2/TableView.mdx @@ -0,0 +1,666 @@ +import {Layout} from '../../src/Layout'; +export default Layout; + +import docs from 'docs:@react-spectrum/s2'; +import {InlineAlert, Heading, Content} from '@react-spectrum/s2' + +# TableView + +{docs.exports.TableView.description} + +```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode', 'overflowMode', 'density', 'isQuiet']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple'}} type="s2" +import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + + + + Name + Type + Date Modified + + + + Games + File folder + 6/7/2020 + + + Program Files + File folder + 4/7/2021 + + + bootmgr + System file + 11/20/2010 + + + log.txt + Text Document + 1/18/2016 + + + +``` + +## Content + +TableView follows the **Collection Components API**, accepting both static and dynamic collections. +In this example, 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. + +```tsx render type="s2" +"use client"; +import {TableView, TableHeader, Column, TableBody, Row, Cell, CheckboxGroup, Checkbox, ActionButton} from '@react-spectrum/s2'; +import {useState} from 'react'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + +///- begin collapse -/// +const columns = [ + {name: 'Name', id: 'name', isRowHeader: true}, + {name: 'Type', id: 'type'}, + {name: 'Date Modified', id: 'date'} +]; +///- end collapse -/// + +///- begin collapse -/// +const initialRows = [ + {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'} +]; +///- end collapse -/// + +function FileTable() { + let [showColumns, setShowColumns] = useState(['name', 'type', 'date']); + let visibleColumns = columns.filter(column => showColumns.includes(column.id)); + + let [rows, setRows] = useState(initialRows); + let addRow = () => { + let date = new Date().toLocaleDateString(); + setRows(rows => [ + ...rows, + {id: rows.length + 1, name: 'file.txt', date, type: 'Text Document'} + ]); + }; + + return ( +
+ + Type + Date Modified + + + + {column => ( + + {column.name} + + )} + + {/*- begin highlight -*/} + + {item => ( + /*- end highlight -*/ + + {column => {item[column.id]}} + + )} + + + Add row +
+ ); +} +``` + + + Memoization + 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). + + + +### Asynchronous loading + +To enable infinite scrolling, provide `loadingState` and `onLoadMore` to the TableView. Use whatever data fetching library you prefer – this example uses `useAsyncList` from `react-stately`. + +```tsx render type="s2" +"use client"; +import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; +import {useAsyncList} from 'react-stately'; + +interface Character { + name: string; + height: number; + mass: number; + birth_year: number; +} + +function AsyncSortTable() { + let list = useAsyncList({ + async load({ signal, cursor }) { + if (cursor) { + cursor = cursor.replace(/^http:\/\//i, 'https://'); + } + + let res = await fetch( + cursor || 'https://swapi.py4e.com/api/people/?search=', + { signal } + ); + let json = await res.json(); + + return { + items: json.results, + cursor: json.next + }; + } + }); + + return ( + + + Name + Height + Mass + Birth Year + + + {(item) => ( + + {item.name} + {item.height} + {item.mass} + {item.birth_year} + + )} + + + ); +} +``` + +### Links + +Use the `href` prop on a Row to create a link. See the **client side routing guide** to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the [selection guide](selection.html) for more details. + +```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode']} initialProps={{'aria-label': 'Bookmarks', selectionMode: 'multiple'}} wide type="s2" hideImports +"use client"; +import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + + + + Name + URL + Date added + + + {/*- begin highlight -*/} + + {/*- end highlight -*/} + Adobe + https://adobe.com/ + January 28, 2023 + + + Google + https://google.com/ + April 5, 2023 + + + New York Times + https://nytimes.com/ + July 12, 2023 + + + +``` + +### Empty state + +```tsx render hideImports type="s2" +"use client"; +import {TableView, TableHeader, Column, TableBody} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + + + + Name + Type + Date Modified + + {/*- begin highlight -*/} + 'No results found.'}> + {/*- end highlight -*/} + {[]} + + +``` + +### Column/Cell alignment and dividers + +Use the `align` prop on a Column and Cell to control the horizontal alignment of the content within. Similarly, use the `showDivider` prop to +render a visual divider between columns and cells for increased clarity. + +```tsx render hideImports type="s2" +"use client"; +import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + +///- begin collapse -/// +const rows = [ + {id: 1, name: 'Charizard', type: 'Fire, Flying', level: 67}, + {id: 2, name: 'Blastoise', type: 'Water', level: 56}, + {id: 3, name: 'Venusaur', type: 'Grass, Poison', level: 83}, + {id: 4, name: 'Pikachu', type: 'Electric', level: 100} +]; +///- end collapse -/// + +const columns = [ + {id: 'name', name: 'Name', isRowHeader: true, showDivider: true}, + {id: 'type', name: 'Type', align: 'center', showDivider: true}, + {id: 'level', name: 'Level', align: 'end'} +]; + +function TableWithDividers() { + return ( + + + {/*- begin highlight -*/} + {(column) => ( + {column.name} + )} + {/*- end highlight -*/} + + + {item => ( + + {(column) => { + {/*- begin highlight -*/} + return {item[column.id]}; + {/*- end highlight -*/} + }} + + )} + + + ); +} +``` + +### Custom menus + +{/* TODO: Add link to S2 Menu when available. */} +Custom column menus can be added via the `menuItems` prop. This prop accepts the same contents that a Menu accepts, please see the docs for +more details. + +```tsx render type="s2" +"use client"; +import {TableView, TableHeader, Column, TableBody, Row, Cell, MenuSection, MenuItem} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + +///- begin collapse -/// +const rows = [ + {id: 1, name: 'Charizard', type: 'Fire, Flying', level: 67}, + {id: 2, name: 'Blastoise', type: 'Water', level: 56}, + {id: 3, name: 'Venusaur', type: 'Grass, Poison', level: 83}, + {id: 4, name: 'Pikachu', type: 'Electric', level: 100} +]; +///- end collapse -/// + +///- begin collapse -/// +const columns = [ + {id: 'name', name: 'Name', isRowHeader: true}, + {id: 'type', name: 'Type'}, + {id: 'level', name: 'Level'} +]; +///- end collapse -/// + +function CustomMenusTable() { + return ( + + + {(column) => ( + + + alert(`Filtering "${column.name}" column`)}>Filter + + + alert(`Hiding "${column.name}" column`)}>Hide column + alert(`Managing the "${column.name}" column`)}>Manage columns + + + } + ///- end highlight -/// + isRowHeader={column?.isRowHeader} + > + {column.name} + + )} + + + {item => ( + + {(column) => { + return {item[column.id]}; + }} + + )} + + + ); +} +``` + + +## Selection and actions + +Use the `selectionMode` prop to enable single or multiple selection. The selected rows can be controlled via the `selectedKeys` prop, matching the `id` prop of the rows. Similarly, rows can be disabled via the `disabledKeys` prop. The `onAction` event handles item actions. See the [selection guide](selection.html) for more details. + +```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode', 'escapeKeyBehavior', 'disallowEmptySelection', 'shouldSelectOnPressUp']} initialProps={{selectionMode: 'multiple'}} wide hideImports type="s2" +"use client"; +import type {Selection} from 'react-aria-components'; +import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2'; +import {useState} from 'react'; + +function Example(props) { + let [selected, setSelected] = useState(new Set()); + + return ( +
+ alert(`Clicked ${key}`)} + ///- end highlight -/// + > + + Name + Type + Level + + + + Charizard + Fire, Flying + 67 + + + Blastoise + Water + 56 + + + Venusaur + Grass, Poison + 83 + + + Pikachu + Electric + 100 + + + +

Current selection: {selected === 'all' ? 'all' : [...selected].join(', ')}

+
+ ); +} +``` + + +## Sorting + +Set the `allowsSorting` prop on a Column to make it sortable. When the column header is pressed, the table will call `onSortChange` with a including the sorted column and direction (ascending or descending). Use this to sort the data accordingly, and pass the `sortDescriptor` prop to the TableView to display the sorted column. + +```tsx render hideImports type="s2" +"use client"; +import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; +import {type SortDescriptor} from 'react-aria-components'; +import {useState} from 'react'; + +///- begin collapse -/// +const rows = [ + {id: 1, name: 'Charizard', type: 'Fire, Flying', level: 67}, + {id: 2, name: 'Blastoise', type: 'Water', level: 56}, + {id: 3, name: 'Venusaur', type: 'Grass, Poison', level: 83}, + {id: 4, name: 'Pikachu', type: 'Electric', level: 100} +]; +///- end collapse -/// + +function SortableTable() { + let [sortDescriptor, setSortDescriptor] = useState(null); + let sortedRows = rows; + if (sortDescriptor) { + sortedRows = rows.toSorted((a, b) => { + let first = a[sortDescriptor.column]; + let second = b[sortDescriptor.column]; + let cmp = first < second ? -1 : 1; + if (sortDescriptor.direction === 'descending') { + cmp = -cmp; + } + return cmp; + }); + } + + return ( + + + {/*- begin highlight -*/} + Name + Type + Level + {/*- end highlight -*/} + + + {item => ( + + {item.name} + {item.type} + {item.level} + + )} + + + ); +} +``` + + +## Column resizing + +Set the `allowsResizing` prop on a Column to make it resizable. Use the `defaultWidth`, `width`, `minWidth`, and `maxWidth` props on a Column to control resizing behavior. These accept pixels, percentages, or fractional values (the [fr](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Basic_Concepts_of_Grid_Layout#the_fr_unit) unit). The default column width is `1fr`. + +```tsx render hideImports type="s2" +"use client"; +import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + +///- begin collapse -/// +const rows = [ + {id: 1, name: '2022 Roadmap Proposal Revision 012822 Copy (2)', date: 'November 27, 2022 at 4:56PM', size: '214 KB'}, + {id: 2, name: 'Budget', date: 'January 27, 2021 at 1:56AM', size: '14 MB'}, + {id: 3, name: 'Welcome Email Template', date: 'July 24, 2022 at 2:48 PM', size: '20 KB'}, + {id: 4, name: 'Job Posting_8301', date: 'May 30, 2025', size: '139 KB'} +]; +///- end collapse -/// + + + + {/*- begin highlight -*/} + + File Name + + Size + + Date Modified + + {/*- end highlight -*/} + + + {item => ( + + {item.name} + {item.size} + {item.date} + + )} + + +``` + +### Resize events + +The TableView's `onResize` event is called when a column resizer is moved by the user. The `onResizeEnd` event is called when the user finishes resizing. These receive a `Map` containing the widths of all columns in the TableView. This example persists the column widths in `localStorage`. + +```tsx render hideImports type="s2" +"use client"; +import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; +import {useSyncExternalStore} from 'react'; + +///- begin collapse -/// +const rows = [ + {id: 1, name: '2022 Roadmap Proposal Revision 012822 Copy (2)', date: 'November 27, 2022 at 4:56PM', size: '214 KB'}, + {id: 2, name: 'Budget', date: 'January 27, 2021 at 1:56AM', size: '14 MB'}, + {id: 3, name: 'Welcome Email Template', date: 'July 24, 2022 at 2:48 PM', size: '20 KB'}, + {id: 4, name: 'Job Posting_8301', date: 'May 30, 2025', size: '139 KB'} +]; +///- end collapse -/// + +///- begin collapse -/// +const columns = [ + {id: 'file', name: 'File Name'}, + {id: 'size', name: 'Size'}, + {id: 'date', name: 'Date'} +]; +///- end collapse -/// + +const initialWidths = new Map([ + ['file', '1fr'], + ['size', 80], + ['date', 100] +]); + +export default function ResizableTable() { + let columnWidths = useSyncExternalStore(subscribe, getColumnWidths, getInitialWidths); + + return ( + + + {column => ( + + {column.name} + + )} + + + {item => ( + + {item.name} + {item.size} + {item.date} + + )} + + + ); +} + +let parsedWidths; +function getColumnWidths() { + // Parse column widths from localStorage. + if (!parsedWidths) { + let data = localStorage.getItem('table-widths'); + if (data) { + parsedWidths = new Map(JSON.parse(data)); + } + } + return parsedWidths || initialWidths; +} + +function setColumnWidths(widths) { + // Store new widths in localStorage, and trigger subscriptions. + localStorage.setItem('table-widths', JSON.stringify(Array.from(widths))); + window.dispatchEvent(new Event('storage')); +} + +function getInitialWidths() { + return initialWidths; +} + +function subscribe(fn) { + let onStorage = () => { + // Invalidate cache. + parsedWidths = null; + fn(); + }; + + window.addEventListener('storage', onStorage); + return () => window.removeEventListener('storage', onStorage); +} +``` + +## API + +### TableView + + + +### TableHeader + + + +### Column + + + +### TableBody + + + +### Row + + + +### Cell + + diff --git a/packages/dev/s2-docs/src/CodeBlock.tsx b/packages/dev/s2-docs/src/CodeBlock.tsx index 879ccdd5355..e8d1a6ba62a 100644 --- a/packages/dev/s2-docs/src/CodeBlock.tsx +++ b/packages/dev/s2-docs/src/CodeBlock.tsx @@ -78,7 +78,7 @@ export function CodeBlock({render, children, files, expanded, hidden, ...props}: } let content = ( - + {code} ); @@ -89,7 +89,7 @@ export function CodeBlock({render, children, files, expanded, hidden, ...props}: component={render} align={props.align} />
- {files + {files ? {content} : content}
@@ -170,6 +170,6 @@ export function getFiles(files: string[]) { } } } - + return fileContents; }