diff --git a/src/components/HighTable/Scroller.tsx b/src/components/HighTable/Scroller.tsx index 69da828b..3dd93f9e 100644 --- a/src/components/HighTable/Scroller.tsx +++ b/src/components/HighTable/Scroller.tsx @@ -3,7 +3,7 @@ import { useCallback, useContext, useMemo } from 'react' import { CellNavigationContext } from '../../contexts/CellNavigationContext.js' import { ScrollContext } from '../../contexts/ScrollContext.js' -import { useSetViewportSize } from '../../contexts/ViewportSizeContext.js' +import { SetViewportSizeContext } from '../../contexts/ViewportSizeContext.js' import styles from '../../HighTable.module.css' interface Props { @@ -13,7 +13,7 @@ interface Props { export default function Scroller({ children }: Props) { /** Callback to set the current viewport size */ - const setViewportSize = useSetViewportSize() + const setViewportSize = useContext(SetViewportSizeContext) const { goToCurrentCell } = useContext(CellNavigationContext) const { canvasHeight, sliceTop, setScrollTop, setScrollTo } = useContext(ScrollContext) diff --git a/src/components/HighTable/Slice.tsx b/src/components/HighTable/Slice.tsx index 49acb9a8..739d8700 100644 --- a/src/components/HighTable/Slice.tsx +++ b/src/components/HighTable/Slice.tsx @@ -3,7 +3,7 @@ import { useCallback, useContext, useMemo } from 'react' import { CellNavigationContext } from '../../contexts/CellNavigationContext.js' import { ColumnsVisibilityContext } from '../../contexts/ColumnsVisibilityContext.js' -import { useData, useDataVersion, useNumRows } from '../../contexts/DataContext.js' +import { DataFrameMethodsContext, DataVersionContext, NumRowsContext } from '../../contexts/DataContext.js' import { OrderByContext } from '../../contexts/OrderByContext.js' import { ScrollContext } from '../../contexts/ScrollContext.js' import { SelectionContext } from '../../contexts/SelectionContext.js' @@ -34,10 +34,10 @@ export default function Slice({ const { visibleColumnsParameters: columnsParameters } = useContext(ColumnsVisibilityContext) const { renderedRowsStart, renderedRowsEnd } = useContext(ScrollContext) /** A version number that increments whenever a data frame is updated or resolved (the key remains the same). */ - const version = useDataVersion() + const version = useContext(DataVersionContext) /** The actual number of rows in the data frame */ - const numRows = useNumRows() - const data = useData() + const numRows = useContext(NumRowsContext) + const dataFrameMethods = useContext(DataFrameMethodsContext) // Fetch the required cells if needed (visible + overscan) // it's a side-effect. @@ -144,9 +144,9 @@ export default function Slice({ const canMeasureColumn: Record = {} const rowContents = rows.map((row) => { - const rowNumber = data.getRowNumber({ row, orderBy })?.value + const rowNumber = dataFrameMethods.getRowNumber({ row, orderBy })?.value const cells = (columnsParameters ?? []).map(({ name: column, index: originalColumnIndex, className }) => { - const cell = data.getCell({ row, column, orderBy }) + const cell = dataFrameMethods.getCell({ row, column, orderBy }) canMeasureColumn[column] ||= cell !== undefined return { columnIndex: originalColumnIndex, cell, className } }) @@ -161,7 +161,7 @@ export default function Slice({ canMeasureColumn, version, } - }, [data, columnsParameters, renderedRowsStart, renderedRowsEnd, orderBy, version]) + }, [dataFrameMethods, columnsParameters, renderedRowsStart, renderedRowsEnd, orderBy, version]) // don't render table if the data frame has no visible columns // (it can have zero rows, but must have at least one visible column) diff --git a/src/components/HighTable/Wrapper.tsx b/src/components/HighTable/Wrapper.tsx index 064c21dd..d41a81be 100644 --- a/src/components/HighTable/Wrapper.tsx +++ b/src/components/HighTable/Wrapper.tsx @@ -1,8 +1,8 @@ -import type { CSSProperties, ReactNode } from 'react' +import { type CSSProperties, type ReactNode, useContext } from 'react' -import { useNumRows } from '../../contexts/DataContext.js' +import { NumRowsContext } from '../../contexts/DataContext.js' import { PortalContainerContext } from '../../contexts/PortalContainerContext.js' -import { useHeaderHeight } from '../../contexts/TableCornerSizeContext.js' +import { TableCornerHeightContext } from '../../contexts/TableCornerSizeContext.js' import styles from '../../HighTable.module.css' import { useHTMLElement } from '../../hooks/useHTMLElement.js' import type { HighTableProps } from '../../types.js' @@ -14,9 +14,9 @@ type Props = Pick & { export default function Wrapper({ children, className, maxRowNumber, styled }: Props) { /** Number of rows in the data frame */ - const numRows = useNumRows() + const numRows = useContext(NumRowsContext) /** Height of the header, used to set a CSS variable for row height calculation in the cells */ - const headerHeight = useHeaderHeight() + const headerHeight = useContext(TableCornerHeightContext) // reserve space for at least 3 characters const numCharacters = Math.max((maxRowNumber ?? numRows).toLocaleString('en-US').length, 3) diff --git a/src/components/TableCorner/TableCorner.tsx b/src/components/TableCorner/TableCorner.tsx index 92c672d3..895042a5 100644 --- a/src/components/TableCorner/TableCorner.tsx +++ b/src/components/TableCorner/TableCorner.tsx @@ -1,7 +1,7 @@ import type { ChangeEvent, CSSProperties, KeyboardEvent, ReactNode } from 'react' -import { useCallback, useEffect, useRef } from 'react' +import { useCallback, useContext, useEffect, useRef } from 'react' -import { useSetTableCornerSize } from '../../contexts/TableCornerSizeContext.js' +import { SetTableCornerSizeContext } from '../../contexts/TableCornerSizeContext.js' import { useCellFocus } from '../../hooks/useCellFocus.js' interface Props { @@ -16,7 +16,7 @@ interface Props { export default function TableCorner({ children, checked, onCheckboxPress, pendingSelectionGesture, style, ariaColIndex, ariaRowIndex }: Props) { const { tabIndex, navigateToCell, focusIfNeeded } = useCellFocus({ ariaColIndex, ariaRowIndex }) - const setTableCornerSize = useSetTableCornerSize() + const setTableCornerSize = useContext(SetTableCornerSizeContext) // Focus the cell if needed. We use an effect, as it acts on the DOM element after render. const ref = useRef(null) diff --git a/src/contexts/DataContext.ts b/src/contexts/DataContext.ts index 585e49ed..887acdf8 100644 --- a/src/contexts/DataContext.ts +++ b/src/contexts/DataContext.ts @@ -1,47 +1,23 @@ -import { createContext, useContext } from 'react' +import { createContext } from 'react' import type { ColumnDescriptor, DataFrame } from '../helpers/dataframe/types.js' +/** + * The data frame, limited to the getRowNumber, getCell, and fetch methods. + * + * The methods might change over time, without the data frame instance changing. + */ export type DataFrameMethods = Pick export type DataFrameWithoutMethods = Omit -export const DataKeyContext = createContext(0) export const DataVersionContext = createContext(0) export const NumRowsContext = createContext(0) export const ColumnDescriptorsContext = createContext[]>([]) export const NumColumnsContext = createContext(0) export const ExclusiveSortContext = createContext(false) -export const DataContext = createContext(undefined) - +export const DataFrameMethodsContext = createContext({ + getRowNumber: () => undefined, + getCell: () => undefined, +}) // the data key is only used in tests -export function useDataKey() { - return useContext(DataKeyContext) -} - -export function useDataVersion() { - return useContext(DataVersionContext) -} - -export function useNumRows() { - return useContext(NumRowsContext) -} - -export function useColumnDescriptors() { - return useContext(ColumnDescriptorsContext) -} - -export function useNumColumns() { - return useContext(NumColumnsContext) -} - -export function useExclusiveSort() { - return useContext(ExclusiveSortContext) -} - -export function useData(): DataFrameMethods { - const data = useContext(DataContext) - if (data === undefined) { - throw new Error('useData must be used within a DataContext.Provider with a valid DataFrameMethods value') - } - return data -} +export const DataKeyContext = createContext(0) diff --git a/src/contexts/TableCornerSizeContext.ts b/src/contexts/TableCornerSizeContext.ts index 42b2c81e..629cbb7f 100644 --- a/src/contexts/TableCornerSizeContext.ts +++ b/src/contexts/TableCornerSizeContext.ts @@ -1,21 +1,10 @@ -import { createContext, useContext } from 'react' +import { createContext } from 'react' import { rowHeight } from '../helpers/constants.js' type SetTableCornerSizeContextType = (element: HTMLElement) => void -export const TableCornerHeightContext = createContext(undefined) +export const defaultTableCornerHeight = rowHeight +export const TableCornerHeightContext = createContext(defaultTableCornerHeight) export const TableCornerWidthContext = createContext(undefined) export const SetTableCornerSizeContext = createContext(undefined) - -export function useTableCornerWidth() { - return useContext(TableCornerWidthContext) -} - -export function useHeaderHeight() { - return useContext(TableCornerHeightContext) ?? rowHeight -} - -export function useSetTableCornerSize() { - return useContext(SetTableCornerSizeContext) -} diff --git a/src/contexts/ViewportSizeContext.ts b/src/contexts/ViewportSizeContext.ts index 89f9ea82..e696b818 100644 --- a/src/contexts/ViewportSizeContext.ts +++ b/src/contexts/ViewportSizeContext.ts @@ -1,19 +1,7 @@ -import { createContext, useContext } from 'react' +import { createContext } from 'react' type SetViewportSizeContextType = (element: HTMLElement) => void export const ViewportHeightContext = createContext(undefined) export const ViewportWidthContext = createContext(undefined) export const SetViewportSizeContext = createContext(undefined) - -export function useViewportWidth() { - return useContext(ViewportWidthContext) -} - -export function useViewportHeight() { - return useContext(ViewportHeightContext) -} - -export function useSetViewportSize() { - return useContext(SetViewportSizeContext) -} diff --git a/src/hooks/useFetchCells.ts b/src/hooks/useFetchCells.ts index acec1211..0d314046 100644 --- a/src/hooks/useFetchCells.ts +++ b/src/hooks/useFetchCells.ts @@ -1,7 +1,7 @@ import { useContext, useEffect, useEffectEvent, useMemo } from 'react' import { ColumnsVisibilityContext } from '../contexts/ColumnsVisibilityContext.js' -import { useData, useNumRows } from '../contexts/DataContext.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' @@ -16,8 +16,8 @@ export function useFetchCells({ overscan = defaultOverscan, onError }: Props) { const { visibleRowsStart, visibleRowsEnd } = useContext(ScrollContext) const { visibleColumnsParameters } = useContext(ColumnsVisibilityContext) const orderBy = useContext(OrderByContext) - const data = useData() - const numRows = useNumRows() + const dataFrameMethods = useContext(DataFrameMethodsContext) + const numRows = useContext(NumRowsContext) const fetchedRowsStart = useMemo(() => { if (visibleRowsStart === undefined) return undefined @@ -44,13 +44,13 @@ export function useFetchCells({ overscan = defaultOverscan, onError }: Props) { // Keep this inside an effect so we don't update state // or perform side-effects during render, for example when calling onError. useEffect(() => { - if (data.fetch === undefined || fetchedRowsStart === undefined || fetchedRowsEnd === undefined) return + if (dataFrameMethods.fetch === undefined || fetchedRowsStart === undefined || fetchedRowsEnd === undefined) return // Create an AbortController per fetch and clean it up on dependency changes. const abortController = new AbortController() // Launch the data fetch. The promise is not awaited here, but it will be aborted if any dependency changes. - data.fetch({ + dataFrameMethods.fetch({ rowStart: fetchedRowsStart, rowEnd: fetchedRowsEnd, columns: columnNames, @@ -66,5 +66,5 @@ export function useFetchCells({ overscan = defaultOverscan, onError }: Props) { return () => { abortController.abort() } - }, [data, fetchedRowsStart, fetchedRowsEnd, columnNames, orderBy]) + }, [dataFrameMethods, fetchedRowsStart, fetchedRowsEnd, columnNames, orderBy]) } diff --git a/src/providers/CellNavigationProvider.tsx b/src/providers/CellNavigationProvider.tsx index 5e114c07..f691bc12 100644 --- a/src/providers/CellNavigationProvider.tsx +++ b/src/providers/CellNavigationProvider.tsx @@ -4,7 +4,7 @@ import { useCallback, useContext, useEffect, useMemo, useReducer } from 'react' import type { FocusAction, FocusState, MoveCellAction } from '../contexts/CellNavigationContext.js' import { CellNavigationContext } from '../contexts/CellNavigationContext.js' import { ColumnsVisibilityContext } from '../contexts/ColumnsVisibilityContext.js' -import { useNumRows } from '../contexts/DataContext.js' +import { NumRowsContext } from '../contexts/DataContext.js' import { defaultNumRowsPerPage } from '../helpers/constants.js' import { useInputState } from '../hooks/useInputState.js' import type { HighTableProps } from '../types.js' @@ -55,7 +55,7 @@ export function CellNavigationProvider({ }: CellNavigationProviderProps) { const [focusState, focusDispatch] = useReducer(reducer, focus, initializeFocusState) /** The actual number of rows in the data frame */ - const numDataRows = useNumRows() + const numDataRows = useContext(NumRowsContext) const notifyChange = useCallback(() => { focusDispatch({ type: 'START' }) diff --git a/src/providers/ColumnParametersProvider.tsx b/src/providers/ColumnParametersProvider.tsx index 0c952ac5..c8e28d64 100644 --- a/src/providers/ColumnParametersProvider.tsx +++ b/src/providers/ColumnParametersProvider.tsx @@ -1,7 +1,7 @@ -import { type ReactNode, useMemo } from 'react' +import { type ReactNode, useContext, useMemo } from 'react' import { type ColumnParameters, ColumnParametersContext, SortableColumnsContext } from '../contexts/ColumnParametersContext.js' -import { useColumnDescriptors } from '../contexts/DataContext.js' +import { ColumnDescriptorsContext } from '../contexts/DataContext.js' import type { HighTableProps } from '../types.js' type Props = Pick & { @@ -15,7 +15,7 @@ type Props = Pick & { * It merges the column descriptors from the data frame with the user-provided configuration. */ export function ColumnParametersProvider({ columnConfiguration, children }: Props) { - const columnDescriptors = useColumnDescriptors() + const columnDescriptors = useContext(ColumnDescriptorsContext) // A column is sortable iif it's marked as sortable in the column descriptors from the data frame. The user configuration can't change that. const sortableColumns = useMemo(() => { diff --git a/src/providers/ColumnWidthsProvider.tsx b/src/providers/ColumnWidthsProvider.tsx index ab3b14df..2832d844 100644 --- a/src/providers/ColumnWidthsProvider.tsx +++ b/src/providers/ColumnWidthsProvider.tsx @@ -3,9 +3,9 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'r import { ColumnParametersContext } from '../contexts/ColumnParametersContext.js' import { ColumnWidthsContext } from '../contexts/ColumnWidthsContext.js' -import { useNumColumns } from '../contexts/DataContext.js' -import { useTableCornerWidth } from '../contexts/TableCornerSizeContext.js' -import { useViewportWidth } from '../contexts/ViewportSizeContext.js' +import { NumColumnsContext } from '../contexts/DataContext.js' +import { TableCornerWidthContext } from '../contexts/TableCornerSizeContext.js' +import { ViewportWidthContext } from '../contexts/ViewportSizeContext.js' import { cellStyle } from '../helpers/width.js' import { useLocalStorageState } from '../hooks/useLocalStorageState.js' @@ -73,11 +73,11 @@ interface ColumnWidthsProviderProps { */ export function ColumnWidthsProvider({ children, localStorageKey, minWidth }: ColumnWidthsProviderProps) { /** Current viewport width (used to compute the maximum total width) */ - const viewportWidth = useViewportWidth() + const viewportWidth = useContext(ViewportWidthContext) /** Current table corner width (used to compute the maximum total width) */ - const tableCornerWidth = useTableCornerWidth() + const tableCornerWidth = useContext(TableCornerWidthContext) /** Number of columns (used to initialize the widths array, and compute the widths) */ - const numColumns = useNumColumns() + const numColumns = useContext(NumColumnsContext) // Number of columns if (!Number.isInteger(numColumns) || numColumns < 0) { diff --git a/src/providers/DataProvider.tsx b/src/providers/DataProvider.tsx index f5cf784a..ad18f31a 100644 --- a/src/providers/DataProvider.tsx +++ b/src/providers/DataProvider.tsx @@ -1,7 +1,7 @@ import { type ReactNode, useEffect, useState } from 'react' import type { DataFrameWithoutMethods } from '../contexts/DataContext.js' -import { ColumnDescriptorsContext, DataContext, DataKeyContext, DataVersionContext, ExclusiveSortContext, NumColumnsContext, NumRowsContext } from '../contexts/DataContext.js' +import { ColumnDescriptorsContext, DataFrameMethodsContext, DataKeyContext, DataVersionContext, ExclusiveSortContext, NumColumnsContext, NumRowsContext } from '../contexts/DataContext.js' import type { HighTableProps } from '../types.js' // Assign stable numeric ids to data instances without triggering state @@ -33,11 +33,11 @@ export function DataProvider({ children, data }: Props) { return ( // The data key context is only used in tests - + {children} - + ) } diff --git a/src/providers/OrderByProvider.tsx b/src/providers/OrderByProvider.tsx index ef5f2460..3a05ac95 100644 --- a/src/providers/OrderByProvider.tsx +++ b/src/providers/OrderByProvider.tsx @@ -1,7 +1,7 @@ import { type ReactNode, useContext, useMemo } from 'react' import { SortableColumnsContext } from '../contexts/ColumnParametersContext.js' -import { useExclusiveSort } from '../contexts/DataContext.js' +import { ExclusiveSortContext } from '../contexts/DataContext.js' import { OrderByContext, SortInfoAndActionsByColumnContext } from '../contexts/OrderByContext.js' import { type OrderBy, toggleColumn, toggleColumnExclusive } from '../helpers/sort.js' import { useInputState } from '../hooks/useInputState.js' @@ -24,7 +24,7 @@ type Props = Pick & { * The context value is memoized and won't change unless the orderBy or the sortable columns change, to avoid unnecessary re-renders of the consumers. */ export function OrderByProvider({ children, orderBy: controlledOrderBy, onOrderByChange }: Props) { - const exclusiveSort = useExclusiveSort() + const exclusiveSort = useContext(ExclusiveSortContext) const sortableColumns = useContext(SortableColumnsContext) const [orderBy, setOrderBy] = useInputState({ diff --git a/src/providers/ScrollProvider.tsx b/src/providers/ScrollProvider.tsx index 6d33c801..60e78b2b 100644 --- a/src/providers/ScrollProvider.tsx +++ b/src/providers/ScrollProvider.tsx @@ -1,10 +1,10 @@ import { type ReactNode, useCallback, useContext, useEffect, useMemo, useReducer, useState } from 'react' import { CellNavigationContext } from '../contexts/CellNavigationContext.js' -import { useNumRows } from '../contexts/DataContext.js' +import { NumRowsContext } from '../contexts/DataContext.js' import { ScrollContext } from '../contexts/ScrollContext.js' -import { useHeaderHeight } from '../contexts/TableCornerSizeContext.js' -import { useViewportHeight } from '../contexts/ViewportSizeContext.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 type { HighTableProps } from '../types.js' @@ -20,11 +20,11 @@ type ScrollProviderProps = Pick & { export function ScrollProvider({ children, padding = defaultPadding }: ScrollProviderProps) { const [{ scale, scrollTop, scrollTopAnchor, localOffset }, dispatch] = useReducer(scrollReducer, undefined, initializeScrollState) const { cellPosition, focusState, focusDispatch } = useContext(CellNavigationContext) - const clientHeight = useViewportHeight() + const clientHeight = useContext(ViewportHeightContext) /** Height of the header row, in pixels */ - const headerHeight = useHeaderHeight() + const headerHeight = useContext(TableCornerHeightContext) /** The actual number of rows in the data frame */ - const numRows = useNumRows() + const numRows = useContext(NumRowsContext) const [scrollTo, setScrollTo] = useState(undefined) const setScrollTop = useCallback((scrollTop: number) => { diff --git a/src/providers/SelectionProvider.tsx b/src/providers/SelectionProvider.tsx index c388a7b8..5b40782a 100644 --- a/src/providers/SelectionProvider.tsx +++ b/src/providers/SelectionProvider.tsx @@ -2,7 +2,7 @@ import type { KeyboardEvent, ReactNode } from 'react' import { useCallback, useContext, useEffect, useEffectEvent, useMemo, useState } from 'react' import type { DataFrameMethods } from '../contexts/DataContext.js' -import { useData, useNumRows } from '../contexts/DataContext.js' +import { DataFrameMethodsContext, NumRowsContext } from '../contexts/DataContext.js' import { OrderByContext } from '../contexts/OrderByContext.js' import { SelectionContext } from '../contexts/SelectionContext.js' import { checkSignal } from '../helpers/dataframe/helpers.js' @@ -29,8 +29,8 @@ interface Gesture { */ export function SelectionProvider({ children, selection: controlledSelection, onError, onSelectionChange }: Props) { /** The actual number of rows in the data frame */ - const numRows = useNumRows() - const data = useData() + const numRows = useContext(NumRowsContext) + const dataFrameMethods = useContext(DataFrameMethodsContext) // The selection is only useful for the parent component. If no props are passed, hide the selection feature. const [isEnabled] = useState(() => controlledSelection !== undefined || onSelectionChange !== undefined) const inputState = useInputState({ @@ -119,7 +119,7 @@ export function SelectionProvider({ children, selection: controlledSelection, on return ({ row, rowNumber }: { row: number, rowNumber: number }) => { const gesture = startGesture() const { signal } = gesture.controller - toggleRange({ data, numRows, row, rowNumber, selection, orderBy, signal, rowByRowNumberAndOrderBy }) + toggleRange({ dataFrameMethods, numRows, row, rowNumber, selection, orderBy, signal, rowByRowNumberAndOrderBy }) .finally(() => { stopGesture({ gesture }) }) .then((newSelection) => { setSelection(newSelection) }) .catch((error: unknown) => { @@ -130,7 +130,7 @@ export function SelectionProvider({ children, selection: controlledSelection, on onError?.(error) }) } - }, [setSelection, selection, rowByRowNumberAndOrderBy, data, numRows, orderBy, startGesture, stopGesture, onError]) + }, [setSelection, selection, rowByRowNumberAndOrderBy, dataFrameMethods, numRows, orderBy, startGesture, stopGesture, onError]) const toggleAllRows = useMemo(() => { if (!selection || !setSelection) return @@ -138,7 +138,7 @@ export function SelectionProvider({ children, selection: controlledSelection, on const gesture = startGesture() const { signal } = gesture.controller // toggle a range to the row number - toggleAll({ data, numRows, selection, signal }) + toggleAll({ dataFrameMethods, numRows, selection, signal }) .finally(() => { stopGesture({ gesture }) }) .then((newSelection) => { setSelection(newSelection) }) .catch((error: unknown) => { @@ -149,7 +149,7 @@ export function SelectionProvider({ children, selection: controlledSelection, on onError?.(error) }) } - }, [setSelection, data, numRows, selection, startGesture, stopGesture, onError]) + }, [setSelection, dataFrameMethods, numRows, selection, startGesture, stopGesture, onError]) const onTableKeyDown = useCallback((event: KeyboardEvent) => { const { key, shiftKey } = event @@ -169,7 +169,7 @@ export function SelectionProvider({ children, selection: controlledSelection, on event.preventDefault() // only select if selection is enabled, but prevent the default behavior in all cases for consistency if (selection && setSelection) { - toggleAll({ data, numRows, selection, signal }) + toggleAll({ dataFrameMethods, numRows, selection, signal }) .finally(() => { stopGesture({ gesture }) }) .then((newSelection) => { setSelection(newSelection) }) .catch((error: unknown) => { @@ -199,14 +199,14 @@ export function SelectionProvider({ children, selection: controlledSelection, on stopGesture({ gesture }) } } - }, [selection, setSelection, startGesture, stopGesture, data, numRows, onError]) + }, [selection, setSelection, startGesture, stopGesture, dataFrameMethods, numRows, onError]) useEffect(() => { if (!selection) return undefined // it's not really a gesture, but we want it to be aborted when a new gesture starts const gesture = startGesture() const { signal } = gesture.controller - fetchAreAllSelected({ data, numRows, selection, signal }) + fetchAreAllSelected({ dataFrameMethods, numRows, selection, signal }) .finally(() => { stopGesture({ gesture }) }) .then((areAllSelected) => { setAllRowsSelected(areAllSelected) }) .catch((error: unknown) => { @@ -216,7 +216,7 @@ export function SelectionProvider({ children, selection: controlledSelection, on } onError?.(error) }) - }, [selection, data, numRows, startGesture, stopGesture, onError]) + }, [selection, dataFrameMethods, numRows, startGesture, stopGesture, onError]) return ( { const row = i + rowStart - const rowNumber = data.getRowNumber({ row, orderBy })?.value + const rowNumber = dataFrameMethods.getRowNumber({ row, orderBy })?.value if (rowNumber === undefined) { throw new Error(`Row number is undefined for row ${row} with orderBy ${JSON.stringify(orderBy ?? [])}`) } @@ -249,7 +249,7 @@ async function fetchRowNumbers({ data, rowStart, rowEnd, orderBy, signal }: { da return rowNumbers } -async function fetchRow({ data, numRows, rowNumber, orderBy, signal, rowByRowNumberAndOrderBy }: { data: DataFrameMethods, numRows: number, rowNumber: number, orderBy?: OrderBy, signal?: AbortSignal, rowByRowNumberAndOrderBy?: Map> }) { +async function fetchRow({ dataFrameMethods, numRows, rowNumber, orderBy, signal, rowByRowNumberAndOrderBy }: { dataFrameMethods: DataFrameMethods, numRows: number, rowNumber: number, orderBy?: OrderBy, signal?: AbortSignal, rowByRowNumberAndOrderBy?: Map> }) { checkSignal(signal) const orderByKey = serializeOrderBy(orderBy ?? []) const cachedMap = rowByRowNumberAndOrderBy?.get(orderByKey) @@ -262,10 +262,10 @@ async function fetchRow({ data, numRows, rowNumber, orderBy, signal, rowByRowNum const rowByRowNumber = new Map() // get all the row numbers in the table // TODO(SL): instead of doing everything in one fetch, we could loop, or maybe do a Promise.race between groups of rows - await data.fetch?.({ rowStart: 0, rowEnd: numRows, orderBy, signal }) + await dataFrameMethods.fetch?.({ rowStart: 0, rowEnd: numRows, orderBy, signal }) // fill the Map for (let i = 0; i < numRows; i++) { - const rowNumber = data.getRowNumber({ row: i, orderBy })?.value + const rowNumber = dataFrameMethods.getRowNumber({ row: i, orderBy })?.value if (rowNumber === undefined) { throw new Error(`Row number is undefined for row ${i} with orderBy ${orderByKey}`) } @@ -277,7 +277,7 @@ async function fetchRow({ data, numRows, rowNumber, orderBy, signal, rowByRowNum return rowByRowNumber.get(rowNumber) } -async function toggleRange({ data, numRows, row, rowNumber, selection, orderBy, signal, rowByRowNumberAndOrderBy }: { data: DataFrameMethods, numRows: number, row: number, rowNumber: number, selection: Selection, orderBy?: OrderBy, signal?: AbortSignal, rowByRowNumberAndOrderBy?: Map> }): Promise { +async function toggleRange({ dataFrameMethods, numRows, row, rowNumber, selection, orderBy, signal, rowByRowNumberAndOrderBy }: { dataFrameMethods: DataFrameMethods, numRows: number, row: number, rowNumber: number, selection: Selection, orderBy?: OrderBy, signal?: AbortSignal, rowByRowNumberAndOrderBy?: Map> }): Promise { const { anchor } = selection if (anchor === undefined || anchor === rowNumber) { // toggle the row without the anchor @@ -285,7 +285,7 @@ async function toggleRange({ data, numRows, row, rowNumber, selection, orderBy, } // try to get the tableIndex of the anchor - const anchorRow: number | undefined = await fetchRow({ data, numRows, rowNumber: anchor, orderBy, signal, rowByRowNumberAndOrderBy }) + const anchorRow: number | undefined = await fetchRow({ dataFrameMethods, numRows, rowNumber: anchor, orderBy, signal, rowByRowNumberAndOrderBy }) if (anchorRow === undefined || anchorRow === row) { // Note: anchorRow should be different from row at that point because we already checked anchor === rowNumber above @@ -296,10 +296,10 @@ async function toggleRange({ data, numRows, row, rowNumber, selection, orderBy, const [rowStart, rowEnd] = anchorRow < row ? [anchorRow + 1, row + 1] : [row, anchorRow] - const rowNumbers = await fetchRowNumbers({ data, rowStart, rowEnd, orderBy, signal }) + const rowNumbers = await fetchRowNumbers({ dataFrameMethods, rowStart, rowEnd, orderBy, signal }) let { ranges } = selection - const newAnchor = data.getRowNumber({ row, orderBy })?.value + const newAnchor = dataFrameMethods.getRowNumber({ row, orderBy })?.value if (newAnchor === undefined) { // should never happen throw new Error(`Row number for row ${row} not found in orderBy ${serializeOrderBy(orderBy ?? [])}`) @@ -314,10 +314,10 @@ async function toggleRange({ data, numRows, row, rowNumber, selection, orderBy, return { ranges, anchor: newAnchor } } -async function toggleAll({ data, numRows, selection, signal }: { data: DataFrameMethods, numRows: number, selection: Selection, signal?: AbortSignal }): Promise { +async function toggleAll({ dataFrameMethods, numRows, selection, signal }: { dataFrameMethods: DataFrameMethods, numRows: number, selection: Selection, signal?: AbortSignal }): Promise { // check if all the rows are already selected - const areAllSelected = await fetchAreAllSelected({ data, numRows, selection, signal }) - const rowNumbers = await fetchRowNumbers({ data, rowStart: 0, rowEnd: numRows, signal }) + const areAllSelected = await fetchAreAllSelected({ dataFrameMethods, numRows, selection, signal }) + const rowNumbers = await fetchRowNumbers({ dataFrameMethods, rowStart: 0, rowEnd: numRows, signal }) let { ranges } = selection for (const rowNumber of rowNumbers) { @@ -344,13 +344,13 @@ function areAllSelected({ numRows, selection }: { numRows: number, selection?: S // At that point, we don't know: it requires an async operation to check if all rows are selected } -async function fetchAreAllSelected({ data, numRows, selection, signal }: { data: DataFrameMethods, numRows: number, selection: Selection, signal?: AbortSignal }): Promise { +async function fetchAreAllSelected({ dataFrameMethods, numRows, selection, signal }: { dataFrameMethods: DataFrameMethods, numRows: number, selection: Selection, signal?: AbortSignal }): Promise { const syncAnswer = areAllSelected({ numRows, selection }) if (syncAnswer !== undefined) { return syncAnswer } // fetch all the row numbers in the table - const rowNumbers = await fetchRowNumbers({ data, rowStart: 0, rowEnd: numRows, signal }) + const rowNumbers = await fetchRowNumbers({ dataFrameMethods, rowStart: 0, rowEnd: numRows, signal }) // check if all these row numbers are in the selection return rowNumbers.length > 0 && rowNumbers.every(rowNumber => isSelected({ ranges: selection.ranges, index: rowNumber })) } diff --git a/src/providers/TableCornerSizeProvider.tsx b/src/providers/TableCornerSizeProvider.tsx index d184d9ba..51fecc9d 100644 --- a/src/providers/TableCornerSizeProvider.tsx +++ b/src/providers/TableCornerSizeProvider.tsx @@ -1,6 +1,6 @@ import { type ReactNode, useCallback, useState } from 'react' -import { SetTableCornerSizeContext, TableCornerHeightContext, TableCornerWidthContext } from '../contexts/TableCornerSizeContext.js' +import { defaultTableCornerHeight, SetTableCornerSizeContext, TableCornerHeightContext, TableCornerWidthContext } from '../contexts/TableCornerSizeContext.js' interface Props { /** Child components */ @@ -14,7 +14,7 @@ interface Props { */ export function TableCornerSizeProvider({ children }: Props) { const [tableCornerWidth, setTableCornerWidth] = useState(undefined) - const [tableCornerHeight, setTableCornerHeight] = useState(undefined) + const [tableCornerHeight, setTableCornerHeight] = useState(defaultTableCornerHeight) const setTableCornerSize = useCallback((element: HTMLElement) => { // we use offsetWidth and offsetHeight as they include padding, borders, and scrollbars (when present) setTableCornerWidth(element.offsetWidth) diff --git a/test/providers/DataProvider.test.tsx b/test/providers/DataProvider.test.tsx index 3d4bceb4..aa7eb39c 100644 --- a/test/providers/DataProvider.test.tsx +++ b/test/providers/DataProvider.test.tsx @@ -1,22 +1,21 @@ import { render } from '@testing-library/react' -import { act } from 'react' -import { describe, expect, it } from 'vitest' +import { act, useContext } from 'react' +import { describe, expect, it, vi } from 'vitest' -import { useColumnDescriptors, useData, useDataKey, useDataVersion, useExclusiveSort, useNumColumns, useNumRows } from '../../src/contexts/DataContext.js' +import { ColumnDescriptorsContext, DataFrameMethodsContext, DataKeyContext, DataVersionContext, ExclusiveSortContext, NumColumnsContext, NumRowsContext } from '../../src/contexts/DataContext.js' import type { DataFrame, DataFrameEvents } from '../../src/helpers/dataframe/index.js' import { arrayDataFrame } from '../../src/helpers/dataframe/index.js' import { createEventTarget } from '../../src/helpers/typedEventTarget.js' import { DataProvider } from '../../src/providers/DataProvider.js' function DisplayComponent() { - const dataKey = useDataKey() - const dataVersion = useDataVersion() - const numRows = useNumRows() - const columnDescriptors = useColumnDescriptors() - const numColumns = useNumColumns() - const exclusiveSort = useExclusiveSort() ? 'true' : 'false' - // used only to check if the data can be obtained (no error thrown) - useData() + const dataKey = useContext(DataKeyContext) + const dataVersion = useContext(DataVersionContext) + const numRows = useContext(NumRowsContext) + const columnDescriptors = useContext(ColumnDescriptorsContext) + const numColumns = useContext(NumColumnsContext) + const exclusiveSort = useContext(ExclusiveSortContext) ? 'true' : 'false' + const dataFrameMethods = useContext(DataFrameMethodsContext) return (
@@ -26,6 +25,30 @@ function DisplayComponent() { {JSON.stringify(columnDescriptors)} {numColumns} {exclusiveSort} + + +
) } @@ -48,6 +71,47 @@ describe('DataProvider', () => { expect(getByTestId('exclusive-sort').textContent).toBe('false') }) + it('should be able to call the data frame methods from the context', () => { + const data = arrayDataFrame([{ a: 1, b: 2 }, { a: 3, b: 4 }]) + data.getCell = vi.fn() + data.getRowNumber = vi.fn() + data.fetch = vi.fn().mockResolvedValue(undefined) + const { getByTestId } = render() + act(() => { + getByTestId('get-cell').click() + }) + expect(data.getCell).toHaveBeenCalledWith({ row: 0, column: 'col1' }) + act(() => { + getByTestId('get-row-number').click() + }) + expect(data.getRowNumber).toHaveBeenCalledWith({ row: 0 }) + act(() => { + getByTestId('fetch').click() + }) + expect(data.fetch).toHaveBeenCalledWith({ rowStart: 0, rowEnd: 10 }) + }) + + it('uses the current data frame methods, even if they change after the initial render', () => { + const data = arrayDataFrame([{ a: 1, b: 2 }, { a: 3, b: 4 }]) + const getCell1 = vi.fn() + const getCell2 = vi.fn() + data.getCell = getCell1 + const { getByTestId, rerender } = render() + act(() => { + getByTestId('get-cell').click() + }) + expect(getCell1).toHaveBeenCalledWith({ row: 0, column: 'col1' }) + + // Change the getCell method in the data frame + data.getCell = getCell2 + // force a re-render without changing the data frame instance + rerender() + act(() => { + getByTestId('get-cell').click() + }) + expect(getCell2).toHaveBeenCalledWith({ row: 0, column: 'col1' }) + }) + describe('on observable data frame change', () => { it('should increment version on data resolution, but keep the same key', async () => { const data = arrayDataFrame([{ a: 1 }, { a: 2 }]) diff --git a/test/providers/SelectionProvider.test.tsx b/test/providers/SelectionProvider.test.tsx index 17ef290b..78e4776d 100644 --- a/test/providers/SelectionProvider.test.tsx +++ b/test/providers/SelectionProvider.test.tsx @@ -2,7 +2,7 @@ import { fireEvent, render } from '@testing-library/react' import { act, useContext } from 'react' import { describe, expect, it, vi } from 'vitest' -import { DataContext, NumRowsContext } from '../../src/contexts/DataContext.js' +import { DataFrameMethodsContext, NumRowsContext } from '../../src/contexts/DataContext.js' import { SelectionContext } from '../../src/contexts/SelectionContext.js' import { arrayDataFrame } from '../../src/helpers/dataframe/array.js' import { SelectionProvider } from '../../src/providers/SelectionProvider.js' @@ -25,13 +25,13 @@ describe('SelectionProvider', () => { const data = arrayDataFrame(Array.from({ length: 5 }, (_, i) => ({ id: i }))) const onSelectionChange = vi.fn() const { getByTestId, rerender } = render( - + - + ) expect(getByTestId('all-rows-selected').textContent).toBe('false') @@ -56,13 +56,13 @@ describe('SelectionProvider', () => { // eslint-disable-next-line @typescript-eslint/require-await await act(async () => { rerender( - + - + ) }) expect(getByTestId('all-rows-selected').textContent).toBe('false') diff --git a/test/providers/TableCornerSizeProvider.test.tsx b/test/providers/TableCornerSizeProvider.test.tsx index bf6eec64..d978f7ca 100644 --- a/test/providers/TableCornerSizeProvider.test.tsx +++ b/test/providers/TableCornerSizeProvider.test.tsx @@ -2,14 +2,14 @@ import { act, render } from '@testing-library/react' import { useContext } from 'react' import { describe, expect, it } from 'vitest' -import { TableCornerHeightContext, useHeaderHeight, useSetTableCornerSize, useTableCornerWidth } from '../../src/contexts/TableCornerSizeContext.js' +import { SetTableCornerSizeContext, TableCornerHeightContext, TableCornerWidthContext } from '../../src/contexts/TableCornerSizeContext.js' +import { rowHeight } from '../../src/helpers/constants.js' import { TableCornerSizeProvider } from '../../src/providers/TableCornerSizeProvider.js' function TestComponent() { - const tableCornerWidth = useTableCornerWidth() + const tableCornerWidth = useContext(TableCornerWidthContext) const tableCornerHeight = useContext(TableCornerHeightContext) - const headerHeight = useHeaderHeight() - const setTableCornerSize = useSetTableCornerSize() + const setTableCornerSize = useContext(SetTableCornerSizeContext) return (
@@ -18,9 +18,6 @@ function TestComponent() {
{tableCornerHeight}
-
- {headerHeight} -
) @@ -34,11 +31,9 @@ describe('TableCornerSizeProvider', () => { ) - // Initially, the size is undefined + // Initially, the width is undefined and the height is the default value expect(getByTestId('width').textContent).toBe('') - expect(getByTestId('height').textContent).toBe('') - // The header height should be the default row height (33) - expect(getByTestId('header-height').textContent).toBe('33') + expect(getByTestId('height').textContent).toBe(rowHeight.toString()) // Click the button to set the size act(() => { @@ -48,6 +43,5 @@ describe('TableCornerSizeProvider', () => { // After clicking the button, the size should be updated expect(getByTestId('width').textContent).toBe('200') expect(getByTestId('height').textContent).toBe('100') - expect(getByTestId('header-height').textContent).toBe('100') // the header height should be updated to the new height }) }) diff --git a/test/providers/ViewportSizeProvider.test.tsx b/test/providers/ViewportSizeProvider.test.tsx index 8a77ee66..24edde8b 100644 --- a/test/providers/ViewportSizeProvider.test.tsx +++ b/test/providers/ViewportSizeProvider.test.tsx @@ -1,13 +1,14 @@ import { act, render } from '@testing-library/react' +import { useContext } from 'react' import { describe, expect, it } from 'vitest' -import { useSetViewportSize, useViewportHeight, useViewportWidth } from '../../src/contexts/ViewportSizeContext.js' +import { SetViewportSizeContext, ViewportHeightContext, ViewportWidthContext } from '../../src/contexts/ViewportSizeContext.js' import { ViewportSizeProvider } from '../../src/providers/ViewportSizeProvider.js' function TestComponent() { - const viewportWidth = useViewportWidth() - const viewportHeight = useViewportHeight() - const setViewportSize = useSetViewportSize() + const viewportWidth = useContext(ViewportWidthContext) + const viewportHeight = useContext(ViewportHeightContext) + const setViewportSize = useContext(SetViewportSizeContext) return (