import React, { Fragment, useCallback, useMemo, useState } from 'react'; import { faker } from '@faker-js/faker'; import { storybookArgTypes, storybookExcludedControlParams, StoryMetaType, } from '@lg-tools/storybook-utils'; import { StoryFn } from '@storybook/react'; import { Badge } from '@leafygreen-ui/badge'; import { Button } from '@leafygreen-ui/button'; import { css } from '@leafygreen-ui/emotion'; import { Tooltip } from '@leafygreen-ui/tooltip'; import { KitchenSink, makeData, makeKitchenSinkData, Person, } from '../utils/makeData.testutils'; import { Cell, type ColumnDef, ExpandedContent, type ExpandedState, flexRender, HeaderCell, type HeaderGroup, HeaderRow, type LeafyGreenTableCell, type LeafyGreenVirtualItem, LGColumnDef, Row, type SortingState, Table, TableBody, TableHead, type TableProps, useLeafyGreenVirtualTable, } from '..'; import { VerticalAlignment } from './Table.types'; type StoryTableProps = TableProps; const meta: StoryMetaType = { title: 'Composition/Data Display/Table/With Virtualized Scrolling', component: Table, argTypes: { children: { control: 'none' }, darkMode: storybookArgTypes.darkMode, ref: { control: 'none' }, shouldAlternateRowColor: { control: 'boolean', defaultValue: false }, shouldTruncate: { control: 'boolean', defaultValue: true }, verticalAlignment: { control: { type: 'radio' }, options: VerticalAlignment, defaultValue: VerticalAlignment.Top, }, }, args: { verticalAlignment: VerticalAlignment.Top, shouldTruncate: true, shouldAlternateRowColor: false, }, parameters: { default: 'Basic', chromatic: { disableSnapshot: true, }, controls: { exclude: [...storybookExcludedControlParams, 'children'], }, docs: { source: { type: 'code' }, }, }, }; export default meta; const virtualScrollingContainerHeight = css` max-height: calc(100vh - 200px); width: 100%; `; const basicColumnDefs: Array> = [ { accessorKey: 'index', header: 'index', size: 40, }, { accessorKey: 'id', header: 'ID', size: 60, }, { accessorKey: 'firstName', header: 'First Name', cell: info => info.getValue(), }, { accessorFn: row => row.lastName, id: 'lastName', cell: info => info.getValue(), header: () => Last Name, }, { accessorKey: 'age', header: () => 'Age', size: 50, align: 'center', }, { accessorKey: 'visits', header: () => Visits, size: 50, }, { accessorKey: 'status', header: 'Status', size: 90, }, ]; export const Basic: StoryFn = args => { const tableContainerRef = React.useRef(null); const data = React.useMemo(() => makeData(false, 10_000), []); const columns = useMemo(() => basicColumnDefs, []); const table = useLeafyGreenVirtualTable({ containerRef: tableContainerRef, data, columns, }); return ( <>

{table.getRowModel().rows.length} total rows

{table?.virtual.getVirtualItems().length} virtual rows

{table?.virtual.getTotalSize()} virtual rows

{table.getHeaderGroups().map((headerGroup: HeaderGroup) => ( {headerGroup.headers.map(header => { return ( {flexRender( header.column.columnDef.header, header.getContext(), )} ); })} ))} {table.virtual.getVirtualItems() && table.virtual .getVirtualItems() .map((virtualRow: LeafyGreenVirtualItem) => { const row = virtualRow.row; const cells = row.getVisibleCells(); return ( {cells.map((cell: LeafyGreenTableCell) => { return ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ); })} ); })}
); }; export const DynamicData: StoryFn = args => { const tableContainerRef = React.useRef(null); const data = React.useMemo(() => makeData(false, 100), []); const [showEmoji, setShowEmoji] = useState(true); const columns = useMemo>>( () => [ { accessorKey: 'index', header: 'index', size: 40, }, { accessorKey: 'id', header: 'ID', size: 60, }, { accessorKey: 'firstName', header: 'First Name', cell: info => info.getValue(), }, { accessorFn: row => row.lastName, id: 'lastName', cell: info => info.getValue(), header: () => Last Name, }, { accessorKey: 'age', header: () => 'Age', size: 50, align: 'center', cell: info => { return `${info.getValue()} ${showEmoji ? '🥬' : ''}`; }, }, { accessorKey: 'visits', header: () => Visits, size: 50, }, ], [showEmoji], ); const table = useLeafyGreenVirtualTable({ containerRef: tableContainerRef, data, columns, }); return ( <>

{table.getRowModel().rows.length} total rows

{table?.virtual.getVirtualItems().length} virtual rows

{table?.virtual.getTotalSize()} virtual rows

{table.getHeaderGroups().map((headerGroup: HeaderGroup) => ( {headerGroup.headers.map(header => { return ( {flexRender( header.column.columnDef.header, header.getContext(), )} ); })} ))} {table.virtual.getVirtualItems() && table.virtual .getVirtualItems() .map((virtualRow: LeafyGreenVirtualItem) => { const row = virtualRow.row; const cells = row.getVisibleCells(); return ( {cells.map((cell: LeafyGreenTableCell) => { return ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ); })} ); })}
); }; DynamicData.parameters = { chromatic: { disableSnapshot: true, }, }; export const NestedRows: StoryFn = args => { const tableContainerRef = React.useRef(null); const data = React.useState(() => makeData(false, 5000, 5, 3))[0]; const columns = useMemo(() => basicColumnDefs, []); const table = useLeafyGreenVirtualTable({ containerRef: tableContainerRef, data, columns, }); return ( <>

{table.getRowModel().rows.length} total rows

{table.getHeaderGroups().map((headerGroup: HeaderGroup) => ( {headerGroup.headers.map(header => { return ( {flexRender( header.column.columnDef.header, header.getContext(), )} ); })} ))} {table.virtual.getVirtualItems() && table.virtual .getVirtualItems() .map((virtualRow: LeafyGreenVirtualItem) => { const row = virtualRow.row; const cells = row.getVisibleCells(); return ( {cells.map((cell: LeafyGreenTableCell) => { return ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ); })} ); })}
); }; export const SortableRows: StoryFn = args => { const tableContainerRef = React.useRef(null); const data = React.useState(() => makeData(false, 5000))[0]; const [sorting, setSorting] = React.useState([]); const columns = React.useMemo>>( () => [ { accessorKey: 'id', header: 'ID', size: 60, enableSorting: true, }, { accessorKey: 'firstName', header: 'First Name', cell: info => info.getValue(), }, { accessorFn: row => row.lastName, id: 'lastName', cell: info => info.getValue(), header: () => Last Name, enableSorting: true, }, { accessorKey: 'age', header: () => 'Age', size: 50, enableSorting: true, }, { accessorKey: 'visits', header: () => Visits, size: 50, }, { accessorKey: 'status', header: 'Status', size: 90, }, ], [], ); const table = useLeafyGreenVirtualTable({ containerRef: tableContainerRef, data, columns, state: { sorting, }, onSortingChange: setSorting, }); return ( <>

{table.getRowModel().rows.length} total rows

{table.getHeaderGroups().map((headerGroup: HeaderGroup) => ( {headerGroup.headers.map(header => { return ( {flexRender( header.column.columnDef.header, header.getContext(), )} ); })} ))} {table.virtual.getVirtualItems() && table.virtual .getVirtualItems() .map((virtualRow: LeafyGreenVirtualItem) => { const row = virtualRow.row; const cells = row.getVisibleCells(); return ( {cells.map((cell: LeafyGreenTableCell) => { return ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ); })} ); })}
); }; export const SelectableRows: StoryFn = args => { const tableContainerRef = React.useRef(null); const data = React.useState(() => makeData(false, 5000))[0]; const [rowSelection, setRowSelection] = React.useState({}); const columns = useMemo(() => basicColumnDefs, []); const table = useLeafyGreenVirtualTable({ containerRef: tableContainerRef, data, columns, state: { rowSelection, }, onRowSelectionChange: setRowSelection, hasSelectableRows: true, }); return ( <>

{table.getRowModel().rows.length} total rows

{table.getHeaderGroups().map((headerGroup: HeaderGroup) => ( {headerGroup.headers.map(header => { return ( {flexRender( header.column.columnDef.header, header.getContext(), )} ); })} ))} {table.virtual.getVirtualItems() && table.virtual .getVirtualItems() .map((virtualRow: LeafyGreenVirtualItem) => { const row = virtualRow.row; const cells = row.getVisibleCells(); return ( {cells.map((cell: LeafyGreenTableCell) => { return ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ); })} ); })}
); }; export const ExpandableContent: StoryFn = args => { const tableContainerRef = React.useRef(null); const data = React.useState(() => makeData(true, 5000))[0]; const [expanded, setExpanded] = React.useState({}); const columns = useMemo(() => basicColumnDefs, []); const table = useLeafyGreenVirtualTable({ containerRef: tableContainerRef, data, columns, state: { expanded, }, onExpandedChange: setExpanded, }); return ( <>

{table.getRowModel().rows.length} total rows

{table.getHeaderGroups().map((headerGroup: HeaderGroup) => ( {headerGroup.headers.map(header => { return ( {flexRender( header.column.columnDef.header, header.getContext(), )} ); })} ))} {table.virtual.getVirtualItems() && table.virtual .getVirtualItems() .map((virtualRow: LeafyGreenVirtualItem) => { const row = virtualRow.row; const isExpandedContent = row.isExpandedContent ?? false; return ( {!isExpandedContent && ( {row .getVisibleCells() .map((cell: LeafyGreenTableCell) => { return ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ); })} )} {isExpandedContent && ( )} ); })}
); }; export const TallRows: StoryFn = args => { const tableContainerRef = React.useRef(null); const data = React.useMemo(() => { return makeData(false, 10_000).map(d => ({ ...d, id: faker.string.uuid() + '--' + faker.string.uuid() + '--' + faker.string.uuid() + '--' + faker.string.uuid(), })); }, []); const columnDefs: Array> = [ { accessorKey: 'index', header: 'index', size: 90, }, { accessorKey: 'id', header: 'ID', // This makes the width auto size: NaN, }, { accessorKey: 'firstName', header: 'First Name', cell: info => info.getValue(), size: 120, }, { accessorFn: row => row.lastName, id: 'lastName', cell: info => info.getValue(), header: () => Last Name, size: 120, }, { accessorKey: 'age', header: () => 'Age', size: 50, align: 'center', }, { accessorKey: 'visits', header: () => Visits, size: 50, }, { accessorKey: 'status', header: 'Status', size: 110, }, ]; // FIXME: // eslint-disable-next-line react-hooks/exhaustive-deps const columns = useMemo(() => columnDefs, []); const estimateSize = useCallback(() => 68, []); const table = useLeafyGreenVirtualTable({ containerRef: tableContainerRef, data, columns, virtualizerOptions: { estimateSize, }, }); return (

{table.getRowModel().rows.length} total rows

{table?.virtual.getVirtualItems().length} virtual rows

{table.getHeaderGroups().map((headerGroup: HeaderGroup) => ( {headerGroup.headers.map(header => { return ( {flexRender( header.column.columnDef.header, header.getContext(), )} ); })} ))} {table.virtual.getVirtualItems() && table.virtual .getVirtualItems() .map((virtualRow: LeafyGreenVirtualItem) => { const row = virtualRow.row; const cells = row.getVisibleCells(); return ( {cells.map((cell: LeafyGreenTableCell) => { return ( div { max-height: unset; } `} cell={cell} > {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ); })} ); })}
); }; export const WithLeafyGreenComponents: StoryFn = args => { const tableContainerRef = React.useRef(null); const [data] = useState(() => makeKitchenSinkData(10_000)); const columns = React.useMemo>>( () => [ { accessorKey: 'dateCreated', header: 'Date Created', enableSorting: true, cell: info => (info.getValue() as Date).toLocaleDateString('en-us', { year: 'numeric', month: 'short', day: 'numeric', }), size: 180, }, { accessorKey: 'clusterType', header: 'Cluster Type', // This makes the size auto size: NaN, }, { accessorKey: 'frequency', header: 'Frequency', align: 'center', size: 140, }, { accessorKey: 'encryptorEnabled', header: 'Encryptor', cell: info => ( {info.getValue() ? 'Enabled' : 'Not enabled'} ), }, { accessorKey: 'mdbVersion', header: 'MongoDB Version', enableSorting: true, size: 120, }, { id: 'actions', header: '', size: 150, cell: _ => { return (
Button}> {"I'm leafy, you're leafy"}
); }, }, ], [], ); const table = useLeafyGreenVirtualTable({ containerRef: tableContainerRef, data, columns, }); return (

{table.getRowModel().rows.length} total rows

{table .getHeaderGroups() .map((headerGroup: HeaderGroup) => ( {headerGroup.headers.map(header => { return ( {flexRender( header.column.columnDef.header, header.getContext(), )} ); })} ))} {table.virtual.getVirtualItems() && table.virtual .getVirtualItems() .map((virtualRow: LeafyGreenVirtualItem) => { const row = virtualRow.row; const isExpandedContent = row.isExpandedContent ?? false; return ( {!isExpandedContent && ( {row .getVisibleCells() .map((cell: LeafyGreenTableCell) => { return ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ); })} )} {isExpandedContent && ( )} ); })}
); }; export const NoTruncation = WithLeafyGreenComponents.bind({}); NoTruncation.args = { shouldTruncate: false, };