From d71af78ff7f38202649b49e634e328578cb3043e Mon Sep 17 00:00:00 2001 From: Andrew Holloway Date: Fri, 6 Sep 2024 09:35:02 -0500 Subject: [PATCH] feat(DataTable): implement TanStack Table (#2055) - use tanstack as basis for table implementation - add in relevant stories for styles - add in snapshots for tests --- package.json | 1 + src/components/DataTable/DataTable.module.css | 135 +- .../DataTable/DataTable.stories.tsx | 304 ++- src/components/DataTable/DataTable.tsx | 332 +++- .../__snapshots__/DataTable.test.ts.snap | 1732 +++++++++++++---- src/components/DataTable/index.ts | 4 + src/components/PopoverContainer/index.ts | 2 +- src/components/Table/Table.tsx | 1 + src/index.ts | 3 +- src/util/variant-types.ts | 18 +- yarn.lock | 20 + 11 files changed, 2046 insertions(+), 506 deletions(-) diff --git a/package.json b/package.json index c9f4e1962..a16d3f015 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "dependencies": { "@headlessui/react": "^1.7.19", "@popperjs/core": "^2.11.8", + "@tanstack/react-table": "^8.20.5", "@tippyjs/react": "^4.2.6", "chalk": "^4.1.2", "clsx": "^2.1.1", diff --git a/src/components/DataTable/DataTable.module.css b/src/components/DataTable/DataTable.module.css index dd2170c2e..e08849a89 100644 --- a/src/components/DataTable/DataTable.module.css +++ b/src/components/DataTable/DataTable.module.css @@ -2,11 +2,8 @@ # DATA TABLE \*------------------------------------*/ -/** - * DataTable - */ - /* Visible table caption */ + /* TODO-AH: make it so that we have the search bar and actions wrap together instead of separately */ .data-table__caption-container { display: flex; align-items: flex-end; @@ -15,7 +12,7 @@ gap: calc(var(--eds-size-3) / 16 * 1rem) calc(var(--eds-size-6) / 16 * 1rem); text-align: start; - margin: 0 calc(var(--eds-size-3) / 16 * 1rem) calc(var(--eds-size-4) / 16 * 1rem); + margin: 0 calc(var(--eds-size-3) / 16 * 1rem) calc(var(--eds-size-4) / 16 * 1rem); } .data-table__caption-text { @@ -37,9 +34,14 @@ } .data-table__table { - border: 1px solid; width: 100%; + /* add class instead of tag for styles? */ + th, td { + padding: 0; + vertical-align: top; + } + .data-table__caption + &, .data-table__subcaption + & { margin-top: calc(var(--eds-size-4) / 16 * 1rem); @@ -50,11 +52,112 @@ width: calc(var(--eds-size-34) / 16 * 1rem); } +.data-table--tableStyle-border { + border: calc(var(--eds-border-width-sm) * 1px) solid; +} + +.data-table__cell-text { + text-align: start; + + .data-table__cell--alignment-leading & { + text-align: start; + } + + .data-table__cell--alignment-trailing & { + text-align: end; + } +} + +.data-table__cell--alignment-leading { + justify-content: flex-start; +} + +.data-table__cell--alignment-trailing { + justify-content: flex-end; +} + +.data-table__header-cell { + display: flex; + gap: calc(var(--eds-size-1) / 16 * 1rem); + align-items: flex-start; + font: var(--eds-theme-typography-title-md); + + .data-table--size-sm & { + font: var(--eds-theme-typography-title-sm); + /* TODO-AH: we want to use top-/bottom-padding of 5px to give overall height divisible by 8 (32px) */ + padding: calc(var(--eds-size-half) / 16 * 1rem) calc(var(--eds-size-1) / 16 * 1rem); + } + + .data-table--size-md & { + padding: calc(var(--eds-size-2) / 16 * 1rem); + } + + .data-table__cell-sublabel { + display: block; + font: var(--eds-theme-typography-body-sm); + } + + .data-cell__cell--icon { + margin-top: calc(var(--eds-size-1) / 16 * 1rem); + } +} + +.data-table__cell { + display: flex; + gap: calc(var(--eds-size-1) / 16 * 1rem); + align-items: flex-start; + font: var(--eds-theme-typography-body-md); + + .data-table--size-sm & { + font: var(--eds-theme-typography-body-sm); + padding: calc(var(--eds-size-half) / 16 * 1rem) calc(var(--eds-size-1) / 16 * 1rem); + } + + .data-table--size-md & { + padding: calc(var(--eds-size-2) / 16 * 1rem); + } + + .data-table__cell-sublabel { + display: block; + font: var(--eds-theme-typography-body-sm); + } + + .data-cell__cell--icon { + margin-top: calc(var(--eds-size-half) / 16 * 1rem); + } +} + +.data-table__row { + .data-table--rowStyle-lined & { + border-bottom: 1px solid; + } + + .data-table--rowStyle-striped &:nth-child(even) { + background-color: var(--eds-theme-color-background-table-row-stripe-2); + } + + .data-table--rowStyle-striped &:nth-child(odd) { + background-color: var(--eds-theme-color-background-table-row-stripe-1); + } +} + +.data-table__header-row { + border-bottom: 1px solid; + /* TODO-AH: figure out positioning styles for sticky headers/columns */ + position: sticky; + top: 0; +} + /** * Color Tokens */ .data-table { display: block; + position: relative; + + .data-table__table { + background-color: var(--eds-theme-color-background-utility-base-1); + } .data-table__caption { color: var(--eds-theme-color-text-utility-default-primary); @@ -63,4 +166,24 @@ .data-table__subcaption { color: var(--eds-theme-color-text-utility-default-secondary); } + + .data-table--tableStyle-border, .data-table__header-row { + border-color: var(--eds-theme-color-border-utility-default-low-emphasis); + } + + .data-table__header-cell { + color: var(--eds-theme-color-text-utility-default-primary); + } + + .data-table__cell { + color: var(--eds-theme-color-text-utility-default-primary); + } + + .data-table--rowStyle-lined { + color: var(--eds-theme-color-border-utility-default-low-emphasis); + } + + .data-table__cell-sublabel, .data-table__header-cell-sublabel { + color: var(--eds-theme-color-text-utility-default-secondary); + } } diff --git a/src/components/DataTable/DataTable.stories.tsx b/src/components/DataTable/DataTable.stories.tsx index 38c077a98..b306e1ee1 100644 --- a/src/components/DataTable/DataTable.stories.tsx +++ b/src/components/DataTable/DataTable.stories.tsx @@ -1,8 +1,24 @@ import { BADGE } from '@geometricpanda/storybook-addon-badges'; import type { StoryObj, Meta } from '@storybook/react'; + +// Import the helpers from the base library, for later use +import { + createColumnHelper, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table'; + import React from 'react'; -import { DataTable } from './DataTable'; +import { + DataTable, + DataTableTable, + DataTableDataCell, + DataTableHeaderCell, + DataTableRow, + DataTableHeader, + type DataTableProps, +} from './DataTable'; import { chromaticViewports } from '../../util/viewports'; import Button from '../Button'; import Menu from '../Menu'; @@ -10,6 +26,14 @@ import Menu from '../Menu'; export default { title: 'Components/DataTable', component: DataTable, + // TODO-AH: generate documentation for sub-components + subcomponents: { + 'DataTable.Table': DataTableTable, + 'DataTable.Row': DataTableRow, + 'DataTable.Header': DataTableHeader, + 'DataTable.HeaderCell': DataTableHeaderCell, + 'DataTable.DataCell': DataTableDataCell, + }, parameters: { badges: [BADGE.BETA, 'intro-1.0', 'current-1.0'], chromatic: { @@ -24,34 +48,219 @@ export default { actions: { control: false, }, + children: { + control: false, + }, + }, +} as Meta; + +type Args = DataTableProps; + +// Specifying an example data type for the rows of a table +type Person = { + firstName: string; + lastName: string; + age: number; + visits: number; + progress: number; +}; + +// Specifying the example (static) data for the table to use with tanstack primitives +const defaultData: Person[] = [ + { + firstName: 'Joe', + lastName: 'Dirte', + age: 45, + visits: 20, + progress: 10, + }, + { + firstName: 'Tandy', + lastName: 'Miller', + age: 40, + visits: 40, + progress: 80, + }, + { + firstName: 'Tanner', + lastName: 'Lindsey', + age: 24, + visits: 100, + progress: 50, + }, +]; + +// We generate a helper object to generate the display columns and use the type for the structure +const columnHelper = createColumnHelper(); + +// TODO: how to make the data type apply the correct alignment and other UI props +const columns = [ + columnHelper.accessor('firstName', { + header: () => ( + + First Name + + ), + cell: (info) => ( + + {info.getValue()} + + ), + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + header: () => ( + Last Name + ), + cell: (info) => {info.getValue()}, + }), + columnHelper.accessor('age', { + header: () => ( + Age + ), + cell: (info) => ( + + {info.renderValue()} + + ), + }), + columnHelper.accessor('visits', { + header: () => ( + Visits + ), + cell: (info) => ( + + {info.renderValue()} + + ), + }), + columnHelper.accessor('progress', { + header: () => ( + + Profile Progress + + ), + cell: (info) => ( + = 80 ? 'Complete' : 'Incomplete'} + > + {info.renderValue()} + + ), + }), +]; + +export const Default: StoryObj = { + args: {}, + render: (args) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [data] = React.useState(() => [...defaultData]); + + // eslint-disable-next-line react-hooks/rules-of-hooks + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ; + }, +}; + +export const TableStyleBorder: StoryObj = { + args: { + tableStyle: 'border', + }, + render: (args) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [data] = React.useState(() => [...defaultData]); + + // eslint-disable-next-line react-hooks/rules-of-hooks + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ; + }, +}; + +/** + * When using table size small, we have less padding on the cells and header + * + * **Note**: using `sublabel`s when `size` = `'sm'` is not allowed. + */ +export const TableSizeSm: StoryObj = { + args: { + tableStyle: 'border', + size: 'sm', + }, + render: (args) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [data] = React.useState(() => [...defaultData]); + + // eslint-disable-next-line react-hooks/rules-of-hooks + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ; + }, +}; + +export const RowStyleLined: StoryObj = { + args: { + tableStyle: 'border', + rowStyle: 'lined', }, + render: (args) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [data] = React.useState(() => [...defaultData]); + + // eslint-disable-next-line react-hooks/rules-of-hooks + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ; + }, +}; + +export const DefaultWithCustomTable: StoryObj = { args: { children: ( - +
TODO: Table rows/cells HereTODO: Custom table rows/cells here
), }, -} as Meta; - -type Args = React.ComponentProps; - -export const Default: StoryObj = { - args: {}, }; export const WithBasicCaption: StoryObj = { args: { + ...DefaultWithCustomTable.args, caption: 'Fruits of the world', }, }; export const WithFullCaption: StoryObj = { + ...DefaultWithCustomTable, args: { + ...DefaultWithCustomTable.args, caption: 'Fruits of the world', subcaption: "Aren't they all so delicious?", }, @@ -59,6 +268,7 @@ export const WithFullCaption: StoryObj = { export const WithSearch: StoryObj = { args: { + ...DefaultWithCustomTable.args, caption: 'Fruits of the world', subcaption: "Aren't they all so delicious?", onSearchChange: () => {}, @@ -67,6 +277,7 @@ export const WithSearch: StoryObj = { export const WithOnlyActions: StoryObj = { args: { + ...DefaultWithCustomTable.args, actions: ( - - - - + Incomplete + + + + + + + + +`; + +exports[` DefaultWithCustomTable story renders snapshot 1`] = ` +
@@ -130,335 +379,1082 @@ exports[` TableA story renders snapshot 1`] = ` `; -exports[` TableB story renders snapshot 1`] = ` +exports[` RowStyleLined story renders snapshot 1`] = `
-
-
- - -
-
+ + - Seriously, who let this happen? - - - + + + + + - - + + +
- TODO: Table rows/cells Here + TODO: Custom table rows/cells here
+
+ First Name + + Given Name + +
+
- - - -
-
- -
+
- - - - - -
- - - - - - + Surname + + + + + + + - -
- TODO: Table rows/cells Here - +
+
+ Age +
+
+
+
+
+ Visits +
+
+
+
+
+ Profile Progress + + "Complete" is > 80% + +
+
+
- -`; - -exports[` TableC story renders snapshot 1`] = ` -
-
-
- -
+
+ Joe +
- - - -
-
- -
+
- - - - - -
- - - - - + Dirte + + + + + - -
+
+
+ 45 +
+
+
+
+
+ 20 +
+
+
- TODO: Table rows/cells Here +
+
+ 10 + + Incomplete + +
+
- -`; - -exports[` TableD story renders snapshot 1`] = ` -
-
-
- - -
-
+
+ Tandy +
- - - -
-
+
+
+
+ Miller +
+
+
+
+
+ 40 +
+
+
+
+
+ 40 +
+
+
+
+
+ 80 + + Complete + +
+
+
+
+
+ Lindsey +
+
+
+
+
+ 24 +
+
+
+
+
+ 100 +
+
+
+
+
+ 50 + - - - - - -
-
- - - + + + + + +
+ +`; + +exports[` TableSizeSm story renders snapshot 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ First Name + + Given Name + +
+ +
+
+
+
+ Last Name + + Surname + +
+
+
+
+
+ Age +
+
+
+
+
+ Visits +
+
+
+
+
+ Profile Progress + + "Complete" is > 80% + +
+
+
+
+ +
+ Joe +
+
+
+
+
+ Dirte +
+
+
+
+
+ 45 +
+
+
+
+
+ 20 +
+
+
+
+
+ 10 + + Incomplete + +
+
+
+
+ +
+ Tandy +
+
+
+
+
+ Miller +
+
+
+
+
+ 40 +
+
+
+
+
+ 40 +
+
+
+
+
+ 80 + + Complete + +
+
+
+
+ +
+ Tanner +
+
+
+
+
+ Lindsey +
+
+
+
+
+ 24 +
+
+
+
+
+ 100 +
+
+
+
+
+ 50 + + Incomplete + +
+
+
+
+`; + +exports[` TableStyleBorder story renders snapshot 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -490,7 +1486,7 @@ exports[` WithBasicCaption story renders snapshot 1`] = ` > @@ -528,7 +1524,7 @@ exports[` WithFullCaption story renders snapshot 1`] = ` > @@ -640,7 +1636,7 @@ exports[` WithLongCaption story renders snapshot 1`] = ` > @@ -699,7 +1695,7 @@ exports[` WithOnlyActions story renders snapshot 1`] = ` > @@ -774,7 +1770,7 @@ exports[` WithSearch story renders snapshot 1`] = ` > @@ -886,7 +1882,7 @@ exports[` WithSearchAndActions story renders snapshot 1`] = ` > @@ -1050,7 +2046,7 @@ exports[` WithSearchAndCustomActions story renders snapshot 1`] = ` > diff --git a/src/components/DataTable/index.ts b/src/components/DataTable/index.ts index cadb02a9a..a298c8aec 100644 --- a/src/components/DataTable/index.ts +++ b/src/components/DataTable/index.ts @@ -1 +1,5 @@ export { DataTable as default } from './DataTable'; +export type { DataTableProps } from './DataTable'; + +// Re-export Tanstack hooks and functions to consumers +export * as api from '@tanstack/react-table'; diff --git a/src/components/PopoverContainer/index.ts b/src/components/PopoverContainer/index.ts index 2d1758bd2..8e40e54f5 100644 --- a/src/components/PopoverContainer/index.ts +++ b/src/components/PopoverContainer/index.ts @@ -2,7 +2,7 @@ export { PopoverContainer as default, defaultPopoverModifiers, } from './PopoverContainer'; -// TODO: remove V2 export +// TODO(next-major): remove V2 export export { PopoverContainer as PopoverContainerV2, defaultPopoverModifiers as defaultPopoverModifiersV2, diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index ac425018a..082993fa1 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -294,6 +294,7 @@ export const TableRow = ({ */ export const Table = ({ children, className, ...other }: TableProps) => { const componentClassName = clsx(styles['table'], className); + // TODO(next-major): decide if we should warn when used return (
+
+ +
+ First Name + + Given Name + +
+ +
+
+
+
+ Last Name + + Surname + +
+
+
+
+
+ Age +
+
+
+
+
+ Visits +
+
+
+
+
+ Profile Progress + + "Complete" is > 80% + +
+
+
+
+ +
+ Joe +
+
+
+
+
+ Dirte +
+
+
+
+
+ 45 +
+
+
- TODO: Table rows/cells Here +
+
+ 20 +
+
+
+
+
+ 10 + + Incomplete + +
+
+
+
+ +
+ Tandy +
+
+
+
+
+ Miller +
+
+
+
+
+ 40 +
+
+
+
+
+ 40 +
+
+
+
+
+ 80 + + Complete + +
+
+
+
+ +
+ Tanner +
+
+
+
+
+ Lindsey +
+
+
+
+
+ 24 +
+
+
+
+
+ 100 +
+
+
+
+
+ 50 + + Incomplete + +
+
- TODO: Table rows/cells Here + TODO: Custom table rows/cells here
- TODO: Table rows/cells Here + TODO: Custom table rows/cells here
- TODO: Table rows/cells Here + TODO: Custom table rows/cells here
- TODO: Table rows/cells Here + TODO: Custom table rows/cells here
- TODO: Table rows/cells Here + TODO: Custom table rows/cells here
- TODO: Table rows/cells Here + TODO: Custom table rows/cells here
- TODO: Table rows/cells Here + TODO: Custom table rows/cells here
{children} diff --git a/src/index.ts b/src/index.ts index e8703047d..7b1401e4b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,7 @@ export { default as ButtonGroup } from './components/ButtonGroup'; export { default as Card } from './components/Card'; export { default as Checkbox } from './components/Checkbox'; export { default as DataTable } from './components/DataTable'; +// TODO-AH: how best to re-export Tanstack stuff export { default as FieldLabel } from './components/FieldLabel'; export { default as FieldNote } from './components/FieldNote'; export { default as Fieldset } from './components/Fieldset'; @@ -61,6 +62,6 @@ export type { LinkProps } from './components/Link'; export type { PageNotificationProps } from './components/PageNotification'; export type { AppNotificationProps } from './components/AppNotification'; -// TODO: Remove the below types at the next major release +// TODO(next-major): Remove the below types at the next major release export type { PageNotificationProps as PageNotificationV2Props } from './components/PageNotification'; export type { AppNotificationProps as AppNotificationV2Props } from './components/AppNotification'; diff --git a/src/util/variant-types.ts b/src/util/variant-types.ts index 1ffd404ca..959ee6b7b 100644 --- a/src/util/variant-types.ts +++ b/src/util/variant-types.ts @@ -1,5 +1,6 @@ /** * This contains the broad types of the common programmatic variants: e.g., + * - base component props * - size * - align * - preset @@ -7,6 +8,20 @@ * use Extract to trim any unsupported variants */ +/** + * Component props used by any/every cmoponent in the system. Pick<> to grab any subset + */ +export type EDSBase = { + /** + * Sub-components and other elements appropriate for this parent component (See Sub-components if applicable) + */ + children?: React.ReactNode; + /** + * CSS class names that can be appended to the component. + */ + className?: string; +}; + /** * Size range for components, set at named intervals. Sizes need not be precisely * some distant or multiple apart; they can be defined as a set that increases with @@ -16,8 +31,9 @@ export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl'; /** * Alignment variants, for horizontal components and sub-components + * TODO(next-major): warn on using left/right */ -export type Align = 'left' | 'center' | 'right'; +export type Align = 'leading' | 'trailing' | 'left' | 'center' | 'right'; /** * Hints are form field directions on how to use the field and whether it requires a value. diff --git a/yarn.lock b/yarn.lock index caea688eb..05b5dac16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2762,6 +2762,7 @@ __metadata: "@storybook/test": "npm:^8.2.9" "@storybook/testing-library": "npm:^0.2.2" "@storybook/theming": "npm:^8.2.9" + "@tanstack/react-table": "npm:^8.20.5" "@testing-library/jest-dom": "npm:^6.5.0" "@testing-library/react": "npm:^16.0.1" "@testing-library/user-event": "npm:^14.5.2" @@ -5879,6 +5880,18 @@ __metadata: languageName: node linkType: hard +"@tanstack/react-table@npm:^8.20.5": + version: 8.20.5 + resolution: "@tanstack/react-table@npm:8.20.5" + dependencies: + "@tanstack/table-core": "npm:8.20.5" + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: 10/df67094795a0b7e4b34f73abe346443c2e806c572fea31b58759aa8ec5274f613e5e6941090eb16f861bda10d3088731bc6e7f15e5f90326db273bc55b9141ce + languageName: node + linkType: hard + "@tanstack/react-virtual@npm:^3.0.0-beta.60": version: 3.0.1 resolution: "@tanstack/react-virtual@npm:3.0.1" @@ -5891,6 +5904,13 @@ __metadata: languageName: node linkType: hard +"@tanstack/table-core@npm:8.20.5": + version: 8.20.5 + resolution: "@tanstack/table-core@npm:8.20.5" + checksum: 10/5408237920d5796951e925278edbbe76f71006627a4e3da248a810970256f75d973538fe7ae75a32155d4a25a95abc4fffaea337b5120f7940d7e664dc9da87f + languageName: node + linkType: hard + "@tanstack/virtual-core@npm:3.0.0": version: 3.0.0 resolution: "@tanstack/virtual-core@npm:3.0.0"