| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- import type { VirtualItem, Virtualizer } from '@tanstack/react-virtual'
- import { useVirtualizer } from '@tanstack/react-virtual'
- import { mergeRefs } from 'common'
- import type { HTMLAttributes, ReactElement, ReactNode, Ref } from 'react'
- import {
- cloneElement,
- createContext,
- forwardRef,
- isValidElement,
- useCallback,
- useContext,
- useMemo,
- useRef,
- } from 'react'
- import {
- cn,
- Table,
- TableBody,
- TableCaption,
- TableCell,
- TableFooter,
- TableHead,
- TableHeader,
- TableRow,
- } from 'ui'
- type TableComponentProps = React.ComponentProps<typeof Table>
- interface VirtualizedTableProps<TItem> extends TableComponentProps {
- scrollContainerProps?: HTMLAttributes<HTMLDivElement>
- scrollContainerRef: React.Ref<HTMLDivElement>
- data: TItem[]
- children: ReactNode
- overscan?: number
- estimateSize: (index: number) => number
- getItemKey?: (item: TItem, index: number) => string
- }
- type VirtualizedTableContextValue<TItem> = {
- virtualizer: Virtualizer<HTMLDivElement, HTMLTableRowElement>
- virtualItems: VirtualItem[]
- data: TItem[]
- paddingTop: number
- paddingBottom: number
- getRowKey: (item: TItem, index: number) => string | number
- }
- const VirtualizedTableContext = createContext<VirtualizedTableContextValue<unknown> | null>(null)
- const useVirtualizedTableContext = <TItem,>() => {
- const context = useContext(VirtualizedTableContext)
- if (!context) {
- throw new Error('VirtualizedTable components must be used within a VirtualizedTable')
- }
- return context as VirtualizedTableContextValue<TItem>
- }
- export const VirtualizedTable = <TItem,>({
- scrollContainerProps,
- scrollContainerRef: externalScrollContainerRef,
- containerProps,
- data,
- children,
- overscan = 5,
- estimateSize,
- getItemKey,
- ...tableProps
- }: VirtualizedTableProps<TItem>) => {
- const scrollContainerRef = useRef<HTMLDivElement>(null)
- const scrollContainerMergedRef = mergeRefs(scrollContainerRef, externalScrollContainerRef)
- const rowKeyGetter = useCallback(
- (item: TItem, index: number) => {
- return getItemKey ? getItemKey(item, index) : index
- },
- [getItemKey]
- )
- const getItemKeyFromIndex = useCallback(
- (index: number) => {
- const item = data[index]
- return item ? rowKeyGetter(item, index) : index
- },
- [data, rowKeyGetter]
- )
- const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
- count: data.length,
- getScrollElement: () => scrollContainerRef.current,
- overscan,
- estimateSize,
- getItemKey: getItemKeyFromIndex,
- })
- const virtualItems = virtualizer.getVirtualItems()
- const totalSize = virtualizer.getTotalSize()
- const paddingTop = virtualItems.length > 0 ? virtualItems[0].start : 0
- const paddingBottom =
- virtualItems.length > 0 ? totalSize - virtualItems[virtualItems.length - 1].end : 0
- const contextValue = useMemo<VirtualizedTableContextValue<TItem>>(
- () => ({
- virtualizer,
- virtualItems,
- data,
- paddingTop,
- paddingBottom,
- getRowKey: rowKeyGetter,
- }),
- [virtualizer, virtualItems, data, paddingTop, paddingBottom, rowKeyGetter]
- )
- const mergedContainerProps = useMemo(
- () => ({
- ...containerProps,
- className: cn('overflow-visible', containerProps?.className),
- }),
- [containerProps]
- )
- const { className: scrollClassName, ...restScrollContainerProps } = scrollContainerProps ?? {}
- return (
- <div
- ref={scrollContainerMergedRef}
- className={cn('h-full overflow-auto', scrollClassName)}
- {...restScrollContainerProps}
- >
- <VirtualizedTableContext.Provider
- value={contextValue as VirtualizedTableContextValue<unknown>}
- >
- <Table containerProps={mergedContainerProps} {...tableProps}>
- {children}
- </Table>
- </VirtualizedTableContext.Provider>
- </div>
- )
- }
- interface VirtualizedTableBodyProps<TItem> extends Omit<
- React.ComponentProps<typeof TableBody>,
- 'children'
- > {
- emptyContent?: ReactNode
- leadingContent?: ReactNode
- trailingContent?: ReactNode
- children: (item: TItem, index: number) => ReactElement
- paddingColSpan?: number
- paddingCellClassName?: string
- }
- export const VirtualizedTableBody = <TItem,>({
- emptyContent,
- leadingContent,
- trailingContent,
- children,
- paddingColSpan = 1,
- paddingCellClassName,
- ...props
- }: VirtualizedTableBodyProps<TItem>) => {
- const { virtualizer, virtualItems, data, paddingTop, paddingBottom, getRowKey } =
- useVirtualizedTableContext<TItem>()
- const measurementRef = virtualizer.measureElement as unknown as Ref<HTMLTableRowElement>
- return (
- <TableBody {...props}>
- {leadingContent}
- {data.length === 0 ? (
- (emptyContent ?? null)
- ) : (
- <>
- {paddingTop > 0 && (
- <TableRow aria-hidden="true" style={{ height: paddingTop }}>
- <VirtualizedTableCell
- colSpan={paddingColSpan}
- className={cn('p-0', paddingCellClassName)}
- />
- </TableRow>
- )}
- {virtualItems.map((virtualItem) => {
- const item = data[virtualItem.index]
- if (item === undefined) return null
- const renderedRow = children(item, virtualItem.index)
- if (
- !isValidElement<
- Record<string, unknown> & {
- ref?: Ref<HTMLTableRowElement> | null
- ['data-index']?: number
- }
- >(renderedRow)
- ) {
- return renderedRow
- }
- const key = renderedRow.key ?? getRowKey(item, virtualItem.index)
- const existingRef = (
- renderedRow as unknown as { ref?: Ref<HTMLTableRowElement> | null }
- ).ref
- const combinedRef =
- existingRef != null
- ? mergeRefs<HTMLTableRowElement>(measurementRef, existingRef)
- : measurementRef
- return cloneElement(renderedRow, {
- key,
- ref: combinedRef,
- 'data-index': virtualItem.index,
- })
- })}
- {paddingBottom > 0 && (
- <TableRow aria-hidden="true" style={{ height: paddingBottom }}>
- <VirtualizedTableCell
- colSpan={paddingColSpan}
- className={cn('p-0', paddingCellClassName)}
- />
- </TableRow>
- )}
- </>
- )}
- {trailingContent}
- </TableBody>
- )
- }
- export const VirtualizedTableHeader = TableHeader
- export const VirtualizedTableHead = forwardRef<
- HTMLTableCellElement,
- React.ComponentProps<typeof TableHead>
- >(({ className, ...props }, ref) => {
- return <TableHead ref={ref} className={cn('sticky top-0 z-10 bg-200', className)} {...props} />
- })
- VirtualizedTableHead.displayName = 'VirtualizedTableHead'
- export const VirtualizedTableRow = TableRow
- export const VirtualizedTableCell = TableCell
- export const VirtualizedTableFooter = TableFooter
- export const VirtualizedTableCaption = TableCaption
|