@opencorestack/opengridx 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -97,6 +97,45 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
97
97
  const expandCellRef = React.useRef<HTMLDivElement>(null);
98
98
  const checkboxCellRef = React.useRef<HTMLDivElement>(null);
99
99
 
100
+ // All hooks must come before any early returns (Rules of Hooks)
101
+ React.useLayoutEffect(() => {
102
+ if (focusedCellField === '__expand_col__' && expandCellRef.current) {
103
+ expandCellRef.current.focus({ preventScroll: true });
104
+ }
105
+ if (focusedCellField === '__checkbox_col__' && checkboxCellRef.current) {
106
+ checkboxCellRef.current.focus({ preventScroll: true });
107
+ }
108
+ }, [focusedCellField]);
109
+
110
+ const pinnedPositions = React.useMemo(() => {
111
+ return calculatePinnedPositions(
112
+ columns,
113
+ columnWidths,
114
+ pinnedColumns,
115
+ checkboxSelection,
116
+ pinCheckboxColumn,
117
+ hasDetailPanel,
118
+ pinExpandColumn,
119
+ rowReordering
120
+ );
121
+ }, [columns, columnWidths, pinnedColumns, checkboxSelection, pinCheckboxColumn, hasDetailPanel, pinExpandColumn, rowReordering]);
122
+
123
+ const isGroupRow = (row as any)._hasChildren === true;
124
+
125
+ const handleCellEditStart = React.useCallback((field: string, value: any) => {
126
+ if (!isGroupRow) {
127
+ onEditStart?.({ id: row.id, field, value });
128
+ }
129
+ }, [isGroupRow, row.id, onEditStart]);
130
+
131
+ const handleCellValueChange = React.useCallback((field: string, newValue: any) => {
132
+ onEditCellValueChange?.({ id: row.id, field, value: newValue });
133
+ }, [onEditCellValueChange, row.id]);
134
+
135
+ const handleEditStopWrapper = React.useCallback((cancel?: boolean) => {
136
+ onEditStop?.({ cancel });
137
+ }, [onEditStop]);
138
+
100
139
  // ── Skeleton row (shown during infinite-scroll fetch) ─────────────────────
101
140
  if ((row as any)._isSkeleton) {
102
141
  // Render shimmer placeholders — one per visible column, matching row height
@@ -117,15 +156,6 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
117
156
  );
118
157
  }
119
158
 
120
- React.useLayoutEffect(() => {
121
- if (focusedCellField === '__expand_col__' && expandCellRef.current) {
122
- expandCellRef.current.focus({ preventScroll: true });
123
- }
124
- if (focusedCellField === '__checkbox_col__' && checkboxCellRef.current) {
125
- checkboxCellRef.current.focus({ preventScroll: true });
126
- }
127
- }, [focusedCellField]);
128
-
129
159
  const handleRowClick = (event: React.MouseEvent) => {
130
160
 
131
161
  if ((event.target as HTMLElement).closest('.ogx-checkbox-wrapper, .ogx-expand-icon, .ogx-drag-handle, .ogx__edit-cell')) {
@@ -144,39 +174,10 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
144
174
  onDetailPanelToggle?.(row.id);
145
175
  };
146
176
 
147
- const pinnedPositions = React.useMemo(() => {
148
- return calculatePinnedPositions(
149
- columns,
150
- columnWidths,
151
- pinnedColumns,
152
- checkboxSelection,
153
- pinCheckboxColumn,
154
- hasDetailPanel,
155
- pinExpandColumn,
156
- rowReordering
157
- );
158
- }, [columns, columnWidths, pinnedColumns, checkboxSelection, pinCheckboxColumn, hasDetailPanel, pinExpandColumn, rowReordering]);
159
-
160
177
  const rowPinnedPosition = isRowPinned(row.id, pinnedRows);
161
178
  const isRowPinnedTop = rowPinnedPosition === 'top';
162
179
  const isRowPinnedBottom = rowPinnedPosition === 'bottom';
163
180
 
164
- const isGroupRow = (row as any)._hasChildren === true;
165
-
166
- const handleCellEditStart = React.useCallback((field: string, value: any) => {
167
- if (!isGroupRow) {
168
- onEditStart?.({ id: row.id, field, value });
169
- }
170
- }, [isGroupRow, row.id, onEditStart]);
171
-
172
- const handleCellValueChange = React.useCallback((field: string, newValue: any) => {
173
- onEditCellValueChange?.({ id: row.id, field, value: newValue });
174
- }, [onEditCellValueChange, row.id]);
175
-
176
- const handleEditStopWrapper = React.useCallback((cancel?: boolean) => {
177
- onEditStop?.({ cancel });
178
- }, [onEditStop]);
179
-
180
181
  const classNames = [
181
182
  'ogx__row',
182
183
  isSelected && 'ogx__row--selected',
@@ -34,7 +34,7 @@ export function GlobalSearch({ value = '', onChange, placeholder = 'Search...',
34
34
  const containerRef = useRef<HTMLDivElement>(null);
35
35
  // Keep onChange always current without adding it to effect deps
36
36
  const onChangeRef = useRef(onChange);
37
- onChangeRef.current = onChange;
37
+ useLayoutEffect(() => { onChangeRef.current = onChange; }, [onChange]);
38
38
  // Track the last value we emitted upward
39
39
  const lastEmittedRef = useRef(value);
40
40
  // Track the last incoming prop value to detect external resets
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef, useEffect, useCallback } from 'react';
1
+ import React, { useState, useRef, useEffect, useCallback, useLayoutEffect } from 'react';
2
2
  import ReactDOM from 'react-dom';
3
3
  import type { GridColDef, GridAggregationModel, GridPivotModel, GridFilterModel } from '../../types';
4
4
  import { PivotPanel, PivotIcon } from './PivotPanel';
@@ -87,6 +87,11 @@ function AggregationPanel({
87
87
  }) {
88
88
  const panelRef = useRef<HTMLDivElement>(null);
89
89
  const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
90
+ const [portalContainer, setPortalContainer] = useState<Element>(document.body);
91
+
92
+ useLayoutEffect(() => {
93
+ setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
94
+ }, [anchorRef]);
90
95
 
91
96
  const computePos = useCallback((): PanelPosition => {
92
97
  if (!anchorRef.current) return { top: 0, right: 0 };
@@ -185,7 +190,7 @@ function AggregationPanel({
185
190
  </div>
186
191
  );
187
192
 
188
- return ReactDOM.createPortal(panel, anchorRef.current?.closest('.ogx-theme-provider') || document.body);
193
+ return ReactDOM.createPortal(panel, portalContainer);
189
194
  }
190
195
 
191
196
  function ColumnsPanelWrapper({
@@ -209,6 +214,11 @@ function ColumnsPanelWrapper({
209
214
  }) {
210
215
  const panelRef = useRef<HTMLDivElement>(null);
211
216
  const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
217
+ const [portalContainer, setPortalContainer] = useState<Element>(document.body);
218
+
219
+ useLayoutEffect(() => {
220
+ setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
221
+ }, [anchorRef]);
212
222
 
213
223
  const computePos = useCallback((): PanelPosition => {
214
224
  if (!anchorRef.current) return { top: 0, right: 0 };
@@ -262,7 +272,7 @@ function ColumnsPanelWrapper({
262
272
  </div>
263
273
  );
264
274
 
265
- return ReactDOM.createPortal(panel, anchorRef.current?.closest('.ogx-theme-provider') || document.body);
275
+ return ReactDOM.createPortal(panel, portalContainer);
266
276
  }
267
277
 
268
278
  function FilterPanelWrapper({
@@ -284,7 +294,12 @@ function FilterPanelWrapper({
284
294
  // tearing down and re-registering on every filterModel update,
285
295
  // which could fire the close callback during the re-register window.
286
296
  const onCloseRef = useRef(onClose);
287
- onCloseRef.current = onClose;
297
+ useLayoutEffect(() => { onCloseRef.current = onClose; }, [onClose]);
298
+ const [portalContainer, setPortalContainer] = useState<Element>(document.body);
299
+
300
+ useLayoutEffect(() => {
301
+ setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
302
+ }, [anchorRef]);
288
303
 
289
304
  const computePos = useCallback((): PanelPosition => {
290
305
  if (!anchorRef.current) return { top: 0, right: 0 };
@@ -371,7 +386,7 @@ function FilterPanelWrapper({
371
386
  </div>
372
387
  );
373
388
 
374
- return ReactDOM.createPortal(panel, anchorRef.current?.closest('.ogx-theme-provider') || document.body);
389
+ return ReactDOM.createPortal(panel, portalContainer);
375
390
  }
376
391
 
377
392
  const EMPTY_PIVOT: GridPivotModel = { rowFields: [], columnFields: [], valueFields: [] };
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
2
  import ReactDOM from 'react-dom';
3
3
  import type { GridColDef } from '../../types';
4
4
  import type { GridPivotModel, GridPivotAggFn, GridPivotValueField } from '../../types';
@@ -31,6 +31,11 @@ export interface PivotPanelProps {
31
31
  export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: PivotPanelProps) {
32
32
  const panelRef = useRef<HTMLDivElement>(null);
33
33
  const [pos, setPos] = useState({ top: 0, right: 0 });
34
+ const [portalContainer, setPortalContainer] = useState<Element>(document.body);
35
+
36
+ useLayoutEffect(() => {
37
+ setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
38
+ }, [anchorRef]);
34
39
 
35
40
  const computePos = useCallback(() => {
36
41
  if (!anchorRef.current) return { top: 0, right: 0 };
@@ -235,5 +240,5 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
235
240
  </div>
236
241
  );
237
242
 
238
- return ReactDOM.createPortal(panel, anchorRef.current?.closest('.ogx-theme-provider') || document.body);
243
+ return ReactDOM.createPortal(panel, portalContainer);
239
244
  }
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
 
3
- export interface DragHandleIconProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { }
3
+ export type DragHandleIconProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
4
4
 
5
5
  export const DragHandleIcon: React.FC<DragHandleIconProps> = ({ className = '', ...props }) => {
6
6
  return (
@@ -1,5 +1,5 @@
1
1
 
2
- import { useReducer, useCallback, useMemo, useRef, useEffect } from 'react';
2
+ import { useReducer, useCallback, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
3
3
  import type {
4
4
  GridState,
5
5
  GridRowModel,
@@ -259,7 +259,7 @@ export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDa
259
259
  });
260
260
 
261
261
  const stateRef = useRef(state);
262
- stateRef.current = state;
262
+ useLayoutEffect(() => { stateRef.current = state; });
263
263
 
264
264
  // Sync internal state when the external `rows` prop changes
265
265
  const prevRowsRef = useRef(rows);
@@ -0,0 +1,371 @@
1
+ import { useState, useCallback } from 'react';
2
+ import type { GridRowModel, GridRowId, GridColDef, GridCellParams, GridSortItem, GridSortDirection } from '../../types';
3
+ import type { GridEditingState } from '../features/useGridEditing';
4
+
5
+ interface ColumnMetrics {
6
+ leftPinnedWidth: number;
7
+ rightPinnedWidth: number;
8
+ unpinnedAccWidths: number[];
9
+ unpinnedCols: { field: string }[];
10
+ totalSpecialsWidth: number;
11
+ pinnedSpecialsWidth: number;
12
+ }
13
+
14
+ interface VirtualizationSnapshot {
15
+ cumulativeHeights: number[];
16
+ pinnedTopHeight: number;
17
+ columnMetrics: ColumnMetrics | null;
18
+ }
19
+
20
+ export interface UseGridKeyboardNavigationParams<R extends GridRowModel> {
21
+ allRenderableRows: R[];
22
+ navigationColumns: { field: string; editable?: boolean; sortable?: boolean }[];
23
+ checkboxSelection: boolean;
24
+ selectedRowIds: Set<GridRowId>;
25
+ handleSelectionChange: (id: GridRowId, selected: boolean) => void;
26
+ handleDetailPanelToggle: (id: GridRowId) => void;
27
+ editingHandlers: {
28
+ editingCell: GridEditingState['editingCell'];
29
+ startCellEdit: (params: { id: GridRowId; field: string; value: unknown }) => void;
30
+ stopCellEdit: (params?: { cancel?: boolean }) => void;
31
+ };
32
+ setKeyboardMode: (mode: boolean) => void;
33
+ sortModel: GridSortItem[];
34
+ handleSort: (field: string, direction: GridSortDirection) => void;
35
+ isCellEditable?: (params: GridCellParams<R>) => boolean;
36
+ pagination: boolean;
37
+ pageSize: number;
38
+ virtualization: VirtualizationSnapshot;
39
+ viewportRef: React.RefObject<HTMLDivElement | null>;
40
+ }
41
+
42
+ export interface FocusedCell {
43
+ id: GridRowId;
44
+ field: string;
45
+ }
46
+
47
+ export interface UseGridKeyboardNavigationReturn {
48
+ focusedCell: FocusedCell | null;
49
+ setFocusedCell: React.Dispatch<React.SetStateAction<FocusedCell | null>>;
50
+ handleFocus: (event: React.FocusEvent<HTMLDivElement>) => void;
51
+ handleBlur: (event: React.FocusEvent<HTMLDivElement>) => void;
52
+ handleKeyDown: (event: React.KeyboardEvent) => void;
53
+ }
54
+
55
+ export function useGridKeyboardNavigation<R extends GridRowModel>(
56
+ params: UseGridKeyboardNavigationParams<R>
57
+ ): UseGridKeyboardNavigationReturn {
58
+ const {
59
+ allRenderableRows,
60
+ navigationColumns,
61
+ checkboxSelection,
62
+ selectedRowIds,
63
+ handleSelectionChange,
64
+ handleDetailPanelToggle,
65
+ editingHandlers,
66
+ setKeyboardMode,
67
+ sortModel,
68
+ handleSort,
69
+ isCellEditable,
70
+ pagination,
71
+ pageSize,
72
+ virtualization,
73
+ viewportRef,
74
+ } = params;
75
+
76
+ const [focusedCell, setFocusedCell] = useState<FocusedCell | null>(null);
77
+
78
+ const handleFocus = useCallback((event: React.FocusEvent<HTMLDivElement>) => {
79
+ if (event.target === event.currentTarget) {
80
+ setFocusedCell(prev => {
81
+ if (prev) return prev;
82
+ if (checkboxSelection) {
83
+ return { id: 'HEADER', field: '__checkbox_col__' };
84
+ }
85
+ const firstRow = allRenderableRows[0];
86
+ const firstCol = navigationColumns[0];
87
+ if (firstRow && firstCol) {
88
+ return { id: firstRow.id, field: firstCol.field };
89
+ }
90
+ return null;
91
+ });
92
+ }
93
+ }, [allRenderableRows, navigationColumns, checkboxSelection]);
94
+
95
+ const handleBlur = useCallback((event: React.FocusEvent<HTMLDivElement>) => {
96
+ if (!event.currentTarget.contains(event.relatedTarget as Node)) {
97
+ setFocusedCell(null);
98
+ }
99
+ }, []);
100
+
101
+ const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
102
+ setKeyboardMode(true);
103
+
104
+ if (!focusedCell) return;
105
+
106
+ const { id, field } = focusedCell;
107
+ const isEditing = Boolean(editingHandlers.editingCell);
108
+
109
+ if (id === 'HEADER' && (event.key === 'Enter' || event.key === ' ')) {
110
+ event.preventDefault();
111
+ const col = navigationColumns.find(c => c.field === field);
112
+ if (col && col.sortable !== false) {
113
+ const currentSort = sortModel.find(item => item.field === field);
114
+ let newDirection: GridSortDirection = 'asc';
115
+ if (currentSort) {
116
+ newDirection = currentSort.sort === 'asc' ? 'desc' : null;
117
+ }
118
+ handleSort(field, newDirection);
119
+ }
120
+ return;
121
+ }
122
+
123
+ if (event.key === 'Enter') {
124
+ event.preventDefault();
125
+ if (isEditing) {
126
+ editingHandlers.stopCellEdit();
127
+ } else {
128
+ const col = navigationColumns.find(c => c.field === field);
129
+ if (col?.editable) {
130
+ const row = allRenderableRows.find(r => r.id === id);
131
+ if (row) {
132
+ editingHandlers.startCellEdit({ id, field, value: (row as R & Record<string, unknown>)[field] });
133
+ }
134
+ }
135
+ }
136
+ return;
137
+ }
138
+
139
+ if (event.key === ' ' || event.key === 'Spacebar') {
140
+ if (!isEditing) {
141
+ if (field === '__checkbox_col__') {
142
+ event.preventDefault();
143
+ handleSelectionChange(id, !selectedRowIds.has(id));
144
+ return;
145
+ }
146
+ if (field === '__expand_col__') {
147
+ event.preventDefault();
148
+ handleDetailPanelToggle(id);
149
+ return;
150
+ }
151
+ }
152
+ }
153
+
154
+ if (event.key === 'Escape') {
155
+ if (isEditing) {
156
+ event.preventDefault();
157
+ editingHandlers.stopCellEdit({ cancel: true });
158
+ }
159
+ return;
160
+ }
161
+
162
+ if (isEditing && event.key !== 'Tab') return;
163
+
164
+ const isHeader = id === 'HEADER';
165
+ const rowIndex = isHeader ? -1 : allRenderableRows.findIndex(r => r.id === id);
166
+ const colIndex = navigationColumns.findIndex(c => c.field === field);
167
+
168
+ if (!isHeader && rowIndex === -1) return;
169
+ if (colIndex === -1) return;
170
+
171
+ const findNextCell = (
172
+ startRow: number,
173
+ startCol: number,
174
+ deltaRow: number,
175
+ deltaCol: number,
176
+ wrapRow: boolean,
177
+ allowHeader = false
178
+ ) => {
179
+ let r = startRow + deltaRow;
180
+ let c = startCol + deltaCol;
181
+
182
+ if (wrapRow && deltaCol !== 0) {
183
+ if (deltaCol > 0 && c >= navigationColumns.length) { c = 0; r++; }
184
+ else if (deltaCol < 0 && c < 0) { c = navigationColumns.length - 1; r--; }
185
+ }
186
+
187
+ if (!wrapRow && (c < 0 || c >= navigationColumns.length)) return null;
188
+ if (r < (allowHeader ? -1 : 0) || r >= allRenderableRows.length) return null;
189
+
190
+ return { r, c };
191
+ };
192
+
193
+ const findNextEditable = (
194
+ startRow: number,
195
+ startCol: number,
196
+ deltaRow: number,
197
+ deltaCol: number,
198
+ wrapRow: boolean,
199
+ allowHeader = false
200
+ ) => {
201
+ let r = startRow;
202
+ let c = startCol;
203
+ let steps = 0;
204
+ const maxSteps = allRenderableRows.length * navigationColumns.length + navigationColumns.length;
205
+
206
+ while (steps < maxSteps) {
207
+ steps++;
208
+ r += deltaRow;
209
+ c += deltaCol;
210
+
211
+ if (wrapRow && deltaCol !== 0) {
212
+ if (deltaCol > 0) {
213
+ if (c >= navigationColumns.length) { c = 0; r++; }
214
+ } else {
215
+ if (c < 0) { c = navigationColumns.length - 1; r--; }
216
+ }
217
+ } else {
218
+ if (c < 0 || c >= navigationColumns.length) return null;
219
+ }
220
+
221
+ if (r < -1 || r >= allRenderableRows.length) return null;
222
+
223
+ if (r === -1) {
224
+ if (!allowHeader) continue;
225
+ const col = navigationColumns[c];
226
+ const isSortable = col.sortable !== false;
227
+ if (['__checkbox_col__'].includes(col.field) || isSortable) return { r, c };
228
+ continue;
229
+ }
230
+
231
+ const row = allRenderableRows[r];
232
+ const col = navigationColumns[c];
233
+ const isInteractable = ['__checkbox_col__', '__expand_col__', '__reorder_col__'].includes(col.field);
234
+ let cellEditable = col.editable || isInteractable;
235
+
236
+ if (isCellEditable && !isInteractable) {
237
+ try {
238
+ cellEditable = isCellEditable({
239
+ row,
240
+ field: col.field,
241
+ value: (row as R & Record<string, unknown>)[col.field],
242
+ colDef: col as GridColDef<R>,
243
+ rowIndex: r,
244
+ colIndex: c
245
+ });
246
+ } catch {
247
+ // isCellEditable threw — treat cell as non-editable
248
+ }
249
+ }
250
+
251
+ if (cellEditable) return { r, c };
252
+ }
253
+ return null;
254
+ };
255
+
256
+ let nextRowIndex = rowIndex;
257
+ let nextColIndex = colIndex;
258
+ let handled = false;
259
+
260
+ if (event.key === 'Tab') {
261
+ const dir = event.shiftKey ? -1 : 1;
262
+ const res = findNextEditable(rowIndex, colIndex, 0, dir, true, false);
263
+ if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
264
+ } else if (event.key === 'ArrowRight') {
265
+ const res = findNextCell(rowIndex, colIndex, 0, 1, true, true);
266
+ if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
267
+ } else if (event.key === 'ArrowLeft') {
268
+ const res = findNextCell(rowIndex, colIndex, 0, -1, true, true);
269
+ if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
270
+ } else if (event.key === 'ArrowDown') {
271
+ const res = findNextCell(rowIndex, colIndex, 1, 0, false, true);
272
+ if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
273
+ } else if (event.key === 'ArrowUp') {
274
+ const res = findNextCell(rowIndex, colIndex, -1, 0, false, true);
275
+ if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
276
+ } else if (event.key === 'Home') {
277
+ nextColIndex = 0;
278
+ if (event.ctrlKey || event.metaKey) nextRowIndex = -1;
279
+ handled = true;
280
+ } else if (event.key === 'End') {
281
+ nextColIndex = navigationColumns.length - 1;
282
+ if (event.ctrlKey || event.metaKey) nextRowIndex = allRenderableRows.length - 1;
283
+ handled = true;
284
+ } else if (event.key === 'PageUp') {
285
+ const pg = pagination ? pageSize : 10;
286
+ nextRowIndex = Math.max(-1, nextRowIndex - pg);
287
+ handled = true;
288
+ } else if (event.key === 'PageDown') {
289
+ const pg = pagination ? pageSize : 10;
290
+ nextRowIndex = Math.min(allRenderableRows.length - 1, nextRowIndex + pg);
291
+ handled = true;
292
+ }
293
+
294
+ if (handled) {
295
+ event.preventDefault();
296
+
297
+ if (isEditing && event.key === 'Tab') {
298
+ editingHandlers.stopCellEdit();
299
+ }
300
+
301
+ if (nextRowIndex >= -1 && nextRowIndex < allRenderableRows.length &&
302
+ nextColIndex >= 0 && nextColIndex < navigationColumns.length) {
303
+
304
+ const nextCol = navigationColumns[nextColIndex];
305
+
306
+ if (nextRowIndex === -1) {
307
+ setFocusedCell({ id: 'HEADER', field: nextCol.field });
308
+ } else {
309
+ setFocusedCell({ id: allRenderableRows[nextRowIndex].id, field: nextCol.field });
310
+ }
311
+
312
+ const el = viewportRef.current;
313
+ if (el) {
314
+ const { cumulativeHeights, pinnedTopHeight, columnMetrics } = virtualization;
315
+ const { clientHeight, clientWidth, scrollTop, scrollLeft } = el;
316
+
317
+ const rowTop = nextRowIndex <= 0 ? 0 : cumulativeHeights[nextRowIndex - 1];
318
+ const rowBottom = cumulativeHeights[nextRowIndex];
319
+ const visibleTop = scrollTop + pinnedTopHeight;
320
+ const visibleBottom = scrollTop + clientHeight;
321
+
322
+ let newScrollTop = scrollTop;
323
+ if (rowTop < visibleTop) {
324
+ newScrollTop = Math.max(0, rowTop - pinnedTopHeight);
325
+ } else if (rowBottom > visibleBottom) {
326
+ newScrollTop = rowBottom - clientHeight;
327
+ }
328
+ if (newScrollTop !== scrollTop) el.scrollTop = newScrollTop;
329
+
330
+ if (columnMetrics) {
331
+ const { leftPinnedWidth, rightPinnedWidth, unpinnedAccWidths, unpinnedCols, totalSpecialsWidth, pinnedSpecialsWidth } = columnMetrics;
332
+ const unpinnedIndex = unpinnedCols.findIndex(c => c.field === nextCol.field);
333
+
334
+ if (unpinnedIndex !== -1) {
335
+ const colLeft = totalSpecialsWidth + leftPinnedWidth + (unpinnedIndex > 0 ? unpinnedAccWidths[unpinnedIndex - 1] : 0);
336
+ const colRight = totalSpecialsWidth + leftPinnedWidth + unpinnedAccWidths[unpinnedIndex];
337
+ const visibleStart = scrollLeft + pinnedSpecialsWidth + leftPinnedWidth;
338
+ const visibleEnd = scrollLeft + clientWidth - rightPinnedWidth;
339
+
340
+ let newScrollLeft = scrollLeft;
341
+ if (colLeft < visibleStart) {
342
+ newScrollLeft = Math.max(0, colLeft - pinnedSpecialsWidth - leftPinnedWidth);
343
+ } else if (colRight > visibleEnd) {
344
+ newScrollLeft = colRight - clientWidth + rightPinnedWidth;
345
+ }
346
+ if (newScrollLeft !== scrollLeft) el.scrollLeft = newScrollLeft;
347
+ }
348
+ }
349
+ }
350
+ }
351
+ }
352
+ }, [
353
+ focusedCell,
354
+ allRenderableRows,
355
+ navigationColumns,
356
+ editingHandlers,
357
+ virtualization,
358
+ selectedRowIds,
359
+ handleSelectionChange,
360
+ handleDetailPanelToggle,
361
+ setKeyboardMode,
362
+ sortModel,
363
+ handleSort,
364
+ isCellEditable,
365
+ pagination,
366
+ pageSize,
367
+ viewportRef,
368
+ ]);
369
+
370
+ return { focusedCell, setFocusedCell, handleFocus, handleBlur, handleKeyDown };
371
+ }