diff --git a/.changeset/quiet-rockets-throw.md b/.changeset/quiet-rockets-throw.md new file mode 100644 index 0000000000..6ba34beeff --- /dev/null +++ b/.changeset/quiet-rockets-throw.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': patch +--- + +Add new DataTable component and update Table component styles. diff --git a/packages/canon/css/components.css b/packages/canon/css/components.css index 264be6375b..af4ea8c4f0 100644 --- a/packages/canon/css/components.css +++ b/packages/canon/css/components.css @@ -94,6 +94,36 @@ height: 1.5rem; } +.canon-DataTableRoot { + gap: var(--canon-space-3); + flex-direction: column; + display: flex; +} + +.canon-DataTablePagination { + padding-top: var(--canon-space-5); + justify-content: space-between; + align-items: center; + display: flex; +} + +.canon-DataTablePagination--left { + justify-content: space-between; + align-items: center; + display: flex; +} + +.canon-DataTablePagination--right { + justify-content: space-between; + align-items: center; + gap: var(--canon-space-2); + display: flex; +} + +.canon-DataTablePagination--select { + min-width: 10.5rem; +} + .canon-Flex { display: flex; } @@ -179,63 +209,41 @@ display: flex; } -.table { - background-color: var(--canon-bg-surface-1); - border-radius: var(--canon-radius-2); +.canon-TableRoot { + caption-side: bottom; + border-collapse: collapse; width: 100%; - padding-bottom: var(--canon-space-0_5); - padding-top: var(--canon-space-0_5); - font-size: var(--canon-font-size-3); - font-family: var(--canon-font-regular); - font-weight: var(--canon-font-weight-regular); - position: relative; - overflow: auto; - - & table { - caption-side: bottom; - width: 100%; - font-size: var(--canon-font-size-sm); - border-collapse: collapse; - } } -.table-head { +.canon-TableHead { text-align: left; padding: var(--canon-space-3); + font-size: var(--canon-font-size-3); + color: var(--canon-fg-primary); } -.table-body { - & tr:last-child { - border-bottom: none; - } +.canon-TableBody { + color: var(--canon-fg-primary); } -.table-row { +.canon-TableRow { + border-bottom: 1px solid var(--canon-border); transition: color .2s ease-in-out; - - &:hover td { - background-color: var(--canon-bg-tint); - } - - & .table-cell:first-child { - border-top-left-radius: var(--canon-radius-2); - border-bottom-left-radius: var(--canon-radius-2); - box-shadow: inset 4px 2px 0 0 var(--canon-bg-surface-1), inset 4px -2px 0 0 var(--canon-bg-surface-1); - padding-left: var(--canon-space-3); - } - - & .table-cell:last-child { - border-top-right-radius: var(--canon-radius-2); - border-bottom-right-radius: var(--canon-radius-2); - box-shadow: inset -4px 2px 0 0 var(--canon-bg-surface-1), inset -4px -2px 0 0 var(--canon-bg-surface-1); - padding-right: var(--canon-space-3); - } } -.table-cell { +.canon-TableBody .canon-TableRow:hover { + background-color: var(--canon-gray-2); +} + +.canon-TableCell { padding: var(--canon-space-3); - background-color: var(--canon-bg); - box-shadow: inset 0px 2px 0 0 var(--canon-bg-surface-1), inset 0px -2px 0 0 var(--canon-bg-surface-1); + font-size: var(--canon-font-size-3); +} + +.canon-TableCellText, .canon-TableCellLink { + gap: var(--canon-space-0_5); + flex-direction: column; + display: flex; } .canon-Text { diff --git a/packages/canon/css/datatable.css b/packages/canon/css/datatable.css new file mode 100644 index 0000000000..edef29f500 --- /dev/null +++ b/packages/canon/css/datatable.css @@ -0,0 +1,23 @@ +.canon-DataTablePagination { + padding-top: var(--canon-space-5); + justify-content: space-between; + align-items: center; + display: flex; +} + +.canon-DataTablePagination--left { + justify-content: space-between; + align-items: center; + display: flex; +} + +.canon-DataTablePagination--right { + justify-content: space-between; + align-items: center; + gap: var(--canon-space-2); + display: flex; +} + +.canon-DataTablePagination--select { + min-width: 10.5rem; +} diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css index ba8fe6dab2..2230922917 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -9318,6 +9318,36 @@ height: 1.5rem; } +.canon-DataTableRoot { + gap: var(--canon-space-3); + flex-direction: column; + display: flex; +} + +.canon-DataTablePagination { + padding-top: var(--canon-space-5); + justify-content: space-between; + align-items: center; + display: flex; +} + +.canon-DataTablePagination--left { + justify-content: space-between; + align-items: center; + display: flex; +} + +.canon-DataTablePagination--right { + justify-content: space-between; + align-items: center; + gap: var(--canon-space-2); + display: flex; +} + +.canon-DataTablePagination--select { + min-width: 10.5rem; +} + .canon-Flex { display: flex; } @@ -9403,63 +9433,41 @@ display: flex; } -.table { - background-color: var(--canon-bg-surface-1); - border-radius: var(--canon-radius-2); +.canon-TableRoot { + caption-side: bottom; + border-collapse: collapse; width: 100%; - padding-bottom: var(--canon-space-0_5); - padding-top: var(--canon-space-0_5); - font-size: var(--canon-font-size-3); - font-family: var(--canon-font-regular); - font-weight: var(--canon-font-weight-regular); - position: relative; - overflow: auto; - - & table { - caption-side: bottom; - width: 100%; - font-size: var(--canon-font-size-sm); - border-collapse: collapse; - } } -.table-head { +.canon-TableHead { text-align: left; padding: var(--canon-space-3); + font-size: var(--canon-font-size-3); + color: var(--canon-fg-primary); } -.table-body { - & tr:last-child { - border-bottom: none; - } +.canon-TableBody { + color: var(--canon-fg-primary); } -.table-row { +.canon-TableRow { + border-bottom: 1px solid var(--canon-border); transition: color .2s ease-in-out; - - &:hover td { - background-color: var(--canon-bg-tint); - } - - & .table-cell:first-child { - border-top-left-radius: var(--canon-radius-2); - border-bottom-left-radius: var(--canon-radius-2); - box-shadow: inset 4px 2px 0 0 var(--canon-bg-surface-1), inset 4px -2px 0 0 var(--canon-bg-surface-1); - padding-left: var(--canon-space-3); - } - - & .table-cell:last-child { - border-top-right-radius: var(--canon-radius-2); - border-bottom-right-radius: var(--canon-radius-2); - box-shadow: inset -4px 2px 0 0 var(--canon-bg-surface-1), inset -4px -2px 0 0 var(--canon-bg-surface-1); - padding-right: var(--canon-space-3); - } } -.table-cell { +.canon-TableBody .canon-TableRow:hover { + background-color: var(--canon-gray-2); +} + +.canon-TableCell { padding: var(--canon-space-3); - background-color: var(--canon-bg); - box-shadow: inset 0px 2px 0 0 var(--canon-bg-surface-1), inset 0px -2px 0 0 var(--canon-bg-surface-1); + font-size: var(--canon-font-size-3); +} + +.canon-TableCellText, .canon-TableCellLink { + gap: var(--canon-space-0_5); + flex-direction: column; + display: flex; } .canon-Text { diff --git a/packages/canon/css/table.css b/packages/canon/css/table.css index a8e904d7ce..f14ee3759e 100644 --- a/packages/canon/css/table.css +++ b/packages/canon/css/table.css @@ -1,58 +1,4 @@ -.table { - background-color: var(--canon-bg-surface-1); - border-radius: var(--canon-radius-2); - width: 100%; - padding-bottom: var(--canon-space-0_5); - padding-top: var(--canon-space-0_5); +.canon-TableCell { + padding: var(--canon-space-3); font-size: var(--canon-font-size-3); - font-family: var(--canon-font-regular); - font-weight: var(--canon-font-weight-regular); - position: relative; - overflow: auto; - - & table { - caption-side: bottom; - width: 100%; - font-size: var(--canon-font-size-sm); - border-collapse: collapse; - } -} - -.table-head { - text-align: left; - padding: var(--canon-space-3); -} - -.table-body { - & tr:last-child { - border-bottom: none; - } -} - -.table-row { - transition: color .2s ease-in-out; - - &:hover td { - background-color: var(--canon-bg-tint); - } - - & .table-cell:first-child { - border-top-left-radius: var(--canon-radius-2); - border-bottom-left-radius: var(--canon-radius-2); - box-shadow: inset 4px 2px 0 0 var(--canon-bg-surface-1), inset 4px -2px 0 0 var(--canon-bg-surface-1); - padding-left: var(--canon-space-3); - } - - & .table-cell:last-child { - border-top-right-radius: var(--canon-radius-2); - border-bottom-right-radius: var(--canon-radius-2); - box-shadow: inset -4px 2px 0 0 var(--canon-bg-surface-1), inset -4px -2px 0 0 var(--canon-bg-surface-1); - padding-right: var(--canon-space-3); - } -} - -.table-cell { - padding: var(--canon-space-3); - background-color: var(--canon-bg); - box-shadow: inset 0px 2px 0 0 var(--canon-bg-surface-1), inset 0px -2px 0 0 var(--canon-bg-surface-1); } diff --git a/packages/canon/package.json b/packages/canon/package.json index 5a369615b6..ce188f1fb0 100644 --- a/packages/canon/package.json +++ b/packages/canon/package.json @@ -56,6 +56,7 @@ "@storybook/react": "^8.6.8", "@storybook/react-webpack5": "^8.6.8", "@storybook/test": "^8.6.8", + "@tanstack/react-table": "^8.21.2", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "chalk": "^5.4.1", diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 241b315220..1e65a51dca 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -17,6 +17,7 @@ import { ReactNode } from 'react'; import { RefAttributes } from 'react'; import type { RemixiconComponentType } from '@remixicon/react'; import { ScrollArea as ScrollArea_2 } from '@base-ui-components/react/scroll-area'; +import { Table as Table_2 } from '@tanstack/react-table'; import { TdHTMLAttributes } from 'react'; import { ThHTMLAttributes } from 'react'; import { Tooltip as Tooltip_2 } from '@base-ui-components/react/tooltip'; @@ -219,6 +220,65 @@ export interface ContainerProps { style?: React.CSSProperties; } +// @public +export const DataTable: { + Root: ( + props: { + table: Table_2; + } & React.HTMLAttributes, + ) => JSX.Element; + Pagination: ForwardRefExoticComponent< + DataTablePaginationProps & RefAttributes + >; + Table: ForwardRefExoticComponent< + DataTableTableProps & RefAttributes + >; + TableRoot: ForwardRefExoticComponent< + Omit< + HTMLAttributes & RefAttributes, + 'ref' + > & + RefAttributes + >; + TableHeader: ForwardRefExoticComponent< + HTMLAttributes & + RefAttributes + >; + TableBody: ForwardRefExoticComponent< + HTMLAttributes & + RefAttributes + >; + TableRow: ForwardRefExoticComponent< + HTMLAttributes & RefAttributes + >; + TableCell: ForwardRefExoticComponent< + TdHTMLAttributes & RefAttributes + >; + TableCellText: ForwardRefExoticComponent< + TableCellTextProps & RefAttributes + >; + TableCellLink: ForwardRefExoticComponent< + TableCellLinkProps & RefAttributes + >; + TableHead: ForwardRefExoticComponent< + ThHTMLAttributes & RefAttributes + >; +}; + +// @public (undocumented) +export interface DataTablePaginationProps + extends React.HTMLAttributes {} + +// @public (undocumented) +export interface DataTableRootProps + extends React.HTMLAttributes { + table: Table_2; +} + +// @public (undocumented) +export interface DataTableTableProps + extends React.HTMLAttributes {} + // @public (undocumented) export type Display = 'none' | 'flex' | 'block' | 'inline'; @@ -1028,43 +1088,61 @@ export type StylingPropDef = { parseValue?: (value: string) => string | undefined; }; -// @public (undocumented) -export const Table: ForwardRefExoticComponent< - HTMLAttributes & RefAttributes ->; +// @public +export const Table: { + Root: ForwardRefExoticComponent< + HTMLAttributes & RefAttributes + >; + Header: ForwardRefExoticComponent< + HTMLAttributes & + RefAttributes + >; + Body: ForwardRefExoticComponent< + HTMLAttributes & + RefAttributes + >; + Head: ForwardRefExoticComponent< + ThHTMLAttributes & RefAttributes + >; + Row: ForwardRefExoticComponent< + HTMLAttributes & RefAttributes + >; + Cell: ForwardRefExoticComponent< + TdHTMLAttributes & RefAttributes + >; + CellText: ForwardRefExoticComponent< + TableCellTextProps & RefAttributes + >; + CellLink: ForwardRefExoticComponent< + TableCellLinkProps & RefAttributes + >; + Caption: ForwardRefExoticComponent< + HTMLAttributes & + RefAttributes + >; +}; // @public (undocumented) -export const TableBody: ForwardRefExoticComponent< - HTMLAttributes & - RefAttributes ->; +export interface TableCellLinkProps + extends React.TdHTMLAttributes { + // (undocumented) + description?: string; + // (undocumented) + href: string; + // (undocumented) + render?: useRender.ComponentProps<'a'>['render']; + // (undocumented) + title: string; +} // @public (undocumented) -export const TableCell: ForwardRefExoticComponent< - TdHTMLAttributes & RefAttributes ->; - -// @public (undocumented) -export const TableFooter: ForwardRefExoticComponent< - HTMLAttributes & - RefAttributes ->; - -// @public (undocumented) -export const TableHead: ForwardRefExoticComponent< - ThHTMLAttributes & RefAttributes ->; - -// @public (undocumented) -export const TableHeader: ForwardRefExoticComponent< - HTMLAttributes & - RefAttributes ->; - -// @public (undocumented) -export const TableRow: ForwardRefExoticComponent< - HTMLAttributes & RefAttributes ->; +export interface TableCellTextProps + extends React.TdHTMLAttributes { + // (undocumented) + description?: string; + // (undocumented) + title: string; +} // @public (undocumented) const Text_2: ForwardRefExoticComponent< diff --git a/packages/canon/src/components/DataTable/DataTable.stories.tsx b/packages/canon/src/components/DataTable/DataTable.stories.tsx new file mode 100644 index 0000000000..266aedc533 --- /dev/null +++ b/packages/canon/src/components/DataTable/DataTable.stories.tsx @@ -0,0 +1,145 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Meta, StoryObj } from '@storybook/react'; +import { DataTable } from '.'; +import { data, DataProps } from './mocked-components'; +import { columns } from './mocked-columns'; +import { + flexRender, + getCoreRowModel, + getPaginationRowModel, + useReactTable, + PaginationState, +} from '@tanstack/react-table'; +import { useState } from 'react'; + +const meta = { + title: 'Components/DataTable', +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Uncontrolled: Story = { + render: () => { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }); + + return ( + + + + + ); + }, +}; + +export const Controlled: Story = { + render: () => { + const [pagination, setPagination] = useState({ + pageIndex: 4, + pageSize: 5, + }); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + state: { + pagination, + }, + onPaginationChange: setPagination, + }); + + return ( + + + + + ); + }, +}; + +export const WithCustomTable: Story = { + render: () => { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }); + + return ( + + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + + + + + ); + }, +}; diff --git a/packages/canon/src/components/DataTable/DataTable.tsx b/packages/canon/src/components/DataTable/DataTable.tsx new file mode 100644 index 0000000000..8be9ae174e --- /dev/null +++ b/packages/canon/src/components/DataTable/DataTable.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { forwardRef } from 'react'; +import { Table } from '../Table'; +import { DataTableRoot } from './Root/DataTableRoot'; +import { DataTablePagination } from './Pagination/DataTablePagination'; +import { Table as TanstackTable } from '@tanstack/react-table'; +import { DataTableTable } from './Table/DataTableTable'; + +const TableRoot = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ); +TableRoot.displayName = Table.Root.displayName; + +/** + * DataTable component for displaying tabular data with pagination + * @public + */ +export const DataTable = { + Root: DataTableRoot as ( + props: { + table: TanstackTable; + } & React.HTMLAttributes, + ) => JSX.Element, + Pagination: DataTablePagination, + Table: DataTableTable, + TableRoot: TableRoot, + TableHeader: Table.Header, + TableBody: Table.Body, + TableRow: Table.Row, + TableCell: Table.Cell, + TableCellText: Table.CellText, + TableCellLink: Table.CellLink, + TableHead: Table.Head, +}; diff --git a/packages/canon/src/components/DataTable/Pagination/DataTablePagination.stories.tsx b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.stories.tsx new file mode 100644 index 0000000000..a2f931aed6 --- /dev/null +++ b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.stories.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Meta, StoryObj } from '@storybook/react'; +import { DataTablePagination } from './DataTablePagination'; +import { + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { data, DataProps } from '../mocked-components'; +import { columns } from '../mocked-columns'; +import { DataTable } from '../DataTable'; + +const meta = { + title: 'Components/DataTable/Pagination', + component: DataTablePagination, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + }); + + return ( + + + + ); + }, +}; diff --git a/packages/canon/src/components/DataTable/Pagination/DataTablePagination.styles.css b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.styles.css new file mode 100644 index 0000000000..02d12e73bb --- /dev/null +++ b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.styles.css @@ -0,0 +1,23 @@ +.canon-DataTablePagination { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: var(--canon-space-5); +} + +.canon-DataTablePagination--left { + display: flex; + align-items: center; + justify-content: space-between; +} + +.canon-DataTablePagination--right { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--canon-space-2); +} + +.canon-DataTablePagination--select { + min-width: 10.5rem; +} diff --git a/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx new file mode 100644 index 0000000000..c1c73ee13c --- /dev/null +++ b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx @@ -0,0 +1,90 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { forwardRef } from 'react'; +import { Text } from '../../Text'; +import { DataTablePaginationProps } from './types'; +import { IconButton } from '../../IconButton'; +import clsx from 'clsx'; +import { Select } from '../../Select'; +import { useDataTable } from '../Root/DataTableRoot'; + +/** @public */ +const DataTablePagination = forwardRef( + ( + props: DataTablePaginationProps, + ref: React.ForwardedRef, + ) => { + const { className, ...rest } = props; + const { table } = useDataTable(); + const pageIndex = table?.getState().pagination.pageIndex; + const pageSize = table?.getState().pagination.pageSize; + + return ( +
+
+ {!table.options.manualPagination && ( +