First pass at bringing new DataTable to BUI
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
+2
-5
@@ -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>;
|
||||
}
|
||||
+4
-9
@@ -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 };
|
||||
+2
-3
@@ -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)} />;
|
||||
};
|
||||
+1
-3
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user