diff --git a/packages/ui/src/components/DataTable/DataTable.stories.tsx b/packages/ui/src/components/DataTable/DataTable.stories.tsx index 29f38e7bc2..cca1fa1f35 100644 --- a/packages/ui/src/components/DataTable/DataTable.stories.tsx +++ b/packages/ui/src/components/DataTable/DataTable.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 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. @@ -16,6 +16,15 @@ import type { Meta, StoryFn, StoryObj } from '@storybook/react'; import { DataTable } from '.'; +import { DataTablePagination } from '../DataTablePagination'; +import { + TableHeader, + TableBody, + TableRow, + TableCell, + TableHead, + Table, +} from '../Table'; import { data, DataProps } from './mocked-components'; import { columns } from './mocked-columns'; import { @@ -52,10 +61,10 @@ export const Uncontrolled: Story = { }); return ( - - - - + <> + + + ); }, }; @@ -79,10 +88,10 @@ export const Controlled: Story = { }); return ( - - - - + <> + + + ); }, }; @@ -97,57 +106,57 @@ export const WithCustomTable: Story = { }); 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/ui/src/components/DataTable/DataTable.styles.css b/packages/ui/src/components/DataTable/DataTable.styles.css new file mode 100644 index 0000000000..78c463a883 --- /dev/null +++ b/packages/ui/src/components/DataTable/DataTable.styles.css @@ -0,0 +1,37 @@ +.bui-DataTableRoot { + display: flex; + flex-direction: column; + gap: var(--bui-space-3); +} + +.bui-DataTableRoot--sort { + cursor: pointer; + user-select: none; + display: inline-flex; + align-items: center; + gap: var(--bui-space-1); +} + +.bui-DataTableRoot--sort .bui-DataTableRoot--unsorted { + opacity: 0; + transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out; +} + +.bui-DataTableRoot--sort:hover .bui-DataTableRoot--unsorted, +.bui-DataTableRoot--sort:focus .bui-DataTableRoot--unsorted { + opacity: 0.5; +} + +.bui-DataTableRoot--sort .bui-DataTableRoot--sorted-asc, +.bui-DataTableRoot--sort:hover .bui-DataTableRoot--sorted-asc, +.bui-DataTableRoot--sort:focus .bui-DataTableRoot--sorted-asc { + opacity: 1; + transform: rotate(0); +} + +.bui-DataTableRoot--sort .bui-DataTableRoot--sorted-desc, +.bui-DataTableRoot--sort:hover .bui-DataTableRoot--sorted-desc, +.bui-DataTableRoot--sort:focus .bui-DataTableRoot--sorted-desc { + opacity: 1; + transform: rotate(180deg); +} diff --git a/packages/ui/src/components/DataTable/DataTable.tsx b/packages/ui/src/components/DataTable/DataTable.tsx index 1f2270bd28..b092b9b94d 100644 --- a/packages/ui/src/components/DataTable/DataTable.tsx +++ b/packages/ui/src/components/DataTable/DataTable.tsx @@ -14,46 +14,107 @@ * limitations under the License. */ -import { forwardRef } from 'react'; +import clsx from 'clsx'; +import { DataTableProps } from './types'; import { Table, + TableRow, + TableHeader, + TableHead, TableBody, TableCell, - TableHead, - TableCellProfile, - TableCellText, - TableHeader, - TableRow, } 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'; +import { flexRender } from '@tanstack/react-table'; +import { DataTableHeadContent } from './DataTableHeadContent'; -const TableRoot = forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ); -TableRoot.displayName = Table.displayName; +function getAriaSort(sortDirection: string | false) { + if (sortDirection === 'asc') { + return 'ascending'; + } + if (sortDirection === 'desc') { + return 'descending'; + } + return 'none'; +} -/** - * 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: TableHeader, - TableBody: TableBody, - TableRow: TableRow, - TableCell: TableCell, - TableCellText: TableCellText, - TableCellProfile: TableCellProfile, - TableHead: TableHead, -}; +/** @public */ +function DataTable( + props: DataTableProps & { ref?: React.ForwardedRef }, +) { + const { className, table, ref, ...rest } = props; + + return ( +
+ + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => { + return ( + + {header.isPlaceholder ? null : ( + + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map(row => { + const rowData = row.original as TData & { onClick?: () => void }; + const handleRowClick = rowData.onClick + ? (e: React.MouseEvent) => { + if (!e.isPropagationStopped()) { + rowData.onClick!(); + } + } + : undefined; + + return ( + + {row.getVisibleCells().map(cell => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ); + }) + ) : ( + + + No results. + + + )} + +
+ ); +} + +DataTable.displayName = 'DataTable'; + +export { DataTable }; diff --git a/packages/ui/src/components/DataTable/DataTableHeadContent.tsx b/packages/ui/src/components/DataTable/DataTableHeadContent.tsx new file mode 100644 index 0000000000..8a8453715a --- /dev/null +++ b/packages/ui/src/components/DataTable/DataTableHeadContent.tsx @@ -0,0 +1,85 @@ +/* + * Copyright 2025 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 { useCallback, useMemo } from 'react'; +import clsx from 'clsx'; +import { flexRender } from '@tanstack/react-table'; +import { Icon } from '../Icon'; +import { Header } from '@tanstack/react-table'; +import { Hidden } from '../Hidden'; + +function getSortTitle(nextOrder: string | false) { + if (nextOrder === 'asc') { + return 'Sort ascending'; + } + if (nextOrder === 'desc') { + return 'Sort descending'; + } + return 'Clear sort'; +} + +interface DataTableHeadContentProps { + header: Header; +} + +export function DataTableHeadContent({ + header, +}: DataTableHeadContentProps) { + const headerContent = useMemo( + () => flexRender(header.column.columnDef.header, header.getContext()), + [header], + ); + + const handleSort = useCallback( + (e: React.MouseEvent | React.KeyboardEvent) => { + if ('key' in e && e.key !== 'Enter' && e.key !== ' ') { + return; + } + e.preventDefault(); + header.column.getToggleSortingHandler()?.(e); + }, + [header], + ); + + if (!header.column.getCanSort()) { + return headerContent; + } + + return ( + + {headerContent} + , {getSortTitle(header.column.getNextSortingOrder())} + + + ); +} diff --git a/packages/ui/src/components/DataTable/Pagination/DataTablePagination.tsx b/packages/ui/src/components/DataTable/Pagination/DataTablePagination.tsx deleted file mode 100644 index eb319d948e..0000000000 --- a/packages/ui/src/components/DataTable/Pagination/DataTablePagination.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 { ButtonIcon } from '../../ButtonIcon'; -import clsx from 'clsx'; -import { Select } from '../../Select'; -import { useDataTable } from '../Root/DataTableRoot'; -import { Icon } from '../../Icon'; - -/** @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; - const rowCount = table?.getRowCount(); - const fromCount = (pageIndex ?? 0) * (pageSize ?? 10) + 1; - const toCount = Math.min( - ((pageIndex ?? 0) + 1) * (pageSize ?? 10), - rowCount, - ); - - return ( -
-
- {!table.options.manualPagination && ( - { + const newPageSize = Number(value); + table?.setPageSize(newPageSize); + onPageSizeChange?.(newPageSize); + }} + className="bui-DataTablePagination--select" + /> + )} +
+
+ {`${fromCount} - ${toCount} of ${rowCount}`} + { + table?.previousPage(); + onPreviousPage?.(); + }} + isDisabled={!table?.getCanPreviousPage()} + icon={} + aria-label="Previous" + /> + { + table?.nextPage(); + onNextPage?.(); + }} + isDisabled={!table?.getCanNextPage()} + icon={} + aria-label="Next" + /> +
+
+ ); +} + +DataTablePagination.displayName = 'DataTablePagination'; + +export { DataTablePagination }; diff --git a/packages/ui/src/components/DataTable/Pagination/types.ts b/packages/ui/src/components/DataTablePagination/index.ts similarity index 83% rename from packages/ui/src/components/DataTable/Pagination/types.ts rename to packages/ui/src/components/DataTablePagination/index.ts index b2a895c465..0ddc63a304 100644 --- a/packages/ui/src/components/DataTable/Pagination/types.ts +++ b/packages/ui/src/components/DataTablePagination/index.ts @@ -14,6 +14,5 @@ * limitations under the License. */ -/** @public */ -export interface DataTablePaginationProps - extends React.HTMLAttributes {} +export { DataTablePagination } from './DataTablePagination'; +export type { DataTablePaginationProps } from './types'; diff --git a/packages/ui/src/components/DataTablePagination/types.ts b/packages/ui/src/components/DataTablePagination/types.ts new file mode 100644 index 0000000000..ca73af1ec7 --- /dev/null +++ b/packages/ui/src/components/DataTablePagination/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2025 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 { Table } from '@tanstack/react-table'; + +/** @public */ +export interface DataTablePaginationProps + extends React.HTMLAttributes { + table?: Table; + onNextPage?: () => void; + onPreviousPage?: () => void; + onPageSizeChange?: (pageSize: number) => void; + showPageSizeOptions?: boolean; +} + +/** @public */ +export type DataTablePaginationComponent = ( + props: DataTablePaginationProps & { + ref?: React.ForwardedRef; + }, +) => React.ReactElement; diff --git a/packages/ui/src/components/Hidden/Hidden.stories.tsx b/packages/ui/src/components/Hidden/Hidden.stories.tsx new file mode 100644 index 0000000000..0957bf2e18 --- /dev/null +++ b/packages/ui/src/components/Hidden/Hidden.stories.tsx @@ -0,0 +1,32 @@ +/* + * Copyright 2025 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 { Hidden } from './Hidden'; + +const meta = { + title: 'Components/Hidden', + component: Hidden, +} satisfies Meta