[Ooditable] Dom optimizations and cleanup

This commit is contained in:
joonas-a
2025-10-21 16:29:54 +03:00
parent 0bb2da9137
commit 9f60d026ad
10 changed files with 125 additions and 183 deletions
@@ -5,7 +5,6 @@ import TableContainer from '@mui/material/TableContainer'
import TableHead from '@mui/material/TableHead'
import { type Table as TableType } from '@tanstack/react-table'
import { OodiTableAggregationRow } from './components/AggregationRow'
import { OodiTableDataRow } from './components/Cell'
import { OodiTableHeaderGroup } from './components/Header'
import { OodiTablePagination } from './components/Pagination'
@@ -57,8 +56,8 @@ export const OodiTableContainer = <OTData,>({
},
}}
>
{table.getAggregationRowModel().rows.map(OodiTableAggregationRow)}
{table.getRowModel().rows.map(OodiTableDataRow)}
{table.getAggregationRowModel().rows.map(row => OodiTableDataRow(row, true))}
{table.getRowModel().rows.map(row => OodiTableDataRow(row))}
</TableBody>
</Table>
</TableContainer>
@@ -1,57 +0,0 @@
import TableCell, { type TableCellProps } from '@mui/material/TableCell'
import TableRow from '@mui/material/TableRow'
import Typography from '@mui/material/Typography'
import { flexRender } from '@tanstack/react-table'
import type { FC } from 'react'
import { AggregationRow } from '../features/aggregationRows'
import { getCommonPinningStyles } from '../styles'
const OodiTableAggregationCell: FC<TableCellProps> = ({ children, ...props }) => {
return (
<TableCell
{...props}
sx={{
borderWidth: '0 1px 1px 0',
borderStyle: 'solid',
borderColor: 'grey.300',
padding: '0 1em',
height: '3em',
...props?.sx,
}}
>
<Typography
sx={{
height: '100%',
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
whiteSpace: 'nowrap',
}}
>
{children}
</Typography>
</TableCell>
)
}
export const OodiTableAggregationRow = <OTData,>(row: AggregationRow<OTData>) => (
<TableRow key={row.id}>
{row.getVisibleCells().map(cell => {
const { maxSize } = cell.column.columnDef
return (
<OodiTableAggregationCell
key={cell.id}
sx={{
...getCommonPinningStyles(cell.column),
maxWidth: maxSize !== Number.MAX_SAFE_INTEGER ? maxSize : '20em',
}}
>
{flexRender(cell.column.columnDef.aggregatedCell, cell.getContext())}
</OodiTableAggregationCell>
)
})}
</TableRow>
)
@@ -1,54 +1,27 @@
import Box from '@mui/material/Box'
import TableCell, { type TableCellProps } from '@mui/material/TableCell'
import './styles.css'
import TableCell from '@mui/material/TableCell'
import TableRow from '@mui/material/TableRow'
import { flexRender } from '@tanstack/react-table'
import type { Row } from '@tanstack/react-table'
import type { FC } from 'react'
import { getCommonPinningStyles } from '../styles'
import { flexRender } from './util'
const OodiTableCell: FC<TableCellProps> = ({ children, ...props }) => {
return (
<TableCell
{...props}
sx={{
borderWidth: '0 1px 1px 0',
borderStyle: 'solid',
borderColor: 'grey.300',
padding: '0 1em',
height: '3em',
...props?.sx,
}}
>
<Box
sx={{
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
whiteSpace: 'nowrap',
}}
>
{children}
</Box>
</TableCell>
)
}
export const OodiTableDataRow = <OTData,>(row: Row<OTData>) => (
export const OodiTableDataRow = <OTData,>(row: Row<OTData>, aggregate = false) => (
<TableRow key={row.id}>
{row.getVisibleCells().map(cell => {
const { maxSize } = cell.column.columnDef
return (
<OodiTableCell
<TableCell
className="ot-data-cell"
key={cell.id}
sx={{
...getCommonPinningStyles(cell.column),
maxWidth: maxSize !== Number.MAX_SAFE_INTEGER ? maxSize : '20em',
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</OodiTableCell>
{flexRender(aggregate ? cell.column.columnDef.aggregatedCell : cell.column.columnDef.cell, cell.getContext())}
</TableCell>
)
})}
</TableRow>
@@ -1,33 +1,33 @@
import Box from '@mui/material/Box'
import { SxProps, Theme } from '@mui/material/styles'
import TableCell, { type TableCellProps } from '@mui/material/TableCell'
import { styled, SxProps, Theme } from '@mui/material/styles'
import TableCell from '@mui/material/TableCell'
import TableRow from '@mui/material/TableRow'
import { flexRender } from '@tanstack/react-table'
import type { HeaderGroup } from '@tanstack/react-table'
import type { FC, ReactNode } from 'react'
import { getCommonPinningStyles, verticalStyles } from '../styles'
import { OodiTableSortIcons } from './SortIcons'
import { flexRender } from './util'
const OodiTableHeader: FC<TableCellProps & { children?: ReactNode }> = ({ children, ...props }) => {
return (
<TableCell
{...props}
sx={{
position: 'relative',
padding: '1.5em',
borderWidth: '0 1px 1px 0',
borderStyle: 'solid',
borderColor: 'grey.300',
fontWeight: 'bold',
backgroundColor: 'white',
...props.sx,
}}
>
{children}
</TableCell>
)
}
const OtHeaderCell = styled(TableCell)(({ theme }) => ({
position: 'relative',
padding: '1.5em',
borderWidth: '0 1px 1px 0',
borderStyle: 'solid',
borderColor: theme.palette.grey[300],
fontWeight: 'bold',
backgroundColor: theme.palette.common.white,
}))
const OtVerticalContentWrapper = styled('div')({
writingMode: 'vertical-rl',
textOrientation: 'mixed',
whiteSpace: 'normal',
overflow: 'visible',
height: '100%',
maxWidth: '15em',
minHeight: '160px',
maxHeight: '240px',
margin: '0 auto',
})
export const OodiTableHeaderGroup = <OTData,>(headerGroup: HeaderGroup<OTData>, verticalHeaders: string[]) => (
<TableRow key={headerGroup.id}>
@@ -41,15 +41,16 @@ export const OodiTableHeaderGroup = <OTData,>(headerGroup: HeaderGroup<OTData>,
}
const isVertical = verticalHeaders.includes(header.id)
const canSort = header.column.getCanSort()
const sx = {
cursor: header.column.getCanSort() ? 'pointer' : 'inherit',
cursor: canSort ? 'pointer' : 'inherit',
...(isVertical && verticalStyles),
...getCommonPinningStyles(header.column),
}
return (
<OodiTableHeader
<OtHeaderCell
colSpan={header.colSpan}
key={header.id}
onClick={header.column.getToggleSortingHandler()}
@@ -57,33 +58,14 @@ export const OodiTableHeaderGroup = <OTData,>(headerGroup: HeaderGroup<OTData>,
sx={sx as SxProps<Theme>}
>
{isVertical ? (
<Box
sx={{
display: 'inline-flex',
width: '100%',
}}
>
<Box
sx={{
writingMode: 'vertical-rl',
textOrientation: 'mixed',
whiteSpace: 'normal',
overflow: 'visible',
height: '100%',
maxWidth: '15em',
minHeight: '160px',
maxHeight: '240px',
margin: '0 auto',
}}
>
{flexRender(header.column.columnDef.header, header.getContext())}
</Box>
</Box>
<OtVerticalContentWrapper>
{flexRender(header.column.columnDef.header, header.getContext())}
</OtVerticalContentWrapper>
) : (
<Box>{flexRender(header.column.columnDef.header, header.getContext())}</Box>
<>{flexRender(header.column.columnDef.header, header.getContext())}</>
)}
<OodiTableSortIcons canSort={header.column.getCanSort()} isSorted={header.column.getIsSorted()} />
</OodiTableHeader>
<OodiTableSortIcons canSort={canSort} isSorted={header.column.getIsSorted()} />
</OtHeaderCell>
)
})}
</TableRow>
@@ -1,33 +1,43 @@
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'
import SwapVertIcon from '@mui/icons-material/SwapVert'
import Box from '@mui/material/Box'
import styled from '@mui/material/styles/styled'
import type { SortDirection } from '@tanstack/react-table'
export const OodiTableSortIcons = ({ canSort, isSorted }: { canSort: boolean; isSorted: SortDirection | false }) => (
<Box
sx={{
position: 'absolute',
right: 0,
bottom: 0,
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'flex-end',
height: 'fit-content',
width: 'fit-content',
const OtSortIconWrapper = styled('div')(() => ({
position: 'absolute',
right: '0',
bottom: '0',
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'flex-end',
height: 'fit-content',
width: 'fit-content',
}))
'& > svg': {
width: '1em',
height: '1em',
},
}}
>
{(canSort &&
{
asc: <ArrowUpwardIcon fontSize="small" sx={{ color: theme => theme.palette.primary.main }} />,
desc: <ArrowDownwardIcon fontSize="small" sx={{ color: theme => theme.palette.primary.main }} />,
false: <SwapVertIcon fontSize="small" sx={{ color: 'grey.300' }} />,
}[isSorted as string]) ??
null}
</Box>
const OtSortAscIcon = styled(ArrowUpwardIcon)(({ theme }) => ({
fontSize: '1.25rem',
color: theme.palette.primary.main,
}))
const OtSortDescIcon = styled(ArrowDownwardIcon)(({ theme }) => ({
fontSize: '1.25rem',
color: theme.palette.primary.main,
}))
const OtSortUnsetIcon = styled(SwapVertIcon)(({ theme }) => ({
fontSize: '1.25rem',
color: theme.palette.grey[300],
}))
export const OodiTableSortIcons = ({ canSort, isSorted }: { canSort: boolean; isSorted: SortDirection | false }) => (
<OtSortIconWrapper>
{canSort
? {
asc: <OtSortAscIcon />,
desc: <OtSortDescIcon />,
false: <OtSortUnsetIcon />,
}[isSorted as string]
: null}
</OtSortIconWrapper>
)
@@ -0,0 +1,30 @@
/*
Using css classes is faster than Mui style/sx/styled components
and in large tables it can add up especially without virtualization
*/
.ot-data-cell.MuiTableCell-root {
border-width: 0 1px 1px 0;
border-style: solid;
border-color: #e0e0e0; /* grey.300 */
padding: 0 1em;
height: 3em;
white-space: nowrap;
}
.ot-data-cell > div:not(.MuiStack-root, .MuiBox-root) {
display: grid;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
/* Zebra stripe layout for the whole table */
.ot-table-body > tr:nth-of-type(odd) > td {
background-color: #f5f5f5; /* grey.100 */
}
.ot-table-body > tr:nth-of-type(even) > td {
background-color: white;
}
@@ -0,0 +1,9 @@
import { ReactNode } from 'react'
export const flexRender = <TProps extends object>(
Comp: ((props: TProps) => ReactNode) | string | undefined,
props: TProps
): ReactNode => {
if (typeof Comp === 'string') return Comp
return Comp ? Comp(props) : null
}
@@ -27,6 +27,9 @@ declare module '@tanstack/react-table' {
}
interface Table<TData> {
/**
* Aggregation rows are not included in pagination/sorting.
*/
getAggregationRowModel: () => AggregationRowModel<TData>
getAllAggregationRowIds: () => string[]
hasAggregationRows: () => boolean
@@ -22,8 +22,5 @@ export const getCommonPinningStyles = <TData>(column: Column<TData>): SxProps<Th
export const verticalStyles: SxProps<Theme> = {
overflow: 'visible',
borderWidth: '1px 1px 1px 0',
borderStyle: 'solid',
borderColor: 'grey.300',
padding: '0.5em',
padding: '1em 0.5em',
}
@@ -162,14 +162,13 @@ export const useGetColumnDefinitions = (modules: Map<string, CourseTabModule>):
substitutePrefix +
`Grade: ${correctCourse.grade}\nCompleted on: ${formatDate(correctCourse.completionDate, dateFormat)}`
return (
<div style={{ display: 'flex', justifyContent: 'center', margin: 'auto' }} title={title}>
<div title={title}>
<CheckIcon sx={{ color: sub ? theme.palette.ooditable.hops : theme.palette.ooditable.success }} />
</div>
)
} else if (correctCourse.enrollmentDate) {
return (
<div
style={{ display: 'flex', justifyContent: 'center', margin: 'auto' }}
title={
substitutePrefix + `Last enrollment ${formatDate(correctCourse.enrollmentDate, dateFormat)}`
}
@@ -185,10 +184,7 @@ export const useGetColumnDefinitions = (modules: Map<string, CourseTabModule>):
)
} else if (correctCourse.inHops) {
return (
<div
style={{ display: 'flex', justifyContent: 'center', margin: 'auto' }}
title={substitutePrefix + 'In primary studyplan'}
>
<div title={substitutePrefix + 'In primary studyplan'}>
<CropSquareIcon sx={{ color: theme.palette.ooditable.hops }} />
</div>
)