[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 TableHead from '@mui/material/TableHead'
import { type Table as TableType } from '@tanstack/react-table' import { type Table as TableType } from '@tanstack/react-table'
import { OodiTableAggregationRow } from './components/AggregationRow'
import { OodiTableDataRow } from './components/Cell' import { OodiTableDataRow } from './components/Cell'
import { OodiTableHeaderGroup } from './components/Header' import { OodiTableHeaderGroup } from './components/Header'
import { OodiTablePagination } from './components/Pagination' import { OodiTablePagination } from './components/Pagination'
@@ -57,8 +56,8 @@ export const OodiTableContainer = <OTData,>({
}, },
}} }}
> >
{table.getAggregationRowModel().rows.map(OodiTableAggregationRow)} {table.getAggregationRowModel().rows.map(row => OodiTableDataRow(row, true))}
{table.getRowModel().rows.map(OodiTableDataRow)} {table.getRowModel().rows.map(row => OodiTableDataRow(row))}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </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 './styles.css'
import TableCell, { type TableCellProps } from '@mui/material/TableCell'
import TableCell from '@mui/material/TableCell'
import TableRow from '@mui/material/TableRow' import TableRow from '@mui/material/TableRow'
import { flexRender } from '@tanstack/react-table'
import type { Row } from '@tanstack/react-table' import type { Row } from '@tanstack/react-table'
import type { FC } from 'react'
import { getCommonPinningStyles } from '../styles' import { getCommonPinningStyles } from '../styles'
import { flexRender } from './util'
const OodiTableCell: FC<TableCellProps> = ({ children, ...props }) => { export const OodiTableDataRow = <OTData,>(row: Row<OTData>, aggregate = false) => (
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>) => (
<TableRow key={row.id}> <TableRow key={row.id}>
{row.getVisibleCells().map(cell => { {row.getVisibleCells().map(cell => {
const { maxSize } = cell.column.columnDef const { maxSize } = cell.column.columnDef
return ( return (
<OodiTableCell <TableCell
className="ot-data-cell"
key={cell.id} key={cell.id}
sx={{ sx={{
...getCommonPinningStyles(cell.column), ...getCommonPinningStyles(cell.column),
maxWidth: maxSize !== Number.MAX_SAFE_INTEGER ? maxSize : '20em', maxWidth: maxSize !== Number.MAX_SAFE_INTEGER ? maxSize : '20em',
}} }}
> >
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(aggregate ? cell.column.columnDef.aggregatedCell : cell.column.columnDef.cell, cell.getContext())}
</OodiTableCell> </TableCell>
) )
})} })}
</TableRow> </TableRow>
@@ -1,33 +1,33 @@
import Box from '@mui/material/Box' import { styled, SxProps, Theme } from '@mui/material/styles'
import { SxProps, Theme } from '@mui/material/styles' import TableCell from '@mui/material/TableCell'
import TableCell, { type TableCellProps } from '@mui/material/TableCell'
import TableRow from '@mui/material/TableRow' import TableRow from '@mui/material/TableRow'
import { flexRender } from '@tanstack/react-table'
import type { HeaderGroup } from '@tanstack/react-table' import type { HeaderGroup } from '@tanstack/react-table'
import type { FC, ReactNode } from 'react'
import { getCommonPinningStyles, verticalStyles } from '../styles' import { getCommonPinningStyles, verticalStyles } from '../styles'
import { OodiTableSortIcons } from './SortIcons' import { OodiTableSortIcons } from './SortIcons'
import { flexRender } from './util'
const OodiTableHeader: FC<TableCellProps & { children?: ReactNode }> = ({ children, ...props }) => { const OtHeaderCell = styled(TableCell)(({ theme }) => ({
return ( position: 'relative',
<TableCell padding: '1.5em',
{...props} borderWidth: '0 1px 1px 0',
sx={{ borderStyle: 'solid',
position: 'relative', borderColor: theme.palette.grey[300],
padding: '1.5em', fontWeight: 'bold',
borderWidth: '0 1px 1px 0', backgroundColor: theme.palette.common.white,
borderStyle: 'solid', }))
borderColor: 'grey.300',
fontWeight: 'bold', const OtVerticalContentWrapper = styled('div')({
backgroundColor: 'white', writingMode: 'vertical-rl',
...props.sx, textOrientation: 'mixed',
}} whiteSpace: 'normal',
> overflow: 'visible',
{children} height: '100%',
</TableCell> maxWidth: '15em',
) minHeight: '160px',
} maxHeight: '240px',
margin: '0 auto',
})
export const OodiTableHeaderGroup = <OTData,>(headerGroup: HeaderGroup<OTData>, verticalHeaders: string[]) => ( export const OodiTableHeaderGroup = <OTData,>(headerGroup: HeaderGroup<OTData>, verticalHeaders: string[]) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
@@ -41,15 +41,16 @@ export const OodiTableHeaderGroup = <OTData,>(headerGroup: HeaderGroup<OTData>,
} }
const isVertical = verticalHeaders.includes(header.id) const isVertical = verticalHeaders.includes(header.id)
const canSort = header.column.getCanSort()
const sx = { const sx = {
cursor: header.column.getCanSort() ? 'pointer' : 'inherit', cursor: canSort ? 'pointer' : 'inherit',
...(isVertical && verticalStyles), ...(isVertical && verticalStyles),
...getCommonPinningStyles(header.column), ...getCommonPinningStyles(header.column),
} }
return ( return (
<OodiTableHeader <OtHeaderCell
colSpan={header.colSpan} colSpan={header.colSpan}
key={header.id} key={header.id}
onClick={header.column.getToggleSortingHandler()} onClick={header.column.getToggleSortingHandler()}
@@ -57,33 +58,14 @@ export const OodiTableHeaderGroup = <OTData,>(headerGroup: HeaderGroup<OTData>,
sx={sx as SxProps<Theme>} sx={sx as SxProps<Theme>}
> >
{isVertical ? ( {isVertical ? (
<Box <OtVerticalContentWrapper>
sx={{ {flexRender(header.column.columnDef.header, header.getContext())}
display: 'inline-flex', </OtVerticalContentWrapper>
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>
) : ( ) : (
<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()} /> <OodiTableSortIcons canSort={canSort} isSorted={header.column.getIsSorted()} />
</OodiTableHeader> </OtHeaderCell>
) )
})} })}
</TableRow> </TableRow>
@@ -1,33 +1,43 @@
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward' import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward' import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'
import SwapVertIcon from '@mui/icons-material/SwapVert' 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' import type { SortDirection } from '@tanstack/react-table'
export const OodiTableSortIcons = ({ canSort, isSorted }: { canSort: boolean; isSorted: SortDirection | false }) => ( const OtSortIconWrapper = styled('div')(() => ({
<Box position: 'absolute',
sx={{ right: '0',
position: 'absolute', bottom: '0',
right: 0, display: 'flex',
bottom: 0, alignItems: 'flex-end',
display: 'flex', justifyContent: 'flex-end',
alignItems: 'flex-end', height: 'fit-content',
justifyContent: 'flex-end', width: 'fit-content',
height: 'fit-content', }))
width: 'fit-content',
'& > svg': { const OtSortAscIcon = styled(ArrowUpwardIcon)(({ theme }) => ({
width: '1em', fontSize: '1.25rem',
height: '1em', color: theme.palette.primary.main,
}, }))
}}
> const OtSortDescIcon = styled(ArrowDownwardIcon)(({ theme }) => ({
{(canSort && fontSize: '1.25rem',
{ color: theme.palette.primary.main,
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' }} />, const OtSortUnsetIcon = styled(SwapVertIcon)(({ theme }) => ({
}[isSorted as string]) ?? fontSize: '1.25rem',
null} color: theme.palette.grey[300],
</Box> }))
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> { interface Table<TData> {
/**
* Aggregation rows are not included in pagination/sorting.
*/
getAggregationRowModel: () => AggregationRowModel<TData> getAggregationRowModel: () => AggregationRowModel<TData>
getAllAggregationRowIds: () => string[] getAllAggregationRowIds: () => string[]
hasAggregationRows: () => boolean hasAggregationRows: () => boolean
@@ -22,8 +22,5 @@ export const getCommonPinningStyles = <TData>(column: Column<TData>): SxProps<Th
export const verticalStyles: SxProps<Theme> = { export const verticalStyles: SxProps<Theme> = {
overflow: 'visible', overflow: 'visible',
borderWidth: '1px 1px 1px 0', padding: '1em 0.5em',
borderStyle: 'solid',
borderColor: 'grey.300',
padding: '0.5em',
} }
@@ -162,14 +162,13 @@ export const useGetColumnDefinitions = (modules: Map<string, CourseTabModule>):
substitutePrefix + substitutePrefix +
`Grade: ${correctCourse.grade}\nCompleted on: ${formatDate(correctCourse.completionDate, dateFormat)}` `Grade: ${correctCourse.grade}\nCompleted on: ${formatDate(correctCourse.completionDate, dateFormat)}`
return ( 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 }} /> <CheckIcon sx={{ color: sub ? theme.palette.ooditable.hops : theme.palette.ooditable.success }} />
</div> </div>
) )
} else if (correctCourse.enrollmentDate) { } else if (correctCourse.enrollmentDate) {
return ( return (
<div <div
style={{ display: 'flex', justifyContent: 'center', margin: 'auto' }}
title={ title={
substitutePrefix + `Last enrollment ${formatDate(correctCourse.enrollmentDate, dateFormat)}` substitutePrefix + `Last enrollment ${formatDate(correctCourse.enrollmentDate, dateFormat)}`
} }
@@ -185,10 +184,7 @@ export const useGetColumnDefinitions = (modules: Map<string, CourseTabModule>):
) )
} else if (correctCourse.inHops) { } else if (correctCourse.inHops) {
return ( return (
<div <div title={substitutePrefix + 'In primary studyplan'}>
style={{ display: 'flex', justifyContent: 'center', margin: 'auto' }}
title={substitutePrefix + 'In primary studyplan'}
>
<CropSquareIcon sx={{ color: theme.palette.ooditable.hops }} /> <CropSquareIcon sx={{ color: theme.palette.ooditable.hops }} />
</div> </div>
) )