mirror of
https://github.com/supabase/supabase.git
synced 2026-05-08 01:40:13 -04:00
4a0bb36ca8
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
243 lines
6.9 KiB
TypeScript
243 lines
6.9 KiB
TypeScript
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
|