import type { Meta, StoryObj } from "@storybook/react-vite"; import { type ColumnDef, columnFilteringFeature, createExpandedRowModel, createFilteredRowModel, createPaginatedRowModel, createSortedRowModel, type ExpandedState, flexRender, globalFilteringFeature, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, type SortingState, tableFeatures, useTable, } from "@tanstack/react-table"; import { Fragment, useEffect, useMemo, useState } from "react"; import { pagination } from "../"; import { Button, Field, Pagination, Table } from "../react"; import styles from "../styles.module.css"; import mockData from "./table.mockData"; const meta = { title: "Designsystem/Table", parameters: { layout: "padded", }, decorators: [ (Story) => (
), ], } satisfies Meta; export default meta; type Story = StoryObj; type ColumnKeys = keyof (typeof mockData)[0]; type ColumnsType = { key: ColumnKeys; label: string; numeric?: boolean; expand?: React.ReactNode; }[]; type RowType = (typeof mockData)[0] & { expand?: React.ReactNode }; const mockDataSmall = mockData.slice(0, 4); const mockExpand = mockData.slice(0, 10).map((row: RowType) => ({ ...row, expand: (
Content {row.firstName} {row.lastName}
), })); const mockColumns = [ { accessorKey: "firstName", header: "First name" }, { accessorKey: "lastName", header: "Last name" }, { header: "Age", accessorKey: "age" }, { header: "Visits", accessorKey: "visits" }, ]; const mobileDecorators: StoryObj["decorators"] = [ (Story) => { useEffect(() => { if (document.querySelector(".sbdocs-wrapper")) return; // Do not shrink in docs mode const el = window.frameElement as HTMLElement; const iframe = el?.nodeName === "IFRAME" ? el : undefined; if (iframe) iframe.style.maxWidth = "400px"; return () => iframe?.removeAttribute("style"); }, []); return ; }, ]; export const Default: Story = { render: () => (
First name Last name Age Visits
Antoni Foyston 74 128
Jenine Healey 22 194
Leigh Klein 26 114
Zara Greenrodd 28 36
), }; export const React: Story = { render: () => ( First name Last name Age Visits Antoni Foyston 74 128 Jenine Healey 22 194 Leigh Klein 26 114 Zara Greenrodd 28 36
), }; export const DefaultTanstack: Story = { render: function Render(args) { const isNumeric = ["age", "visits"]; const table = useTable({ features: tableFeatures({}), data: mockDataSmall, columns: mockColumns, }); return ( {table.getHeaderGroups().map(({ id, headers }) => ( {headers.map((header) => ( {flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} {table.getRowModel().rows.map((row) => ( {row.getAllCells().map((cell) => ( {cell.getValue() as React.ReactNode} ))} ))}
); }, }; export const HeadingsSimple: Story = { render: (args) => { const columns: ColumnsType = [ { key: "firstName", label: "First name" }, { key: "lastName", label: "Last name" }, { key: "age", label: "Age", numeric: true }, { key: "visits", label: "Visits", numeric: true }, { key: "date", label: "Date", numeric: true }, ]; return ( {columns.map(({ label, numeric }) => ( ))} {mockDataSmall.map((row) => ( {columns.map(({ key, numeric }) => ( ))} ))}
Name Stats
{label}
{key === "date" ? new Date(Number(row[key])).toLocaleDateString() : row[key]}
); }, }; export const HeadingsTanstack: Story = { render: function Render(args) { const features = tableFeatures({}); const columns: ColumnDef[] = useMemo( () => [ { header: "Name", columns: [ { accessorKey: "firstName", header: "First name", }, { accessorKey: "lastName", header: "Last name", }, ], }, { header: "Stats", columns: [ { header: "Age", accessorKey: "age", }, { header: "Visits", accessorKey: "visits", }, { header: "Date", accessorKey: "date", cell: (info) => new Date(Number(info.getValue())).toLocaleDateString(), }, ], }, ], [], ); const table = useTable({ data: mockDataSmall, columns, features, }); return ( {table.getHeaderGroups().map(({ id, headers }) => ( {headers.map((header) => ( {header.isPlaceholder || flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} {table.getRowModel().rows.map((row) => ( {row.getAllCells().map((cell) => ( {cell.getValue() as React.ReactNode} ))} ))}
); }, }; export const SortableSimple: Story = { render: function Render(args) { const [sort, setSort] = useState<{ key: ColumnKeys; value: "none" | "ascending" | "descending"; }>({ key: "firstName", value: "none", }); const updateSort = (newKey: ColumnKeys) => setSort(({ key, value }) => ({ key: newKey, value: newKey !== key || value === "none" ? "ascending" : value === "ascending" ? "descending" : "none", })); const columns: ColumnsType = [ { key: "firstName", label: "First name" }, { key: "lastName", label: "Last name" }, { key: "age", label: "Age", numeric: true }, { key: "visits", label: "Visits", numeric: true }, ]; return ( {columns.map(({ key, label, numeric }) => ( ))} {mockDataSmall .slice() // Make a copy for mutability .sort((a, b) => { if (sort.value === "none") return 0; const asc = sort.value === "ascending"; const aVal = asc ? a[sort.key] : b[sort.key]; const bVal = asc ? b[sort.key] : a[sort.key]; return typeof aVal === "number" ? Number(aVal) - Number(bVal) : String(aVal).localeCompare(String(bVal)); }) .map((row) => ( {columns.map(({ key, numeric }) => ( ))} ))}
{row[key]}
); }, }; export const SortableTanstack: Story = { render: function Render(args) { const [sorting, setSorting] = useState([]); const table = useTable({ features: tableFeatures({ sortedRowModel: createSortedRowModel(), rowSortingFeature, }), onSortingChange: setSorting, state: { sorting }, data: mockData, columns: mockColumns, }); return ( {table.getHeaderGroups().map(({ id, headers }) => ( {headers.map((header) => ( {flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} {table.getRowModel().rows.map((row) => ( {row.getAllCells().map((cell) => ( {cell.getValue() as React.ReactNode} ))} ))}
); }, }; export const PaginatableSimple: Story = { render: function Render(args) { const size = 10; const [page, setPage] = useState(0); const index = page * size; const columns: ColumnsType = [ { key: "firstName", label: "First name" }, { key: "lastName", label: "Last name" }, { key: "age", label: "Age", numeric: true }, { key: "visits", label: "Visits", numeric: true }, ]; const { pages, next, prev } = pagination({ current: page + 1, total: Math.ceil(mockData.length / size), show: 7, }); return ( <> {columns.map(({ label, numeric }) => ( ))} {mockData.slice(index, index + size).map((row) => ( {columns.map(({ key, numeric }) => ( ))} ))}
{label}
{row[key]}
); }, }; export const PaginatableTanstack: Story = { render: function Render(args) { const table = useTable({ features: tableFeatures({ paginatedRowModel: createPaginatedRowModel(), rowPaginationFeature, }), data: mockData, columns: mockColumns, }); const { pages, next, prev } = pagination({ current: table.state.pagination.pageIndex + 1, total: table.getPageCount(), show: 7, }); return ( <> {table.getHeaderGroups().map(({ id, headers }) => ( {headers.map((header) => ( {flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} {table.getRowModel().rows.map((row) => ( {row.getAllCells().map((cell) => ( {cell.getValue() as React.ReactNode} ))} ))}
  • {pages.map(({ current, key, page }) => (
  • {!!page && ( )}
  • ))}
); }, }; export const SearchableSimple: Story = { render: function Render(args) { const [search, setSearch] = useState(""); const columns: ColumnsType = [ { key: "firstName", label: "First name" }, { key: "lastName", label: "Last name" }, { key: "age", label: "Age", numeric: true }, { key: "visits", label: "Visits", numeric: true }, ]; const filtered = mockData.filter((row) => { const text = Object.values(row).join(" "); return text.toLowerCase().includes(search.toLowerCase()); }); return ( <> setSearch(target.value)} value={search} /> {columns.map(({ label, numeric }) => ( ))} {filtered.map((row) => ( {columns.map(({ key, numeric }) => ( ))} ))}
{label}
{row[key]}
); }, }; export const SearchableTanstack: Story = { render: function Render(args) { const [search, setSearch] = useState(""); const table = useTable({ features: tableFeatures({ filteredRowModel: createFilteredRowModel(), columnFilteringFeature, globalFilteringFeature, }), onGlobalFilterChange: setSearch, data: mockData, state: { globalFilter: search }, columns: mockColumns, }); return ( <> setSearch(target.value)} value={search} /> {table.getHeaderGroups().map(({ id, headers }) => ( {headers.map((header) => ( {flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} {table.getRowModel().rows.map((row) => ( {row.getAllCells().map((cell) => ( {cell.getValue() as React.ReactNode} ))} ))}
); }, }; export const ExpandableSimple: Story = { render: function Render(args) { const columns: ColumnsType = [ { key: "firstName", label: "First name" }, { key: "lastName", label: "Last name" }, { key: "age", label: "Age", numeric: true }, { key: "visits", label: "Visits", numeric: true }, ]; return ( {columns.map(({ label, numeric }) => ( ))} {mockExpand.map(function Row(row) { const [expanded, setExpanded] = useState(false); return ( {columns.map(({ key, numeric }, cellIndex) => ( ))} ); })}
{label}
{cellIndex === 0 ? ( ) : ( row[key] )}
{row.expand}
); }, }; export const ExpandableTanstack: Story = { render: function Render(args) { const [expanded, setExpanded] = useState({}); const table = useTable({ features: tableFeatures({ expandedRowModel: createExpandedRowModel(), rowExpandingFeature, }), onExpandedChange: setExpanded, state: { expanded }, data: mockExpand, columns: mockColumns, getRowCanExpand: () => true, }); return ( {table.getHeaderGroups().map(({ id, headers }) => ( {headers.map((header) => ( {flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} {table.getRowModel().rows.map((row) => ( {row.getAllCells().map((cell, cellIndex) => ( {cellIndex === 0 ? ( ) : ( (cell.getValue() as React.ReactNode) )} ))} ))}
); }, }; export const CheckableSimple: Story = { render: function Render(args) { const columns: ColumnsType = [ { key: "firstName", label: "First name" }, { key: "lastName", label: "Last name" }, { key: "age", label: "Age", numeric: true }, { key: "visits", label: "Visits", numeric: true }, ]; return ( {columns.map(({ label, numeric }) => ( ))} {mockDataSmall.map((row, i) => ( {columns.map(({ key, numeric }, cellIndex) => ( ))} ))}
{label}
{cellIndex ? ( row[key] ) : ( )}
); }, }; export const CheckableTanstack: Story = { render: function Render(args) { const table = useTable({ features: tableFeatures({ rowSelectionFeature, }), data: mockData, columns: mockColumns, }); return ( {table.getHeaderGroups().map(({ id, headers }) => ( {headers.map((header) => ( {header.isPlaceholder || flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} {table.getRowModel().rows.map((row) => ( {row.getAllCells().map((cell, index) => ( {index ? ( (cell.getValue() as React.ReactNode) ) : ( )} ))} ))}
); }, }; export const ClickableSimple: Story = { render: () => (
First name Last name Age Visits
Foyston 74 128
Healey 22 194
Klein 26 114
), }; export const ClickableTanstack: Story = { render: function Render(args) { const table = useTable({ features: tableFeatures({}), data: mockData, columns: mockColumns, }); return ( {table.getHeaderGroups().map(({ id, headers }) => ( {headers.map((header) => ( {header.isPlaceholder || flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} {table.getRowModel().rows.map((row, rowIndex) => ( {row.getAllCells().map((cell, index) => ( {index ? ( (cell.getValue() as React.ReactNode) ) : ( )} ))} ))}
); }, }; export const WithHorizontalTitles: Story = { render: () => (
First name Last name Age Visits
Antoni Foyston 74 128
Jenine Healey 22 194
Leigh Klein 26 114
Zara Greenrodd 28 36
), }; export const WithFixedWidths: Story = { render: () => (
First name Last name Age Visits
Antoni Foyston 74 128
Jenine Healey 22 194
Leigh Klein 26 114
Zara Greenrodd 28 36
), }; export const WithAlign: Story = { render: () => (
data-align="start":
Reference number 1
Description A preliminary version.
An application for a certificate has been initiated.
Template Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut sed enim ut ex posuere suscipit id eu justo. Cras vehicula ornare efficitur. Etiam commodo est velit, eget mattis felis sollicitudin sit amet. Etiam non dui fermentum, malesuada augue in, elementum felis.

data-align="center":
Reference number 1
Description A preliminary version.
An application for a certificate has been initiated.
Template Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut sed enim ut ex posuere suscipit id eu justo. Cras vehicula ornare efficitur. Etiam commodo est velit, eget mattis felis sollicitudin sit amet. Etiam non dui fermentum, malesuada augue in, elementum felis.
), }; export const WithNumericValues: Story = { render: () => (
First name Last name Age Visits
Antoni Foyston 74 128
Jenine Healey 22 194
Leigh Klein 26 114
Zara Greenrodd 28 36
), }; export const WithJustify: Story = { render: () => (
Kostnad Pris
Gebyr 1 128 kr
Gebyr 2 194 kr
Gebyr 3 114 kr
Total 194 kr
), }; export const WithFooter: Story = { render: () => (
First name Last name Age Visits
Antoni Foyston 74 128
Jenine Healey 22 194
Leigh Klein 26 114
Zara Greenrodd 28 36
First name Last name Age Visits
), }; export const WithBorderAround: Story = { render: () => (
First name Last name Age Visits
Antoni Foyston 74 128
Jenine Healey 22 194
Leigh Klein Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam felis quam, pulvinar et lacus et, molestie semper ante. 114
Zara Greenrodd 28 36
), }; export const WithoutBorders: Story = { render: () => (

Table heading

Antoni Foyston 74
Jenine Healey 22
Leigh Klein 14
Zara Greenrodd 28
), }; export const Sizes: Story = { render: () => ( <>
Size Attr
Small data-size="sm"
Size Attr
Medium data-size="md"
Size Attr
Large data-size="lg"
), }; export const MobileScroll: Story = { decorators: mobileDecorators, parameters: { viewport: { defaultViewport: "mobile2", // Large mobile default viewport }, }, render: () => (
First name Last name Description Age Visits
Antoni Foyston Lorem ipsum dolor sit amet consectetur. 74 128
Jenine Healey Lorem ipsum dolor sit amet consectetur. 22 194
Leigh Klein Lorem ipsum dolor sit amet consectetur. 26 114
Zara Greenrodd Lorem ipsum dolor sit amet consectetur. 28 36
), }; export const MobileDivided: Story = { decorators: mobileDecorators, parameters: { viewport: { defaultViewport: "mobile2", // Large mobile default viewport }, }, render: () => (
First name Last name Age Visits
Antoni Foyston 74 128
Jenine Healey 22 194
Leigh Klein Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam felis quam, pulvinar et lacus et, molestie semper ante. 114
Zara Greenrodd 28 36
), }; export const MobileSpaced: Story = { decorators: mobileDecorators, parameters: { viewport: { defaultViewport: "mobile2", // Large mobile default viewport }, }, render: () => (
First name Last name Age Visits
Antoni Foyston 74 128
Jenine Healey 22 194
Leigh Klein Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam felis quam, pulvinar et lacus et, molestie semper ante. 114
Zara Greenrodd 28 36
), }; export const MobileStacked: Story = { decorators: mobileDecorators, parameters: { viewport: { defaultViewport: "mobile2", // Large mobile default viewport }, }, render: () => (
First name Last name Age Visits
Antoni Foyston 74 128
Jenine Healey 22 194
Leigh Klein Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam felis quam, pulvinar et lacus et, molestie semper ante. 114
Zara Greenrodd 28 36
), };