'use client'
import * as React from 'react'
import * as ReactDOM from 'react-dom/client'
import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer'
import {
DndContext,
PointerSensor,
closestCenter,
useSensor,
useSensors,
} from '@dnd-kit/core'
import {
SortableContext,
arrayMove,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'
import CheckIcon from '@mui/icons-material/Check'
import CodeIcon from '@mui/icons-material/Code'
import CreditCardIcon from '@mui/icons-material/CreditCard'
import DarkModeIcon from '@mui/icons-material/DarkMode'
import DeleteIcon from '@mui/icons-material/Delete'
import DragIndicatorIcon from '@mui/icons-material/DragIndicator'
import ExpandLessIcon from '@mui/icons-material/ExpandLess'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import FilterListIcon from '@mui/icons-material/FilterList'
import GroupIcon from '@mui/icons-material/Group'
import LightModeIcon from '@mui/icons-material/LightMode'
import MoreVertIcon from '@mui/icons-material/MoreVert'
import PushPinIcon from '@mui/icons-material/PushPin'
import SearchIcon from '@mui/icons-material/Search'
import SettingsIcon from '@mui/icons-material/Settings'
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'
import SortIcon from '@mui/icons-material/Sort'
import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt'
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'
import {
Autocomplete,
Box,
Button,
Checkbox,
Chip,
Container,
CssBaseline,
Divider,
FormControl,
IconButton,
InputAdornment,
InputLabel,
LinearProgress,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Table as MuiTable,
Paper,
Popover,
Select,
Stack,
TableBody,
TableCell,
TableContainer,
TableHead,
TablePagination,
TableRow,
TableSortLabel,
TextField,
ThemeProvider,
Toolbar,
Tooltip,
Typography,
createTheme,
useMediaQuery,
} from '@mui/material'
import {
aggregationFns,
columnFacetingFeature,
columnFilteringFeature,
columnGroupingFeature,
columnOrderingFeature,
columnPinningFeature,
columnResizingFeature,
columnSizingFeature,
columnVisibilityFeature,
createColumnHelper,
createCoreRowModel,
createExpandedRowModel,
createFacetedRowModel,
createFacetedUniqueValues,
createFilteredRowModel,
createGroupedRowModel,
createPaginatedRowModel,
createSortedRowModel,
filterFns,
globalFilteringFeature,
rowExpandingFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
sortFns,
tableFeatures,
useTable,
} from '@tanstack/react-table'
import type { Person } from '@/lib/make-data'
import type { DragEndEvent } from '@dnd-kit/core'
import type {
CellData,
Column,
ColumnPinningState,
ColumnSizingState,
ExpandedState,
GroupingState,
Header,
RowData,
SortingState,
Table,
TableFeatures,
} from '@tanstack/react-table'
import type { ExtendedColumnFilter } from '@/types'
import {
dynamicFilterFn,
fuzzyFilter,
getFilterOperators,
} from '@/lib/data-table'
import { departments, makeData, statuses } from '@/lib/make-data'
import './styles/globals.css'
declare module '@tanstack/react-table' {
interface ColumnMeta<
TFeatures extends TableFeatures,
TData extends RowData,
TValue extends CellData = CellData,
> {
label?: string
variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select'
options?: Array<{ label: string; value: string; count?: number }>
}
}
const _features = tableFeatures({
rowSortingFeature,
rowPaginationFeature,
rowSelectionFeature,
rowExpandingFeature,
columnFilteringFeature,
columnFacetingFeature,
columnOrderingFeature,
columnVisibilityFeature,
columnSizingFeature,
columnResizingFeature,
columnPinningFeature,
columnGroupingFeature,
globalFilteringFeature,
})
const columnHelper = createColumnHelper<typeof _features, Person>()
type AppTable = Table<typeof _features, Person>
type AppColumn = Column<typeof _features, Person, any>
function SortableFrame({
id,
children,
}: {
id: string
children: React.ReactNode
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id })
return (
<Box
ref={setNodeRef}
{...attributes}
{...listeners}
sx={{
opacity: isDragging ? 0.6 : 1,
transform: CSS.Transform.toString(transform),
transition,
cursor: 'grab',
'&:active': {
cursor: 'grabbing',
},
}}
>
{children}
</Box>
)
}
function toSentenceCase(value: string) {
return value
.replace(/[-_]/g, ' ')
.replace(/\w\S*/g, (word) => word[0].toUpperCase() + word.slice(1))
}
function formatDate(value: string) {
return new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
}).format(new Date(value))
}
function toDateInputValue(value: unknown) {
if (!value) return ''
const date = new Date(String(value))
return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 10)
}
function getAriaSort(sortDirection: false | 'asc' | 'desc') {
if (sortDirection === 'asc') return 'ascending'
if (sortDirection === 'desc') return 'descending'
return 'none'
}
const SortingContext = React.createContext<SortingState>([])
function getSortDirection(sorting: SortingState, columnId: string) {
const sort = sorting.find((sort) => sort.id === columnId)
return sort ? (sort.desc ? 'desc' : 'asc') : undefined
}
function getCommonPinningStyles(
column: AppColumn,
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))'
: isPinned
? 'var(--mui-palette-background-paper)'
: undefined,
zIndex: isPinned ? 2 : 0,
}
}
function DepartmentIcon({ department }: { department: Person['department'] }) {
const icons: Record<Person['department'], React.ReactElement> = {
engineering: <CodeIcon fontSize="inherit" />,
marketing: <SystemUpdateAltIcon fontSize="inherit" />,
sales: <ShoppingCartIcon fontSize="inherit" />,
hr: <GroupIcon fontSize="inherit" />,
finance: <CreditCardIcon fontSize="inherit" />,
}
return icons[department]
}
function DepartmentChip({ department }: { department: Person['department'] }) {
return (
<Box
component="span"
sx={{
display: 'inline-flex',
maxWidth: '100%',
height: 24,
minWidth: 0,
alignItems: 'center',
gap: 0.75,
px: 1,
borderRadius: 13,
border: 1,
borderColor: 'divider',
fontSize: '0.8125rem',
}}
>
<Box
component="span"
sx={{
display: 'inline-flex',
width: 16,
height: 16,
flex: '0 0 16px',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
fontSize: 16,
lineHeight: 1,
}}
>
<DepartmentIcon department={department} />
</Box>
<Box
component="span"
sx={{
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{toSentenceCase(department)}
</Box>
</Box>
)
}
function EllipsisText({ children }: { children: React.ReactNode }) {
return (
<Box
component="span"
sx={{
display: 'block',
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{children}
</Box>
)
}
function StatusChip({ status }: { status: Person['status'] }) {
const color: Record<Person['status'], 'success' | 'error' | 'warning'> = {
active: 'success',
inactive: 'error',
pending: 'warning',
}
return (
<Chip
icon={<CheckIcon fontSize="small" />}
label={toSentenceCase(status)}
color={color[status]}
variant="outlined"
size="small"
/>
)
}
function RowActions({ person }: { person: Person }) {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
const open = Boolean(anchorEl)
return (
<>
<IconButton
size="small"
aria-label="Open row actions"
aria-controls={open ? `row-actions-${person.id}` : undefined}
aria-haspopup="menu"
onClick={(event) => setAnchorEl(event.currentTarget)}
>
<MoreVertIcon fontSize="small" />
</IconButton>
<Menu
id={`row-actions-${person.id}`}
anchorEl={anchorEl}
open={open}
onClose={() => setAnchorEl(null)}
>
<MenuItem
onClick={() => {
void navigator.clipboard.writeText(person.id)
setAnchorEl(null)
}}
>
<ListItemText>Copy ID</ListItemText>
</MenuItem>
<Divider />
<MenuItem onClick={() => setAnchorEl(null)}>View details</MenuItem>
<MenuItem onClick={() => setAnchorEl(null)}>View profile</MenuItem>
</Menu>
</>
)
}
function ColumnHeaderMenu({
column,
title,
}: {
column: AppColumn
title: string
}) {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
const canSort = column.getCanSort()
const canHide = column.getCanHide()
const canPin = column.getCanPin()
const canGroup = column.getCanGroup()
const sorting = React.useContext(SortingContext)
const direction = canSort ? getSortDirection(sorting, column.id) : undefined
const isSorted = !!direction
const pinned = canPin ? column.getIsPinned() : false
const grouped = canGroup ? column.getIsGrouped() : false
if (!canSort && !canHide && !canPin && !canGroup) {
return <Typography variant="subtitle2">{title}</Typography>
}
return (
<Stack direction="row" spacing={0.5} sx={{ alignItems: 'center' }}>
{canSort ? (
<TableSortLabel
active={isSorted}
direction={direction}
IconComponent={ArrowDownwardIcon}
onClick={column.getToggleSortingHandler()}
sx={{
'& .MuiTableSortLabel-icon': {
opacity: isSorted ? 1 : 0,
transition: 'opacity 120ms ease',
},
'&:hover .MuiTableSortLabel-icon, &:focus-visible .MuiTableSortLabel-icon':
{
opacity: 1,
},
}}
>
{title}
</TableSortLabel>
) : (
<Typography variant="subtitle2">{title}</Typography>
)}
<IconButton
size="small"
aria-label={`Open ${title} column menu`}
onClick={(event) => setAnchorEl(event.currentTarget)}
>
<ArrowDropDownIcon fontSize="small" />
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
{canSort && (
<Box>
<MenuItem onClick={() => column.toggleSorting(false)}>
<ListItemIcon>
<ArrowUpwardIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Asc</ListItemText>
</MenuItem>
<MenuItem onClick={() => column.toggleSorting(true)}>
<ListItemIcon>
<ArrowDownwardIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Desc</ListItemText>
</MenuItem>
</Box>
)}
{canGroup && (
<MenuItem onClick={column.getToggleGroupingHandler()}>
<ListItemIcon>
<GroupIcon fontSize="small" />
</ListItemIcon>
<ListItemText>{grouped ? 'Ungroup' : 'Group by'}</ListItemText>
</MenuItem>
)}
{canPin && (
<Box>
<Divider />
<MenuItem
disabled={pinned === 'left'}
onClick={() => column.pin('left')}
>
<ListItemIcon>
<PushPinIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Pin left</ListItemText>
</MenuItem>
<MenuItem
disabled={pinned === 'right'}
onClick={() => column.pin('right')}
>
<ListItemIcon>
<PushPinIcon fontSize="small" sx={{ rotate: '180deg' }} />
</ListItemIcon>
<ListItemText>Pin right</ListItemText>
</MenuItem>
{pinned ? (
<MenuItem onClick={() => column.pin(false)}>
<ListItemIcon>
<PushPinIcon fontSize="small" color="disabled" />
</ListItemIcon>
<ListItemText>Unpin</ListItemText>
</MenuItem>
) : null}
</Box>
)}
{canHide && (
<Box>
<Divider />
<MenuItem onClick={() => column.toggleVisibility(false)}>
<ListItemIcon>
<VisibilityOffIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Hide</ListItemText>
</MenuItem>
</Box>
)}
</Menu>
</Stack>
)
}
function ViewOptionsPopover({
table,
columnOrder,
onColumnOrderChange,
}: {
table: AppTable
columnOrder: Array<string>
onColumnOrderChange: React.Dispatch<React.SetStateAction<Array<string>>>
}) {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
const [query, setQuery] = React.useState('')
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
)
const columns = table
.getAllColumns()
.filter((column) => typeof column.accessorFn !== 'undefined')
.sort((a, b) => columnOrder.indexOf(a.id) - columnOrder.indexOf(b.id))
.filter((column) =>
(column.columnDef.meta?.label ?? column.id)
.toLowerCase()
.includes(query.toLowerCase()),
)
const onDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!over || active.id === over.id) return
onColumnOrderChange((current) => {
const oldIndex = current.indexOf(String(active.id))
const newIndex = current.indexOf(String(over.id))
return oldIndex >= 0 && newIndex >= 0
? arrayMove(current, oldIndex, newIndex)
: current
})
}
return (
<>
<Button
variant="outlined"
size="small"
startIcon={<SettingsIcon />}
onClick={(event) => setAnchorEl(event.currentTarget)}
>
View
</Button>
<Popover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<Stack spacing={1.5} sx={{ width: 300, p: 2 }}>
<TextField
size="small"
label="Search columns"
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onDragEnd}
>
<SortableContext
items={columns.map((column) => column.id)}
strategy={verticalListSortingStrategy}
>
<List dense disablePadding>
{columns.map((column) => (
<SortableFrame key={column.id} id={column.id}>
<ListItem
disablePadding
secondaryAction={
<DragIndicatorIcon color="disabled" fontSize="small" />
}
>
<ListItemButton
dense
onClick={() =>
column.toggleVisibility(!column.getIsVisible())
}
>
<ListItemIcon>
<Checkbox
edge="start"
size="small"
checked={column.getIsVisible()}
tabIndex={-1}
/>
</ListItemIcon>
<ListItemText
primary={column.columnDef.meta?.label ?? column.id}
/>
</ListItemButton>
</ListItem>
</SortableFrame>
))}
</List>
</SortableContext>
</DndContext>
</Stack>
</Popover>
</>
)
}
function SortListPopover({
table,
sorting,
onSortingChange,
}: {
table: AppTable
sorting: SortingState
onSortingChange: React.Dispatch<React.SetStateAction<SortingState>>
}) {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
)
const sortableColumns = table
.getAllColumns()
.filter((column) => column.getCanSort())
const updateSort = (index: number, patch: Partial<SortingState[number]>) => {
onSortingChange((current) =>
current.map((sort, sortIndex) =>
sortIndex === index ? { ...sort, ...patch } : sort,
),
)
}
const addSort = () => {
const nextColumn = sortableColumns.find(
(column) => !sorting.some((sort) => sort.id === column.id),
)
if (nextColumn)
onSortingChange((current) => [
...current,
{ id: nextColumn.id, desc: false },
])
}
const onDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!over || active.id === over.id) return
onSortingChange((current) => {
const oldIndex = current.findIndex((sort) => sort.id === active.id)
const newIndex = current.findIndex((sort) => sort.id === over.id)
return oldIndex >= 0 && newIndex >= 0
? arrayMove(current, oldIndex, newIndex)
: current
})
}
return (
<>
<Button
variant="outlined"
size="small"
startIcon={<SortIcon />}
endIcon={
sorting.length ? <Chip size="small" label={sorting.length} /> : null
}
onClick={(event) => setAnchorEl(event.currentTarget)}
>
Sort
</Button>
<Popover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
>
<Stack spacing={2} sx={{ width: 480, p: 2 }}>
<Typography variant="subtitle1">
{sorting.length ? 'Sort by' : 'No sorting applied'}
</Typography>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onDragEnd}
>
<SortableContext
items={sorting.map((sort) => sort.id)}
strategy={verticalListSortingStrategy}
>
<Stack spacing={1}>
{sorting.map((sort, index) => (
<SortableFrame key={sort.id} id={sort.id}>
<Stack
direction="row"
spacing={1}
sx={{ alignItems: 'center' }}
>
<DragIndicatorIcon color="disabled" />
<Autocomplete
size="small"
fullWidth
options={sortableColumns}
value={
sortableColumns.find(
(column) => column.id === sort.id,
) ?? null
}
getOptionLabel={(column) =>
column.columnDef.meta?.label ?? column.id
}
onChange={(_, column) => {
if (column) updateSort(index, { id: column.id })
}}
renderInput={(params) => (
<TextField {...params} label="Column" />
)}
/>
<FormControl size="small" sx={{ minWidth: 110 }}>
<InputLabel>Direction</InputLabel>
<Select
label="Direction"
value={sort.desc ? 'desc' : 'asc'}
onChange={(event) =>
updateSort(index, {
desc: event.target.value === 'desc',
})
}
>
<MenuItem value="asc">Asc</MenuItem>
<MenuItem value="desc">Desc</MenuItem>
</Select>
</FormControl>
<IconButton
size="small"
aria-label="Remove sort"
onClick={() =>
onSortingChange((current) =>
current.filter(
(_, sortIndex) => sortIndex !== index,
),
)
}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Stack>
</SortableFrame>
))}
</Stack>
</SortableContext>
</DndContext>
<Stack direction="row" spacing={1}>
<Button
variant="contained"
size="small"
onClick={addSort}
disabled={sorting.length >= sortableColumns.length}
>
Add sort
</Button>
<Button size="small" onClick={() => table.resetSorting()}>
Reset
</Button>
</Stack>
</Stack>
</Popover>
</>
)
}
function FilterValueInput({
column,
filter,
onFilterUpdate,
}: {
column: AppColumn
filter: ExtendedColumnFilter
onFilterUpdate: (
filterId: string,
patch: Partial<ExtendedColumnFilter>,
) => void
}) {
if (!filter.filterId) return null
const variant = column.columnDef.meta?.variant ?? 'text'
const operator = filter.operator ?? 'includesString'
const disabled = operator === 'isEmpty' || operator === 'isNotEmpty'
if (disabled) {
return <Typography color="text.secondary">No value required</Typography>
}
if (variant === 'select' || variant === 'multi-select') {
const options = column.columnDef.meta?.options ?? []
const multiple = variant === 'multi-select'
const value = multiple
? options.filter(
(option) =>
Array.isArray(filter.value) && filter.value.includes(option.value),
)
: (options.find((option) => option.value === filter.value) ?? null)
return (
<Autocomplete
size="small"
multiple={multiple}
options={options}
value={value}
getOptionLabel={(option) => option.label}
onChange={(_, nextValue) => {
onFilterUpdate(filter.filterId!, {
value: Array.isArray(nextValue)
? nextValue.map((option) => option.value)
: nextValue?.value,
})
}}
renderInput={(params) => <TextField {...params} label="Value" />}
/>
)
}
if (variant === 'date') {
if (operator === 'inRange') {
const value = Array.isArray(filter.value) ? filter.value : []
return (
<Stack direction="row" spacing={1}>
<TextField
size="small"
label="From"
type="date"
value={toDateInputValue(value[0])}
onChange={(event) =>
onFilterUpdate(filter.filterId!, {
value: [
event.target.value
? new Date(event.target.value).toISOString()
: undefined,
value[1],
],
})
}
slotProps={{ inputLabel: { shrink: true } }}
/>
<TextField
size="small"
label="To"
type="date"
value={toDateInputValue(value[1])}
onChange={(event) =>
onFilterUpdate(filter.filterId!, {
value: [
value[0],
event.target.value
? new Date(event.target.value).toISOString()
: undefined,
],
})
}
slotProps={{ inputLabel: { shrink: true } }}
/>
</Stack>
)
}
return (
<TextField
size="small"
label="Value"
type="date"
value={toDateInputValue(filter.value)}
onChange={(event) =>
onFilterUpdate(filter.filterId!, {
value: event.target.value
? new Date(event.target.value).toISOString()
: undefined,
})
}
slotProps={{ inputLabel: { shrink: true } }}
/>
)
}
if (variant === 'number') {
return (
<TextField
size="small"
label="Value"
type="number"
value={filter.value ?? ''}
onChange={(event) =>
onFilterUpdate(filter.filterId!, {
value: event.target.value === '' ? '' : Number(event.target.value),
})
}
/>
)
}
return (
<TextField
size="small"
label="Value"
value={filter.value ?? ''}
onChange={(event) =>
onFilterUpdate(filter.filterId!, { value: event.target.value })
}
/>
)
}
function FilterListPopover({
table,
columnFilters,
onColumnFiltersChange,
}: {
table: AppTable
columnFilters: Array<ExtendedColumnFilter>
onColumnFiltersChange: React.Dispatch<
React.SetStateAction<Array<ExtendedColumnFilter>>
>
}) {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
const filterableColumns = table
.getAllColumns()
.filter((column) => column.getCanFilter())
const updateFilter = (
filterId: string,
patch: Partial<ExtendedColumnFilter>,
) => {
onColumnFiltersChange((current) =>
current.map((filter) =>
filter.filterId === filterId ? { ...filter, ...patch } : filter,
),
)
}
const addFilter = () => {
if (filterableColumns.length === 0) return
const [column] = filterableColumns
onColumnFiltersChange((current) => [
...current,
{
id: column.id,
filterId: crypto.randomUUID(),
value: '',
operator: 'includesString',
joinOperator: current[0]?.joinOperator ?? 'and',
},
])
}
return (
<>
<Button
variant="outlined"
size="small"
startIcon={<FilterListIcon />}
endIcon={
columnFilters.length ? (
<Chip size="small" label={columnFilters.length} />
) : null
}
onClick={(event) => setAnchorEl(event.currentTarget)}
>
Filter
</Button>
<Popover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
>
<Stack spacing={2} sx={{ width: 720, p: 2 }}>
<Typography variant="subtitle1">Filters</Typography>
{columnFilters.map((filter, index) => {
const column = table.getColumn(filter.id)
if (!column || !filter.filterId) return null
const variant = column.columnDef.meta?.variant ?? 'text'
const operators = getFilterOperators(variant)
return (
<Stack
key={filter.filterId}
direction="row"
spacing={1}
sx={{ alignItems: 'center' }}
>
{index === 0 ? (
<Typography sx={{ width: 70 }}>Where</Typography>
) : index === 1 ? (
<FormControl size="small" sx={{ width: 90 }}>
<Select
value={filter.joinOperator ?? 'and'}
onChange={(event) => {
const joinOperator = event.target.value
onColumnFiltersChange((current) =>
current.map((item) => ({ ...item, joinOperator })),
)
}}
>
<MenuItem value="and">and</MenuItem>
<MenuItem value="or">or</MenuItem>
</Select>
</FormControl>
) : (
<Typography sx={{ width: 70 }}>
{filter.joinOperator ?? 'and'}
</Typography>
)}
<Autocomplete
size="small"
sx={{ width: 190 }}
options={filterableColumns}
value={column}
getOptionLabel={(option) =>
option.columnDef.meta?.label ?? option.id
}
onChange={(_, nextColumn) => {
if (nextColumn) {
updateFilter(filter.filterId!, {
id: nextColumn.id,
operator: getFilterOperators(
nextColumn.columnDef.meta?.variant ?? 'text',
)[0].value,
value: '',
})
}
}}
renderInput={(params) => (
<TextField {...params} label="Field" />
)}
/>
<FormControl size="small" sx={{ width: 180 }}>
<InputLabel>Operator</InputLabel>
<Select
label="Operator"
value={filter.operator ?? operators[0].value}
onChange={(event) =>
updateFilter(filter.filterId!, {
operator: event.target.value,
value: '',
})
}
>
{operators.map((operator) => (
<MenuItem key={operator.value} value={operator.value}>
{operator.label}
</MenuItem>
))}
</Select>
</FormControl>
<Box sx={{ flex: 1 }}>
<FilterValueInput
column={column}
filter={filter}
onFilterUpdate={updateFilter}
/>
</Box>
<IconButton
aria-label="Remove filter"
onClick={() =>
onColumnFiltersChange((current) =>
current.filter(
(item) => item.filterId !== filter.filterId,
),
)
}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Stack>
)
})}
<Stack direction="row" spacing={1}>
<Button variant="contained" size="small" onClick={addFilter}>
Add filter
</Button>
<Button size="small" onClick={() => onColumnFiltersChange([])}>
Reset
</Button>
</Stack>
</Stack>
</Popover>
</>
)
}
function Pagination({ table }: { table: AppTable }) {
return (
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={2}
sx={{ p: 1, alignItems: 'center', justifyContent: 'space-between' }}
>
<Typography variant="body2" color="text.secondary">
{table.getFilteredSelectedRowModel().rows.length.toLocaleString()} of{' '}
{table.getFilteredRowModel().rows.length.toLocaleString()} row(s)
selected.
</Typography>
<Stack direction="row" spacing={0.5} sx={{ alignItems: 'center' }}>
<TablePagination
component="div"
count={table.getFilteredRowModel().rows.length}
page={table.store.state.pagination.pageIndex}
rowsPerPage={table.store.state.pagination.pageSize}
rowsPerPageOptions={[10, 20, 30, 40, 50]}
showFirstButton
showLastButton
onPageChange={(_, page) => table.setPageIndex(page)}
onRowsPerPageChange={(event) => {
table.setPageSize(Number(event.target.value))
table.setPageIndex(0)
}}
/>
</Stack>
</Stack>
)
}
function ModeMenu({
mode,
setMode,
}: {
mode: 'light' | 'dark' | 'system'
setMode: React.Dispatch<React.SetStateAction<'light' | 'dark' | 'system'>>
}) {
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 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 App({
mode,
setMode,
}: {
mode: 'light' | 'dark' | 'system'
setMode: React.Dispatch<React.SetStateAction<'light' | 'dark' | 'system'>>
}) {
const rerender = React.useReducer(() => ({}), {})[1]
const [rowSelection, setRowSelection] = React.useState({})
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<
Array<ExtendedColumnFilter>
>([])
const [columnVisibility, setColumnVisibility] = React.useState({})
const [columnSizing, setColumnSizing] = React.useState<ColumnSizingState>({})
const [globalFilter, setGlobalFilter] = React.useState('')
const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>({
left: ['select'],
right: ['actions'],
})
const [grouping, setGrouping] = React.useState<GroupingState>([])
const [expanded, setExpanded] = React.useState<ExpandedState>({})
const [data, setData] = React.useState(() => makeData(1_000))
const columns = React.useMemo(
() =>
columnHelper.columns([
columnHelper.display({
id: 'select',
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
indeterminate={
!table.getIsAllPageRowsSelected() &&
table.getIsSomePageRowsSelected()
}
onChange={(_, checked) =>
table.toggleAllPageRowsSelected(checked)
}
slotProps={{ input: { 'aria-label': 'Select all' } }}
size="small"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onChange={(_, checked) => row.toggleSelected(checked)}
slotProps={{ input: { 'aria-label': 'Select row' } }}
size="small"
/>
),
maxSize: 48,
enableSorting: false,
enableHiding: false,
enableResizing: false,
}),
columnHelper.accessor('firstName', {
id: 'firstName',
header: ({ column }) => (
<ColumnHeaderMenu column={column} title="First Name" />
),
cell: (info) => (
<EllipsisText>{String(info.getValue())}</EllipsisText>
),
meta: { label: 'First Name', variant: 'text' },
}),
columnHelper.accessor((row) => row.lastName, {
id: 'lastName',
header: ({ column }) => (
<ColumnHeaderMenu column={column} title="Last Name" />
),
cell: (info) => (
<EllipsisText>{String(info.getValue())}</EllipsisText>
),
meta: { label: 'Last Name', variant: 'text' },
}),
columnHelper.accessor('age', {
id: 'age',
header: ({ column }) => (
<ColumnHeaderMenu column={column} title="Age" />
),
cell: (info) => (
<Typography variant="body2">{String(info.getValue())}</Typography>
),
aggregationFn: 'mean',
aggregatedCell: ({ getValue }) => (
<Typography variant="body2" color="text.secondary">
Avg: {Math.round(Number(getValue()) * 10) / 10}
</Typography>
),
meta: { label: 'Age', variant: 'number' },
}),
columnHelper.accessor('email', {
id: 'email',
header: ({ column }) => (
<ColumnHeaderMenu column={column} title="Email" />
),
cell: (info) => (
<EllipsisText>{info.cell.getValue<string>()}</EllipsisText>
),
meta: { label: 'Email', variant: 'text' },
}),
columnHelper.accessor('status', {
id: 'status',
header: ({ column }) => (
<ColumnHeaderMenu column={column} title="Status" />
),
cell: (info) => {
const status = info.getValue<Person['status'] | undefined>()
return status ? <StatusChip status={status} /> : null
},
aggregatedCell: () => null,
meta: {
label: 'Status',
variant: 'select',
options: statuses.map((status) => ({
label: toSentenceCase(status),
value: status,
})),
},
}),
columnHelper.accessor('department', {
id: 'department',
header: ({ column }) => (
<ColumnHeaderMenu column={column} title="Department" />
),
cell: (info) => {
const department = info.getValue<Person['department'] | undefined>()
return department ? (
<DepartmentChip department={department} />
) : null
},
aggregatedCell: () => null,
meta: {
label: 'Department',
variant: 'multi-select',
options: departments.map((department) => ({
label: toSentenceCase(department),
value: department,
})),
},
}),
columnHelper.accessor('joinDate', {
id: 'joinDate',
header: ({ column }) => (
<ColumnHeaderMenu column={column} title="Join Date" />
),
cell: (info) => formatDate(info.getValue<string>()),
aggregationFn: 'min',
aggregatedCell: ({ getValue }) => {
const earliest = getValue<string>()
return (
<Typography variant="body2" color="text.secondary">
Earliest: {earliest ? formatDate(earliest) : '—'}
</Typography>
)
},
meta: { label: 'Join Date', variant: 'date' },
}),
columnHelper.display({
id: 'actions',
enableHiding: false,
cell: ({ row }) => <RowActions person={row.original} />,
maxSize: 44,
enableResizing: false,
}),
]),
[],
)
const [columnOrder, setColumnOrder] = React.useState<Array<string>>(() =>
columns.map((column) => column.id ?? ''),
)
const table = useTable(
{
_features,
_rowModels: {
coreRowModel: createCoreRowModel(),
filteredRowModel: createFilteredRowModel({
...filterFns,
fuzzy: fuzzyFilter,
}),
facetedRowModel: createFacetedRowModel(),
facetedUniqueValues: createFacetedUniqueValues(),
paginatedRowModel: createPaginatedRowModel(),
sortedRowModel: createSortedRowModel(sortFns),
groupedRowModel: createGroupedRowModel(aggregationFns),
expandedRowModel: createExpandedRowModel(),
},
columns,
data,
defaultColumn: {
minSize: 60,
maxSize: 800,
filterFn: dynamicFilterFn,
},
globalFilterFn: 'fuzzy',
state: {
rowSelection,
sorting,
columnVisibility,
columnOrder,
columnSizing,
columnFilters,
globalFilter,
columnPinning,
grouping,
expanded,
},
onSortingChange: setSorting,
onColumnVisibilityChange: setColumnVisibility,
onColumnOrderChange: setColumnOrder,
onColumnSizingChange: setColumnSizing,
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
onColumnPinningChange: setColumnPinning,
onGroupingChange: setGrouping,
onExpandedChange: setExpanded,
getRowId: (row) => row.id,
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
columnResizeMode: 'onChange',
debugTable: true,
},
(state) => state, // default selector
)
const columnSizeVars = React.useMemo(() => {
const headers = table.getFlatHeaders()
const colSizes: Record<string, number> = {}
for (const header of headers) {
colSizes[`--header-${header.id}-size`] = header.getSize()
colSizes[`--col-${header.column.id}-size`] = header.column.getSize()
}
return colSizes
}, [table.store.state.columnSizing])
const refreshData = () => setData(makeData(1_000))
const stressTest = () => setData(makeData(200_000))
return (
<SortingContext.Provider value={sorting}>
<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 (200k rows)
</Button>
<Button
variant="outlined"
size="small"
onClick={() => rerender()}
>
Force Rerender
</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>
),
},
}}
/>
<FilterListPopover
table={table}
columnFilters={columnFilters}
onColumnFiltersChange={setColumnFilters}
/>
<SortListPopover
table={table}
sorting={sorting}
onSortingChange={setSorting}
/>
<ViewOptionsPopover
table={table}
columnOrder={columnOrder}
onColumnOrderChange={setColumnOrder}
/>
</Stack>
<Paper variant="outlined">
<TableContainer sx={{ maxHeight: 680 }}>
<MuiTable
stickyHeader
size="small"
sx={{
width: '100%',
tableLayout: 'fixed',
...columnSizeVars,
}}
>
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers
.filter((header) => header.column.getIsVisible())
.map((header) => (
<ResizableHeaderCell
key={header.id}
header={header}
table={table}
/>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
hover
selected={row.getIsSelected()}
aria-selected={row.getIsSelected()}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
align={
cell.column.id === 'select' ? 'center' : 'left'
}
sx={{
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
overflow: 'hidden',
borderRight:
cell.column.id === 'actions' ? undefined : 1,
borderColor: 'divider',
...getCommonPinningStyles(
cell.column,
row.getIsSelected(),
),
}}
>
{cell.getIsGrouped() ? (
<Button
size="small"
variant="text"
startIcon={
row.getIsExpanded() ? (
<ExpandLessIcon />
) : (
<ExpandMoreIcon />
)
}
onClick={row.getToggleExpandedHandler()}
disabled={!row.getCanExpand()}
sx={{ pl: row.depth * 2 + 1 }}
>
<table.FlexRender cell={cell} />
<Typography
component="span"
color="text.secondary"
sx={{ ml: 1 }}
>
({row.subRows.length})
</Typography>
</Button>
) : cell.column.id === 'progress' ? (
<Stack spacing={0.5}>
<Typography variant="body2">
{String(cell.getValue())}%
</Typography>
<LinearProgress
variant="determinate"
value={Number(cell.getValue())}
/>
</Stack>
) : (
<table.FlexRender cell={cell} />
)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</MuiTable>
</TableContainer>
<Pagination table={table} />
</Paper>
</Stack>
</Container>
</SortingContext.Provider>
)
}
function ResizableHeaderCell({
header,
table,
}: {
header: Header<typeof _features, Person>
table: {
FlexRender: React.ComponentType<{
header: Header<typeof _features, Person>
}>
}
}) {
const sorting = React.useContext(SortingContext)
const sortDirection = getSortDirection(sorting, header.column.id)
return (
<TableCell
colSpan={header.colSpan}
align={header.column.id === 'select' ? 'center' : 'left'}
sortDirection={sortDirection || false}
aria-sort={getAriaSort(sortDirection || false)}
data-sort={sortDirection}
sx={{
width: `calc(var(--header-${header.id}-size) * 1px)`,
borderRight: header.id === 'actions' ? undefined : 1,
borderColor: 'divider',
p: 1,
...getCommonPinningStyles(header.column),
}}
>
<Box
sx={{ position: 'relative', pr: header.column.getCanResize() ? 1 : 0 }}
>
{header.isPlaceholder ? null : <table.FlexRender header={header} />}
{header.column.getCanResize() ? (
<Box
onDoubleClick={() => header.column.resetSize()}
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
sx={{
position: 'absolute',
top: 0,
right: -6,
width: 6,
height: '100%',
cursor: 'col-resize',
touchAction: 'none',
bgcolor: header.column.getIsResizing()
? 'primary.main'
: 'transparent',
'&:hover': { bgcolor: 'primary.main' },
}}
/>
) : null}
</Box>
</TableCell>
)
}
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({
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>,
)
'use client'
import * as React from 'react'
import * as ReactDOM from 'react-dom/client'
import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer'
import {
DndContext,
PointerSensor,
closestCenter,
useSensor,
useSensors,
} from '@dnd-kit/core'
import {
SortableContext,
arrayMove,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'
import CheckIcon from '@mui/icons-material/Check'
import CodeIcon from '@mui/icons-material/Code'
import CreditCardIcon from '@mui/icons-material/CreditCard'
import DarkModeIcon from '@mui/icons-material/DarkMode'
import DeleteIcon from '@mui/icons-material/Delete'
import DragIndicatorIcon from '@mui/icons-material/DragIndicator'
import ExpandLessIcon from '@mui/icons-material/ExpandLess'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import FilterListIcon from '@mui/icons-material/FilterList'
import GroupIcon from '@mui/icons-material/Group'
import LightModeIcon from '@mui/icons-material/LightMode'
import MoreVertIcon from '@mui/icons-material/MoreVert'
import PushPinIcon from '@mui/icons-material/PushPin'
import SearchIcon from '@mui/icons-material/Search'
import SettingsIcon from '@mui/icons-material/Settings'
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'
import SortIcon from '@mui/icons-material/Sort'
import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt'
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'
import {
Autocomplete,
Box,
Button,
Checkbox,
Chip,
Container,
CssBaseline,
Divider,
FormControl,
IconButton,
InputAdornment,
InputLabel,
LinearProgress,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Table as MuiTable,
Paper,
Popover,
Select,
Stack,
TableBody,
TableCell,
TableContainer,
TableHead,
TablePagination,
TableRow,
TableSortLabel,
TextField,
ThemeProvider,
Toolbar,
Tooltip,
Typography,
createTheme,
useMediaQuery,
} from '@mui/material'
import {
aggregationFns,
columnFacetingFeature,
columnFilteringFeature,
columnGroupingFeature,
columnOrderingFeature,
columnPinningFeature,
columnResizingFeature,
columnSizingFeature,
columnVisibilityFeature,
createColumnHelper,
createCoreRowModel,
createExpandedRowModel,
createFacetedRowModel,
createFacetedUniqueValues,
createFilteredRowModel,
createGroupedRowModel,
createPaginatedRowModel,
createSortedRowModel,
filterFns,
globalFilteringFeature,
rowExpandingFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
sortFns,
tableFeatures,
useTable,
} from '@tanstack/react-table'
import type { Person } from '@/lib/make-data'
import type { DragEndEvent } from '@dnd-kit/core'
import type {
CellData,
Column,
ColumnPinningState,
ColumnSizingState,
ExpandedState,
GroupingState,
Header,
RowData,
SortingState,
Table,
TableFeatures,
} from '@tanstack/react-table'
import type { ExtendedColumnFilter } from '@/types'
import {
dynamicFilterFn,
fuzzyFilter,
getFilterOperators,
} from '@/lib/data-table'
import { departments, makeData, statuses } from '@/lib/make-data'
import './styles/globals.css'
declare module '@tanstack/react-table' {
interface ColumnMeta<
TFeatures extends TableFeatures,
TData extends RowData,
TValue extends CellData = CellData,
> {
label?: string
variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select'
options?: Array<{ label: string; value: string; count?: number }>
}
}
const _features = tableFeatures({
rowSortingFeature,
rowPaginationFeature,
rowSelectionFeature,
rowExpandingFeature,
columnFilteringFeature,
columnFacetingFeature,
columnOrderingFeature,
columnVisibilityFeature,
columnSizingFeature,
columnResizingFeature,
columnPinningFeature,
columnGroupingFeature,
globalFilteringFeature,
})
const columnHelper = createColumnHelper<typeof _features, Person>()
type AppTable = Table<typeof _features, Person>
type AppColumn = Column<typeof _features, Person, any>
function SortableFrame({
id,
children,
}: {
id: string
children: React.ReactNode
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id })
return (
<Box
ref={setNodeRef}
{...attributes}
{...listeners}
sx={{
opacity: isDragging ? 0.6 : 1,
transform: CSS.Transform.toString(transform),
transition,
cursor: 'grab',
'&:active': {
cursor: 'grabbing',
},
}}
>
{children}
</Box>
)
}
function toSentenceCase(value: string) {
return value
.replace(/[-_]/g, ' ')
.replace(/\w\S*/g, (word) => word[0].toUpperCase() + word.slice(1))
}
function formatDate(value: string) {
return new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
}).format(new Date(value))
}
function toDateInputValue(value: unknown) {
if (!value) return ''
const date = new Date(String(value))
return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 10)
}
function getAriaSort(sortDirection: false | 'asc' | 'desc') {
if (sortDirection === 'asc') return 'ascending'
if (sortDirection === 'desc') return 'descending'
return 'none'
}
const SortingContext = React.createContext<SortingState>([])
function getSortDirection(sorting: SortingState, columnId: string) {
const sort = sorting.find((sort) => sort.id === columnId)
return sort ? (sort.desc ? 'desc' : 'asc') : undefined
}
function getCommonPinningStyles(
column: AppColumn,
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))'
: isPinned
? 'var(--mui-palette-background-paper)'
: undefined,
zIndex: isPinned ? 2 : 0,
}
}
function DepartmentIcon({ department }: { department: Person['department'] }) {
const icons: Record<Person['department'], React.ReactElement> = {
engineering: <CodeIcon fontSize="inherit" />,
marketing: <SystemUpdateAltIcon fontSize="inherit" />,
sales: <ShoppingCartIcon fontSize="inherit" />,
hr: <GroupIcon fontSize="inherit" />,
finance: <CreditCardIcon fontSize="inherit" />,
}
return icons[department]
}
function DepartmentChip({ department }: { department: Person['department'] }) {
return (
<Box
component="span"
sx={{
display: 'inline-flex',
maxWidth: '100%',
height: 24,
minWidth: 0,
alignItems: 'center',
gap: 0.75,
px: 1,
borderRadius: 13,
border: 1,
borderColor: 'divider',
fontSize: '0.8125rem',
}}
>
<Box
component="span"
sx={{
display: 'inline-flex',
width: 16,
height: 16,
flex: '0 0 16px',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
fontSize: 16,
lineHeight: 1,
}}
>
<DepartmentIcon department={department} />
</Box>
<Box
component="span"
sx={{
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{toSentenceCase(department)}
</Box>
</Box>
)
}
function EllipsisText({ children }: { children: React.ReactNode }) {
return (
<Box
component="span"
sx={{
display: 'block',
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{children}
</Box>
)
}
function StatusChip({ status }: { status: Person['status'] }) {
const color: Record<Person['status'], 'success' | 'error' | 'warning'> = {
active: 'success',
inactive: 'error',
pending: 'warning',
}
return (
<Chip
icon={<CheckIcon fontSize="small" />}
label={toSentenceCase(status)}
color={color[status]}
variant="outlined"
size="small"
/>
)
}
function RowActions({ person }: { person: Person }) {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
const open = Boolean(anchorEl)
return (
<>
<IconButton
size="small"
aria-label="Open row actions"
aria-controls={open ? `row-actions-${person.id}` : undefined}
aria-haspopup="menu"
onClick={(event) => setAnchorEl(event.currentTarget)}
>
<MoreVertIcon fontSize="small" />
</IconButton>
<Menu
id={`row-actions-${person.id}`}
anchorEl={anchorEl}
open={open}
onClose={() => setAnchorEl(null)}
>
<MenuItem
onClick={() => {
void navigator.clipboard.writeText(person.id)
setAnchorEl(null)
}}
>
<ListItemText>Copy ID</ListItemText>
</MenuItem>
<Divider />
<MenuItem onClick={() => setAnchorEl(null)}>View details</MenuItem>
<MenuItem onClick={() => setAnchorEl(null)}>View profile</MenuItem>
</Menu>
</>
)
}
function ColumnHeaderMenu({
column,
title,
}: {
column: AppColumn
title: string
}) {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
const canSort = column.getCanSort()
const canHide = column.getCanHide()
const canPin = column.getCanPin()
const canGroup = column.getCanGroup()
const sorting = React.useContext(SortingContext)
const direction = canSort ? getSortDirection(sorting, column.id) : undefined
const isSorted = !!direction
const pinned = canPin ? column.getIsPinned() : false
const grouped = canGroup ? column.getIsGrouped() : false
if (!canSort && !canHide && !canPin && !canGroup) {
return <Typography variant="subtitle2">{title}</Typography>
}
return (
<Stack direction="row" spacing={0.5} sx={{ alignItems: 'center' }}>
{canSort ? (
<TableSortLabel
active={isSorted}
direction={direction}
IconComponent={ArrowDownwardIcon}
onClick={column.getToggleSortingHandler()}
sx={{
'& .MuiTableSortLabel-icon': {
opacity: isSorted ? 1 : 0,
transition: 'opacity 120ms ease',
},
'&:hover .MuiTableSortLabel-icon, &:focus-visible .MuiTableSortLabel-icon':
{
opacity: 1,
},
}}
>
{title}
</TableSortLabel>
) : (
<Typography variant="subtitle2">{title}</Typography>
)}
<IconButton
size="small"
aria-label={`Open ${title} column menu`}
onClick={(event) => setAnchorEl(event.currentTarget)}
>
<ArrowDropDownIcon fontSize="small" />
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
{canSort && (
<Box>
<MenuItem onClick={() => column.toggleSorting(false)}>
<ListItemIcon>
<ArrowUpwardIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Asc</ListItemText>
</MenuItem>
<MenuItem onClick={() => column.toggleSorting(true)}>
<ListItemIcon>
<ArrowDownwardIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Desc</ListItemText>
</MenuItem>
</Box>
)}
{canGroup && (
<MenuItem onClick={column.getToggleGroupingHandler()}>
<ListItemIcon>
<GroupIcon fontSize="small" />
</ListItemIcon>
<ListItemText>{grouped ? 'Ungroup' : 'Group by'}</ListItemText>
</MenuItem>
)}
{canPin && (
<Box>
<Divider />
<MenuItem
disabled={pinned === 'left'}
onClick={() => column.pin('left')}
>
<ListItemIcon>
<PushPinIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Pin left</ListItemText>
</MenuItem>
<MenuItem
disabled={pinned === 'right'}
onClick={() => column.pin('right')}
>
<ListItemIcon>
<PushPinIcon fontSize="small" sx={{ rotate: '180deg' }} />
</ListItemIcon>
<ListItemText>Pin right</ListItemText>
</MenuItem>
{pinned ? (
<MenuItem onClick={() => column.pin(false)}>
<ListItemIcon>
<PushPinIcon fontSize="small" color="disabled" />
</ListItemIcon>
<ListItemText>Unpin</ListItemText>
</MenuItem>
) : null}
</Box>
)}
{canHide && (
<Box>
<Divider />
<MenuItem onClick={() => column.toggleVisibility(false)}>
<ListItemIcon>
<VisibilityOffIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Hide</ListItemText>
</MenuItem>
</Box>
)}
</Menu>
</Stack>
)
}
function ViewOptionsPopover({
table,
columnOrder,
onColumnOrderChange,
}: {
table: AppTable
columnOrder: Array<string>
onColumnOrderChange: React.Dispatch<React.SetStateAction<Array<string>>>
}) {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
const [query, setQuery] = React.useState('')
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
)
const columns = table
.getAllColumns()
.filter((column) => typeof column.accessorFn !== 'undefined')
.sort((a, b) => columnOrder.indexOf(a.id) - columnOrder.indexOf(b.id))
.filter((column) =>
(column.columnDef.meta?.label ?? column.id)
.toLowerCase()
.includes(query.toLowerCase()),
)
const onDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!over || active.id === over.id) return
onColumnOrderChange((current) => {
const oldIndex = current.indexOf(String(active.id))
const newIndex = current.indexOf(String(over.id))
return oldIndex >= 0 && newIndex >= 0
? arrayMove(current, oldIndex, newIndex)
: current
})
}
return (
<>
<Button
variant="outlined"
size="small"
startIcon={<SettingsIcon />}
onClick={(event) => setAnchorEl(event.currentTarget)}
>
View
</Button>
<Popover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<Stack spacing={1.5} sx={{ width: 300, p: 2 }}>
<TextField
size="small"
label="Search columns"
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onDragEnd}
>
<SortableContext
items={columns.map((column) => column.id)}
strategy={verticalListSortingStrategy}
>
<List dense disablePadding>
{columns.map((column) => (
<SortableFrame key={column.id} id={column.id}>
<ListItem
disablePadding
secondaryAction={
<DragIndicatorIcon color="disabled" fontSize="small" />
}
>
<ListItemButton
dense
onClick={() =>
column.toggleVisibility(!column.getIsVisible())
}
>
<ListItemIcon>
<Checkbox
edge="start"
size="small"
checked={column.getIsVisible()}
tabIndex={-1}
/>
</ListItemIcon>
<ListItemText
primary={column.columnDef.meta?.label ?? column.id}
/>
</ListItemButton>
</ListItem>
</SortableFrame>
))}
</List>
</SortableContext>
</DndContext>
</Stack>
</Popover>
</>
)
}
function SortListPopover({
table,
sorting,
onSortingChange,
}: {
table: AppTable
sorting: SortingState
onSortingChange: React.Dispatch<React.SetStateAction<SortingState>>
}) {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
)
const sortableColumns = table
.getAllColumns()
.filter((column) => column.getCanSort())
const updateSort = (index: number, patch: Partial<SortingState[number]>) => {
onSortingChange((current) =>
current.map((sort, sortIndex) =>
sortIndex === index ? { ...sort, ...patch } : sort,
),
)
}
const addSort = () => {
const nextColumn = sortableColumns.find(
(column) => !sorting.some((sort) => sort.id === column.id),
)
if (nextColumn)
onSortingChange((current) => [
...current,
{ id: nextColumn.id, desc: false },
])
}
const onDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!over || active.id === over.id) return
onSortingChange((current) => {
const oldIndex = current.findIndex((sort) => sort.id === active.id)
const newIndex = current.findIndex((sort) => sort.id === over.id)
return oldIndex >= 0 && newIndex >= 0
? arrayMove(current, oldIndex, newIndex)
: current
})
}
return (
<>
<Button
variant="outlined"
size="small"
startIcon={<SortIcon />}
endIcon={
sorting.length ? <Chip size="small" label={sorting.length} /> : null
}
onClick={(event) => setAnchorEl(event.currentTarget)}
>
Sort
</Button>
<Popover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
>
<Stack spacing={2} sx={{ width: 480, p: 2 }}>
<Typography variant="subtitle1">
{sorting.length ? 'Sort by' : 'No sorting applied'}
</Typography>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onDragEnd}
>
<SortableContext
items={sorting.map((sort) => sort.id)}
strategy={verticalListSortingStrategy}
>
<Stack spacing={1}>
{sorting.map((sort, index) => (
<SortableFrame key={sort.id} id={sort.id}>
<Stack
direction="row"
spacing={1}
sx={{ alignItems: 'center' }}
>
<DragIndicatorIcon color="disabled" />
<Autocomplete
size="small"
fullWidth
options={sortableColumns}
value={
sortableColumns.find(
(column) => column.id === sort.id,
) ?? null
}
getOptionLabel={(column) =>
column.columnDef.meta?.label ?? column.id
}
onChange={(_, column) => {
if (column) updateSort(index, { id: column.id })
}}
renderInput={(params) => (
<TextField {...params} label="Column" />
)}
/>
<FormControl size="small" sx={{ minWidth: 110 }}>
<InputLabel>Direction</InputLabel>
<Select
label="Direction"
value={sort.desc ? 'desc' : 'asc'}
onChange={(event) =>
updateSort(index, {
desc: event.target.value === 'desc',
})
}
>
<MenuItem value="asc">Asc</MenuItem>
<MenuItem value="desc">Desc</MenuItem>
</Select>
</FormControl>
<IconButton
size="small"
aria-label="Remove sort"
onClick={() =>
onSortingChange((current) =>
current.filter(
(_, sortIndex) => sortIndex !== index,
),
)
}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Stack>
</SortableFrame>
))}
</Stack>
</SortableContext>
</DndContext>
<Stack direction="row" spacing={1}>
<Button
variant="contained"
size="small"
onClick={addSort}
disabled={sorting.length >= sortableColumns.length}
>
Add sort
</Button>
<Button size="small" onClick={() => table.resetSorting()}>
Reset
</Button>
</Stack>
</Stack>
</Popover>
</>
)
}
function FilterValueInput({
column,
filter,
onFilterUpdate,
}: {
column: AppColumn
filter: ExtendedColumnFilter
onFilterUpdate: (
filterId: string,
patch: Partial<ExtendedColumnFilter>,
) => void
}) {
if (!filter.filterId) return null
const variant = column.columnDef.meta?.variant ?? 'text'
const operator = filter.operator ?? 'includesString'
const disabled = operator === 'isEmpty' || operator === 'isNotEmpty'
if (disabled) {
return <Typography color="text.secondary">No value required</Typography>
}
if (variant === 'select' || variant === 'multi-select') {
const options = column.columnDef.meta?.options ?? []
const multiple = variant === 'multi-select'
const value = multiple
? options.filter(
(option) =>
Array.isArray(filter.value) && filter.value.includes(option.value),
)
: (options.find((option) => option.value === filter.value) ?? null)
return (
<Autocomplete
size="small"
multiple={multiple}
options={options}
value={value}
getOptionLabel={(option) => option.label}
onChange={(_, nextValue) => {
onFilterUpdate(filter.filterId!, {
value: Array.isArray(nextValue)
? nextValue.map((option) => option.value)
: nextValue?.value,
})
}}
renderInput={(params) => <TextField {...params} label="Value" />}
/>
)
}
if (variant === 'date') {
if (operator === 'inRange') {
const value = Array.isArray(filter.value) ? filter.value : []
return (
<Stack direction="row" spacing={1}>
<TextField
size="small"
label="From"
type="date"
value={toDateInputValue(value[0])}
onChange={(event) =>
onFilterUpdate(filter.filterId!, {
value: [
event.target.value
? new Date(event.target.value).toISOString()
: undefined,
value[1],
],
})
}
slotProps={{ inputLabel: { shrink: true } }}
/>
<TextField
size="small"
label="To"
type="date"
value={toDateInputValue(value[1])}
onChange={(event) =>
onFilterUpdate(filter.filterId!, {
value: [
value[0],
event.target.value
? new Date(event.target.value).toISOString()
: undefined,
],
})
}
slotProps={{ inputLabel: { shrink: true } }}
/>
</Stack>
)
}
return (
<TextField
size="small"
label="Value"
type="date"
value={toDateInputValue(filter.value)}
onChange={(event) =>
onFilterUpdate(filter.filterId!, {
value: event.target.value
? new Date(event.target.value).toISOString()
: undefined,
})
}
slotProps={{ inputLabel: { shrink: true } }}
/>
)
}
if (variant === 'number') {
return (
<TextField
size="small"
label="Value"
type="number"
value={filter.value ?? ''}
onChange={(event) =>
onFilterUpdate(filter.filterId!, {
value: event.target.value === '' ? '' : Number(event.target.value),
})
}
/>
)
}
return (
<TextField
size="small"
label="Value"
value={filter.value ?? ''}
onChange={(event) =>
onFilterUpdate(filter.filterId!, { value: event.target.value })
}
/>
)
}
function FilterListPopover({
table,
columnFilters,
onColumnFiltersChange,
}: {
table: AppTable
columnFilters: Array<ExtendedColumnFilter>
onColumnFiltersChange: React.Dispatch<
React.SetStateAction<Array<ExtendedColumnFilter>>
>
}) {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
const filterableColumns = table
.getAllColumns()
.filter((column) => column.getCanFilter())
const updateFilter = (
filterId: string,
patch: Partial<ExtendedColumnFilter>,
) => {
onColumnFiltersChange((current) =>
current.map((filter) =>
filter.filterId === filterId ? { ...filter, ...patch } : filter,
),
)
}
const addFilter = () => {
if (filterableColumns.length === 0) return
const [column] = filterableColumns
onColumnFiltersChange((current) => [
...current,
{
id: column.id,
filterId: crypto.randomUUID(),
value: '',
operator: 'includesString',
joinOperator: current[0]?.joinOperator ?? 'and',
},
])
}
return (
<>
<Button
variant="outlined"
size="small"
startIcon={<FilterListIcon />}
endIcon={
columnFilters.length ? (
<Chip size="small" label={columnFilters.length} />
) : null
}
onClick={(event) => setAnchorEl(event.currentTarget)}
>
Filter
</Button>
<Popover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
>
<Stack spacing={2} sx={{ width: 720, p: 2 }}>
<Typography variant="subtitle1">Filters</Typography>
{columnFilters.map((filter, index) => {
const column = table.getColumn(filter.id)
if (!column || !filter.filterId) return null
const variant = column.columnDef.meta?.variant ?? 'text'
const operators = getFilterOperators(variant)
return (
<Stack
key={filter.filterId}
direction="row"
spacing={1}
sx={{ alignItems: 'center' }}
>
{index === 0 ? (
<Typography sx={{ width: 70 }}>Where</Typography>
) : index === 1 ? (
<FormControl size="small" sx={{ width: 90 }}>
<Select
value={filter.joinOperator ?? 'and'}
onChange={(event) => {
const joinOperator = event.target.value
onColumnFiltersChange((current) =>
current.map((item) => ({ ...item, joinOperator })),
)
}}
>
<MenuItem value="and">and</MenuItem>
<MenuItem value="or">or</MenuItem>
</Select>
</FormControl>
) : (
<Typography sx={{ width: 70 }}>
{filter.joinOperator ?? 'and'}
</Typography>
)}
<Autocomplete
size="small"
sx={{ width: 190 }}
options={filterableColumns}
value={column}
getOptionLabel={(option) =>
option.columnDef.meta?.label ?? option.id
}
onChange={(_, nextColumn) => {
if (nextColumn) {
updateFilter(filter.filterId!, {
id: nextColumn.id,
operator: getFilterOperators(
nextColumn.columnDef.meta?.variant ?? 'text',
)[0].value,
value: '',
})
}
}}
renderInput={(params) => (
<TextField {...params} label="Field" />
)}
/>
<FormControl size="small" sx={{ width: 180 }}>
<InputLabel>Operator</InputLabel>
<Select
label="Operator"
value={filter.operator ?? operators[0].value}
onChange={(event) =>
updateFilter(filter.filterId!, {
operator: event.target.value,
value: '',
})
}
>
{operators.map((operator) => (
<MenuItem key={operator.value} value={operator.value}>
{operator.label}
</MenuItem>
))}
</Select>
</FormControl>
<Box sx={{ flex: 1 }}>
<FilterValueInput
column={column}
filter={filter}
onFilterUpdate={updateFilter}
/>
</Box>
<IconButton
aria-label="Remove filter"
onClick={() =>
onColumnFiltersChange((current) =>
current.filter(
(item) => item.filterId !== filter.filterId,
),
)
}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Stack>
)
})}
<Stack direction="row" spacing={1}>
<Button variant="contained" size="small" onClick={addFilter}>
Add filter
</Button>
<Button size="small" onClick={() => onColumnFiltersChange([])}>
Reset
</Button>
</Stack>
</Stack>
</Popover>
</>
)
}
function Pagination({ table }: { table: AppTable }) {
return (
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={2}
sx={{ p: 1, alignItems: 'center', justifyContent: 'space-between' }}
>
<Typography variant="body2" color="text.secondary">
{table.getFilteredSelectedRowModel().rows.length.toLocaleString()} of{' '}
{table.getFilteredRowModel().rows.length.toLocaleString()} row(s)
selected.
</Typography>
<Stack direction="row" spacing={0.5} sx={{ alignItems: 'center' }}>
<TablePagination
component="div"
count={table.getFilteredRowModel().rows.length}
page={table.store.state.pagination.pageIndex}
rowsPerPage={table.store.state.pagination.pageSize}
rowsPerPageOptions={[10, 20, 30, 40, 50]}
showFirstButton
showLastButton
onPageChange={(_, page) => table.setPageIndex(page)}
onRowsPerPageChange={(event) => {
table.setPageSize(Number(event.target.value))
table.setPageIndex(0)
}}
/>
</Stack>
</Stack>
)
}
function ModeMenu({
mode,
setMode,
}: {
mode: 'light' | 'dark' | 'system'
setMode: React.Dispatch<React.SetStateAction<'light' | 'dark' | 'system'>>
}) {
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 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 App({
mode,
setMode,
}: {
mode: 'light' | 'dark' | 'system'
setMode: React.Dispatch<React.SetStateAction<'light' | 'dark' | 'system'>>
}) {
const rerender = React.useReducer(() => ({}), {})[1]
const [rowSelection, setRowSelection] = React.useState({})
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<
Array<ExtendedColumnFilter>
>([])
const [columnVisibility, setColumnVisibility] = React.useState({})
const [columnSizing, setColumnSizing] = React.useState<ColumnSizingState>({})
const [globalFilter, setGlobalFilter] = React.useState('')
const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>({
left: ['select'],
right: ['actions'],
})
const [grouping, setGrouping] = React.useState<GroupingState>([])
const [expanded, setExpanded] = React.useState<ExpandedState>({})
const [data, setData] = React.useState(() => makeData(1_000))
const columns = React.useMemo(
() =>
columnHelper.columns([
columnHelper.display({
id: 'select',
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
indeterminate={
!table.getIsAllPageRowsSelected() &&
table.getIsSomePageRowsSelected()
}
onChange={(_, checked) =>
table.toggleAllPageRowsSelected(checked)
}
slotProps={{ input: { 'aria-label': 'Select all' } }}
size="small"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onChange={(_, checked) => row.toggleSelected(checked)}
slotProps={{ input: { 'aria-label': 'Select row' } }}
size="small"
/>
),
maxSize: 48,
enableSorting: false,
enableHiding: false,
enableResizing: false,
}),
columnHelper.accessor('firstName', {
id: 'firstName',
header: ({ column }) => (
<ColumnHeaderMenu column={column} title="First Name" />
),
cell: (info) => (
<EllipsisText>{String(info.getValue())}</EllipsisText>
),
meta: { label: 'First Name', variant: 'text' },
}),
columnHelper.accessor((row) => row.lastName, {
id: 'lastName',
header: ({ column }) => (
<ColumnHeaderMenu column={column} title="Last Name" />
),
cell: (info) => (
<EllipsisText>{String(info.getValue())}</EllipsisText>
),
meta: { label: 'Last Name', variant: 'text' },
}),
columnHelper.accessor('age', {
id: 'age',
header: ({ column }) => (
<ColumnHeaderMenu column={column} title="Age" />
),
cell: (info) => (
<Typography variant="body2">{String(info.getValue())}</Typography>
),
aggregationFn: 'mean',
aggregatedCell: ({ getValue }) => (
<Typography variant="body2" color="text.secondary">
Avg: {Math.round(Number(getValue()) * 10) / 10}
</Typography>
),
meta: { label: 'Age', variant: 'number' },
}),
columnHelper.accessor('email', {
id: 'email',
header: ({ column }) => (
<ColumnHeaderMenu column={column} title="Email" />
),
cell: (info) => (
<EllipsisText>{info.cell.getValue<string>()}</EllipsisText>
),
meta: { label: 'Email', variant: 'text' },
}),
columnHelper.accessor('status', {
id: 'status',
header: ({ column }) => (
<ColumnHeaderMenu column={column} title="Status" />
),
cell: (info) => {
const status = info.getValue<Person['status'] | undefined>()
return status ? <StatusChip status={status} /> : null
},
aggregatedCell: () => null,
meta: {
label: 'Status',
variant: 'select',
options: statuses.map((status) => ({
label: toSentenceCase(status),
value: status,
})),
},
}),
columnHelper.accessor('department', {
id: 'department',
header: ({ column }) => (
<ColumnHeaderMenu column={column} title="Department" />
),
cell: (info) => {
const department = info.getValue<Person['department'] | undefined>()
return department ? (
<DepartmentChip department={department} />
) : null
},
aggregatedCell: () => null,
meta: {
label: 'Department',
variant: 'multi-select',
options: departments.map((department) => ({
label: toSentenceCase(department),
value: department,
})),
},
}),
columnHelper.accessor('joinDate', {
id: 'joinDate',
header: ({ column }) => (
<ColumnHeaderMenu column={column} title="Join Date" />
),
cell: (info) => formatDate(info.getValue<string>()),
aggregationFn: 'min',
aggregatedCell: ({ getValue }) => {
const earliest = getValue<string>()
return (
<Typography variant="body2" color="text.secondary">
Earliest: {earliest ? formatDate(earliest) : '—'}
</Typography>
)
},
meta: { label: 'Join Date', variant: 'date' },
}),
columnHelper.display({
id: 'actions',
enableHiding: false,
cell: ({ row }) => <RowActions person={row.original} />,
maxSize: 44,
enableResizing: false,
}),
]),
[],
)
const [columnOrder, setColumnOrder] = React.useState<Array<string>>(() =>
columns.map((column) => column.id ?? ''),
)
const table = useTable(
{
_features,
_rowModels: {
coreRowModel: createCoreRowModel(),
filteredRowModel: createFilteredRowModel({
...filterFns,
fuzzy: fuzzyFilter,
}),
facetedRowModel: createFacetedRowModel(),
facetedUniqueValues: createFacetedUniqueValues(),
paginatedRowModel: createPaginatedRowModel(),
sortedRowModel: createSortedRowModel(sortFns),
groupedRowModel: createGroupedRowModel(aggregationFns),
expandedRowModel: createExpandedRowModel(),
},
columns,
data,
defaultColumn: {
minSize: 60,
maxSize: 800,
filterFn: dynamicFilterFn,
},
globalFilterFn: 'fuzzy',
state: {
rowSelection,
sorting,
columnVisibility,
columnOrder,
columnSizing,
columnFilters,
globalFilter,
columnPinning,
grouping,
expanded,
},
onSortingChange: setSorting,
onColumnVisibilityChange: setColumnVisibility,
onColumnOrderChange: setColumnOrder,
onColumnSizingChange: setColumnSizing,
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
onColumnPinningChange: setColumnPinning,
onGroupingChange: setGrouping,
onExpandedChange: setExpanded,
getRowId: (row) => row.id,
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
columnResizeMode: 'onChange',
debugTable: true,
},
(state) => state, // default selector
)
const columnSizeVars = React.useMemo(() => {
const headers = table.getFlatHeaders()
const colSizes: Record<string, number> = {}
for (const header of headers) {
colSizes[`--header-${header.id}-size`] = header.getSize()
colSizes[`--col-${header.column.id}-size`] = header.column.getSize()
}
return colSizes
}, [table.store.state.columnSizing])
const refreshData = () => setData(makeData(1_000))
const stressTest = () => setData(makeData(200_000))
return (
<SortingContext.Provider value={sorting}>
<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 (200k rows)
</Button>
<Button
variant="outlined"
size="small"
onClick={() => rerender()}
>
Force Rerender
</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>
),
},
}}
/>
<FilterListPopover
table={table}
columnFilters={columnFilters}
onColumnFiltersChange={setColumnFilters}
/>
<SortListPopover
table={table}
sorting={sorting}
onSortingChange={setSorting}
/>
<ViewOptionsPopover
table={table}
columnOrder={columnOrder}
onColumnOrderChange={setColumnOrder}
/>
</Stack>
<Paper variant="outlined">
<TableContainer sx={{ maxHeight: 680 }}>
<MuiTable
stickyHeader
size="small"
sx={{
width: '100%',
tableLayout: 'fixed',
...columnSizeVars,
}}
>
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers
.filter((header) => header.column.getIsVisible())
.map((header) => (
<ResizableHeaderCell
key={header.id}
header={header}
table={table}
/>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
hover
selected={row.getIsSelected()}
aria-selected={row.getIsSelected()}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
align={
cell.column.id === 'select' ? 'center' : 'left'
}
sx={{
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
overflow: 'hidden',
borderRight:
cell.column.id === 'actions' ? undefined : 1,
borderColor: 'divider',
...getCommonPinningStyles(
cell.column,
row.getIsSelected(),
),
}}
>
{cell.getIsGrouped() ? (
<Button
size="small"
variant="text"
startIcon={
row.getIsExpanded() ? (
<ExpandLessIcon />
) : (
<ExpandMoreIcon />
)
}
onClick={row.getToggleExpandedHandler()}
disabled={!row.getCanExpand()}
sx={{ pl: row.depth * 2 + 1 }}
>
<table.FlexRender cell={cell} />
<Typography
component="span"
color="text.secondary"
sx={{ ml: 1 }}
>
({row.subRows.length})
</Typography>
</Button>
) : cell.column.id === 'progress' ? (
<Stack spacing={0.5}>
<Typography variant="body2">
{String(cell.getValue())}%
</Typography>
<LinearProgress
variant="determinate"
value={Number(cell.getValue())}
/>
</Stack>
) : (
<table.FlexRender cell={cell} />
)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</MuiTable>
</TableContainer>
<Pagination table={table} />
</Paper>
</Stack>
</Container>
</SortingContext.Provider>
)
}
function ResizableHeaderCell({
header,
table,
}: {
header: Header<typeof _features, Person>
table: {
FlexRender: React.ComponentType<{
header: Header<typeof _features, Person>
}>
}
}) {
const sorting = React.useContext(SortingContext)
const sortDirection = getSortDirection(sorting, header.column.id)
return (
<TableCell
colSpan={header.colSpan}
align={header.column.id === 'select' ? 'center' : 'left'}
sortDirection={sortDirection || false}
aria-sort={getAriaSort(sortDirection || false)}
data-sort={sortDirection}
sx={{
width: `calc(var(--header-${header.id}-size) * 1px)`,
borderRight: header.id === 'actions' ? undefined : 1,
borderColor: 'divider',
p: 1,
...getCommonPinningStyles(header.column),
}}
>
<Box
sx={{ position: 'relative', pr: header.column.getCanResize() ? 1 : 0 }}
>
{header.isPlaceholder ? null : <table.FlexRender header={header} />}
{header.column.getCanResize() ? (
<Box
onDoubleClick={() => header.column.resetSize()}
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
sx={{
position: 'absolute',
top: 0,
right: -6,
width: 6,
height: '100%',
cursor: 'col-resize',
touchAction: 'none',
bgcolor: header.column.getIsResizing()
? 'primary.main'
: 'transparent',
'&:hover': { bgcolor: 'primary.main' },
}}
/>
) : null}
</Box>
</TableCell>
)
}
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({
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>,
)