'use client'
import * as React from 'react'
import * as ReactDOM from 'react-dom/client'
import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer'
import {
Box,
Button,
Container,
CssBaseline,
IconButton,
InputAdornment,
Menu,
MenuItem,
Table as MuiTable,
Paper,
Stack,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
ThemeProvider,
Toolbar,
Tooltip,
createTheme,
useMediaQuery,
} from '@mui/material'
import DarkModeIcon from '@mui/icons-material/DarkMode'
import LightModeIcon from '@mui/icons-material/LightMode'
import SearchIcon from '@mui/icons-material/Search'
import { 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 { toSentenceCase } from '@/components/data-table/shared'
import './styles/globals.css'
function getAriaSort(
sortDirection: false | 'asc' | 'desc',
): 'ascending' | 'descending' | 'none' {
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 rgba(0, 0, 0, 0.3) inset'
: isFirstRightPinnedColumn
? '4px 0 4px -4px rgba(0, 0, 0, 0.3) inset'
: undefined,
left: isPinned === 'left' ? `${column.getStart('left')}px` : undefined,
right: isPinned === 'right' ? `${column.getAfter('right')}px` : undefined,
position: isPinned ? 'sticky' : 'relative',
background: isSelected
? 'rgba(var(--mui-palette-primary-mainChannel) / var(--mui-palette-action-selectedOpacity))'
: 'var(--mui-palette-background-paper)',
zIndex: isPinned ? 2 : 0,
}
}
function App({
mode,
setMode,
}: {
mode: 'light' | 'dark' | 'system'
setMode: React.Dispatch<React.SetStateAction<'light' | 'dark' | 'system'>>
}) {
const [columnFilters, setColumnFilters] = React.useState<
Array<ExtendedColumnFilter>
>([])
const [globalFilter, setGlobalFilter] = React.useState('')
const [data, setData] = React.useState(() => makeData(1_000))
const refreshData = () => setData(makeData(1_000))
const stressTest = () => setData(makeData(1_000_000))
const table = useAppTable(
{
key: 'kitchen-sink-material-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)
return (
<table.AppTable>
<Container maxWidth={false} sx={{ py: 3 }}>
<Stack spacing={2}>
<Paper variant="outlined">
<Toolbar
sx={{ gap: 1, justifyContent: 'flex-end', flexWrap: 'wrap' }}
>
<ModeMenu mode={mode} setMode={setMode} />
<Button variant="outlined" size="small" onClick={refreshData}>
Regenerate Data
</Button>
<Button variant="outlined" size="small" onClick={stressTest}>
Stress Test (1M rows)
</Button>
<Button
variant="outlined"
size="small"
onClick={() =>
console.info(
'table.getSelectedRowModel().flatRows',
table.getSelectedRowModel().flatRows,
)
}
>
Log Selected Rows
</Button>
</Toolbar>
</Paper>
<Stack
direction={{ xs: 'column', md: 'row' }}
spacing={1}
sx={{ alignItems: { md: 'center' } }}
>
<DebouncedTextField
value={globalFilter}
onChange={(value) => setGlobalFilter(String(value))}
placeholder="Search all columns..."
size="small"
sx={{ width: { xs: '100%', md: 360 } }}
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
),
},
}}
/>
<table.FilterList />
<table.SortList />
<table.ViewOptions />
</Stack>
<Paper variant="outlined">
<TableContainer sx={{ maxHeight: 680 }}>
<MuiTable
stickyHeader
size="small"
sx={{
width: '100%',
display: 'grid',
}}
>
<TableHead sx={{ display: 'grid' }}>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow
key={headerGroup.id}
sx={{ display: 'flex', width: '100%' }}
>
{headerGroup.headers
.filter((header) => header.column.getIsVisible())
.map((header) => (
<table.AppHeader header={header} key={header.id}>
{(h) => {
const sortDirection = h.column.getCanSort()
? h.column.getIsSorted()
: false
return (
<TableCell
component="th"
colSpan={h.colSpan}
sortDirection={sortDirection || false}
aria-sort={getAriaSort(
sortDirection || false,
)}
sx={{
display: 'flex',
flexShrink: 0,
alignItems: 'center',
justifyContent:
h.column.id === 'select'
? 'center'
: undefined,
overflow: 'clip',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
flexGrow: h.getSize(),
width: h.getSize(),
borderRight:
h.id === 'actions' ? undefined : 1,
borderColor: 'divider',
p: 1,
...getCommonPinningStyles(h.column),
}}
>
<Box
sx={{
position: 'relative',
pr: h.column.getCanResize() ? 1 : 0,
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent:
h.column.id === 'select'
? 'center'
: 'flex-start',
}}
>
{h.isPlaceholder ? null : <h.FlexRender />}
<h.ResizeHandle />
</Box>
</TableCell>
)
}}
</table.AppHeader>
))}
</TableRow>
))}
</TableHead>
<TableBody sx={{ display: 'grid' }}>
{table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
hover
selected={row.getIsSelected()}
aria-selected={row.getIsSelected()}
sx={{ display: 'flex', width: '100%' }}
>
{row.getVisibleCells().map((cell) => (
<table.AppCell cell={cell} key={cell.id}>
{(c) => (
<TableCell
sx={{
display: 'flex',
flexShrink: 0,
alignItems: 'center',
justifyContent:
c.column.id === 'select'
? 'center'
: undefined,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
flexGrow: c.column.getSize(),
width: c.column.getSize(),
borderRight:
c.column.id === 'actions' ? undefined : 1,
borderColor: 'divider',
...getCommonPinningStyles(
c.column,
row.getIsSelected(),
),
}}
>
{c.getIsGrouped() ? (
<c.GroupedCell />
) : (
<c.FlexRender />
)}
</TableCell>
)}
</table.AppCell>
))}
</TableRow>
))}
</TableBody>
</MuiTable>
</TableContainer>
<table.Pagination />
</Paper>
</Stack>
</Container>
</table.AppTable>
)
}
function DebouncedTextField({
value: initialValue,
onChange,
debounce = 300,
...props
}: {
value: string | number
onChange: (value: string | number) => void
debounce?: number
} & Omit<React.ComponentProps<typeof TextField>, 'onChange'>) {
const [value, setValue] = React.useState(initialValue)
React.useEffect(() => {
setValue(initialValue)
}, [initialValue])
const debouncedOnChange = useDebouncedCallback(onChange, { wait: debounce })
return (
<TextField
{...props}
value={value}
onChange={(event) => {
setValue(event.target.value)
debouncedOnChange(event.target.value)
}}
/>
)
}
function ModeMenu({
mode,
setMode,
}: {
mode: 'light' | 'dark' | 'system'
setMode: React.Dispatch<React.SetStateAction<'light' | 'dark' | 'system'>>
}): React.ReactNode {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
return (
<>
<Tooltip title="Theme">
<IconButton onClick={(event) => setAnchorEl(event.currentTarget)}>
{mode === 'dark' ? <DarkModeIcon /> : <LightModeIcon />}
</IconButton>
</Tooltip>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
{(['light', 'dark', 'system'] as const).map((themeMode) => (
<MenuItem
key={themeMode}
selected={themeMode === mode}
onClick={() => {
setMode(themeMode)
setAnchorEl(null)
}}
>
{toSentenceCase(themeMode)}
</MenuItem>
))}
</Menu>
</>
)
}
function Root() {
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)')
const [mode, setMode] = React.useState<'light' | 'dark' | 'system'>('system')
const resolvedMode =
mode === 'system' ? (prefersDark ? 'dark' : 'light') : mode
const theme = React.useMemo(
() =>
createTheme({
cssVariables: true,
palette: {
mode: resolvedMode,
},
}),
[resolvedMode],
)
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<App mode={mode} setMode={setMode} />
</ThemeProvider>
)
}
const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<Root />
</React.StrictMode>,
)