'use client'
import * as React from 'react'
import { TanStackDevtools } from '@tanstack/react-devtools'
import * as ReactDOM from 'react-dom/client'
import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer'
import {
Button,
Table as HeroTable,
Input,
Surface,
Switch,
Tooltip,
cn,
useTheme,
} from '@heroui/react'
import {
tableDevtoolsPlugin,
useTanStackTableDevtools,
} from '@tanstack/react-table-devtools'
import type { Column } from '@tanstack/react-table'
import type { ExtendedColumnFilter } from '@/types'
import type { Person } from '@/lib/make-data'
import type { features } from '@/hooks/features'
import { makeData } from '@/lib/make-data'
import { useAppTable } from '@/hooks/table'
import { columns } from '@/columns'
import './styles/globals.css'
function getAriaSort(sortDirection: false | 'asc' | 'desc') {
if (sortDirection === 'asc') return 'ascending'
if (sortDirection === 'desc') return 'descending'
return 'none'
}
function getCommonPinningStyles(
column: Column<typeof features, Person>,
isSelected = false,
): React.CSSProperties {
const isPinned = column.getIsPinned()
const isLastLeftPinnedColumn =
isPinned === 'left' && column.getIsLastColumn('left')
const isFirstRightPinnedColumn =
isPinned === 'right' && column.getIsFirstColumn('right')
return {
boxShadow: isLastLeftPinnedColumn
? '-4px 0 4px -4px hsl(var(--heroui-border)) inset'
: isFirstRightPinnedColumn
? '4px 0 4px -4px hsl(var(--heroui-border)) inset'
: undefined,
left: isPinned === 'left' ? `${column.getStart('left')}px` : undefined,
right: isPinned === 'right' ? `${column.getAfter('right')}px` : undefined,
position: isPinned ? 'sticky' : 'relative',
borderRight: isLastLeftPinnedColumn
? '1px solid hsl(var(--heroui-border))'
: undefined,
borderLeft: isFirstRightPinnedColumn
? '1px solid hsl(var(--heroui-border))'
: undefined,
background: isSelected
? 'hsl(var(--heroui-primary) / 0.12)'
: isPinned
? 'hsl(var(--heroui-background))'
: undefined,
zIndex: isPinned ? 2 : 0,
}
}
function ModeSwitch() {
const { theme, setTheme } = useTheme('system')
return (
<Tooltip>
<Switch
aria-label="Theme"
size="sm"
isSelected={theme === 'dark'}
onChange={(selected) => setTheme(selected ? 'dark' : 'light')}
>
<Switch.Content>
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
</Switch.Content>
</Switch>
<Tooltip.Content>Theme</Tooltip.Content>
</Tooltip>
)
}
function DebouncedTextInput({
value: initialValue,
onChange,
debounce = 300,
...props
}: {
value: string | number
onChange: (value: string | number) => void
debounce?: number
} & Omit<React.ComponentProps<typeof Input>, 'onChange'>) {
const [value, setValue] = React.useState(initialValue)
React.useEffect(() => {
setValue(initialValue)
}, [initialValue])
const debouncedOnChange = useDebouncedCallback(onChange, { wait: debounce })
return (
<Input
{...props}
value={String(value)}
onChange={(event) => {
setValue(event.currentTarget.value)
debouncedOnChange(event.currentTarget.value)
}}
/>
)
}
function App() {
const [columnFilters, setColumnFilters] = React.useState<
Array<ExtendedColumnFilter>
>([])
const [globalFilter, setGlobalFilter] = React.useState('')
const [data, setData] = React.useState(() => makeData(1_000))
const table = useAppTable(
{
key: 'kitchen-sink-hero-ui',
columns,
data,
debugTable: true,
state: {
columnFilters,
globalFilter,
},
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
initialState: {
columnPinning: { left: ['select'], right: ['actions'] },
columnOrder: columns.map((c) => c.id ?? ''),
},
},
(state) => state,
)
useTanStackTableDevtools(table)
const refreshData = () => setData(makeData(1_000))
const stressTest = () => setData(makeData(1_000_000))
return (
<table.AppTable>
<main className="px-4 py-4">
<div className="flex flex-col gap-4">
<Surface className="rounded-lg border border-border p-3">
<div className="flex flex-wrap justify-end gap-2">
<ModeSwitch />
<Button variant="secondary" size="sm" onPress={refreshData}>
Regenerate Data
</Button>
<Button variant="secondary" size="sm" onPress={stressTest}>
Stress Test (1M rows)
</Button>
<Button
variant="secondary"
size="sm"
onPress={() =>
console.info(
'table.getSelectedRowModel().flatRows',
table.getSelectedRowModel().flatRows,
)
}
>
Log Selected Rows
</Button>
</div>
</Surface>
<div className="flex flex-wrap items-center gap-2">
<DebouncedTextInput
aria-label="Search all columns"
className="w-full md:w-[360px]"
value={globalFilter}
onChange={(value) => setGlobalFilter(String(value))}
placeholder="Search all columns..."
/>
<table.FilterList />
<table.SortList />
<table.ViewOptions />
</div>
<HeroTable className="overflow-hidden rounded-lg border border-border">
<HeroTable.ScrollContainer className="max-h-[680px]">
<HeroTable.Content
aria-label="Hero UI TanStack Table kitchen sink"
style={{
width: `max(100%, ${table.getTotalSize()}px)`,
tableLayout: 'fixed',
}}
>
<HeroTable.Header>
{table
.getHeaderGroups()[0]
?.headers.filter((header) => header.column.getIsVisible())
.map((header) => (
<HeroTable.Column
key={header.id}
id={header.id}
allowsSorting={header.column.getCanSort()}
isRowHeader={header.column.id === 'firstName'}
aria-sort={getAriaSort(header.column.getIsSorted())}
className={cn(
header.column.id === 'select' && 'text-center',
)}
style={{
width: header.getSize(),
padding: 8,
...getCommonPinningStyles(header.column),
}}
>
<table.AppHeader header={header}>
{(h) => (
<div className="relative pr-2">
{h.isPlaceholder ? null : <h.FlexRender />}
<h.ResizeHandle />
</div>
)}
</table.AppHeader>
</HeroTable.Column>
))}
</HeroTable.Header>
<HeroTable.Body>
{table.getRowModel().rows.map((row) => {
const selected = row.getIsSelected()
return (
<HeroTable.Row
key={row.id}
id={row.id}
aria-selected={selected}
className={cn(selected && 'bg-primary/10')}
>
{row.getVisibleCells().map((cell) => (
<HeroTable.Cell
key={cell.id}
className={cn(
'overflow-hidden',
cell.column.id === 'select' && 'text-center',
)}
style={{
width: cell.column.getSize(),
...getCommonPinningStyles(cell.column, selected),
}}
>
<table.AppCell cell={cell}>
{(c) =>
c.getIsGrouped() ? (
<c.GroupedCell />
) : (
<c.FlexRender />
)
}
</table.AppCell>
</HeroTable.Cell>
))}
</HeroTable.Row>
)
})}
</HeroTable.Body>
</HeroTable.Content>
</HeroTable.ScrollContainer>
<table.Pagination />
</HeroTable>
</div>
</main>
</table.AppTable>
)
}
const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<App />
<TanStackDevtools plugins={[tableDevtoolsPlugin()]} />
</React.StrictMode>,
)