Skip to content

Commit

Permalink
Merge pull request #1485 from nickgros/PORTALS-2795
Browse files Browse the repository at this point in the history
  • Loading branch information
nickgros authored Jan 7, 2025
2 parents 99a8ac0 + b28f309 commit 30f191c
Show file tree
Hide file tree
Showing 10 changed files with 661 additions and 119 deletions.
3 changes: 2 additions & 1 deletion packages/synapse-react-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"@tanstack/query-core": "5.22.2",
"@tanstack/react-query": "5.22.2",
"@tanstack/react-query-devtools": "5.24.0",
"@tanstack/react-table": "^8.20.5",
"@tanstack/react-table": "^8.20.6",
"@tanstack/react-virtual": "^3.11.1",
"@upsetjs/react": "^1.11.0",
"animate.css": "^4.1.1",
"bootstrap": "^4.6.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,59 @@
import { flexRender, Row, Table } from '@tanstack/react-table'
import { useMemo } from 'react'
import { flexRender, Table } from '@tanstack/react-table'
import {
StyledTableContainer,
StyledTableContainerProps,
} from '../styled/StyledTableContainer'
import { MemoizedTableBody, TableBody } from './TableBody'
import { MemoizedTableBody, TableBody, TableBodyProps } from './TableBody'
import {
getColumnSizeCssVariable,
getHeaderSizeCssVariable,
} from './TanStackTableUtils'
import { StyledTanStackTableSlotProps, StyledTanStackTableSlots } from './types'

type StyledTanStackTableProps<T = unknown> = {
table: Table<T>
export type StyledTanStackTableProps<TData = unknown, TRowType = Row<TData>> = {
table: Table<TData>
styledTableContainerProps?: StyledTableContainerProps
fullWidth?: boolean
}
slots?: StyledTanStackTableSlots<TData, TRowType>
slotProps?: StyledTanStackTableSlotProps<TData, TRowType>
} & Pick<TableBodyProps<TData, TRowType>, 'rows' | 'rowTransform'>

/**
* Component that renders a styled table using @tanstack/react-table. Pass your table instance into the component and
* it will render the table with the appropriate headers and rows.
*/
export default function StyledTanStackTable<
TData = unknown,
TRowType = Row<TData>,
>(props: StyledTanStackTableProps<TData, TRowType>) {
const {
table,
styledTableContainerProps,
fullWidth = true,
slots = {},
slotProps = {},
} = props
const {
Thead = 'thead',
Th = 'th',
Table = 'table',
...tableBodySlots
} = slots
const {
Table: tableSlotProps = {},
Thead: theadSlotProps = {},
Th: thSlotProps = {},
...tableBodySlotProps
} = slotProps

export default function StyledTanStackTable<T = unknown>(
props: StyledTanStackTableProps<T>,
) {
const { table, styledTableContainerProps, fullWidth = true } = props
const tableBodyProps: TableBodyProps<TData, TRowType> = {
table,
slots: tableBodySlots,
slotProps: tableBodySlotProps,
rows: props.rows!,
rowTransform: props.rowTransform!,
}

/**
* Instead of calling `column.getSize()` on every render for every header
Expand Down Expand Up @@ -51,19 +85,28 @@ export default function StyledTanStackTable<T = unknown>(

return (
<StyledTableContainer {...styledTableContainerProps}>
<table style={{ ...columnSizeVars, width: tableWidth }}>
<thead>
<Table
{...tableSlotProps}
style={{
...columnSizeVars,
width: tableWidth,
...tableSlotProps['style'],
}}
>
<Thead {...theadSlotProps}>
{table.getHeaderGroups().map(headerGroup => {
return (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
<Th
key={header.id}
colSpan={header.colSpan}
{...thSlotProps}
style={{
width: `calc(var(${getHeaderSizeCssVariable(
header.id,
)}) * 1px)`,
...thSlotProps['style'],
}}
>
{header.isPlaceholder
Expand All @@ -81,14 +124,14 @@ export default function StyledTanStackTable<T = unknown>(
onTouchStart={header.getResizeHandler()}
/>
)}
</th>
</Th>
))}
</tr>
)
})}
</thead>
<TableBodyElement table={table} />
</table>
</Thead>
<TableBodyElement<TData, TRowType> {...tableBodyProps} />
</Table>
</StyledTableContainer>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { VirtualItem, Virtualizer } from '@tanstack/react-virtual'
import { noop } from 'lodash-es'
import React, { useContext } from 'react'
import { mergeSlotProps } from '../../utils/slots/SlotUtils'
import StyledTanStackTable, {
StyledTanStackTableProps,
} from './StyledTanStackTable'

import { TrProps } from './types'

type StyledVirtualTanStackTableProps<T = unknown> = Omit<
StyledTanStackTableProps<T, VirtualItem>,
'slots' | 'rows' | 'rowTransform'
> & {
rowVirtualizer: Virtualizer<any, any>
onTableContainerScroll?: (target: EventTarget) => void
}

// Context to pass the row virtualizer to the Tr component
const RowVirtualizerContext = React.createContext<
Virtualizer<any, any> | undefined
>(undefined)

/**
* A table row component modified to be used with @tanstack/react-virtual.
*/
export function VirtualizedTr<TData = unknown>(
props: TrProps<TData, VirtualItem>,
) {
const { row: virtualItem, tableRow: _tableRow, ...rest } = props
const rowVirtualizer = useContext(RowVirtualizerContext)
return (
<tr
data-index={virtualItem.index} // needed for dynamic row height measurement
ref={node => rowVirtualizer?.measureElement(node)} //measure dynamic row height
{...rest}
style={{
...rest.style,
display: 'flex',
position: 'absolute',
transform: `translateY(${virtualItem.start}px)`, //this should always be a `style` as it changes on scroll
width: '100%',
}}
/>
)
}

/**
* Stylized table component that is compatible with @tanstack/react-table and @tanstack/react-virtual to display a
* virtualized table, i.e. a table that uses a virtualizer to only render the DOM nodes that contain data that is visible
* within the viewport. For cases where the table rows do not need to be virtualized, use {@link StyledTanStackTable}.
*/
export default function StyledVirtualTanStackTable<T = unknown>(
props: StyledVirtualTanStackTableProps<T>,
) {
const {
table,
styledTableContainerProps,
fullWidth = true,
rowVirtualizer,
slotProps = {},
onTableContainerScroll = noop,
} = props

const virtualRows = rowVirtualizer.getVirtualItems()

return (
<RowVirtualizerContext.Provider value={rowVirtualizer}>
<StyledTanStackTable<T, VirtualItem>
table={table}
rows={virtualRows}
rowTransform={row => table.getRowModel().rows[row.index]}
fullWidth={fullWidth}
styledTableContainerProps={{
...styledTableContainerProps,
style: {
overflow: 'auto', //our scrollable table container
position: 'relative', //needed for sticky header
...styledTableContainerProps?.style,
},
sx: {
'thead > tr': {
display: 'flex',
width: '100%',
},
},
onScroll: e => onTableContainerScroll(e.target),
}}
{...styledTableContainerProps}
slots={{
Tr: VirtualizedTr,
}}
slotProps={mergeSlotProps(slotProps, {
Table: {
style: {
display: 'grid',
},
},
Thead: {
style: {
display: 'grid',
position: 'sticky',
top: 0,
zIndex: 1,
},
},
Th: {
style: {
display: 'flex',
},
},
Tbody: {
style: {
display: 'grid',
height: `${rowVirtualizer.getTotalSize()}px`, //tells scrollbar how big the table is
position: 'relative', //needed for absolute positioning of rows
},
},
})}
/>
</RowVirtualizerContext.Provider>
)
}
Original file line number Diff line number Diff line change
@@ -1,52 +1,74 @@
import { styled } from '@mui/material'
import { Row, Table } from '@tanstack/react-table'
import { identity } from 'lodash-es'
import { memo } from 'react'
import { Cell, flexRender, Table } from '@tanstack/react-table'
import { getColumnSizeCssVariable } from './TanStackTableUtils'
import ExpandableTableDataCell from '../SynapseTable/ExpandableTableDataCell'
import { Skeleton } from '@mui/material'

function CellRenderer<T = unknown>(cell: Cell<T, unknown>) {
const getWrapInExpandableTd =
cell.getContext().table.options.meta?.getWrapInExpandableTd
const wrapInExpandableTd =
getWrapInExpandableTd && getWrapInExpandableTd(cell)
const TableDataCellElement = wrapInExpandableTd
? ExpandableTableDataCell
: 'td'

const renderPlaceholderData =
cell.getContext().table.options.meta?.renderPlaceholderData
import { getSlotProps } from '../../utils/slots/SlotUtils'
import { TableCellRenderer as DefaultTableCellRenderer } from './TableCellRenderer'
import {
TableBodyPropsRowOverride,
TableBodySlotProps,
TableBodySlots,
TrOwnerState,
} from './types'

return (
<TableDataCellElement
key={cell.id}
style={{
width: `calc(var(${getColumnSizeCssVariable(cell.column.id)}) * 1px)`,
textAlign: cell.column.columnDef.meta?.textAlign,
}}
>
{renderPlaceholderData ? (
<p>
<Skeleton width={'80%'} height={'20px'} />
</p>
) : (
flexRender(cell.column.columnDef.cell, cell.getContext())
)}
</TableDataCellElement>
)
}
// Simple wrapper around 'tr' to prevent forwarding invalid HTML attributes
const TableRow = styled('tr', {
shouldForwardProp: prop => prop !== 'row' && prop !== 'tableRow',
})({})

type TableBodyProps<T = unknown> = {
table: Table<T>
}
export type TableBodyProps<TData = unknown, TRowType = Row<TData>> = {
/** The table instance */
table: Table<TData>
slots?: TableBodySlots<TData, TRowType>
slotProps?: TableBodySlotProps<TData, TRowType>
} & TableBodyPropsRowOverride<TData, TRowType>

/**
* A table body component for use with @tanstack/react-table. This component renders the rows of the table.
* @param props
* @constructor
*/
export function TableBody<TData = unknown, TRowType = Row<TData>>(
props: TableBodyProps<TData, TRowType>,
) {
const { table, slots = {}, slotProps = {} } = props

// By default, use TanStack Table Rows and the identity function as a transform.
// This can be overridden e.g. to accomplish row virtualization.
const {
rows = table.getRowModel().rows as TRowType[],
rowTransform = identity,
} = props

const {
Tbody = 'tbody',
Tr = TableRow,
TableCellRenderer = DefaultTableCellRenderer<TData>,
} = slots
const { Tbody: tbodySlotProps = {}, Tr: _trSlotProps = {} } = slotProps

export function TableBody<T = unknown>(props: TableBodyProps<T>) {
const { table } = props
return (
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id}>{row.getVisibleCells().map(CellRenderer)}</tr>
))}
</tbody>
<Tbody {...tbodySlotProps}>
{rows.map((row, index) => {
const tableRow: Row<TData> | undefined = rowTransform(row)

const trOwnerState: TrOwnerState<TData, TRowType> = { row, tableRow }
const trSlotProps = getSlotProps(_trSlotProps, trOwnerState)

return (
<Tr
key={tableRow?.id ?? index}
{...trSlotProps}
row={row}
tableRow={tableRow}
>
{tableRow?.getVisibleCells().map((props, index) => (
<TableCellRenderer key={index} {...props} />
))}
</Tr>
)
})}
</Tbody>
)
}

Expand Down
Loading

0 comments on commit 30f191c

Please sign in to comment.