|
1 | | -import type { KeyboardEvent } from 'react' |
2 | | -import { useCallback, useContext, useMemo } from 'react' |
| 1 | +import { useContext, useMemo } from 'react' |
3 | 2 |
|
4 | | -import { CellNavigationContext } from '../contexts/CellNavigationContext.js' |
5 | | -import { ColumnsVisibilityContext } from '../contexts/ColumnsVisibilityContext.js' |
6 | | -import { DataFrameMethodsContext, DataVersionContext, NumRowsContext } from '../contexts/DataContext.js' |
7 | | -import { OrderByContext } from '../contexts/OrderByContext.js' |
8 | | -import { ScrollContext } from '../contexts/ScrollContext.js' |
9 | | -import { SelectionContext } from '../contexts/SelectionContext.js' |
10 | | -import { ariaOffset } from '../helpers/constants.js' |
11 | | -import { useFetchCells } from '../hooks/useFetchCells.js' |
12 | | -import type { HighTableProps } from '../types.js' |
13 | | -import { stringify as stringifyDefault } from '../utils/stringify.js' |
14 | | -import Cell from './Cell.js' |
15 | | -import Row from './Row.js' |
16 | | -import RowHeader from './RowHeader.js' |
17 | | -import TableCorner from './TableCorner.js' |
18 | | -import TableHeader from './TableHeader.js' |
| 3 | +import { SliceTopContext } from '../contexts/ScrollContext.js' |
19 | 4 |
|
20 | | -type SliceProps = Pick<HighTableProps, 'onDoubleClickCell' | 'onError' | 'onKeyDownCell' | 'onMouseDownCell' | 'overscan' | 'renderCellContent' | 'stringify'> |
21 | | - |
22 | | -export default function Slice({ |
23 | | - overscan, |
24 | | - onDoubleClickCell, |
25 | | - onError, |
26 | | - onKeyDownCell, |
27 | | - onMouseDownCell, |
28 | | - renderCellContent, |
29 | | - stringify = stringifyDefault, |
30 | | -}: SliceProps) { |
31 | | - const { moveCell } = useContext(CellNavigationContext) |
32 | | - const orderBy = useContext(OrderByContext) |
33 | | - const { selectable, toggleAllRows, pendingSelectionGesture, onTableKeyDown: onSelectionTableKeyDown, allRowsSelected, isRowSelected, toggleRowNumber, toggleRangeToRowNumber } = useContext(SelectionContext) |
34 | | - const { visibleColumnsParameters: columnsParameters } = useContext(ColumnsVisibilityContext) |
35 | | - const { renderedRowsStart, renderedRowsEnd } = useContext(ScrollContext) |
36 | | - /** A version number that increments whenever a data frame is updated or resolved (the key remains the same). */ |
37 | | - const version = useContext(DataVersionContext) |
38 | | - /** The actual number of rows in the data frame */ |
39 | | - const numRows = useContext(NumRowsContext) |
40 | | - const dataFrameMethods = useContext(DataFrameMethodsContext) |
41 | | - |
42 | | - // Fetch the required cells if needed (visible + overscan) |
43 | | - // it's a side-effect. |
44 | | - useFetchCells({ overscan, onError }) |
45 | | - |
46 | | - const onNavigationTableKeyDown = useMemo(() => { |
47 | | - if (!moveCell) { |
48 | | - // disable keyboard navigation if moveCell is not provided |
49 | | - return |
50 | | - } |
51 | | - return (event: KeyboardEvent) => { |
52 | | - const { key, altKey, ctrlKey, metaKey, shiftKey } = event |
53 | | - // if the user is pressing Alt, Meta or Shift, do not handle the event |
54 | | - if (altKey || metaKey || shiftKey) { |
55 | | - return |
56 | | - } |
57 | | - if (key === 'ArrowRight') { |
58 | | - if (ctrlKey) { |
59 | | - moveCell({ type: 'LAST_COLUMN' }) |
60 | | - } else { |
61 | | - moveCell({ type: 'NEXT_COLUMN' }) |
62 | | - } |
63 | | - } else if (key === 'ArrowLeft') { |
64 | | - if (ctrlKey) { |
65 | | - moveCell({ type: 'FIRST_COLUMN' }) |
66 | | - } else { |
67 | | - moveCell({ type: 'PREVIOUS_COLUMN' }) |
68 | | - } |
69 | | - } else if (key === 'ArrowDown') { |
70 | | - if (ctrlKey) { |
71 | | - moveCell({ type: 'LAST_ROW' }) |
72 | | - } else { |
73 | | - moveCell({ type: 'NEXT_ROW' }) |
74 | | - } |
75 | | - } else if (key === 'ArrowUp') { |
76 | | - if (ctrlKey) { |
77 | | - moveCell({ type: 'FIRST_ROW' }) |
78 | | - } else { |
79 | | - moveCell({ type: 'PREVIOUS_ROW' }) |
80 | | - } |
81 | | - } else if (key === 'Home') { |
82 | | - if (ctrlKey) { |
83 | | - moveCell({ type: 'FIRST_CELL' }) |
84 | | - } else { |
85 | | - moveCell({ type: 'FIRST_COLUMN' }) |
86 | | - } |
87 | | - } else if (key === 'End') { |
88 | | - if (ctrlKey) { |
89 | | - moveCell({ type: 'LAST_CELL' }) |
90 | | - } else { |
91 | | - moveCell({ type: 'LAST_COLUMN' }) |
92 | | - } |
93 | | - } else if (key === 'PageDown') { |
94 | | - moveCell({ type: 'NEXT_ROWS_PAGE' }) |
95 | | - // TODO(SL): same for horizontal scrolling with Alt+PageDown? |
96 | | - } else if (key === 'PageUp') { |
97 | | - moveCell({ type: 'PREVIOUS_ROWS_PAGE' }) |
98 | | - // TODO(SL): same for horizontal scrolling with Alt+PageUp? |
99 | | - } else if (key !== ' ') { |
100 | | - // if the key is not one of the above, do not handle it |
101 | | - // special case: no action is associated with the Space key, but it's captured |
102 | | - // anyway to prevent the default action (scrolling the page) and stay in navigation mode |
103 | | - return |
104 | | - } |
105 | | - // avoid scrolling the table when the user is navigating with the keyboard |
106 | | - event.stopPropagation() |
107 | | - event.preventDefault() |
108 | | - } |
109 | | - }, [moveCell]) |
110 | | - |
111 | | - const onTableKeyDown = useMemo(() => { |
112 | | - if (onNavigationTableKeyDown || onSelectionTableKeyDown) { |
113 | | - return (event: KeyboardEvent) => { |
114 | | - onNavigationTableKeyDown?.(event) |
115 | | - onSelectionTableKeyDown?.(event) |
116 | | - } |
117 | | - } |
118 | | - }, [onNavigationTableKeyDown, onSelectionTableKeyDown]) |
119 | | - |
120 | | - const getOnCheckboxPress = useCallback(({ row, rowNumber }: { row: number, rowNumber?: number }) => { |
121 | | - if (rowNumber === undefined || !toggleRowNumber || !toggleRangeToRowNumber) { |
122 | | - return undefined |
123 | | - } |
124 | | - return ({ shiftKey }: { shiftKey: boolean }) => { |
125 | | - if (shiftKey) { |
126 | | - toggleRangeToRowNumber({ row, rowNumber }) |
127 | | - } else { |
128 | | - toggleRowNumber({ rowNumber }) |
129 | | - } |
130 | | - } |
131 | | - }, [toggleRowNumber, toggleRangeToRowNumber]) |
132 | | - |
133 | | - // Prepare the slice of data to render |
134 | | - // TODO(SL): also compute progress percentage here, to show a loading indicator |
135 | | - const slice = useMemo(() => { |
136 | | - if (renderedRowsStart === undefined || renderedRowsEnd === undefined) { |
137 | | - return { |
138 | | - rowContents: [], |
139 | | - canMeasureColumn: {}, |
140 | | - version, |
141 | | - } |
142 | | - } |
143 | | - const rows = Array.from({ length: renderedRowsEnd - renderedRowsStart }, (_, i) => renderedRowsStart + i) |
| 5 | +interface Props { |
| 6 | + /** Child components */ |
| 7 | + children?: React.ReactNode |
| 8 | +} |
144 | 9 |
|
145 | | - const canMeasureColumn: Record<string, boolean> = {} |
146 | | - const rowContents = rows.map((row) => { |
147 | | - const rowNumber = dataFrameMethods.getRowNumber({ row, orderBy })?.value |
148 | | - const cells = (columnsParameters ?? []).map(({ name: column, index: originalColumnIndex, className }) => { |
149 | | - const cell = dataFrameMethods.getCell({ row, column, orderBy }) |
150 | | - canMeasureColumn[column] ||= cell !== undefined |
151 | | - return { columnIndex: originalColumnIndex, cell, className } |
152 | | - }) |
153 | | - return { |
154 | | - row, |
155 | | - rowNumber, |
156 | | - cells, |
157 | | - } |
158 | | - }) |
159 | | - return { |
160 | | - rowContents, |
161 | | - canMeasureColumn, |
162 | | - version, |
163 | | - } |
164 | | - }, [dataFrameMethods, columnsParameters, renderedRowsStart, renderedRowsEnd, orderBy, version]) |
| 10 | +export default function Slice({ children }: Props) { |
| 11 | + const sliceTop = useContext(SliceTopContext) |
165 | 12 |
|
166 | | - // don't render table if the data frame has no visible columns |
167 | | - // (it can have zero rows, but must have at least one visible column) |
168 | | - if (!columnsParameters) return |
| 13 | + const sliceTopStyle = useMemo(() => { |
| 14 | + return sliceTop !== undefined ? { top: `${sliceTop}px` } : {} |
| 15 | + }, [sliceTop]) |
169 | 16 |
|
170 | | - const ariaColCount = columnsParameters.length + 1 // don't forget the selection column |
171 | | - const ariaRowCount = numRows + 1 // don't forget the header row |
172 | 17 | return ( |
173 | | - <table |
174 | | - aria-readonly={true} |
175 | | - aria-colcount={ariaColCount} |
176 | | - aria-rowcount={ariaRowCount} |
177 | | - aria-multiselectable={selectable} |
178 | | - aria-busy={pendingSelectionGesture /* TODO(SL): add other busy states? Used only for tests right now */} |
179 | | - role="grid" |
180 | | - onKeyDown={onTableKeyDown} |
181 | | - > |
182 | | - <caption id="caption" hidden>Virtual-scroll table</caption> |
183 | | - <thead role="rowgroup"> |
184 | | - <Row ariaRowIndex={1}> |
185 | | - <TableCorner |
186 | | - onCheckboxPress={toggleAllRows} |
187 | | - checked={allRowsSelected} |
188 | | - pendingSelectionGesture={pendingSelectionGesture} |
189 | | - ariaColIndex={1} |
190 | | - ariaRowIndex={1} |
191 | | - /> |
192 | | - <TableHeader |
193 | | - canMeasureColumn={slice.canMeasureColumn} |
194 | | - columnsParameters={columnsParameters} |
195 | | - ariaRowIndex={1} |
196 | | - /> |
197 | | - </Row> |
198 | | - </thead> |
199 | | - <tbody role="rowgroup"> |
200 | | - {slice.rowContents.map(({ row, rowNumber, cells }) => { |
201 | | - const ariaRowIndex = row + ariaOffset |
202 | | - const selected = isRowSelected?.({ rowNumber }) |
203 | | - const rowKey = `${row}` |
204 | | - return ( |
205 | | - <Row |
206 | | - key={rowKey} |
207 | | - ariaRowIndex={ariaRowIndex} |
208 | | - selected={selected} |
209 | | - rowNumber={rowNumber} |
210 | | - // title={rowError(row, columns.length)} // TODO(SL): re-enable later? |
211 | | - > |
212 | | - <RowHeader |
213 | | - selected={selected} |
214 | | - rowNumber={rowNumber} |
215 | | - onCheckboxPress={getOnCheckboxPress({ rowNumber, row })} |
216 | | - pendingSelectionGesture={pendingSelectionGesture} |
217 | | - ariaColIndex={1} |
218 | | - ariaRowIndex={ariaRowIndex} |
219 | | - /> |
220 | | - {cells.map(({ columnIndex, cell, className }, visibleColumnIndex) => { |
221 | | - return ( |
222 | | - <Cell |
223 | | - key={columnIndex} |
224 | | - onDoubleClickCell={onDoubleClickCell} |
225 | | - onMouseDownCell={onMouseDownCell} |
226 | | - onKeyDownCell={onKeyDownCell} |
227 | | - stringify={stringify} |
228 | | - columnIndex={columnIndex} |
229 | | - visibleColumnIndex={visibleColumnIndex} |
230 | | - className={className} |
231 | | - ariaColIndex={visibleColumnIndex + ariaOffset} |
232 | | - ariaRowIndex={ariaRowIndex} |
233 | | - cellValue={cell?.value} |
234 | | - hasResolved={cell !== undefined} |
235 | | - rowNumber={rowNumber} |
236 | | - renderCellContent={renderCellContent} |
237 | | - /> |
238 | | - ) |
239 | | - })} |
240 | | - </Row> |
241 | | - ) |
242 | | - })} |
243 | | - </tbody> |
244 | | - </table> |
| 18 | + <div style={sliceTopStyle}> |
| 19 | + {children} |
| 20 | + </div> |
245 | 21 | ) |
246 | 22 | } |
0 commit comments