From e6218c2bdccc23b5229c9a51424f9e25c4a93aaa Mon Sep 17 00:00:00 2001 From: Sylvain Lesage Date: Sat, 11 Jan 2025 00:41:34 +0100 Subject: [PATCH] replace selection in state by onSelectionChange for HighTable and by selectionAndAnchor and setSelectionAndAnchor in ControlledHighTable --- src/ControlledHighTable.tsx | 50 ++++++++++++++++++++----------------- src/HighTable.tsx | 30 +++++++++++++++++----- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/src/ControlledHighTable.tsx b/src/ControlledHighTable.tsx index 5797454..19de844 100644 --- a/src/ControlledHighTable.tsx +++ b/src/ControlledHighTable.tsx @@ -21,24 +21,26 @@ export type { Selection } from './selection.js' const rowHeight = 33 // row height px -export type State = { +export interface SelectionAndAnchor { + selection: Selection + anchor?: number // anchor row index for selection, the first element when selecting a range +} + +export type InternalState = { columnWidths: Array invalidate: boolean hasCompleteRow: boolean startIndex: number rows: AsyncRow[] orderBy?: string - selection: Selection - anchor?: number // anchor row index for selection, the first element when selecting a range } -export type Action = +export type InternalAction = | { type: 'SET_ROWS', start: number, rows: AsyncRow[], hasCompleteRow: boolean } | { type: 'SET_COLUMN_WIDTH', columnIndex: number, columnWidth: number | undefined } | { type: 'SET_COLUMN_WIDTHS', columnWidths: Array } | { type: 'SET_ORDER', orderBy: string | undefined } | { type: 'DATA_CHANGED' } - | { type: 'SET_SELECTION', selection: Selection, anchor?: number } export interface TableProps { data: DataFrame @@ -46,15 +48,16 @@ export interface TableProps { overscan?: number // number of rows to fetch outside of the viewport padding?: number // number of padding rows to render outside of the viewport focus?: boolean // focus table on mount? (default true) - selectable?: boolean // enable row selection (default false) onDoubleClickCell?: (event: React.MouseEvent, col: number, row: number) => void onMouseDownCell?: (event: React.MouseEvent, col: number, row: number) => void onError?: (error: Error) => void } export type ControlledTableProps = TableProps & { - state: State - dispatch: React.Dispatch + state: InternalState + dispatch: React.Dispatch + selectionAndAnchor?: SelectionAndAnchor // controlled selection state + setSelectionAndAnchor?: (selectionAndAnchor: SelectionAndAnchor) => void // controlled selection state setter } /** @@ -66,14 +69,19 @@ export default function ControlledHighTable({ overscan = 20, padding = 20, focus = true, - selectable = false, + selectionAndAnchor, + setSelectionAndAnchor, state, dispatch, onDoubleClickCell, onMouseDownCell, onError = console.error, }: ControlledTableProps) { - const { anchor, columnWidths, startIndex, rows, orderBy, invalidate, hasCompleteRow, selection } = state + const { columnWidths, startIndex, rows, orderBy, invalidate, hasCompleteRow } = state + + const selectable = selectionAndAnchor && setSelectionAndAnchor + const { selection, anchor } = selectionAndAnchor ?? { selection: [], anchor: undefined } + const offsetTopRef = useRef(0) const scrollRef = useRef(null) @@ -233,17 +241,13 @@ export default function ControlledHighTable({ const onRowNumberClick = useCallback(({ useAnchor, index }: {useAnchor: boolean, index: number}) => { - if (!selectable) return false + if (!setSelectionAndAnchor) return if (useAnchor) { - const newSelection = extendFromAnchor({ selection, anchor, index }) - // did not throw: we can set the anchor (keep the same) - dispatch({ type: 'SET_SELECTION', selection: newSelection, anchor }) + setSelectionAndAnchor({ selection: extendFromAnchor({ selection, anchor, index }), anchor }) } else { - const newSelection = toggleIndex({ selection, index }) - // did not throw: we can set the anchor - dispatch({ type: 'SET_SELECTION', selection: newSelection, anchor: index }) + setSelectionAndAnchor({ selection: toggleIndex({ selection, index }), anchor: index }) } - }, [selection, anchor]) + }, [setSelectionAndAnchor, selection, anchor]) // add empty pre and post rows to fill the viewport const prePadding = Array.from({ length: Math.min(padding, startIndex) }, () => []) @@ -287,10 +291,10 @@ export default function ControlledHighTable({ )} {rows.map((row, rowIndex) => - - onRowNumberClick({ useAnchor: event.shiftKey, index: rowNumber(rowIndex) })}> + + onRowNumberClick({ useAnchor: event.shiftKey, index: rowNumber(rowIndex) }))}> {rowNumber(rowIndex).toLocaleString()} - + { selectable && } {data.header.map((col, colIndex) => Cell(row[col], colIndex, startIndex + rowIndex, row.__index__?.resolved) @@ -308,9 +312,9 @@ export default function ControlledHighTable({ -
selectable && dispatch({ type: 'SET_SELECTION', selection: toggleAll({ selection, length: rows.length }), anchor: undefined })}> +
setSelectionAndAnchor({ selection: toggleAll({ selection, length: rows.length }), anchor: undefined }))}>   - + {selectable && }
 
diff --git a/src/HighTable.tsx b/src/HighTable.tsx index c91ae43..b5285bf 100644 --- a/src/HighTable.tsx +++ b/src/HighTable.tsx @@ -1,8 +1,9 @@ -import { useReducer } from 'react' -import type { Action, State, TableProps } from './ControlledHighTable.js' +import { useCallback, useReducer } from 'react' +import type { InternalAction, InternalState, SelectionAndAnchor, TableProps } from './ControlledHighTable.js' import ControlledHighTable from './ControlledHighTable.js' +import type { Selection } from './selection.js' export { stringify, throttle } from './ControlledHighTable.js' -export type { Action, ControlledTableProps, State, TableProps } from './ControlledHighTable.js' +export type { ControlledTableProps, InternalAction, InternalState, SelectionAndAnchor, TableProps } from './ControlledHighTable.js' export { arrayDataFrame, AsyncRow, asyncRows, awaitRow, @@ -24,6 +25,11 @@ export const initialState = { selection: [], } +type State = InternalState & SelectionAndAnchor + +type Action = InternalAction + | ({ type: 'SET_SELECTION' } & SelectionAndAnchor) + export function reducer(state: State, action: Action): State { switch (action.type) { case 'SET_ROWS': @@ -57,6 +63,10 @@ export function reducer(state: State, action: Action): State { } } +type HighTableProps = TableProps & { + onSelectionChange?: (selection: Selection) => void +} + /** * Render a table with streaming rows on demand from a DataFrame. */ @@ -66,20 +76,28 @@ export default function HighTable({ overscan = 20, padding = 20, focus = true, - selectable = false, + onSelectionChange, onDoubleClickCell, onMouseDownCell, onError = console.error, -}: TableProps) { +}: HighTableProps) { const [state, dispatch] = useReducer(reducer, initialState) + const selectable = onSelectionChange !== undefined + const selectionAndAnchor = selectable ? { selection: state.selection, anchor: state.anchor } : undefined + const setSelectionAndAnchor = useCallback((selectionAndAnchor: SelectionAndAnchor) => { + onSelectionChange?.(selectionAndAnchor.selection) + dispatch({ type: 'SET_SELECTION', ...selectionAndAnchor }) + }, [dispatch, onSelectionChange]) + return