First pass at bringing new DataTable to BUI

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-07-25 19:20:35 +01:00
parent 660a378678
commit 917d004aa6
23 changed files with 512 additions and 373 deletions
@@ -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 (
<DataTable.Root table={table}>
<DataTable.Table />
<DataTable.Pagination />
</DataTable.Root>
<>
<DataTable table={table} />
<DataTablePagination table={table} />
</>
);
},
};
@@ -79,10 +88,10 @@ export const Controlled: Story = {
});
return (
<DataTable.Root table={table}>
<DataTable.Table />
<DataTable.Pagination />
</DataTable.Root>
<>
<DataTable table={table} />
<DataTablePagination table={table} />
</>
);
},
};
@@ -97,57 +106,57 @@ export const WithCustomTable: Story = {
});
return (
<DataTable.Root table={table}>
<DataTable.TableRoot>
<DataTable.TableHeader>
<>
<Table>
<TableHeader>
{table.getHeaderGroups().map(headerGroup => (
<DataTable.TableRow key={headerGroup.id}>
<TableRow key={headerGroup.id}>
{headerGroup.headers.map(header => {
return (
<DataTable.TableHead key={header.id}>
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</DataTable.TableHead>
</TableHead>
);
})}
</DataTable.TableRow>
</TableRow>
))}
</DataTable.TableHeader>
<DataTable.TableBody>
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map(row => (
<DataTable.TableRow
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map(cell => (
<DataTable.TableCell key={cell.id}>
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</DataTable.TableCell>
</TableCell>
))}
</DataTable.TableRow>
</TableRow>
))
) : (
<DataTable.TableRow>
<DataTable.TableCell
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</DataTable.TableCell>
</DataTable.TableRow>
</TableCell>
</TableRow>
)}
</DataTable.TableBody>
</DataTable.TableRoot>
<DataTable.Pagination />
</DataTable.Root>
</TableBody>
</Table>
<DataTablePagination table={table} />
</>
);
},
};
@@ -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);
}
@@ -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<typeof Table>,
React.ComponentPropsWithoutRef<typeof Table>
>(({ className, ...props }, ref) => <Table ref={ref} {...props} />);
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 <TData>(
props: {
table: TanstackTable<TData>;
} & React.HTMLAttributes<HTMLDivElement>,
) => 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<TData>(
props: DataTableProps<TData> & { ref?: React.ForwardedRef<HTMLTableElement> },
) {
const { className, table, ref, ...rest } = props;
return (
<Table
ref={ref}
style={{ minWidth: table.getTotalSize() }}
className={clsx(className)}
{...rest}
>
<TableHeader>
{table.getHeaderGroups().map(headerGroup => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map(header => {
return (
<TableHead
key={header.id}
style={{ width: header.getSize() }}
aria-sort={getAriaSort(header.column.getIsSorted())}
>
{header.isPlaceholder ? null : (
<DataTableHeadContent header={header} />
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map(row => {
const rowData = row.original as TData & { onClick?: () => void };
const handleRowClick = rowData.onClick
? (e: React.MouseEvent<HTMLTableRowElement>) => {
if (!e.isPropagationStopped()) {
rowData.onClick!();
}
}
: undefined;
return (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
data-clickable={!!rowData.onClick}
onClick={handleRowClick}
>
{row.getVisibleCells().map(cell => (
<TableCell
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
);
})
) : (
<TableRow>
<TableCell
colSpan={table.getAllColumns().length}
className="h-24 text-center"
style={{ width: table.getTotalSize() }}
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
}
DataTable.displayName = 'DataTable';
export { DataTable };
@@ -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<TData> {
header: Header<TData, unknown>;
}
export function DataTableHeadContent<TData>({
header,
}: DataTableHeadContentProps<TData>) {
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 (
<span
role="button"
tabIndex={0}
className="bui-DataTableRoot--sort"
onClick={handleSort}
onKeyDown={handleSort}
>
{headerContent}
<Hidden>, {getSortTitle(header.column.getNextSortingOrder())}</Hidden>
<Icon
aria-hidden
tab-index={-1}
name="arrow-down"
className={clsx(
'bui-DataTableRoot--unsorted',
{
asc: 'bui-DataTableRoot--sorted-asc',
desc: 'bui-DataTableRoot--sorted-desc',
}[header.column.getIsSorted() as string],
)}
data-testid="datatable--sort-icon"
/>
</span>
);
}
@@ -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<HTMLDivElement>,
) => {
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 (
<div
ref={ref}
style={{ minWidth: table?.getTotalSize() }}
className={clsx('bui-DataTablePagination', className)}
{...rest}
>
<div className="bui-DataTablePagination--left">
{!table.options.manualPagination && (
<Select
name="pageSize"
size="small"
placeholder="Show 10 results"
options={[
{ label: 'Show 5 results', value: '5' },
{ label: 'Show 10 results', value: '10' },
{ label: 'Show 20 results', value: '20' },
{ label: 'Show 30 results', value: '30' },
{ label: 'Show 40 results', value: '40' },
{ label: 'Show 50 results', value: '50' },
]}
selectedKey={pageSize?.toString()}
onSelectionChange={value => {
table?.setPageSize(Number(value));
}}
className="bui-DataTablePagination--select"
/>
)}
</div>
<div className="bui-DataTablePagination--right">
<Text variant="body-medium">{`${fromCount} - ${toCount} of ${rowCount}`}</Text>
<ButtonIcon
variant="secondary"
size="small"
onClick={() => table?.previousPage()}
isDisabled={!table?.getCanPreviousPage()}
icon={<Icon name="chevron-left" />}
/>
<ButtonIcon
variant="secondary"
size="small"
onClick={() => table?.nextPage()}
isDisabled={!table?.getCanNextPage()}
icon={<Icon name="chevron-right" />}
/>
</div>
</div>
);
},
);
DataTablePagination.displayName = 'DataTablePagination';
export { DataTablePagination };
@@ -1,5 +0,0 @@
.bui-DataTableRoot {
display: flex;
flex-direction: column;
gap: var(--bui-space-3);
}
@@ -1,58 +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, createContext, useContext } from 'react';
import clsx from 'clsx';
import { DataTableRootProps } from './types';
import { Table } from '@tanstack/react-table';
type DataTableContextType<TData> = {
table: Table<TData>;
};
/** @public */
export const DataTableContext = createContext<DataTableContextType<any> | null>(
null,
);
/** @public */
const DataTableRoot = forwardRef(
<TData,>(
props: DataTableRootProps<TData>,
ref: React.ForwardedRef<HTMLDivElement>,
) => {
const { className, table, ...rest } = props;
return (
<DataTableContext.Provider value={{ table }}>
<div ref={ref} className={clsx('bui-DataTable', className)} {...rest} />
</DataTableContext.Provider>
);
},
);
DataTableRoot.displayName = 'DataTableRoot';
export { DataTableRoot };
/** @public */
export function useDataTable<TData>() {
const context = useContext(DataTableContext);
if (!context) {
throw new Error('useDataTable must be used within a DataTableRoot');
}
return context as DataTableContextType<TData>;
}
@@ -1,101 +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 clsx from 'clsx';
import { DataTableTableProps } from './types';
import {
Table,
TableRow,
TableHeader,
TableHead,
TableBody,
TableCell,
} from '../../Table';
import { useDataTable } from '../Root/DataTableRoot';
import { flexRender } from '@tanstack/react-table';
/** @public */
const DataTableTable = forwardRef(
(props: DataTableTableProps, ref: React.ForwardedRef<HTMLTableElement>) => {
const { className, ...rest } = props;
const { table } = useDataTable();
return (
<Table
ref={ref}
style={{ minWidth: table.getTotalSize() }}
className={clsx(className)}
{...rest}
>
<TableHeader>
{table.getHeaderGroups().map(headerGroup => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map(header => {
return (
<TableHead
key={header.id}
style={{ width: header.getSize() }}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map(row => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map(cell => (
<TableCell
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={table.getAllColumns().length}
className="h-24 text-center"
style={{ width: table.getTotalSize() }}
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
},
);
DataTableTable.displayName = 'DataTableRoot';
export { DataTableTable };
@@ -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.
@@ -14,7 +14,5 @@
* limitations under the License.
*/
export * from './DataTable';
export * from './Root/types';
export * from './Pagination/types';
export * from './Table/types';
export { DataTable } from './DataTable';
export type { DataTableProps } from './types';
@@ -17,7 +17,8 @@
import { ColumnDef } from '@tanstack/react-table';
import { DataProps } from './mocked-components';
import { Checkbox } from '../Checkbox';
import { DataTable } from './DataTable';
import { TableCellText } from '../Table/TableCellText/TableCellText';
import { TableCellProfile } from '../Table/TableCellProfile/TableCellProfile';
export const columns: ColumnDef<DataProps>[] = [
{
@@ -48,7 +49,7 @@ export const columns: ColumnDef<DataProps>[] = [
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => (
<DataTable.TableCellText
<TableCellText
title={row.getValue('name')}
description={row.original.description}
href="/"
@@ -63,7 +64,7 @@ export const columns: ColumnDef<DataProps>[] = [
const owner = row.getValue('owner') as DataProps['owner'];
return (
<DataTable.TableCellProfile
<TableCellProfile
name={owner.name}
src={owner.profilePicture}
href={owner.link}
@@ -74,13 +75,13 @@ export const columns: ColumnDef<DataProps>[] = [
{
accessorKey: 'type',
header: 'Type',
cell: ({ row }) => <DataTable.TableCellText title={row.getValue('type')} />,
cell: ({ row }) => <TableCellText title={row.getValue('type')} />,
size: 150,
},
{
accessorKey: 'tags',
header: 'Tags',
cell: ({ row }) => <DataTable.TableCellText title={row.getValue('tags')} />,
cell: ({ row }) => <TableCellText title={row.getValue('tags')} />,
size: 150,
},
];
@@ -90,16 +91,14 @@ export const columns2: ColumnDef<DataProps>[] = [
accessorKey: 'type',
header: 'Type',
cell: ({ row }) => (
<DataTable.TableCellText title={row.getValue('type')} color="secondary" />
<TableCellText title={row.getValue('type')} color="secondary" />
),
size: 100,
},
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => (
<DataTable.TableCellText title={row.getValue('name')} href="/" />
),
cell: ({ row }) => <TableCellText title={row.getValue('name')} href="/" />,
size: 450,
},
{
@@ -109,7 +108,7 @@ export const columns2: ColumnDef<DataProps>[] = [
const owner = row.getValue('owner') as DataProps['owner'];
return (
<DataTable.TableCellProfile
<TableCellProfile
name={owner.name}
src={owner.profilePicture}
href={owner.link}
@@ -122,10 +121,7 @@ export const columns2: ColumnDef<DataProps>[] = [
accessorKey: 'lifecycle',
header: 'Lifecycle',
cell: ({ row }) => (
<DataTable.TableCellText
title={row.getValue('lifecycle')}
color="secondary"
/>
<TableCellText title={row.getValue('lifecycle')} color="secondary" />
),
size: 100,
},
@@ -133,10 +129,7 @@ export const columns2: ColumnDef<DataProps>[] = [
accessorKey: 'system',
header: 'System',
cell: ({ row }) => (
<DataTable.TableCellText
title={row.getValue('lifecycle')}
color="secondary"
/>
<TableCellText title={row.getValue('lifecycle')} color="secondary" />
),
size: 100,
},
@@ -17,10 +17,7 @@
import { Table } from '@tanstack/react-table';
/** @public */
export interface DataTableRootProps<TData>
extends React.HTMLAttributes<HTMLDivElement> {
/**
* The table instance.
*/
export interface DataTableProps<TData>
extends React.HTMLAttributes<HTMLTableElement> {
table: Table<TData>;
}
@@ -23,12 +23,11 @@ import {
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { data, DataProps } from '../mocked-components';
import { columns } from '../mocked-columns';
import { DataTable } from '../DataTable';
import { data, DataProps } from '../DataTable/mocked-components';
import { columns } from '../DataTable/mocked-columns';
const meta = {
title: 'Components/DataTable/Pagination',
title: 'Components/DataTablePagination',
component: DataTablePagination,
} satisfies Meta<typeof DataTablePagination>;
@@ -46,10 +45,6 @@ export const Default: Story = {
getFilteredRowModel: getFilteredRowModel(),
});
return (
<DataTable.Root table={table}>
<DataTable.Pagination />
</DataTable.Root>
);
return <DataTablePagination table={table} />;
},
};
@@ -0,0 +1,108 @@
/*
* 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 { Text } from '../Text';
import { DataTablePaginationProps } from './types';
import { ButtonIcon } from '../ButtonIcon';
import clsx from 'clsx';
import { Select } from '../Select';
import { Icon } from '../Icon';
/** @public */
function DataTablePagination<TData>(props: DataTablePaginationProps<TData>) {
const {
className,
table,
onNextPage,
onPreviousPage,
onPageSizeChange,
showPageSizeOptions = true,
...rest
} = props;
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 ?? 0,
);
return (
<div
style={{ minWidth: table?.getTotalSize() }}
className={clsx('bui-DataTablePagination', className)}
{...rest}
>
<div className="bui-DataTablePagination--left">
{showPageSizeOptions && (
<Select
name="pageSize"
size="small"
placeholder="Show 10 results"
options={[
{ label: 'Show 5 results', value: '5' },
{ label: 'Show 10 results', value: '10' },
{ label: 'Show 20 results', value: '20' },
{ label: 'Show 30 results', value: '30' },
{ label: 'Show 40 results', value: '40' },
{ label: 'Show 50 results', value: '50' },
]}
selectedKey={pageSize?.toString()}
onSelectionChange={value => {
const newPageSize = Number(value);
table?.setPageSize(newPageSize);
onPageSizeChange?.(newPageSize);
}}
className="bui-DataTablePagination--select"
/>
)}
</div>
<div className="bui-DataTablePagination--right">
<Text
as="p"
variant="body-medium"
>{`${fromCount} - ${toCount} of ${rowCount}`}</Text>
<ButtonIcon
variant="secondary"
size="small"
onClick={() => {
table?.previousPage();
onPreviousPage?.();
}}
isDisabled={!table?.getCanPreviousPage()}
icon={<Icon name="chevron-left" />}
aria-label="Previous"
/>
<ButtonIcon
variant="secondary"
size="small"
onClick={() => {
table?.nextPage();
onNextPage?.();
}}
isDisabled={!table?.getCanNextPage()}
icon={<Icon name="chevron-right" />}
aria-label="Next"
/>
</div>
</div>
);
}
DataTablePagination.displayName = 'DataTablePagination';
export { DataTablePagination };
@@ -14,6 +14,5 @@
* limitations under the License.
*/
/** @public */
export interface DataTablePaginationProps
extends React.HTMLAttributes<HTMLDivElement> {}
export { DataTablePagination } from './DataTablePagination';
export type { DataTablePaginationProps } from './types';
@@ -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<TData>
extends React.HTMLAttributes<HTMLDivElement> {
table?: Table<TData>;
onNextPage?: () => void;
onPreviousPage?: () => void;
onPageSizeChange?: (pageSize: number) => void;
showPageSizeOptions?: boolean;
}
/** @public */
export type DataTablePaginationComponent = <TData>(
props: DataTablePaginationProps<TData> & {
ref?: React.ForwardedRef<HTMLDivElement>;
},
) => React.ReactElement;
@@ -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<typeof Hidden>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'Visually hidden content',
},
};
@@ -0,0 +1,28 @@
/*
* 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.
*/
.bui-Hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important; /* move it off-screen */
overflow: hidden !important;
clip: rect(0 0 0 0) !important; /* legacy clip for IE/Edge */
clip-path: inset(100%) !important; /* modern clip */
white-space: nowrap !important; /* avoid line breaks */
border: 0 !important;
}
@@ -0,0 +1,22 @@
/*
* 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 { Box, BoxProps } from '../Box';
import clsx from 'clsx';
export const Hidden = (props: BoxProps) => {
return <Box {...props} className={clsx('bui-Hidden', props.className)} />;
};
@@ -14,6 +14,4 @@
* limitations under the License.
*/
/** @public */
export interface DataTableTableProps
extends React.HTMLAttributes<HTMLTableElement> {}
export { Hidden } from './Hidden';
@@ -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.
+4 -3
View File
@@ -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.
@@ -22,14 +22,15 @@
@import '../components/Checkbox/styles.css';
@import '../components/Collapsible/Collapsible.styles.css';
@import '../components/Container/styles.css';
@import '../components/DataTable/Root/DataTableRoot.styles.css';
@import '../components/DataTable/Pagination/DataTablePagination.styles.css';
@import '../components/DataTable/DataTable.styles.css';
@import '../components/DataTablePagination/DataTablePagination.styles.css';
@import '../components/FieldError/FieldError.styles.css';
@import '../components/FieldLabel/FieldLabel.styles.css';
@import '../components/Flex/styles.css';
@import '../components/Grid/styles.css';
@import '../components/Header/Header.styles.css';
@import '../components/HeaderPage/HeaderPage.styles.css';
@import '../components/Hidden/Hidden.styles.css';
@import '../components/Icon/styles.css';
@import '../components/Link/styles.css';
@import '../components/Menu/Menu.styles.css';
+2
View File
@@ -35,9 +35,11 @@ export * from './components/Button';
export * from './components/Card';
export * from './components/Collapsible';
export * from './components/DataTable';
export * from './components/DataTablePagination';
export * from './components/FieldLabel';
export * from './components/Header';
export * from './components/HeaderPage';
export * from './components/Hidden';
export * from './components/Icon';
export * from './components/ButtonIcon';
export * from './components/ButtonLink';