diff --git a/src/components/HighTable/Scroller.tsx b/src/components/HighTable/Scroller.tsx
index 058f8e5c..258843d6 100644
--- a/src/components/HighTable/Scroller.tsx
+++ b/src/components/HighTable/Scroller.tsx
@@ -5,21 +5,16 @@ import { CellNavigationContext } from '../../contexts/CellNavigationContext.js'
import { DataContext } from '../../contexts/DataContext.js'
import { RowsAndColumnsContext } from '../../contexts/RowsAndColumnsContext.js'
import styles from '../../HighTable.module.css'
-import { ariaOffset, defaultOverscan, rowHeight } from './constants.js'
+import { ariaOffset, rowHeight } from './constants.js'
-export interface ScrollerProps {
- overscan?: number // number of rows to fetch outside of the viewport
-}
-
-type Props = {
+interface Props {
headerHeight?: number // height of the table header
setViewportWidth: (width: number) => void // callback to set the current viewport width
children?: React.ReactNode
-} & ScrollerProps
+}
export default function Scroller({
headerHeight = rowHeight,
- overscan = defaultOverscan,
setViewportWidth,
children,
}: Props) {
@@ -32,7 +27,7 @@ export default function Scroller({
const { numRows } = useContext(DataContext)
const { onScrollKeyDown } = useContext(CellNavigationContext)
const { shouldScroll, setShouldScroll, cellPosition } = useContext(CellNavigationContext)
- const { rowsRangeWithPadding, setRowsRange } = useContext(RowsAndColumnsContext)
+ const { fetchedRowsRange, renderedRowsRange, setVisibleRowsRange } = useContext(RowsAndColumnsContext)
/**
* Compute the values:
@@ -55,19 +50,15 @@ export default function Scroller({
// TODO(SL): remove this fallback? It's only for the tests, where the elements have zero height
const clientHeight = viewportHeight === 0 ? 100 : viewportHeight
- // determine rows to fetch based on current scroll position (indexes refer to the virtual table domain)
- const startView = Math.floor(numRows * scrollTop / scrollHeight)
- const endView = Math.ceil(numRows * (scrollTop + clientHeight) / scrollHeight)
- const start = Math.max(0, startView - overscan)
- const end = Math.min(numRows, endView + overscan)
+ // determine visible rows based on current scroll position (indexes refer to the virtual table domain)
+ const start = Math.max(0, Math.floor(numRows * scrollTop / scrollHeight))
+ const end = Math.min(numRows, Math.ceil(numRows * (scrollTop + clientHeight) / scrollHeight))
if (isNaN(start)) throw new Error(`invalid start row ${start}`)
if (isNaN(end)) throw new Error(`invalid end row ${end}`)
if (end - start > 1000) throw new Error(`attempted to render too many rows ${end - start} table must be contained in a scrollable div`)
-
- const rowsRange = { start, end }
- setRowsRange?.(rowsRange)
- }, [numRows, overscan, scrollHeight, setRowsRange])
+ setVisibleRowsRange?.({ start, end })
+ }, [numRows, scrollHeight, setVisibleRowsRange])
/**
* Handle keyboard events for scrolling
@@ -89,22 +80,23 @@ export default function Scroller({
* back to it
*/
useEffect(() => {
- if (!shouldScroll || scrollTop === undefined || scrollToTop === undefined || rowsRangeWithPadding === undefined) {
+ if (!shouldScroll || scrollTop === undefined || scrollToTop === undefined || fetchedRowsRange === undefined) {
return
}
setShouldScroll?.(false)
const row = cellPosition.rowIndex - ariaOffset
let nextScrollTop = scrollTop
- // if row outside of the rows range, scroll to the estimated position of the cell,
+ // if the row is outside of the fetched rows range, scroll to the estimated position of the cell,
// to wait for the cell to be fetched and rendered
- if (row < rowsRangeWithPadding.start || row >= rowsRangeWithPadding.end) {
+ // TODO(SL): should fetchedRowsRange be replaced with visibleRowsRange?
+ if (row < fetchedRowsRange.start || row >= fetchedRowsRange.end) {
nextScrollTop = row * rowHeight
}
if (nextScrollTop !== scrollTop) {
// scroll to the cell
scrollToTop(nextScrollTop)
}
- }, [cellPosition, shouldScroll, rowsRangeWithPadding, setShouldScroll, scrollToTop, scrollTop])
+ }, [cellPosition, shouldScroll, fetchedRowsRange, setShouldScroll, scrollToTop, scrollTop])
/**
* Track viewport size and scroll position
@@ -169,8 +161,8 @@ export default function Scroller({
// Note: it does not depend on headerHeight, because the header is always present in the DOM
const top = useMemo(() => {
- return (rowsRangeWithPadding?.startPadding ?? 0) * rowHeight
- }, [rowsRangeWithPadding])
+ return (renderedRowsRange?.start ?? 0) * rowHeight
+ }, [renderedRowsRange])
return (
diff --git a/src/components/HighTable/Slice.tsx b/src/components/HighTable/Slice.tsx
index a122e900..a2468c30 100644
--- a/src/components/HighTable/Slice.tsx
+++ b/src/components/HighTable/Slice.tsx
@@ -43,7 +43,7 @@ export default function Slice({
const { onTableKeyDown: onNavigationTableKeyDown, focusFirstCell } = useContext(CellNavigationContext)
const { orderBy, onOrderByChange } = useContext(OrderByContext)
const { selectable, toggleAllRows, pendingSelectionGesture, onTableKeyDown: onSelectionTableKeyDown, allRowsSelected, isRowSelected, toggleRowNumber, toggleRangeToRowNumber } = useContext(SelectionContext)
- const { columnsParameters, rowsRangeWithPadding } = useContext(RowsAndColumnsContext)
+ const { columnsParameters, renderedRowsRange, fetchedRowsRange } = useContext(RowsAndColumnsContext)
const onTableKeyDown = useCallback((event: KeyboardEvent) => {
onNavigationTableKeyDown?.(event, { numRowsPerPage })
@@ -76,7 +76,7 @@ export default function Slice({
// Prepare the slice of data to render
// TODO(SL): also compute progress percentage here, to show a loading indicator
const slice = useMemo(() => {
- if (!rowsRangeWithPadding) {
+ if (!renderedRowsRange || !fetchedRowsRange) {
return {
prePadding: [],
postPadding: [],
@@ -85,11 +85,13 @@ export default function Slice({
version,
}
}
- const { startPadding, start, end, endPadding } = rowsRangeWithPadding
+ const prePaddingRowCount = Math.max(0, fetchedRowsRange.start - renderedRowsRange.start)
+ const fetchedRowCount = fetchedRowsRange.end - fetchedRowsRange.start
+ const postPaddingRowCount = Math.max(0, renderedRowsRange.end - fetchedRowsRange.end)
// add empty pre and post rows to fill the viewport
- const prePadding = Array.from({ length: start - startPadding }, (_, i) => ({ row: startPadding + i }))
- const rows = Array.from({ length: end - start }, (_, i) => start + i)
- const postPadding = Array.from({ length: endPadding - end }, (_, i) => ({ row: end + i }))
+ const prePadding = Array.from({ length: prePaddingRowCount }, (_, i) => ({ row: renderedRowsRange.start + i }))
+ const rows = Array.from({ length: fetchedRowCount }, (_, i) => fetchedRowsRange.start + i)
+ const postPadding = Array.from({ length: postPaddingRowCount }, (_, i) => ({ row: fetchedRowsRange.end + i }))
const canMeasureColumn: Record = {}
const rowContents = rows.map((row) => {
@@ -112,7 +114,7 @@ export default function Slice({
canMeasureColumn,
version,
}
- }, [data, columnsParameters, rowsRangeWithPadding, orderBy, version])
+ }, [data, columnsParameters, renderedRowsRange, fetchedRowsRange, orderBy, version])
// don't render table if header is empty
if (!columnsParameters) return
diff --git a/src/components/HighTable/Wrapper.tsx b/src/components/HighTable/Wrapper.tsx
index 86fad9c4..dd9d03a6 100644
--- a/src/components/HighTable/Wrapper.tsx
+++ b/src/components/HighTable/Wrapper.tsx
@@ -18,7 +18,6 @@ import { RowsAndColumnsProvider } from '../../providers/RowsAndColumnsProvider.j
import { SelectionProvider } from '../../providers/SelectionProvider.js'
import { rowHeight } from './constants.js'
import { columnVisibilityStatesSuffix, columnWidthsSuffix } from './constants.js'
-import type { ScrollerProps } from './Scroller.js'
import Scroller from './Scroller.js'
import type { SliceProps } from './Slice.js'
import Slice from './Slice.js'
@@ -33,7 +32,7 @@ export type WrapperProps = {
onColumnsVisibilityChange?: (columns: Record) => void // callback which is called whenever the set of hidden columns changes.
onOrderByChange?: (orderBy: OrderBy) => void // callback to call when a user interaction changes the order. The interactions are disabled if undefined.
onSelectionChange?: (selection: Selection) => void // callback to call when a user interaction changes the selection. The selection is expressed as data indexes (not as indexes in the table). The interactions are disabled if undefined.
-} & ScrollerProps & RowsAndColumnsProviderProps & SliceProps
+} & RowsAndColumnsProviderProps & SliceProps
export default function Wrapper({
columnConfiguration,
@@ -100,9 +99,9 @@ export default function Wrapper({
{/* Create a new navigation context if the dataframe has changed, because the focused cell might not exist anymore */}
-
+
-
+
void
+ visibleRowsRange?: RowsRange // range of rows visible in the viewport
+ fetchedRowsRange?: RowsRange // range of rows fetched from the data source (including overscan)
+ renderedRowsRange?: RowsRange // range of rows rendered in the DOM as table rows (including padding and overscan)
+ setVisibleRowsRange?: (rowsRange: RowsRange | undefined) => void
}
export const defaultRowsAndColumnsContext: RowsAndColumnsContextType = {}
diff --git a/src/providers/RowsAndColumnsProvider.tsx b/src/providers/RowsAndColumnsProvider.tsx
index b6d5d435..652660ba 100644
--- a/src/providers/RowsAndColumnsProvider.tsx
+++ b/src/providers/RowsAndColumnsProvider.tsx
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react'
import { useContext, useMemo, useState } from 'react'
-import { defaultPadding } from '../components/HighTable/constants.js'
+import { defaultOverscan, defaultPadding } from '../components/HighTable/constants.js'
import { ColumnParametersContext } from '../contexts/ColumnParametersContext.js'
import { ColumnVisibilityStatesContext } from '../contexts/ColumnVisibilityStatesContext.js'
import { DataContext } from '../contexts/DataContext.js'
@@ -11,15 +11,16 @@ import type { RowsRange } from '../contexts/RowsAndColumnsContext.js'
import { RowsAndColumnsContext } from '../contexts/RowsAndColumnsContext.js'
export interface RowsAndColumnsProviderProps {
- padding?: number
+ overscan?: number // number of rows to fetch outside of the viewport
+ padding?: number // number of empty placeholder rows to render beyond the fetched data range
}
type Props = {
children: ReactNode
} & RowsAndColumnsProviderProps
-export function RowsAndColumnsProvider({ padding = defaultPadding, children }: Props) {
- const [rowsRange, setRowsRange] = useState(undefined)
+export function RowsAndColumnsProvider({ padding = defaultPadding, overscan = defaultOverscan, children }: Props) {
+ const [visibleRowsRange, setVisibleRowsRange] = useState(undefined)
const { onError } = useContext(ErrorContext)
const { data, numRows } = useContext(DataContext)
@@ -33,12 +34,29 @@ export function RowsAndColumnsProvider({ padding = defaultPadding, children }: P
})
}, [allColumnsParameters, isHiddenColumn])
+ const fetchedRowsRange = useMemo(() => {
+ if (!visibleRowsRange) return undefined
+
+ return {
+ start: Math.max(0, visibleRowsRange.start - overscan),
+ end: Math.min(numRows, visibleRowsRange.end + overscan),
+ }
+ }, [visibleRowsRange, numRows, overscan])
+ const renderedRowsRange = useMemo(() => {
+ if (!fetchedRowsRange) return undefined
+
+ return {
+ start: Math.max(0, fetchedRowsRange.start - padding),
+ end: Math.min(numRows, fetchedRowsRange.end + padding),
+ }
+ }, [fetchedRowsRange, numRows, padding])
+
const fetchOptions = useMemo(() => ({
orderBy,
columnsParameters,
- rowsRange,
+ fetchedRowsRange,
abortController: new AbortController(),
- }), [orderBy, columnsParameters, rowsRange])
+ }), [orderBy, columnsParameters, fetchedRowsRange])
const [lastFetchOptions, setLastFetchOptions] = useState(fetchOptions)
// fetch the rows if needed
@@ -47,15 +65,15 @@ export function RowsAndColumnsProvider({ padding = defaultPadding, children }: P
// No need for useEffect
if (
lastFetchOptions.orderBy !== orderBy
- || lastFetchOptions.rowsRange !== rowsRange
+ || lastFetchOptions.fetchedRowsRange !== fetchedRowsRange
|| lastFetchOptions.columnsParameters !== columnsParameters
) {
lastFetchOptions.abortController.abort()
setLastFetchOptions(fetchOptions)
- if (data.fetch !== undefined && rowsRange !== undefined) {
+ if (data.fetch !== undefined && fetchedRowsRange !== undefined) {
data.fetch({
- rowStart: rowsRange.start,
- rowEnd: rowsRange.end,
+ rowStart: fetchedRowsRange.start,
+ rowEnd: fetchedRowsRange.end,
columns: columnsParameters.map(({ name }) => name),
orderBy,
signal: fetchOptions.abortController.signal,
@@ -69,23 +87,13 @@ export function RowsAndColumnsProvider({ padding = defaultPadding, children }: P
}
}
- const rowsRangeWithPadding = useMemo(() => {
- if (!rowsRange) return undefined
-
- const startPadding = Math.max(rowsRange.start - padding, 0)
- const endPadding = Math.min(rowsRange.end + padding, numRows)
- return {
- ...rowsRange,
- startPadding,
- endPadding,
- }
- }, [rowsRange, numRows, padding])
-
const value = useMemo(() => ({
columnsParameters,
- rowsRangeWithPadding,
- setRowsRange,
- }), [columnsParameters, rowsRangeWithPadding, setRowsRange])
+ visibleRowsRange,
+ renderedRowsRange,
+ fetchedRowsRange,
+ setVisibleRowsRange,
+ }), [columnsParameters, fetchedRowsRange, setVisibleRowsRange, renderedRowsRange, visibleRowsRange])
return (