Table restructure

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-07-26 09:09:49 +01:00
parent 917d004aa6
commit ef8855a9db
28 changed files with 699 additions and 775 deletions
+132 -79
View File
@@ -9523,6 +9523,125 @@
display: flex;
}
.bui-DataTableRoot--sort {
cursor: pointer;
user-select: none;
align-items: center;
gap: var(--bui-space-1);
display: inline-flex;
}
.bui-DataTableRoot--sort .bui-DataTableRoot--unsorted {
opacity: 0;
transition: opacity .1s ease-in-out, transform .1s ease-in-out;
}
.bui-DataTableRoot--sort:hover .bui-DataTableRoot--unsorted, .bui-DataTableRoot--sort:focus .bui-DataTableRoot--unsorted {
opacity: .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);
}
.bui-TableRoot {
caption-side: bottom;
border-collapse: collapse;
width: 100%;
}
.bui-TableHead {
text-align: left;
padding: var(--bui-space-3);
font-size: var(--bui-font-size-3);
color: var(--bui-fg-primary);
}
.bui-TableBody {
color: var(--bui-fg-primary);
}
.bui-TableRow {
border-bottom: 1px solid var(--bui-border);
transition: color .2s ease-in-out;
}
.bui-TableBody .bui-TableRow:hover {
background-color: var(--bui-gray-2);
}
.bui-TableCell {
padding: var(--bui-space-3);
font-size: var(--bui-font-size-3);
}
.bui-TableCellText {
align-items: center;
gap: var(--bui-space-2);
flex-direction: row;
display: inline-flex;
}
.bui-TableCellTextIcon, .bui-TableCellTextIcon svg {
color: var(--bui-fg-primary);
align-items: center;
display: inline-flex;
}
.bui-TableCellTextContent {
gap: var(--bui-space-0_5);
flex-direction: column;
display: flex;
}
.bui-TableCellProfile {
gap: var(--bui-space-2);
flex-direction: row;
align-items: center;
display: flex;
}
.bui-TableCellProfileAvatar {
vertical-align: middle;
user-select: none;
color: var(--bui-fg-primary);
background-color: var(--bui-bg-surface-2);
border-radius: 100%;
justify-content: center;
align-items: center;
width: 1.25rem;
height: 1.25rem;
font-size: 1rem;
font-weight: 500;
line-height: 1;
display: inline-flex;
overflow: hidden;
}
.bui-TableCellProfileAvatarImage {
object-fit: cover;
width: 100%;
height: 100%;
}
.bui-TableCellProfileAvatarFallback {
width: 100%;
height: 100%;
font-size: var(--bui-font-size-2);
font-weight: var(--bui-font-weight-regular);
box-shadow: inset 0 0 0 1px var(--bui-border);
border-radius: var(--bui-radius-full);
justify-content: center;
align-items: center;
display: flex;
}
.bui-DataTablePagination {
padding-top: var(--bui-space-5);
justify-content: space-between;
@@ -9732,6 +9851,19 @@
display: flex;
}
.bui-Hidden {
clip: rect(0 0 0 0) !important;
clip-path: inset(100%) !important;
white-space: nowrap !important;
border: 0 !important;
width: 1px !important;
height: 1px !important;
margin: -1px !important;
padding: 0 !important;
position: absolute !important;
overflow: hidden !important;
}
.bui-Icon {
width: 1rem;
height: 1rem;
@@ -10034,85 +10166,6 @@
}
}
.bui-TableRoot {
caption-side: bottom;
border-collapse: collapse;
width: 100%;
}
.bui-TableHead {
text-align: left;
padding: var(--bui-space-3);
font-size: var(--bui-font-size-3);
color: var(--bui-fg-primary);
}
.bui-TableBody {
color: var(--bui-fg-primary);
}
.bui-TableRow {
border-bottom: 1px solid var(--bui-border);
transition: color .2s ease-in-out;
}
.bui-TableBody .bui-TableRow:hover {
background-color: var(--bui-gray-2);
}
.bui-TableCell {
padding: var(--bui-space-3);
font-size: var(--bui-font-size-3);
}
.bui-TableCellText, .bui-TableCellLink {
gap: var(--bui-space-0_5);
flex-direction: column;
display: flex;
}
.bui-TableCellProfile {
gap: var(--bui-space-2);
flex-direction: row;
align-items: center;
display: flex;
}
.bui-TableCellProfileAvatar {
vertical-align: middle;
user-select: none;
color: var(--bui-fg-primary);
background-color: var(--bui-bg-surface-2);
border-radius: 100%;
justify-content: center;
align-items: center;
width: 1.25rem;
height: 1.25rem;
font-size: 1rem;
font-weight: 500;
line-height: 1;
display: inline-flex;
overflow: hidden;
}
.bui-TableCellProfileAvatarImage {
object-fit: cover;
width: 100%;
height: 100%;
}
.bui-TableCellProfileAvatarFallback {
width: 100%;
height: 100%;
font-size: var(--bui-font-size-2);
font-weight: var(--bui-font-weight-regular);
box-shadow: inset 0 0 0 1px var(--bui-border);
border-radius: var(--bui-radius-full);
justify-content: center;
align-items: center;
display: flex;
}
.bui-Tabs {
--active-tab-left: 0px;
--active-tab-right: 0px;
+73 -117
View File
@@ -11,7 +11,6 @@ import type { ComponentPropsWithRef } from 'react';
import { Context } from 'react';
import type { ElementType } from 'react';
import { ForwardRefExoticComponent } from 'react';
import { HTMLAttributes } from 'react';
import { JSX as JSX_2 } from 'react/jsx-runtime';
import { LinkProps as LinkProps_2 } from 'react-aria-components';
import { Menu as Menu_2 } from '@base-ui-components/react/menu';
@@ -30,12 +29,9 @@ import type { TabListProps as TabListProps_2 } from 'react-aria-components';
import type { TabPanelProps as TabPanelProps_2 } from 'react-aria-components';
import type { TabProps as TabProps_2 } from 'react-aria-components';
import { TabsProps as TabsProps_2 } from 'react-aria-components';
import { TdHTMLAttributes } from 'react';
import type { TextFieldProps as TextFieldProps_2 } from 'react-aria-components';
import { ThHTMLAttributes } from 'react';
import { TooltipProps as TooltipProps_2 } from 'react-aria-components';
import { TooltipTriggerComponentProps } from 'react-aria-components';
import type { useRender } from '@base-ui-components/react/use-render';
// @public (undocumented)
export type AlignItems = 'stretch' | 'start' | 'center' | 'end';
@@ -519,7 +515,8 @@ export const componentDefinitions: {
readonly caption: 'bui-TableCaption';
readonly cell: 'bui-TableCell';
readonly cellText: 'bui-TableCellText';
readonly cellLink: 'bui-TableCellLink';
readonly cellTextContent: 'bui-TableCellTextContent';
readonly cellTextIcon: 'bui-TableCellTextIcon';
readonly cellProfile: 'bui-TableCellProfile';
readonly cellProfileAvatar: 'bui-TableCellProfileAvatar';
readonly cellProfileAvatarImage: 'bui-TableCellProfileAvatarImage';
@@ -609,68 +606,6 @@ export type DataAttributesMap = Record<string, DataAttributeValues>;
// @public
export type DataAttributeValues = readonly (string | number | boolean)[];
// @public
export const DataTable: {
Root: <TData>(
props: {
table: Table_2<TData>;
} & React.HTMLAttributes<HTMLDivElement>,
) => JSX.Element;
Pagination: ForwardRefExoticComponent<
DataTablePaginationProps & RefAttributes<HTMLDivElement>
>;
Table: ForwardRefExoticComponent<
DataTableTableProps & RefAttributes<HTMLTableElement>
>;
TableRoot: ForwardRefExoticComponent<
Omit<
HTMLAttributes<HTMLTableElement> & RefAttributes<HTMLTableElement>,
'ref'
> &
RefAttributes<HTMLTableElement>
>;
TableHeader: ForwardRefExoticComponent<
HTMLAttributes<HTMLTableSectionElement> &
RefAttributes<HTMLTableSectionElement>
>;
TableBody: ForwardRefExoticComponent<
HTMLAttributes<HTMLTableSectionElement> &
RefAttributes<HTMLTableSectionElement>
>;
TableRow: ForwardRefExoticComponent<
HTMLAttributes<HTMLTableRowElement> & RefAttributes<HTMLTableRowElement>
>;
TableCell: ForwardRefExoticComponent<
TdHTMLAttributes<HTMLTableCellElement> & RefAttributes<HTMLTableCellElement>
>;
TableCellText: ForwardRefExoticComponent<
TableCellTextProps & RefAttributes<HTMLDivElement>
>;
TableCellLink: ForwardRefExoticComponent<
TableCellLinkProps & RefAttributes<HTMLDivElement>
>;
TableCellProfile: ForwardRefExoticComponent<
TableCellProfileProps & RefAttributes<HTMLDivElement>
>;
TableHead: ForwardRefExoticComponent<
ThHTMLAttributes<HTMLTableCellElement> & RefAttributes<HTMLTableCellElement>
>;
};
// @public (undocumented)
export interface DataTablePaginationProps
extends React.HTMLAttributes<HTMLDivElement> {}
// @public (undocumented)
export interface DataTableRootProps<TData>
extends React.HTMLAttributes<HTMLDivElement> {
table: Table_2<TData>;
}
// @public (undocumented)
export interface DataTableTableProps
extends React.HTMLAttributes<HTMLTableElement> {}
// @public (undocumented)
export type Display = 'none' | 'flex' | 'block' | 'inline';
@@ -1066,6 +1001,9 @@ export const heightPropDefs: {
// @public (undocumented)
export type HeightProps = GetPropDefTypes<typeof heightPropDefs>;
// @public (undocumented)
export const Hidden: (props: BoxProps) => JSX_2.Element;
// @public (undocumented)
export const Icon: (props: IconProps) => JSX_2.Element | null;
@@ -1567,78 +1505,96 @@ export interface SwitchProps extends SwitchProps_2 {
// @public
export const Tab: (props: TabProps) => JSX_2.Element;
// @public
export const Table: {
Root: ForwardRefExoticComponent<
HTMLAttributes<HTMLTableElement> & RefAttributes<HTMLTableElement>
>;
Header: ForwardRefExoticComponent<
HTMLAttributes<HTMLTableSectionElement> &
RefAttributes<HTMLTableSectionElement>
>;
Body: ForwardRefExoticComponent<
HTMLAttributes<HTMLTableSectionElement> &
RefAttributes<HTMLTableSectionElement>
>;
Head: ForwardRefExoticComponent<
ThHTMLAttributes<HTMLTableCellElement> & RefAttributes<HTMLTableCellElement>
>;
Row: ForwardRefExoticComponent<
HTMLAttributes<HTMLTableRowElement> & RefAttributes<HTMLTableRowElement>
>;
Cell: ForwardRefExoticComponent<
TdHTMLAttributes<HTMLTableCellElement> & RefAttributes<HTMLTableCellElement>
>;
CellText: ForwardRefExoticComponent<
TableCellTextProps & RefAttributes<HTMLDivElement>
>;
CellLink: ForwardRefExoticComponent<
TableCellLinkProps & RefAttributes<HTMLDivElement>
>;
CellProfile: ForwardRefExoticComponent<
TableCellProfileProps & RefAttributes<HTMLDivElement>
>;
Caption: ForwardRefExoticComponent<
HTMLAttributes<HTMLTableCaptionElement> &
RefAttributes<HTMLTableCaptionElement>
>;
};
// Warning: (ae-missing-release-tag) "Table" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function Table<TData>(
props: TableProps<TData> & {
ref?: React.ForwardedRef<HTMLTableElement>;
},
): JSX_2.Element;
// @public (undocumented)
export interface TableCellLinkProps
extends React.HTMLAttributes<HTMLDivElement> {
// (undocumented)
description?: string;
// (undocumented)
href: string;
// (undocumented)
render?: useRender.ComponentProps<'a'>['render'];
// (undocumented)
title: string;
export namespace Table {
var // (undocumented)
displayName: string;
}
// @public (undocumented)
export const TableCellProfile: ForwardRefExoticComponent<
TableCellProfileProps & RefAttributes<HTMLDivElement>
>;
// @public (undocumented)
export interface TableCellProfileProps
extends React.HTMLAttributes<HTMLDivElement> {
// (undocumented)
color?: 'primary' | 'secondary';
// (undocumented)
description?: string;
// (undocumented)
href?: string;
// (undocumented)
name?: string;
// (undocumented)
src?: string;
// (undocumented)
to?: string;
// (undocumented)
withImage?: boolean;
}
// @public (undocumented)
export const TableCellText: ForwardRefExoticComponent<
TableCellTextProps & RefAttributes<HTMLDivElement>
>;
// @public (undocumented)
export interface TableCellTextProps
extends React.HTMLAttributes<HTMLDivElement> {
// (undocumented)
color?: 'primary' | 'secondary';
// (undocumented)
description?: string;
// (undocumented)
href?: string;
// (undocumented)
leadingIcon?: React.ReactNode;
// (undocumented)
title: string;
}
// Warning: (ae-missing-release-tag) "TablePagination" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function TablePagination<TData>(
props: TablePaginationProps<TData>,
): JSX_2.Element;
// @public (undocumented)
export namespace TablePagination {
var // (undocumented)
displayName: string;
}
// @public (undocumented)
export interface TablePaginationProps<TData>
extends React.HTMLAttributes<HTMLDivElement> {
// (undocumented)
onNextPage?: () => void;
// (undocumented)
onPageSizeChange?: (pageSize: number) => void;
// (undocumented)
onPreviousPage?: () => void;
// (undocumented)
showPageSizeOptions?: boolean;
// (undocumented)
table?: Table_2<TData>;
}
// @public (undocumented)
export interface TableProps<TData>
extends React.HTMLAttributes<HTMLTableElement> {
// (undocumented)
table: Table_2<TData>;
}
// @public
export const TabList: (props: TabListProps) => JSX_2.Element;
@@ -1,162 +0,0 @@
/*
* 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, 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 {
flexRender,
getCoreRowModel,
getPaginationRowModel,
useReactTable,
PaginationState,
} from '@tanstack/react-table';
import { useState } from 'react';
import { MemoryRouter } from 'react-router-dom';
const meta = {
title: 'Components/DataTable',
decorators: [
(Story: StoryFn) => (
<MemoryRouter>
<Story />
</MemoryRouter>
),
],
} satisfies Meta;
export default meta;
type Story = StoryObj<typeof meta>;
export const Uncontrolled: Story = {
render: () => {
const table = useReactTable<DataProps>({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<>
<DataTable table={table} />
<DataTablePagination table={table} />
</>
);
},
};
export const Controlled: Story = {
render: () => {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 4,
pageSize: 5,
});
const table = useReactTable<DataProps>({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
state: {
pagination,
},
onPaginationChange: setPagination,
});
return (
<>
<DataTable table={table} />
<DataTablePagination table={table} />
</>
);
},
};
export const WithCustomTable: Story = {
render: () => {
const table = useReactTable<DataProps>({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<>
<Table>
<TableHeader>
{table.getHeaderGroups().map(headerGroup => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map(header => {
return (
<TableHead key={header.id}>
{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}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<DataTablePagination table={table} />
</>
);
},
};
@@ -1,120 +0,0 @@
/*
* 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 clsx from 'clsx';
import { DataTableProps } from './types';
import {
Table,
TableRow,
TableHeader,
TableHead,
TableBody,
TableCell,
} from '../Table';
import { flexRender } from '@tanstack/react-table';
import { DataTableHeadContent } from './DataTableHeadContent';
function getAriaSort(sortDirection: string | false) {
if (sortDirection === 'asc') {
return 'ascending';
}
if (sortDirection === 'desc') {
return 'descending';
}
return 'none';
}
/** @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 };
@@ -17,6 +17,7 @@
import { Box, BoxProps } from '../Box';
import clsx from 'clsx';
/** @public */
export const Hidden = (props: BoxProps) => {
return <Box {...props} className={clsx('bui-Hidden', props.className)} />;
};
@@ -13,12 +13,13 @@
* 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';
import { Icon } from '../../Icon';
import { Hidden } from '../../Hidden';
import { RawHeadContentProps } from './types';
function getSortTitle(nextOrder: string | false) {
if (nextOrder === 'asc') {
@@ -30,13 +31,8 @@ function getSortTitle(nextOrder: string | false) {
return 'Clear sort';
}
interface DataTableHeadContentProps<TData> {
header: Header<TData, unknown>;
}
export function DataTableHeadContent<TData>({
header,
}: DataTableHeadContentProps<TData>) {
/** @internal */
export function RawHeadContent<TData>({ header }: RawHeadContentProps<TData>) {
const headerContent = useMemo(
() => flexRender(header.column.columnDef.header, header.getContext()),
[header],
@@ -14,5 +14,5 @@
* limitations under the License.
*/
export { DataTable } from './DataTable';
export type { DataTableProps } from './types';
export { RawHeadContent } from './RawHeadContent';
export type { RawHeadContentProps } from './types';
@@ -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 { Header } from '@tanstack/react-table';
/** @internal */
export interface RawHeadContentProps<TData> {
header: Header<TData, unknown>;
}
@@ -0,0 +1,115 @@
/*
* 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 { ComponentType } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import {
RawTable,
RawTableBody,
RawTableHead,
RawTableHeader,
RawTableRow,
RawTableCell,
} from '.';
const invoices = [
{
invoice: 'INV001',
paymentStatus: 'Paid',
totalAmount: '$250.00',
paymentMethod: 'Credit Card',
},
{
invoice: 'INV002',
paymentStatus: 'Pending',
totalAmount: '$150.00',
paymentMethod: 'PayPal',
},
{
invoice: 'INV003',
paymentStatus: 'Unpaid',
totalAmount: '$350.00',
paymentMethod: 'Bank Transfer',
},
{
invoice: 'INV004',
paymentStatus: 'Paid',
totalAmount: '$450.00',
paymentMethod: 'Credit Card',
},
{
invoice: 'INV005',
paymentStatus: 'Paid',
totalAmount: '$550.00',
paymentMethod: 'PayPal',
},
{
invoice: 'INV006',
paymentStatus: 'Pending',
totalAmount: '$200.00',
paymentMethod: 'Bank Transfer',
},
{
invoice: 'INV007',
paymentStatus: 'Unpaid',
totalAmount: '$300.00',
paymentMethod: 'Credit Card',
},
];
const meta = {
title: 'Components/Table/RawTable',
component: RawTable,
subcomponents: {
Body: RawTableBody as ComponentType<unknown>,
Cell: RawTableCell as ComponentType<unknown>,
Head: RawTableHead as ComponentType<unknown>,
Header: RawTableHeader as ComponentType<unknown>,
Row: RawTableRow as ComponentType<unknown>,
},
} satisfies Meta<typeof RawTable>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => (
<RawTable>
<RawTableHeader>
<RawTableRow>
<RawTableHead className="w-[100px]">Invoice</RawTableHead>
<RawTableHead>Status</RawTableHead>
<RawTableHead>Method</RawTableHead>
<RawTableHead className="text-right">Amount</RawTableHead>
</RawTableRow>
</RawTableHeader>
<RawTableBody>
{invoices.map(invoice => (
<RawTableRow key={invoice.invoice}>
<RawTableCell className="font-medium">
{invoice.invoice}
</RawTableCell>
<RawTableCell>{invoice.paymentStatus}</RawTableCell>
<RawTableCell>{invoice.paymentMethod}</RawTableCell>
<RawTableCell className="text-right">
{invoice.totalAmount}
</RawTableCell>
</RawTableRow>
))}
</RawTableBody>
</RawTable>
),
};
@@ -14,21 +14,33 @@
* limitations under the License.
*/
import { forwardRef } from 'react';
import clsx from 'clsx';
import { useStyles } from '../../../hooks/useStyles';
.bui-TableRoot {
width: 100%;
caption-side: bottom;
border-collapse: collapse;
}
/** @public */
const TableCell = forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
.bui-TableHead {
text-align: left;
padding: var(--bui-space-3);
font-size: var(--bui-font-size-3);
color: var(--bui-fg-primary);
}
return (
<td ref={ref} className={clsx(classNames.cell, className)} {...props} />
);
});
TableCell.displayName = 'TableCell';
.bui-TableBody {
color: var(--bui-fg-primary);
}
export { TableCell };
.bui-TableRow {
border-bottom: 1px solid var(--bui-border);
transition: color 0.2s ease-in-out;
}
.bui-TableBody .bui-TableRow:hover {
background-color: var(--bui-gray-2);
}
.bui-TableCell {
padding: var(--bui-space-3);
font-size: var(--bui-font-size-3);
}
@@ -0,0 +1,120 @@
/*
* 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 { forwardRef } from 'react';
import clsx from 'clsx';
import { useStyles } from '../../../hooks/useStyles';
/** @internal */
export const RawTable = forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<table ref={ref} className={clsx(classNames.root, className)} {...props} />
);
});
RawTable.displayName = 'RawTable';
/** @internal */
export const RawTableHeader = forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<thead
ref={ref}
className={clsx(classNames.header, className)}
{...props}
/>
);
});
RawTableHeader.displayName = 'RawTableHeader';
/** @internal */
export const RawTableBody = forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<tbody ref={ref} className={clsx(classNames.body, className)} {...props} />
);
});
RawTableBody.displayName = 'RawTableBody';
/** @internal */
export const RawTableRow = forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<tr ref={ref} className={clsx(classNames.row, className)} {...props}>
{props.children}
</tr>
);
});
RawTableRow.displayName = 'RawTableRow';
/** @internal */
export const RawTableHead = forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<th ref={ref} className={clsx(classNames.head, className)} {...props} />
);
});
RawTableHead.displayName = 'RawTableHead';
/** @internal */
export const RawTableCaption = forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<caption
ref={ref}
className={clsx(classNames.caption, className)}
{...props}
/>
);
});
RawTableCaption.displayName = 'RawTableCaption';
/** @internal */
export const RawTableCell = forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<td ref={ref} className={clsx(classNames.cell, className)} {...props} />
);
});
RawTableCell.displayName = 'RawTableCell';
@@ -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,11 @@
* limitations under the License.
*/
.bui-TableCell {
padding: var(--bui-space-3);
font-size: var(--bui-font-size-3);
}
export {
RawTable,
RawTableBody,
RawTableHead,
RawTableHeader,
RawTableRow,
RawTableCell,
} from './RawTable';
@@ -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,98 +14,74 @@
* limitations under the License.
*/
import { ComponentType } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import type { Meta, StoryFn, StoryObj } from '@storybook/react';
import { Table, TablePagination } from '.';
import { data, DataProps } from './mocked-components';
import { columns } from './mocked-columns';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '.';
const invoices = [
{
invoice: 'INV001',
paymentStatus: 'Paid',
totalAmount: '$250.00',
paymentMethod: 'Credit Card',
},
{
invoice: 'INV002',
paymentStatus: 'Pending',
totalAmount: '$150.00',
paymentMethod: 'PayPal',
},
{
invoice: 'INV003',
paymentStatus: 'Unpaid',
totalAmount: '$350.00',
paymentMethod: 'Bank Transfer',
},
{
invoice: 'INV004',
paymentStatus: 'Paid',
totalAmount: '$450.00',
paymentMethod: 'Credit Card',
},
{
invoice: 'INV005',
paymentStatus: 'Paid',
totalAmount: '$550.00',
paymentMethod: 'PayPal',
},
{
invoice: 'INV006',
paymentStatus: 'Pending',
totalAmount: '$200.00',
paymentMethod: 'Bank Transfer',
},
{
invoice: 'INV007',
paymentStatus: 'Unpaid',
totalAmount: '$300.00',
paymentMethod: 'Credit Card',
},
];
getCoreRowModel,
getPaginationRowModel,
useReactTable,
PaginationState,
} from '@tanstack/react-table';
import { useState } from 'react';
import { MemoryRouter } from 'react-router-dom';
const meta = {
title: 'Components/Table',
component: Table,
subcomponents: {
Body: TableBody as ComponentType<unknown>,
Cell: TableCell as ComponentType<unknown>,
Head: TableHead as ComponentType<unknown>,
Header: TableHeader as ComponentType<unknown>,
Row: TableRow as ComponentType<unknown>,
},
} satisfies Meta<typeof Table>;
decorators: [
(Story: StoryFn) => (
<MemoryRouter>
<Story />
</MemoryRouter>
),
],
} satisfies Meta;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map(invoice => (
<TableRow key={invoice.invoice}>
<TableCell className="font-medium">{invoice.invoice}</TableCell>
<TableCell>{invoice.paymentStatus}</TableCell>
<TableCell>{invoice.paymentMethod}</TableCell>
<TableCell className="text-right">{invoice.totalAmount}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
),
export const Uncontrolled: Story = {
render: () => {
const table = useReactTable<DataProps>({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<>
<Table table={table} />
<TablePagination table={table} />
</>
);
},
};
export const Controlled: Story = {
render: () => {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 4,
pageSize: 5,
});
const table = useReactTable<DataProps>({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
state: {
pagination,
},
onPaginationChange: setPagination,
});
return (
<>
<Table table={table} />
<TablePagination table={table} />
</>
);
},
};
+96 -83
View File
@@ -14,94 +14,107 @@
* limitations under the License.
*/
import { forwardRef } from 'react';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import { TableProps } from './types';
import {
RawTable,
RawTableRow,
RawTableHeader,
RawTableHead,
RawTableBody,
RawTableCell,
} from './RawTable';
import { RawHeadContent } from './RawHeadContent';
import { flexRender } from '@tanstack/react-table';
function getAriaSort(sortDirection: string | false) {
if (sortDirection === 'asc') {
return 'ascending';
}
if (sortDirection === 'desc') {
return 'descending';
}
return 'none';
}
/** @public */
export const Table = forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
function Table<TData>(
props: TableProps<TData> & { ref?: React.ForwardedRef<HTMLTableElement> },
) {
const { className, table, ref, ...rest } = props;
return (
<table ref={ref} className={clsx(classNames.root, className)} {...props} />
<RawTable
ref={ref}
style={{ minWidth: table.getTotalSize() }}
className={clsx(className)}
{...rest}
>
<RawTableHeader>
{table.getHeaderGroups().map(headerGroup => (
<RawTableRow key={headerGroup.id}>
{headerGroup.headers.map(header => {
return (
<RawTableHead
key={header.id}
style={{ width: header.getSize() }}
aria-sort={getAriaSort(header.column.getIsSorted())}
>
{header.isPlaceholder ? null : (
<RawHeadContent header={header} />
)}
</RawTableHead>
);
})}
</RawTableRow>
))}
</RawTableHeader>
<RawTableBody>
{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 (
<RawTableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
data-clickable={!!rowData.onClick}
onClick={handleRowClick}
>
{row.getVisibleCells().map(cell => (
<RawTableCell
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</RawTableCell>
))}
</RawTableRow>
);
})
) : (
<RawTableRow>
<RawTableCell
colSpan={table.getAllColumns().length}
className="h-24 text-center"
style={{ width: table.getTotalSize() }}
>
No results.
</RawTableCell>
</RawTableRow>
)}
</RawTableBody>
</RawTable>
);
});
}
Table.displayName = 'Table';
/** @public */
export const TableHeader = forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<thead
ref={ref}
className={clsx(classNames.header, className)}
{...props}
/>
);
});
TableHeader.displayName = 'TableHeader';
/** @public */
export const TableBody = forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<tbody ref={ref} className={clsx(classNames.body, className)} {...props} />
);
});
TableBody.displayName = 'TableBody';
/** @public */
export const TableRow = forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<tr ref={ref} className={clsx(classNames.row, className)} {...props}>
{props.children}
</tr>
);
});
TableRow.displayName = 'TableRow';
/** @public */
export const TableHead = forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<th ref={ref} className={clsx(classNames.head, className)} {...props} />
);
});
TableHead.displayName = 'TableHead';
/** @public */
export const TableCaption = forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<caption
ref={ref}
className={clsx(classNames.caption, className)}
{...props}
/>
);
});
TableCaption.displayName = 'TableCaption';
export { Table };
@@ -1,32 +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 type { Meta, StoryObj } from '@storybook/react';
import { TableCell } from './TableCell';
const meta = {
title: 'Components/Table/TableCell',
component: TableCell,
} satisfies Meta<typeof TableCell>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'Hello world',
},
};
@@ -15,7 +15,7 @@
*/
import type { Meta, StoryObj } from '@storybook/react';
import { DataTablePagination } from './DataTablePagination';
import { TablePagination } from './TablePagination';
import {
getCoreRowModel,
getFilteredRowModel,
@@ -23,13 +23,13 @@ import {
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { data, DataProps } from '../DataTable/mocked-components';
import { columns } from '../DataTable/mocked-columns';
import { data, DataProps } from '../mocked-components';
import { columns } from '../mocked-columns';
const meta = {
title: 'Components/DataTablePagination',
component: DataTablePagination,
} satisfies Meta<typeof DataTablePagination>;
title: 'Components/Table/TablePagination',
component: TablePagination,
} satisfies Meta<typeof TablePagination>;
export default meta;
type Story = StoryObj<typeof meta>;
@@ -45,6 +45,6 @@ export const Default: Story = {
getFilteredRowModel: getFilteredRowModel(),
});
return <DataTablePagination table={table} />;
return <TablePagination table={table} />;
},
};
@@ -14,15 +14,15 @@
* limitations under the License.
*/
import { Text } from '../Text';
import { DataTablePaginationProps } from './types';
import { ButtonIcon } from '../ButtonIcon';
import { Text } from '../../Text';
import { TablePaginationProps } from './types';
import { ButtonIcon } from '../../ButtonIcon';
import clsx from 'clsx';
import { Select } from '../Select';
import { Icon } from '../Icon';
import { Select } from '../../Select';
import { Icon } from '../../Icon';
/** @public */
function DataTablePagination<TData>(props: DataTablePaginationProps<TData>) {
function TablePagination<TData>(props: TablePaginationProps<TData>) {
const {
className,
table,
@@ -103,6 +103,6 @@ function DataTablePagination<TData>(props: DataTablePaginationProps<TData>) {
);
}
DataTablePagination.displayName = 'DataTablePagination';
TablePagination.displayName = 'TablePagination';
export { DataTablePagination };
export { TablePagination };
@@ -14,5 +14,5 @@
* limitations under the License.
*/
export { DataTablePagination } from './DataTablePagination';
export type { DataTablePaginationProps } from './types';
export { TablePagination } from './TablePagination';
export type { TablePaginationProps } from './types';
@@ -17,7 +17,7 @@
import { Table } from '@tanstack/react-table';
/** @public */
export interface DataTablePaginationProps<TData>
export interface TablePaginationProps<TData>
extends React.HTMLAttributes<HTMLDivElement> {
table?: Table<TData>;
onNextPage?: () => void;
@@ -25,10 +25,3 @@ export interface DataTablePaginationProps<TData>
onPageSizeChange?: (pageSize: number) => void;
showPageSizeOptions?: boolean;
}
/** @public */
export type DataTablePaginationComponent = <TData>(
props: DataTablePaginationProps<TData> & {
ref?: React.ForwardedRef<HTMLDivElement>;
},
) => React.ReactElement;
+5 -2
View File
@@ -14,9 +14,12 @@
* limitations under the License.
*/
export { Table, TableBody, TableHead, TableHeader, TableRow } from './Table';
export { TableCell } from './TableCell/TableCell';
export { Table } from './Table';
export { TableCellText } from './TableCellText/TableCellText';
export { TableCellProfile } from './TableCellProfile/TableCellProfile';
export { TablePagination } from './TablePagination';
export type { TableProps } from './types';
export type { TableCellTextProps } from './TableCellText/types';
export type { TableCellProfileProps } from './TableCellProfile/types';
export type { TablePaginationProps } from './TablePagination/types';
@@ -17,8 +17,8 @@
import { ColumnDef } from '@tanstack/react-table';
import { DataProps } from './mocked-components';
import { Checkbox } from '../Checkbox';
import { TableCellText } from '../Table/TableCellText/TableCellText';
import { TableCellProfile } from '../Table/TableCellProfile/TableCellProfile';
import { TableCellText } from './TableCellText/TableCellText';
import { TableCellProfile } from './TableCellProfile/TableCellProfile';
export const columns: ColumnDef<DataProps>[] = [
{
@@ -1,25 +0,0 @@
.bui-TableRoot {
width: 100%;
caption-side: bottom;
border-collapse: collapse;
}
.bui-TableHead {
text-align: left;
padding: var(--bui-space-3);
font-size: var(--bui-font-size-3);
color: var(--bui-fg-primary);
}
.bui-TableBody {
color: var(--bui-fg-primary);
}
.bui-TableRow {
border-bottom: 1px solid var(--bui-border);
transition: color 0.2s ease-in-out;
}
.bui-TableBody .bui-TableRow:hover {
background-color: var(--bui-gray-2);
}
@@ -17,7 +17,7 @@
import { Table } from '@tanstack/react-table';
/** @public */
export interface DataTableProps<TData>
export interface TableProps<TData>
extends React.HTMLAttributes<HTMLTableElement> {
table: Table<TData>;
}
+7 -6
View File
@@ -22,8 +22,13 @@
@import '../components/Checkbox/styles.css';
@import '../components/Collapsible/Collapsible.styles.css';
@import '../components/Container/styles.css';
@import '../components/DataTable/DataTable.styles.css';
@import '../components/DataTablePagination/DataTablePagination.styles.css';
@import '../components/Table/Table.styles.css';
@import '../components/Table/RawTable/RawTable.styles.css';
@import '../components/Table/TableCellText/TableCellText.styles.css';
@import '../components/Table/TableCellProfile/TableCellProfile.styles.css';
@import '../components/Table/TablePagination/TablePagination.styles.css';
@import '../components/FieldError/FieldError.styles.css';
@import '../components/FieldLabel/FieldLabel.styles.css';
@import '../components/Flex/styles.css';
@@ -36,10 +41,6 @@
@import '../components/Menu/Menu.styles.css';
@import '../components/Popover/Popover.styles.css';
@import '../components/RadioGroup/RadioGroup.styles.css';
@import '../components/Table/styles.css';
@import '../components/Table/TableCell/TableCell.styles.css';
@import '../components/Table/TableCellText/TableCellText.styles.css';
@import '../components/Table/TableCellProfile/TableCellProfile.styles.css';
@import '../components/Tabs/Tabs.styles.css';
@import '../components/Text/styles.css';
@import '../components/TextField/TextField.styles.css';
-2
View File
@@ -34,8 +34,6 @@ export * from './components/Avatar';
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';