Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions src/components/HighTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function HighTable({ data, ...props }: HighTableProps) {
)
}

type StateProps = Pick<HighTableProps, 'columnConfiguration' | 'cacheKey' | 'cellPosition' | 'columnsVisibility' | 'focus' | 'numRowsPerPage' | 'orderBy' | 'padding' | 'selection' | 'onCellPositionChange' | 'onColumnsVisibilityChange' | 'onError' | 'onOrderByChange' | 'onSelectionChange'>
type StateProps = Pick<HighTableProps, 'columnConfiguration' | 'cacheKey' | 'cellPosition' | 'columnsVisibility' | 'focus' | 'numRowsPerPage' | 'orderBy' | 'overscan' | 'padding' | 'selection' | 'onCellPositionChange' | 'onColumnsVisibilityChange' | 'onError' | 'onOrderByChange' | 'onSelectionChange'>
& { children: ReactNode }

function State({
Expand All @@ -42,6 +42,7 @@ function State({
focus,
numRowsPerPage,
orderBy,
overscan,
padding,
selection,
onCellPositionChange,
Expand Down Expand Up @@ -80,7 +81,7 @@ function State({
numRowsPerPage={numRowsPerPage}
onCellPositionChange={onCellPositionChange}
>
<ScrollProvider padding={padding}>
<ScrollProvider padding={padding} onError={onError} overscan={overscan}>
{children}
</ScrollProvider>
</CellNavigationProvider>
Expand All @@ -94,15 +95,13 @@ function State({
)
}

type DOMProps = Pick<HighTableProps, 'className' | 'maxRowNumber' | 'onError' | 'styled' | 'onDoubleClickCell' | 'onKeyDownCell' | 'onMouseDownCell' | 'overscan' | 'renderCellContent' | 'stringify'>
type DOMProps = Pick<HighTableProps, 'className' | 'maxRowNumber' | 'styled' | 'onDoubleClickCell' | 'onKeyDownCell' | 'onMouseDownCell' | 'renderCellContent' | 'stringify'>

function DOM({
className = '',
maxRowNumber,
overscan,
styled = true,
onDoubleClickCell,
onError,
onKeyDownCell,
onMouseDownCell,
renderCellContent,
Expand All @@ -114,9 +113,7 @@ function DOM({

<Scroller>
<Slice
overscan={overscan}
onDoubleClickCell={onDoubleClickCell}
onError={onError}
onKeyDownCell={onKeyDownCell}
onMouseDownCell={onMouseDownCell}
renderCellContent={renderCellContent}
Expand Down
7 changes: 5 additions & 2 deletions src/components/Scroller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { KeyboardEvent } from 'react'
import { useCallback, useContext, useMemo } from 'react'

import { CellNavigationContext } from '../contexts/CellNavigationContext.js'
import { ScrollContext } from '../contexts/ScrollContext.js'
import { CanvasHeightContext, SetScrollToContext, SetScrollTopContext, SliceTopContext } from '../contexts/ScrollContext.js'
import { SetViewportSizeContext } from '../contexts/ViewportSizeContext.js'
import styles from '../HighTable.module.css'

Expand All @@ -15,7 +15,10 @@ export default function Scroller({ children }: Props) {
/** Callback to set the current viewport size */
const setViewportSize = useContext(SetViewportSizeContext)
const { goToCurrentCell } = useContext(CellNavigationContext)
const { canvasHeight, sliceTop, setScrollTop, setScrollTo } = useContext(ScrollContext)
const setScrollTop = useContext(SetScrollTopContext)
const setScrollTo = useContext(SetScrollToContext)
const canvasHeight = useContext(CanvasHeightContext)
const sliceTop = useContext(SliceTopContext)

/**
* Handle keyboard events for scrolling
Expand Down
13 changes: 3 additions & 10 deletions src/components/Slice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import { CellNavigationContext } from '../contexts/CellNavigationContext.js'
import { ColumnsVisibilityContext } from '../contexts/ColumnsVisibilityContext.js'
import { DataFrameMethodsContext, DataVersionContext, NumRowsContext } from '../contexts/DataContext.js'
import { OrderByContext } from '../contexts/OrderByContext.js'
import { ScrollContext } from '../contexts/ScrollContext.js'
import { RenderedRowsContext } from '../contexts/ScrollContext.js'
import { SelectionContext } from '../contexts/SelectionContext.js'
import { ariaOffset } from '../helpers/constants.js'
import { useFetchCells } from '../hooks/useFetchCells.js'
import type { HighTableProps } from '../types.js'
import { stringify as stringifyDefault } from '../utils/stringify.js'
import Cell from './Cell.js'
Expand All @@ -17,12 +16,10 @@ import RowHeader from './RowHeader.js'
import TableCorner from './TableCorner.js'
import TableHeader from './TableHeader.js'

type SliceProps = Pick<HighTableProps, 'onDoubleClickCell' | 'onError' | 'onKeyDownCell' | 'onMouseDownCell' | 'overscan' | 'renderCellContent' | 'stringify'>
type SliceProps = Pick<HighTableProps, 'onDoubleClickCell' | 'onKeyDownCell' | 'onMouseDownCell' | 'renderCellContent' | 'stringify'>

export default function Slice({
overscan,
onDoubleClickCell,
onError,
onKeyDownCell,
onMouseDownCell,
renderCellContent,
Expand All @@ -32,17 +29,13 @@ export default function Slice({
const orderBy = useContext(OrderByContext)
const { selectable, toggleAllRows, pendingSelectionGesture, onTableKeyDown: onSelectionTableKeyDown, allRowsSelected, isRowSelected, toggleRowNumber, toggleRangeToRowNumber } = useContext(SelectionContext)
const { visibleColumnsParameters: columnsParameters } = useContext(ColumnsVisibilityContext)
const { renderedRowsStart, renderedRowsEnd } = useContext(ScrollContext)
const { renderedRowsStart, renderedRowsEnd } = useContext(RenderedRowsContext)
/** A version number that increments whenever a data frame is updated or resolved (the key remains the same). */
const version = useContext(DataVersionContext)
/** The actual number of rows in the data frame */
const numRows = useContext(NumRowsContext)
const dataFrameMethods = useContext(DataFrameMethodsContext)

// Fetch the required cells if needed (visible + overscan)
// it's a side-effect.
useFetchCells({ overscan, onError })

const onNavigationTableKeyDown = useMemo(() => {
if (!moveCell) {
// disable keyboard navigation if moveCell is not provided
Expand Down
48 changes: 22 additions & 26 deletions src/contexts/ScrollContext.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,28 @@
import { createContext } from 'react'

export interface ScrollContextType {
/** Total scrollable height, in pixels */
canvasHeight?: number
/** Offset of the top of the visible slice from the top of the canvas, in pixels */
sliceTop?: number
/** Index of the first row visible in the viewport (inclusive). Indexes refer to the virtual table domain. */
visibleRowsStart?: number
/** Index of the last row visible in the viewport (exclusive). */
visibleRowsEnd?: number
/**
* Function to call when the current scroll top position changes (on scroll)
*
* @param scrollTop The new scroll top position in pixels
*/
export const SetScrollTopContext = createContext<((scrollTop: number) => void) | undefined>(undefined)

/**
* Function to set the scrollTo function
*
* @param scrollTo The scrollTo function of the viewport element (on component mount), or undefined (on unmount)
*/
export const SetScrollToContext = createContext<((scrollTo: HTMLElement['scrollTo'] | undefined) => void) | undefined>(undefined)

Comment thread
severo marked this conversation as resolved.
/** Total scrollable height, in pixels */
export const CanvasHeightContext = createContext<number | undefined>(undefined)

/** Offset of the top of the visible slice from the top of the canvas, in pixels */
export const SliceTopContext = createContext<number | undefined>(undefined)

export const RenderedRowsContext = createContext<{
/** Index of the first row rendered in the DOM as a table row (inclusive). */
renderedRowsStart?: number
/** Index of the last row rendered in the DOM as a table row (exclusive). */
renderedRowsEnd?: number
/**
* Function to set the scrollTo function
*
* @param scrollTo The scrollTo function of the viewport element (on component mount), or undefined (on unmount)
*/
setScrollTo?: (scrollTo: HTMLElement['scrollTo'] | undefined) => void
/**
* Function to call when the current scroll top position changes (on scroll)
*
* @param scrollTop The new scroll top position in pixels
*/
setScrollTop?: (scrollTop: number) => void
}

export const defaultScrollContext: ScrollContextType = {}

export const ScrollContext = createContext<ScrollContextType>(defaultScrollContext)
}>({})
25 changes: 12 additions & 13 deletions src/hooks/useFetchCells.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,30 @@ import { useContext, useEffect, useEffectEvent, useMemo } from 'react'
import { ColumnsVisibilityContext } from '../contexts/ColumnsVisibilityContext.js'
import { DataFrameMethodsContext, NumRowsContext } from '../contexts/DataContext.js'
import { OrderByContext } from '../contexts/OrderByContext.js'
import { ScrollContext } from '../contexts/ScrollContext.js'
import { defaultOverscan } from '../helpers/constants.js'
import type { HighTableProps } from '../types.js'

type Props = Pick<HighTableProps, 'onError' | 'overscan'>
type Props = Pick<HighTableProps, 'onError' | 'overscan'> & {
range?: {
/** Index of the first row visible in the viewport (inclusive). Indexes refer to the virtual table domain. */
visibleRowsStart?: number
/** Index of the last row visible in the viewport (exclusive). */
visibleRowsEnd?: number
}
}

/**
* Fetch the required cells (visible + overscan).
*/
export function useFetchCells({ overscan = defaultOverscan, onError }: Props) {
const { visibleRowsStart, visibleRowsEnd } = useContext(ScrollContext)
export function useFetchCells({ overscan = defaultOverscan, range = {}, onError }: Props) {
const { visibleColumnsParameters } = useContext(ColumnsVisibilityContext)
const orderBy = useContext(OrderByContext)
const dataFrameMethods = useContext(DataFrameMethodsContext)
const numRows = useContext(NumRowsContext)
const { visibleRowsStart, visibleRowsEnd } = range

const fetchedRowsStart = useMemo(() => {
if (visibleRowsStart === undefined) return undefined
return Math.max(0, visibleRowsStart - overscan)
}, [visibleRowsStart, overscan])

const fetchedRowsEnd = useMemo(() => {
if (visibleRowsEnd === undefined) return undefined
return Math.min(numRows, visibleRowsEnd + overscan)
}, [visibleRowsEnd, numRows, overscan])
const fetchedRowsStart = visibleRowsStart ? Math.max(0, visibleRowsStart - overscan) : undefined
const fetchedRowsEnd = visibleRowsEnd ? Math.min(numRows, visibleRowsEnd + overscan) : undefined

const columnNames = useMemo(() => {
return (visibleColumnsParameters ?? []).map(({ name }) => name)
Expand Down
57 changes: 37 additions & 20 deletions src/providers/ScrollProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@ import { type ReactNode, useCallback, useContext, useEffect, useMemo, useReducer

import { CellNavigationContext } from '../contexts/CellNavigationContext.js'
import { NumRowsContext } from '../contexts/DataContext.js'
import { ScrollContext } from '../contexts/ScrollContext.js'
import { CanvasHeightContext, RenderedRowsContext, SetScrollToContext, SetScrollTopContext, SliceTopContext } from '../contexts/ScrollContext.js'
import { TableCornerHeightContext } from '../contexts/TableCornerSizeContext.js'
import { ViewportHeightContext } from '../contexts/ViewportSizeContext.js'
import { defaultPadding, maxElementHeight, rowHeight } from '../helpers/constants.js'
import { computeDerivedValues, createScale, getScrollActionForRow, initializeScrollState, scrollReducer } from '../helpers/scroll.js'
import { useFetchCells } from '../hooks/useFetchCells.js'
import type { HighTableProps } from '../types.js'

type ScrollProviderProps = Pick<HighTableProps, 'padding'> & {
type ScrollProviderProps = Pick<HighTableProps, 'overscan' | 'padding' | 'onError'> & {
/** Child components */
children: ReactNode
}

/**
* Provide the scroll state and logic to the table, through the ScrollContext.
* Provide the scroll state and logic to the table, through the ScrollContext contexts.
*/
export function ScrollProvider({ children, padding = defaultPadding }: ScrollProviderProps) {
export function ScrollProvider({ children, overscan, padding = defaultPadding, onError }: ScrollProviderProps) {
const [{ scale, scrollTop, scrollTopAnchor, localOffset }, dispatch] = useReducer(scrollReducer, undefined, initializeScrollState)
const { cellPosition, focusState, focusDispatch } = useContext(CellNavigationContext)
const clientHeight = useContext(ViewportHeightContext)
Expand Down Expand Up @@ -82,24 +83,40 @@ export function ScrollProvider({ children, padding = defaultPadding }: ScrollPro
}
}, [cellPosition, scrollTo, scrollTopAnchor, localOffset, scale, focusDispatch, focusState])

const value = useMemo(() => {
const derivedValues = useMemo(() => {
if (!scale) {
return undefined
}
return computeDerivedValues({
scale,
scrollTop,
scrollTopAnchor,
localOffset,
padding })
}, [scale, scrollTop, scrollTopAnchor, localOffset, padding])

const renderedRows = useMemo(() => {
return {
scrollMode: 'virtual' as const,
canvasHeight: scale ? scale.canvasHeight : undefined,
setScrollTop,
setScrollTo,
...computeDerivedValues({
scale,
scrollTop,
scrollTopAnchor,
localOffset,
padding,
}),
renderedRowsStart: derivedValues?.renderedRowsStart,
renderedRowsEnd: derivedValues?.renderedRowsEnd,
}
}, [scale, scrollTop, scrollTopAnchor, localOffset, padding, setScrollTop])
}, [derivedValues])

// Fetch the required cells if needed (visible + overscan)
// it's a side-effect.
useFetchCells({ overscan, onError, range: derivedValues })

return (
<ScrollContext.Provider value={value}>
{children}
</ScrollContext.Provider>
<SetScrollToContext.Provider value={setScrollTo}>
<SetScrollTopContext.Provider value={setScrollTop}>
<CanvasHeightContext.Provider value={scale ? scale.canvasHeight : undefined}>
<SliceTopContext.Provider value={derivedValues?.sliceTop}>
<RenderedRowsContext.Provider value={renderedRows}>
{children}
</RenderedRowsContext.Provider>
</SliceTopContext.Provider>
</CanvasHeightContext.Provider>
</SetScrollTopContext.Provider>
</SetScrollToContext.Provider>
)
}
Loading