@ornery/ui-grid-react 0.1.4 → 0.1.6

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.
@@ -1,4 +1,12 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ buildGridTemplateColumns,
4
+ computeViewportHeightPx,
5
+ computeViewportRows,
6
+ formatPaginationSummary,
7
+ orderVisibleColumns,
8
+ resolveBenchmarkIterations,
9
+ } from './gridStateMath';
2
10
  import {
3
11
  createGridApi,
4
12
  UiGridApi,
@@ -16,7 +24,7 @@ import {
16
24
  getCellValue,
17
25
  setPathValue,
18
26
  SORT_DIRECTIONS,
19
- buildGridPipeline,
27
+ defaultGridEngine,
20
28
  resolveGridLabels,
21
29
  gridColumnWidth,
22
30
  headerLabel as coreHeaderLabel,
@@ -79,19 +87,11 @@ import {
79
87
  FEATURE_CSV_EXPORT,
80
88
  FEATURE_AUTO_RESIZE,
81
89
  FEATURE_SAVE_STATE,
82
- } from '@ornery/ui-grid';
83
- import type {
84
- DisplayItem,
85
- GroupItem,
86
- ExpandableItem,
87
- RowItem,
88
- PipelineResult,
89
- GridInfiniteScrollState,
90
- GridMoveDirection,
91
- GridCellTemplateContext,
92
- GridExpandableTemplateContext,
93
- } from '@ornery/ui-grid';
94
- import {
90
+ FEATURE_PINNING,
91
+ buildInitialPinnedState,
92
+ computePinnedOffset,
93
+ isColumnPinnable,
94
+ isPinningEnabled,
95
95
  applyGridSortStateCommand,
96
96
  updateGridFilterCommand,
97
97
  clearGridFiltersCommand,
@@ -117,8 +117,7 @@ import {
117
117
  saveGridInfiniteScrollPercentageCommand,
118
118
  setGridInfiniteScrollDirectionsCommand,
119
119
  restoreGridStateCommand,
120
- } from '../../ui-grid/src/lib/grid/ui-grid.commands';
121
- import {
120
+ pinGridColumnCommand,
122
121
  raiseGridRenderingComplete,
123
122
  raiseGridRowsRendered,
124
123
  raiseGridRowsVisibleChanged,
@@ -127,11 +126,69 @@ import {
127
126
  raiseGridScrollBegin,
128
127
  raiseGridScrollEnd,
129
128
  raiseGridBenchmarkComplete,
130
- } from '../../ui-grid/src/lib/grid/ui-grid.events';
131
- import {
132
129
  downloadGridCsvFile,
133
130
  observeGridHostSize,
134
- } from '../../ui-grid/src/lib/grid/ui-grid.host';
131
+ } from '@ornery/ui-grid';
132
+ import type {
133
+ DisplayItem,
134
+ GroupItem,
135
+ ExpandableItem,
136
+ RowItem,
137
+ PipelineResult,
138
+ GridInfiniteScrollState,
139
+ GridMoveDirection,
140
+ GridCellTemplateContext,
141
+ GridExpandableTemplateContext,
142
+ PinDirection,
143
+ PinnedColumnState,
144
+ } from '@ornery/ui-grid';
145
+
146
+ function escapeCssSelectorValue(value: string): string {
147
+ const nativeEscape = globalThis.CSS?.escape;
148
+ if (typeof nativeEscape === 'function') {
149
+ return nativeEscape(value);
150
+ }
151
+
152
+ let output = '';
153
+ for (let index = 0; index < value.length; index += 1) {
154
+ const codePoint = value.charCodeAt(index);
155
+ const character = value.charAt(index);
156
+
157
+ if (codePoint === 0x0000) {
158
+ output += '\uFFFD';
159
+ continue;
160
+ }
161
+
162
+ const isControlCharacter = (codePoint >= 0x0001 && codePoint <= 0x001f) || codePoint === 0x007f;
163
+ const startsWithDigit = index === 0 && codePoint >= 0x0030 && codePoint <= 0x0039;
164
+ const secondCharDigitAfterHyphen =
165
+ index === 1 &&
166
+ codePoint >= 0x0030 && codePoint <= 0x0039 &&
167
+ value.charCodeAt(0) === 0x002d;
168
+
169
+ if (isControlCharacter || startsWithDigit || secondCharDigitAfterHyphen) {
170
+ output += `\\${codePoint.toString(16)} `;
171
+ continue;
172
+ }
173
+
174
+ if (index === 0 && value.length === 1 && codePoint === 0x002d) {
175
+ output += `\\${character}`;
176
+ continue;
177
+ }
178
+
179
+ const isSafeCharacter =
180
+ codePoint >= 0x0080 ||
181
+ codePoint === 0x002d ||
182
+ codePoint === 0x005f ||
183
+ (codePoint >= 0x0030 && codePoint <= 0x0039) ||
184
+ (codePoint >= 0x0041 && codePoint <= 0x005a) ||
185
+ (codePoint >= 0x0061 && codePoint <= 0x007a);
186
+
187
+ output += isSafeCharacter ? character : `\\${character}`;
188
+ }
189
+
190
+ return output;
191
+ }
135
192
 
136
193
  export interface UseGridStateResult {
137
194
  pipeline: PipelineResult;
@@ -201,9 +258,21 @@ export interface UseGridStateResult {
201
258
  showPaginationControls: () => boolean;
202
259
  paginationSummary: () => string;
203
260
  pageSizeOptions: () => number[];
204
- isCellEditable: (row: GridRow, column: GridColumnDef, triggerEvent?: Event | KeyboardEvent | null) => boolean;
261
+ isCellEditable: (
262
+ row: GridRow,
263
+ column: GridColumnDef,
264
+ triggerEvent?: Event | KeyboardEvent | null,
265
+ ) => boolean;
205
266
  shouldEditOnFocus: (column: GridColumnDef) => boolean;
206
267
 
268
+ // Pinning
269
+ isPinned: (column: GridColumnDef) => boolean;
270
+ pinnedOffset: (column: GridColumnDef) => { side: 'left' | 'right'; offset: string } | null;
271
+ isPinningEnabled: () => boolean;
272
+ isColumnPinnable: (column: GridColumnDef) => boolean;
273
+ togglePin: (column: GridColumnDef) => void;
274
+ pinningFeature: boolean;
275
+
207
276
  // Feature flags
208
277
  sortingFeature: boolean;
209
278
  filteringFeature: boolean;
@@ -226,7 +295,11 @@ export interface UseGridStateResult {
226
295
  clearAllFilters: () => void;
227
296
  toggleGrouping: (column: GridColumnDef, event?: React.MouseEvent) => void;
228
297
  toggleGroup: (item: GroupItem) => void;
229
- focusCell: (row: GridRow, column: GridColumnDef, triggerEvent?: Event | KeyboardEvent | null) => void;
298
+ focusCell: (
299
+ row: GridRow,
300
+ column: GridColumnDef,
301
+ triggerEvent?: Event | KeyboardEvent | null,
302
+ ) => void;
230
303
  handleCellKeyDown: (row: GridRow, column: GridColumnDef, event: React.KeyboardEvent) => void;
231
304
  handleCellDoubleClick: (row: GridRow, column: GridColumnDef, event: React.MouseEvent) => void;
232
305
  updateEditingValue: (value: string) => void;
@@ -243,13 +316,19 @@ export interface UseGridStateResult {
243
316
  onViewportScroll: (startIndex: number) => void;
244
317
  }
245
318
 
246
- export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridApi) => void): UseGridStateResult {
319
+ export function useGridState(
320
+ options: GridOptions,
321
+ onRegisterApi?: (api: UiGridApi) => void,
322
+ ): UseGridStateResult {
247
323
  const [activeFilters, setActiveFilters] = useState<Record<string, string>>({});
248
324
  const [groupByColumns, setGroupByColumns] = useState<string[]>([]);
249
325
  const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
250
326
  const [columnOrder, setColumnOrder] = useState<string[]>([]);
251
327
  const [hiddenRowReasons, setHiddenRowReasons] = useState<Record<string, string[]>>({});
252
- const [sortState, setSortState] = useState<SortState>({ columnName: null, direction: SORT_DIRECTIONS.none });
328
+ const [sortState, setSortState] = useState<SortState>({
329
+ columnName: null,
330
+ direction: SORT_DIRECTIONS.none,
331
+ });
253
332
  const [focusedCell, setFocusedCell] = useState<GridCellPosition | null>(null);
254
333
  const [editingCell, setEditingCell] = useState<GridCellPosition | null>(null);
255
334
  const [editingValue, setEditingValue] = useState('');
@@ -265,6 +344,7 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
265
344
  previousVisibleRows: 0,
266
345
  });
267
346
  const [autoViewportHeight, setAutoViewportHeight] = useState<number | null>(null);
347
+ const [pinnedColumns, setPinnedColumns] = useState<PinnedColumnState>({});
268
348
 
269
349
  const gridContainerRef = useRef<HTMLDivElement | null>(null);
270
350
  const initializedGridIdRef = useRef<string | null>(null);
@@ -299,6 +379,8 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
299
379
  expandedRowsRef.current = expandedRows;
300
380
  const expandedTreeRowsRef = useRef(expandedTreeRows);
301
381
  expandedTreeRowsRef.current = expandedTreeRows;
382
+ const pinnedColumnsRef = useRef(pinnedColumns);
383
+ pinnedColumnsRef.current = pinnedColumns;
302
384
  const currentPageRef = useRef(currentPage);
303
385
  currentPageRef.current = currentPage;
304
386
  const pageSizeRef = useRef(pageSize);
@@ -311,17 +393,31 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
311
393
  const rowSize = options.rowHeight ?? 44;
312
394
 
313
395
  const visibleColumns = useMemo(() => {
314
- const order = columnOrder;
315
- return [...options.columnDefs]
316
- .filter((column) => column.visible !== false)
317
- .sort((left, right) => order.indexOf(left.name) - order.indexOf(right.name));
318
- }, [options.columnDefs, columnOrder]);
396
+ const orderedColumns = orderVisibleColumns(options.columnDefs, columnOrder);
397
+ const pinnedEntries = Object.entries(pinnedColumns);
398
+ if (pinnedEntries.length === 0) {
399
+ return orderedColumns;
400
+ }
401
+
402
+ const columnByName = new Map(orderedColumns.map((column) => [column.name, column]));
403
+ const pinnedLeft = pinnedEntries
404
+ .filter(([, direction]) => direction === 'left')
405
+ .map(([columnName]) => columnByName.get(columnName))
406
+ .filter((column): column is GridColumnDef => column !== undefined);
407
+ const pinnedRight = pinnedEntries
408
+ .filter(([, direction]) => direction === 'right')
409
+ .map(([columnName]) => columnByName.get(columnName))
410
+ .filter((column): column is GridColumnDef => column !== undefined);
411
+ const centerColumns = orderedColumns.filter((column) => pinnedColumns[column.name] === undefined);
412
+
413
+ return [...pinnedLeft, ...centerColumns, ...pinnedRight];
414
+ }, [options.columnDefs, columnOrder, pinnedColumns]);
319
415
 
320
416
  const visibleColumnsRef = useRef(visibleColumns);
321
417
  visibleColumnsRef.current = visibleColumns;
322
418
 
323
419
  const pipeline = useMemo<PipelineResult>(() => {
324
- return buildGridPipeline({
420
+ return defaultGridEngine.buildPipeline({
325
421
  options,
326
422
  columns: visibleColumns,
327
423
  activeFilters,
@@ -335,7 +431,20 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
335
431
  pageSize,
336
432
  rowSize,
337
433
  });
338
- }, [options, visibleColumns, activeFilters, sortState, groupByColumns, collapsedGroups, hiddenRowReasons, expandedRows, expandedTreeRows, currentPage, pageSize, rowSize]);
434
+ }, [
435
+ options,
436
+ visibleColumns,
437
+ activeFilters,
438
+ sortState,
439
+ groupByColumns,
440
+ collapsedGroups,
441
+ hiddenRowReasons,
442
+ expandedRows,
443
+ expandedTreeRows,
444
+ currentPage,
445
+ pageSize,
446
+ rowSize,
447
+ ]);
339
448
 
340
449
  const pipelineRef = useRef(pipeline);
341
450
  pipelineRef.current = pipeline;
@@ -343,10 +452,26 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
343
452
  const labels = useMemo(() => resolveGridLabels(options.labels), [options.labels]);
344
453
 
345
454
  const gridTemplateColumns = useMemo(
346
- () => visibleColumns.map((column) => gridColumnWidth(column)).join(' '),
347
- [visibleColumns]
455
+ () => buildGridTemplateColumns(visibleColumns),
456
+ [visibleColumns],
348
457
  );
349
458
 
459
+ const isPinningEnabledFn = useCallback((): boolean => {
460
+ return isPinningEnabled(optionsRef.current);
461
+ }, []);
462
+
463
+ const isColumnPinnableFn = useCallback((column: GridColumnDef): boolean => {
464
+ return isColumnPinnable(optionsRef.current, column);
465
+ }, []);
466
+
467
+ const isPinnedFn = useCallback((column: GridColumnDef): boolean => {
468
+ return pinnedColumnsRef.current[column.name] !== undefined;
469
+ }, []);
470
+
471
+ const pinnedOffsetFn = useCallback((column: GridColumnDef) => {
472
+ return computePinnedOffset(visibleColumnsRef.current, pinnedColumnsRef.current, column);
473
+ }, []);
474
+
350
475
  // --- Helper functions (all pure, no state closures needed beyond refs) ---
351
476
 
352
477
  const resolveRowId = useCallback((row: GridRow | GridRecord | string): string => {
@@ -358,13 +483,16 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
358
483
  { ...optionsRef.current, data },
359
484
  optionsRef.current.rowHeight ?? 44,
360
485
  hiddenRowReasonsRef.current,
361
- expandedRowsRef.current
486
+ expandedRowsRef.current,
362
487
  );
363
488
  }, []);
364
489
 
365
- const findRowById = useCallback((rowId: string): GridRow | null => {
366
- return coreFindGridRowById(buildRowsFromData(optionsRef.current.data), rowId);
367
- }, [buildRowsFromData]);
490
+ const findRowById = useCallback(
491
+ (rowId: string): GridRow | null => {
492
+ return coreFindGridRowById(buildRowsFromData(optionsRef.current.data), rowId);
493
+ },
494
+ [buildRowsFromData],
495
+ );
368
496
 
369
497
  const canExpandRowsFn = useCallback((): boolean => {
370
498
  return FEATURE_EXPANDABLE && canGridExpandRows(optionsRef.current);
@@ -376,7 +504,12 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
376
504
 
377
505
  const getCurrentPageValueFn = useCallback((totalItems?: number): number => {
378
506
  const ti = totalItems ?? pipelineRef.current.totalItems;
379
- return coreGetCurrentPageValue(optionsRef.current, currentPageRef.current, ti, pageSizeRef.current);
507
+ return coreGetCurrentPageValue(
508
+ optionsRef.current,
509
+ currentPageRef.current,
510
+ ti,
511
+ pageSizeRef.current,
512
+ );
380
513
  }, []);
381
514
 
382
515
  const getTotalPagesValueFn = useCallback((totalItems?: number): number => {
@@ -386,30 +519,44 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
386
519
 
387
520
  const getFirstRowIndexValueFn = useCallback((totalItems?: number): number => {
388
521
  const ti = totalItems ?? pipelineRef.current.totalItems;
389
- return coreGetFirstRowIndexValue(optionsRef.current, currentPageRef.current, ti, pageSizeRef.current);
522
+ return coreGetFirstRowIndexValue(
523
+ optionsRef.current,
524
+ currentPageRef.current,
525
+ ti,
526
+ pageSizeRef.current,
527
+ );
390
528
  }, []);
391
529
 
392
530
  const getLastRowIndexValueFn = useCallback((totalItems?: number): number => {
393
531
  const ti = totalItems ?? pipelineRef.current.totalItems;
394
- return coreGetLastRowIndexValue(optionsRef.current, currentPageRef.current, ti, pageSizeRef.current);
532
+ return coreGetLastRowIndexValue(
533
+ optionsRef.current,
534
+ currentPageRef.current,
535
+ ti,
536
+ pageSizeRef.current,
537
+ );
395
538
  }, []);
396
539
 
397
- const isCellEditable = useCallback((row: GridRow, column: GridColumnDef, triggerEvent?: Event | KeyboardEvent | null): boolean => {
398
- if (!FEATURE_CELL_EDIT) return false;
399
- const editable = column.enableCellEdit ?? optionsRef.current.enableCellEdit ?? false;
400
- if (!editable) return false;
401
-
402
- const condition = column.cellEditableCondition ?? optionsRef.current.cellEditableCondition ?? true;
403
- if (typeof condition === 'boolean') return condition;
404
-
405
- const context: GridCellEditableContext = {
406
- row: row.entity,
407
- column,
408
- rowIndex: row.index,
409
- triggerEvent,
410
- };
411
- return condition(context);
412
- }, []);
540
+ const isCellEditable = useCallback(
541
+ (row: GridRow, column: GridColumnDef, triggerEvent?: Event | KeyboardEvent | null): boolean => {
542
+ if (!FEATURE_CELL_EDIT) return false;
543
+ const editable = column.enableCellEdit ?? optionsRef.current.enableCellEdit ?? false;
544
+ if (!editable) return false;
545
+
546
+ const condition =
547
+ column.cellEditableCondition ?? optionsRef.current.cellEditableCondition ?? true;
548
+ if (typeof condition === 'boolean') return condition;
549
+
550
+ const context: GridCellEditableContext = {
551
+ row: row.entity,
552
+ column,
553
+ rowIndex: row.index,
554
+ triggerEvent,
555
+ };
556
+ return condition(context);
557
+ },
558
+ [],
559
+ );
413
560
 
414
561
  const shouldEditOnFocusFn = useCallback((column: GridColumnDef): boolean => {
415
562
  return column.enableCellEditOnFocus ?? optionsRef.current.enableCellEditOnFocus ?? false;
@@ -419,7 +566,7 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
419
566
 
420
567
  const focusRenderedCell = useCallback((position: GridCellPosition): void => {
421
568
  const focusToken = ++renderedCellFocusTokenRef.current;
422
- const selector = `.body-cell[data-row-id="${position.rowId}"][data-col-name="${position.columnName}"]`;
569
+ const selector = `.body-cell[data-row-id="${escapeCssSelectorValue(position.rowId)}"][data-col-name="${escapeCssSelectorValue(position.columnName)}"]`;
423
570
 
424
571
  const doFocus = (retry = true): void => {
425
572
  if (focusToken !== renderedCellFocusTokenRef.current) return;
@@ -444,12 +591,13 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
444
591
  const ec = editingCellRef.current;
445
592
  if (!ec) return;
446
593
 
447
- const selector = `.cell-editor[data-row-id="${ec.rowId}"][data-col-name="${ec.columnName}"]`;
594
+ const selector = `.cell-editor[data-row-id="${escapeCssSelectorValue(ec.rowId)}"][data-col-name="${escapeCssSelectorValue(ec.columnName)}"]`;
448
595
 
449
596
  const doFocus = (retry = true): void => {
450
597
  if (focusToken !== editorFocusTokenRef.current) return;
451
598
  const currentEc = editingCellRef.current;
452
- if (!currentEc || currentEc.rowId !== ec.rowId || currentEc.columnName !== ec.columnName) return;
599
+ if (!currentEc || currentEc.rowId !== ec.rowId || currentEc.columnName !== ec.columnName)
600
+ return;
453
601
 
454
602
  const container = gridContainerRef.current;
455
603
  if (!container) return;
@@ -500,10 +648,10 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
500
648
  moveColumn: (fromIndex, toIndex) => {
501
649
  moveGridColumnCommand(
502
650
  gridApiRef.current!,
503
- FEATURE_COLUMN_MOVING && (optionsRef.current.enableColumnMoving === true),
651
+ FEATURE_COLUMN_MOVING && optionsRef.current.enableColumnMoving === true,
504
652
  (updater) => setColumnOrder((current) => updater(current)),
505
653
  fromIndex,
506
- toIndex
654
+ toIndex,
507
655
  );
508
656
  },
509
657
  toggleGrouping: (columnName) => {
@@ -517,7 +665,11 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
517
665
  gridApiRef.current!.core.raise.groupingChanged(next);
518
666
  },
519
667
  clearGrouping: () => {
520
- clearGridGroupingCommand(gridApiRef.current!, (grouping) => setGroupByColumns(grouping), false);
668
+ clearGridGroupingCommand(
669
+ gridApiRef.current!,
670
+ (grouping) => setGroupByColumns(grouping),
671
+ false,
672
+ );
521
673
  },
522
674
  benchmark: (iterations) => {
523
675
  return runBenchmarkFn(iterations);
@@ -543,7 +695,7 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
543
695
  expandAllGridTreeRowsCommand(
544
696
  (data) => buildRowsFromData(data),
545
697
  optionsRef.current.data,
546
- (e) => setExpandedTreeRows(e)
698
+ (e) => setExpandedTreeRows(e),
547
699
  );
548
700
  },
549
701
  treeCollapseAllRows: () => {
@@ -563,35 +715,35 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
563
715
  infiniteScrollStateRef.current,
564
716
  (s) => setInfiniteScrollState(s),
565
717
  scrollUp ?? infiniteScrollStateRef.current.scrollUp,
566
- scrollDown ?? infiniteScrollStateRef.current.scrollDown
718
+ scrollDown ?? infiniteScrollStateRef.current.scrollDown,
567
719
  );
568
720
  },
569
721
  infiniteScrollReset: (scrollUp, scrollDown) => {
570
722
  resetGridInfiniteScrollCommand(
571
723
  (s) => setInfiniteScrollState(s),
572
724
  scrollUp ?? infiniteScrollStateRef.current.scrollUp,
573
- scrollDown ?? infiniteScrollStateRef.current.scrollDown
725
+ scrollDown ?? infiniteScrollStateRef.current.scrollDown,
574
726
  );
575
727
  },
576
728
  infiniteScrollSaveScrollPercentage: () => {
577
729
  saveGridInfiniteScrollPercentageCommand(
578
730
  infiniteScrollStateRef.current,
579
731
  pipelineRef.current.visibleRows.length,
580
- (s) => setInfiniteScrollState(s)
732
+ (s) => setInfiniteScrollState(s),
581
733
  );
582
734
  },
583
735
  infiniteScrollDataRemovedTop: (scrollUp, scrollDown) => {
584
736
  resetGridInfiniteScrollCommand(
585
737
  (s) => setInfiniteScrollState(s),
586
738
  scrollUp ?? infiniteScrollStateRef.current.scrollUp,
587
- scrollDown ?? infiniteScrollStateRef.current.scrollDown
739
+ scrollDown ?? infiniteScrollStateRef.current.scrollDown,
588
740
  );
589
741
  },
590
742
  infiniteScrollDataRemovedBottom: (scrollUp, scrollDown) => {
591
743
  resetGridInfiniteScrollCommand(
592
744
  (s) => setInfiniteScrollState(s),
593
745
  scrollUp ?? infiniteScrollStateRef.current.scrollUp,
594
- scrollDown ?? infiniteScrollStateRef.current.scrollDown
746
+ scrollDown ?? infiniteScrollStateRef.current.scrollDown,
595
747
  );
596
748
  },
597
749
  infiniteScrollSetDirections: (scrollUp, scrollDown) => {
@@ -599,7 +751,7 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
599
751
  infiniteScrollStateRef.current,
600
752
  (s) => setInfiniteScrollState(s),
601
753
  scrollUp,
602
- scrollDown
754
+ scrollDown,
603
755
  );
604
756
  },
605
757
  saveState: () => {
@@ -613,6 +765,7 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
613
765
  totalItems: pipelineRef.current.totalItems,
614
766
  expandedRows: expandedRowsRef.current,
615
767
  expandedTreeRows: expandedTreeRowsRef.current,
768
+ pinnedColumns: pinnedColumnsRef.current,
616
769
  });
617
770
  },
618
771
  restoreState: (state) => {
@@ -625,6 +778,7 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
625
778
  setPageSize: (ps) => setPageSize(ps),
626
779
  setExpandedRows: (e) => setExpandedRows(e),
627
780
  setExpandedTreeRows: (e) => setExpandedTreeRows(e),
781
+ setPinnedColumns: (p) => setPinnedColumns(p),
628
782
  getEffectivePageSize: () => effectivePageSizeFn(pipelineRef.current.totalItems),
629
783
  });
630
784
  },
@@ -638,6 +792,16 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
638
792
  endCellEdit: () => commitCellEditFn(),
639
793
  cancelCellEdit: () => cancelCellEditFn(),
640
794
  getEditingCell: () => editingCellRef.current,
795
+ pinColumn: (columnName: string, direction: PinDirection) => {
796
+ pinGridColumnCommand(
797
+ gridApiRef.current!,
798
+ isPinningEnabledFn(),
799
+ (v) => setPinnedColumns(v),
800
+ () => pinnedColumnsRef.current,
801
+ columnName,
802
+ direction,
803
+ );
804
+ },
641
805
  };
642
806
 
643
807
  gridApiRef.current = createGridApi(bindings);
@@ -647,48 +811,71 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
647
811
 
648
812
  // --- Memoized action functions ---
649
813
 
650
- const seekPageFn = useCallback((page: number): void => {
651
- seekGridPaginationCommand(
814
+ const seekPageFn = useCallback(
815
+ (page: number): void => {
816
+ seekGridPaginationCommand(
817
+ gridApiRef.current!,
818
+ (nextPage) => setCurrentPage(nextPage),
819
+ () => getTotalPagesValueFn(),
820
+ () => effectivePageSizeFn(pipelineRef.current.totalItems),
821
+ page,
822
+ );
823
+ },
824
+ [getTotalPagesValueFn, effectivePageSizeFn],
825
+ );
826
+
827
+ const togglePinFn = useCallback((column: GridColumnDef): void => {
828
+ const current = pinnedColumnsRef.current[column.name];
829
+ const next: PinDirection = current === 'left' ? 'right' : current === 'right' ? 'none' : 'left';
830
+ pinGridColumnCommand(
652
831
  gridApiRef.current!,
653
- (nextPage) => setCurrentPage(nextPage),
654
- () => getTotalPagesValueFn(),
655
- () => effectivePageSizeFn(pipelineRef.current.totalItems),
656
- page
832
+ isPinningEnabledFn(),
833
+ (v) => setPinnedColumns(v),
834
+ () => pinnedColumnsRef.current,
835
+ column.name,
836
+ next,
657
837
  );
658
- }, [getTotalPagesValueFn, effectivePageSizeFn]);
838
+ }, []);
659
839
 
660
840
  const setPaginationPageSizeFn = useCallback((ps: number): void => {
661
841
  setGridPaginationPageSizeCommand(
662
842
  gridApiRef.current!,
663
843
  (nextPageSize) => setPageSize(nextPageSize),
664
844
  (nextPage) => setCurrentPage(nextPage),
665
- ps
845
+ ps,
666
846
  );
667
847
  }, []);
668
848
 
669
- const toggleRowExpansionByRefFn = useCallback((row: GridRow | GridRecord | string): void => {
670
- const rowId = coreResolveGridRowId(optionsRef.current, row);
671
- toggleGridRowExpansionCommand(
672
- gridApiRef.current!,
673
- FEATURE_EXPANDABLE && canGridExpandRows(optionsRef.current),
674
- expandedRowsRef.current,
675
- rowId,
676
- (e) => setExpandedRows(e),
677
- (resolvedRowId) => coreFindGridRowById(buildRowsFromData(optionsRef.current.data), resolvedRowId)
678
- );
679
- }, [buildRowsFromData]);
849
+ const toggleRowExpansionByRefFn = useCallback(
850
+ (row: GridRow | GridRecord | string): void => {
851
+ const rowId = coreResolveGridRowId(optionsRef.current, row);
852
+ toggleGridRowExpansionCommand(
853
+ gridApiRef.current!,
854
+ FEATURE_EXPANDABLE && canGridExpandRows(optionsRef.current),
855
+ expandedRowsRef.current,
856
+ rowId,
857
+ (e) => setExpandedRows(e),
858
+ (resolvedRowId) =>
859
+ coreFindGridRowById(buildRowsFromData(optionsRef.current.data), resolvedRowId),
860
+ );
861
+ },
862
+ [buildRowsFromData],
863
+ );
680
864
 
681
865
  const expandAllRowsFn = useCallback((): void => {
682
866
  if (!canGridExpandRows(optionsRef.current)) return;
683
867
  expandAllGridRowsCommand(
684
868
  (data) => buildRowsFromData(data),
685
869
  optionsRef.current.data,
686
- (e) => setExpandedRows(e)
870
+ (e) => setExpandedRows(e),
687
871
  );
688
872
  }, [buildRowsFromData]);
689
873
 
690
874
  const toggleAllRowsFn = useCallback((): void => {
691
- const allExpanded = areAllGridRowsExpanded(buildRowsFromData(optionsRef.current.data), expandedRowsRef.current);
875
+ const allExpanded = areAllGridRowsExpanded(
876
+ buildRowsFromData(optionsRef.current.data),
877
+ expandedRowsRef.current,
878
+ );
692
879
  if (allExpanded) {
693
880
  collapseAllGridRowsCommand((e) => setExpandedRows(e));
694
881
  } else {
@@ -696,89 +883,115 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
696
883
  }
697
884
  }, [buildRowsFromData, expandAllRowsFn]);
698
885
 
699
- const toggleTreeRowByRefFn = useCallback((row: GridRow | GridRecord | string): void => {
700
- const rowId = coreResolveGridRowId(optionsRef.current, row);
701
- toggleGridTreeRowCommand(
702
- gridApiRef.current!,
703
- expandedTreeRowsRef.current,
704
- rowId,
705
- (e) => setExpandedTreeRows(e),
706
- (resolvedRowId) => coreFindGridRowById(buildRowsFromData(optionsRef.current.data), resolvedRowId)
707
- );
708
- }, [buildRowsFromData]);
709
-
710
- const expandTreeRowByRefFn = useCallback((row: GridRow | GridRecord | string): void => {
711
- const rowId = coreResolveGridRowId(optionsRef.current, row);
712
- setGridTreeRowExpandedCommand(
713
- gridApiRef.current!,
714
- expandedTreeRowsRef.current,
715
- rowId,
716
- true,
717
- (e) => setExpandedTreeRows(e),
718
- (resolvedRowId) => coreFindGridRowById(buildRowsFromData(optionsRef.current.data), resolvedRowId)
719
- );
720
- }, [buildRowsFromData]);
886
+ const toggleTreeRowByRefFn = useCallback(
887
+ (row: GridRow | GridRecord | string): void => {
888
+ const rowId = coreResolveGridRowId(optionsRef.current, row);
889
+ toggleGridTreeRowCommand(
890
+ gridApiRef.current!,
891
+ expandedTreeRowsRef.current,
892
+ rowId,
893
+ (e) => setExpandedTreeRows(e),
894
+ (resolvedRowId) =>
895
+ coreFindGridRowById(buildRowsFromData(optionsRef.current.data), resolvedRowId),
896
+ );
897
+ },
898
+ [buildRowsFromData],
899
+ );
721
900
 
722
- const collapseTreeRowByRefFn = useCallback((row: GridRow | GridRecord | string): void => {
723
- const rowId = coreResolveGridRowId(optionsRef.current, row);
724
- setGridTreeRowExpandedCommand(
725
- gridApiRef.current!,
726
- expandedTreeRowsRef.current,
727
- rowId,
728
- false,
729
- (e) => setExpandedTreeRows(e),
730
- (resolvedRowId) => coreFindGridRowById(buildRowsFromData(optionsRef.current.data), resolvedRowId)
731
- );
732
- }, [buildRowsFromData]);
901
+ const expandTreeRowByRefFn = useCallback(
902
+ (row: GridRow | GridRecord | string): void => {
903
+ const rowId = coreResolveGridRowId(optionsRef.current, row);
904
+ setGridTreeRowExpandedCommand(
905
+ gridApiRef.current!,
906
+ expandedTreeRowsRef.current,
907
+ rowId,
908
+ true,
909
+ (e) => setExpandedTreeRows(e),
910
+ (resolvedRowId) =>
911
+ coreFindGridRowById(buildRowsFromData(optionsRef.current.data), resolvedRowId),
912
+ );
913
+ },
914
+ [buildRowsFromData],
915
+ );
733
916
 
734
- const startCellEditFn = useCallback((row: GridRow, column: GridColumnDef, triggerEvent?: Event | KeyboardEvent | null, initialValue?: string): void => {
735
- const currentValue = getCellValue(row.entity, column);
736
- const focusToken = ++editorFocusTokenRef.current;
737
- const ec = beginGridCellEditCommand(
738
- gridApiRef.current!,
739
- {
740
- setFocusedCell: (fc) => setFocusedCell(fc),
741
- setEditingCell: (ec2) => setEditingCell(ec2),
742
- setEditingValue: (ev) => setEditingValue(ev),
743
- },
744
- row,
745
- column,
746
- currentValue,
747
- triggerEvent,
748
- initialValue
749
- );
917
+ const collapseTreeRowByRefFn = useCallback(
918
+ (row: GridRow | GridRecord | string): void => {
919
+ const rowId = coreResolveGridRowId(optionsRef.current, row);
920
+ setGridTreeRowExpandedCommand(
921
+ gridApiRef.current!,
922
+ expandedTreeRowsRef.current,
923
+ rowId,
924
+ false,
925
+ (e) => setExpandedTreeRows(e),
926
+ (resolvedRowId) =>
927
+ coreFindGridRowById(buildRowsFromData(optionsRef.current.data), resolvedRowId),
928
+ );
929
+ },
930
+ [buildRowsFromData],
931
+ );
750
932
 
751
- if (ec) {
752
- queueMicrotask(() => focusEditorInput(focusToken));
753
- }
754
- }, [focusEditorInput]);
933
+ const startCellEditFn = useCallback(
934
+ (
935
+ row: GridRow,
936
+ column: GridColumnDef,
937
+ triggerEvent?: Event | KeyboardEvent | null,
938
+ initialValue?: string,
939
+ ): void => {
940
+ const currentValue = getCellValue(row.entity, column);
941
+ const focusToken = ++editorFocusTokenRef.current;
942
+ const ec = beginGridCellEditCommand(
943
+ gridApiRef.current!,
944
+ {
945
+ setFocusedCell: (fc) => setFocusedCell(fc),
946
+ setEditingCell: (ec2) => setEditingCell(ec2),
947
+ setEditingValue: (ev) => setEditingValue(ev),
948
+ },
949
+ row,
950
+ column,
951
+ currentValue,
952
+ triggerEvent,
953
+ initialValue,
954
+ );
955
+
956
+ if (ec) {
957
+ queueMicrotask(() => focusEditorInput(focusToken));
958
+ }
959
+ },
960
+ [focusEditorInput],
961
+ );
755
962
 
756
- const commitCellEditFn = useCallback((direction?: GridMoveDirection, restoreFocus = true): void => {
757
- const result = commitGridCellEditCommand(gridApiRef.current!, {
758
- getEditingCell: () => editingCellRef.current,
759
- getEditingValue: () => editingValueRef.current,
760
- setEditingCell: (ec) => setEditingCell(ec),
761
- setEditingValue: (ev) => setEditingValue(ev),
762
- findRowById: (rowId) => coreFindGridRowById(buildRowsFromData(optionsRef.current.data), rowId),
763
- findColumnByName: (columnName) => visibleColumnsRef.current.find((c) => c.name === columnName),
764
- parseEditedValue: (column, value, oldValue) => parseGridEditedValue(column, value, oldValue),
765
- setCellValue: (rowEntity, column, value) => {
766
- const fieldPath = column.editModelField ?? column.field ?? column.name;
767
- setPathValue(rowEntity, fieldPath, value);
768
- },
769
- });
963
+ const commitCellEditFn = useCallback(
964
+ (direction?: GridMoveDirection, restoreFocus = true): void => {
965
+ const result = commitGridCellEditCommand(gridApiRef.current!, {
966
+ getEditingCell: () => editingCellRef.current,
967
+ getEditingValue: () => editingValueRef.current,
968
+ setEditingCell: (ec) => setEditingCell(ec),
969
+ setEditingValue: (ev) => setEditingValue(ev),
970
+ findRowById: (rowId) =>
971
+ coreFindGridRowById(buildRowsFromData(optionsRef.current.data), rowId),
972
+ findColumnByName: (columnName) =>
973
+ visibleColumnsRef.current.find((c) => c.name === columnName),
974
+ parseEditedValue: (column, value, oldValue) =>
975
+ parseGridEditedValue(column, value, oldValue),
976
+ setCellValue: (rowEntity, column, value) => {
977
+ const fieldPath = column.editModelField ?? column.field ?? column.name;
978
+ setPathValue(rowEntity, fieldPath, value);
979
+ },
980
+ });
770
981
 
771
- if (!result.committed || !result.row || !result.column || !result.focusTarget) return;
982
+ if (!result.committed || !result.row || !result.column || !result.focusTarget) return;
772
983
 
773
- editorFocusTokenRef.current += 1;
984
+ editorFocusTokenRef.current += 1;
774
985
 
775
- if (direction) {
776
- const moved = moveFocusFn(result.row, result.column, direction);
777
- if (!moved) focusRenderedCell(result.focusTarget);
778
- } else if (restoreFocus) {
779
- focusRenderedCell(result.focusTarget);
780
- }
781
- }, [buildRowsFromData, focusRenderedCell]);
986
+ if (direction) {
987
+ const moved = moveFocusFn(result.row, result.column, direction);
988
+ if (!moved) focusRenderedCell(result.focusTarget);
989
+ } else if (restoreFocus) {
990
+ focusRenderedCell(result.focusTarget);
991
+ }
992
+ },
993
+ [buildRowsFromData, focusRenderedCell],
994
+ );
782
995
 
783
996
  const cancelCellEditFn = useCallback((): void => {
784
997
  const hadEditingCell = editingCellRef.current !== null;
@@ -786,8 +999,10 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
786
999
  getEditingCell: () => editingCellRef.current,
787
1000
  setEditingCell: (ec) => setEditingCell(ec),
788
1001
  setEditingValue: (ev) => setEditingValue(ev),
789
- findRowById: (rowId) => coreFindGridRowById(buildRowsFromData(optionsRef.current.data), rowId),
790
- findColumnByName: (columnName) => visibleColumnsRef.current.find((c) => c.name === columnName),
1002
+ findRowById: (rowId) =>
1003
+ coreFindGridRowById(buildRowsFromData(optionsRef.current.data), rowId),
1004
+ findColumnByName: (columnName) =>
1005
+ visibleColumnsRef.current.find((c) => c.name === columnName),
791
1006
  });
792
1007
 
793
1008
  if (!hadEditingCell) return;
@@ -795,30 +1010,44 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
795
1010
  if (result.focusTarget) focusRenderedCell(result.focusTarget);
796
1011
  }, [buildRowsFromData, focusRenderedCell]);
797
1012
 
798
- const moveFocusFn = useCallback((row: GridRow, column: GridColumnDef, direction: GridMoveDirection, triggerEvent?: Event | KeyboardEvent | null): boolean => {
799
- const nextCell = findNextGridCell({
800
- rows: pipelineRef.current.visibleRows,
801
- columns: visibleColumnsRef.current,
802
- rowId: row.id,
803
- columnName: column.name,
804
- direction,
805
- });
806
- if (!nextCell) return false;
1013
+ const moveFocusFn = useCallback(
1014
+ (
1015
+ row: GridRow,
1016
+ column: GridColumnDef,
1017
+ direction: GridMoveDirection,
1018
+ triggerEvent?: Event | KeyboardEvent | null,
1019
+ ): boolean => {
1020
+ const nextCell = findNextGridCell({
1021
+ rows: pipelineRef.current.visibleRows,
1022
+ columns: visibleColumnsRef.current,
1023
+ rowId: row.id,
1024
+ columnName: column.name,
1025
+ direction,
1026
+ });
1027
+ if (!nextCell) return false;
807
1028
 
808
- setFocusedCell({ rowId: nextCell.row.id, columnName: nextCell.column.name });
809
- focusRenderedCell({ rowId: nextCell.row.id, columnName: nextCell.column.name });
1029
+ setFocusedCell({ rowId: nextCell.row.id, columnName: nextCell.column.name });
1030
+ focusRenderedCell({ rowId: nextCell.row.id, columnName: nextCell.column.name });
810
1031
 
811
- if (shouldEditOnFocusFn(nextCell.column) && isCellEditable(nextCell.row, nextCell.column, triggerEvent)) {
812
- startCellEditFn(nextCell.row, nextCell.column, triggerEvent);
813
- }
1032
+ if (
1033
+ shouldEditOnFocusFn(nextCell.column) &&
1034
+ isCellEditable(nextCell.row, nextCell.column, triggerEvent)
1035
+ ) {
1036
+ startCellEditFn(nextCell.row, nextCell.column, triggerEvent);
1037
+ }
814
1038
 
815
- return true;
816
- }, [focusRenderedCell, isCellEditable, shouldEditOnFocusFn, startCellEditFn]);
1039
+ return true;
1040
+ },
1041
+ [focusRenderedCell, isCellEditable, shouldEditOnFocusFn, startCellEditFn],
1042
+ );
817
1043
 
818
1044
  const runBenchmarkFn = useCallback((iterations?: number): GridBenchmarkResult => {
819
- const safeIterations = Math.max(1, iterations ?? optionsRef.current.benchmark?.iterations ?? 25);
1045
+ const safeIterations = resolveBenchmarkIterations(
1046
+ iterations,
1047
+ optionsRef.current.benchmark?.iterations,
1048
+ );
820
1049
  const startedAt = performance.now();
821
- let lastResult = buildGridPipeline({
1050
+ let lastResult = defaultGridEngine.buildPipeline({
822
1051
  options: optionsRef.current,
823
1052
  columns: visibleColumnsRef.current,
824
1053
  activeFilters: activeFiltersRef.current,
@@ -834,7 +1063,7 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
834
1063
  });
835
1064
 
836
1065
  for (let i = 1; i < safeIterations; i++) {
837
- lastResult = buildGridPipeline({
1066
+ lastResult = defaultGridEngine.buildPipeline({
838
1067
  options: optionsRef.current,
839
1068
  columns: visibleColumnsRef.current,
840
1069
  activeFilters: activeFiltersRef.current,
@@ -887,6 +1116,7 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
887
1116
  setExpandedTreeRows({});
888
1117
  setColumnOrder(options.columnDefs.map((column) => column.name));
889
1118
  setGroupByColumns(options.grouping?.groupBy ?? []);
1119
+ setPinnedColumns(buildInitialPinnedState(options.columnDefs));
890
1120
  setCurrentPage(options.paginationCurrentPage ?? 1);
891
1121
  setPageSize(coreGetEffectivePageSize(options, 0, options.data.length));
892
1122
 
@@ -898,7 +1128,7 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
898
1128
  });
899
1129
 
900
1130
  const initialSort = options.columnDefs.find(
901
- (column) => column.sort?.direction && !column.sort.ignoreSort
1131
+ (column) => column.sort?.direction && !column.sort.ignoreSort,
902
1132
  );
903
1133
  setSortState({
904
1134
  columnName: initialSort?.name ?? null,
@@ -931,9 +1161,16 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
931
1161
  if (!container) return;
932
1162
 
933
1163
  const observer = observeGridHostSize(container, ({ height: nextHeight, width: nextWidth }) => {
934
- if (nextHeight === lastGridHeightRef.current && nextWidth === lastGridWidthRef.current) return;
1164
+ if (nextHeight === lastGridHeightRef.current && nextWidth === lastGridWidthRef.current)
1165
+ return;
935
1166
 
936
- raiseGridDimensionChanged(gridApi, lastGridHeightRef.current, lastGridWidthRef.current, nextHeight, nextWidth);
1167
+ raiseGridDimensionChanged(
1168
+ gridApi,
1169
+ lastGridHeightRef.current,
1170
+ lastGridWidthRef.current,
1171
+ nextHeight,
1172
+ nextWidth,
1173
+ );
937
1174
  lastGridHeightRef.current = nextHeight;
938
1175
  lastGridWidthRef.current = nextWidth;
939
1176
 
@@ -956,55 +1193,90 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
956
1193
  const paginationCurrentPage = getCurrentPageValueFn();
957
1194
  const paginationTotalPages = getTotalPagesValueFn();
958
1195
  const paginationSelectedPageSize = effectivePageSizeFn(pipeline.totalItems);
959
- const viewportHeightPx = `${options.viewportHeight ?? autoViewportHeight ?? 560}px`;
1196
+ const viewportHeightPx = computeViewportHeightPx(options.viewportHeight, autoViewportHeight);
960
1197
 
961
1198
  // --- Display helper functions ---
962
1199
 
963
1200
  const headerLabelFn = useCallback((column: GridColumnDef): string => coreHeaderLabel(column), []);
964
- const isGroupItemFn = useCallback((item: DisplayItem): item is GroupItem => item.kind === 'group', []);
965
- const isExpandableItemFn = useCallback((item: DisplayItem): item is ExpandableItem => item.kind === 'expandable', []);
1201
+ const isGroupItemFn = useCallback(
1202
+ (item: DisplayItem): item is GroupItem => item.kind === 'group',
1203
+ [],
1204
+ );
1205
+ const isExpandableItemFn = useCallback(
1206
+ (item: DisplayItem): item is ExpandableItem => item.kind === 'expandable',
1207
+ [],
1208
+ );
966
1209
  const isRowItemFn = useCallback((item: DisplayItem): item is RowItem => item.kind === 'row', []);
967
- const isOddStripedRowFn = useCallback((item: DisplayItem): boolean => item.kind === 'row' && item.visibleIndex % 2 === 0, []);
1210
+ const isOddStripedRowFn = useCallback(
1211
+ (item: DisplayItem): boolean => item.kind === 'row' && item.visibleIndex % 2 === 0,
1212
+ [],
1213
+ );
968
1214
 
969
1215
  const sortDirectionFn = useCallback((column: GridColumnDef): string => {
970
- return sortStateRef.current.columnName === column.name ? sortStateRef.current.direction : SORT_DIRECTIONS.none;
1216
+ return sortStateRef.current.columnName === column.name
1217
+ ? sortStateRef.current.direction
1218
+ : SORT_DIRECTIONS.none;
971
1219
  }, []);
972
1220
 
973
- const sortButtonLabelFn = useCallback((column: GridColumnDef): string => {
974
- return gridSortButtonLabel(sortDirectionFn(column) as any, labels);
975
- }, [labels, sortDirectionFn]);
1221
+ const sortButtonLabelFn = useCallback(
1222
+ (column: GridColumnDef): string => {
1223
+ return gridSortButtonLabel(sortDirectionFn(column) as any, labels);
1224
+ },
1225
+ [labels, sortDirectionFn],
1226
+ );
976
1227
 
977
- const sortAriaSortFn = useCallback((column: GridColumnDef): string => {
978
- return gridSortAriaSort(sortDirectionFn(column) as any);
979
- }, [sortDirectionFn]);
1228
+ const sortAriaSortFn = useCallback(
1229
+ (column: GridColumnDef): string => {
1230
+ return gridSortAriaSort(sortDirectionFn(column) as any);
1231
+ },
1232
+ [sortDirectionFn],
1233
+ );
980
1234
 
981
- const groupingButtonLabelFn = useCallback((column: GridColumnDef): string => {
982
- return gridGroupingButtonLabel(isGridColumnGrouped(groupByColumnsRef.current, column), labels);
983
- }, [labels]);
1235
+ const groupingButtonLabelFn = useCallback(
1236
+ (column: GridColumnDef): string => {
1237
+ return gridGroupingButtonLabel(
1238
+ isGridColumnGrouped(groupByColumnsRef.current, column),
1239
+ labels,
1240
+ );
1241
+ },
1242
+ [labels],
1243
+ );
984
1244
 
985
1245
  const filterValueFn = useCallback((columnName: string): string => {
986
1246
  return activeFiltersRef.current[columnName] ?? '';
987
1247
  }, []);
988
1248
 
989
- const filterPlaceholderFn = useCallback((column: GridColumnDef): string => {
990
- return gridFilterPlaceholder(isGridColumnFilterable(optionsRef.current, column), labels);
991
- }, [labels]);
1249
+ const filterPlaceholderFn = useCallback(
1250
+ (column: GridColumnDef): string => {
1251
+ return gridFilterPlaceholder(isGridColumnFilterable(optionsRef.current, column), labels);
1252
+ },
1253
+ [labels],
1254
+ );
992
1255
 
993
1256
  const isFilterInputDisabledFn = useCallback((column: GridColumnDef): boolean => {
994
1257
  return !isGridColumnFilterable(optionsRef.current, column);
995
1258
  }, []);
996
1259
 
997
- const groupDisclosureLabelFn = useCallback((item: GroupItem): string => {
998
- return gridGroupDisclosureLabel(item.collapsed, labels);
999
- }, [labels]);
1260
+ const groupDisclosureLabelFn = useCallback(
1261
+ (item: GroupItem): string => {
1262
+ return gridGroupDisclosureLabel(item.collapsed, labels);
1263
+ },
1264
+ [labels],
1265
+ );
1000
1266
 
1001
- const cellContextFn = useCallback((row: GridRow, column: GridColumnDef): GridCellTemplateContext => {
1002
- return buildGridCellContext(row, column);
1003
- }, []);
1267
+ const cellContextFn = useCallback(
1268
+ (row: GridRow, column: GridColumnDef): GridCellTemplateContext => {
1269
+ return buildGridCellContext(row, column);
1270
+ },
1271
+ [],
1272
+ );
1004
1273
 
1005
- const displayValueFn = useCallback((row: GridRow, column: GridColumnDef): string => {
1006
- return formatGridCellDisplayValue(cellContextFn(row, column));
1007
- }, [cellContextFn]);
1274
+ const displayValueFn = useCallback(
1275
+ (row: GridRow, column: GridColumnDef): string => {
1276
+ return formatGridCellDisplayValue(cellContextFn(row, column));
1277
+ },
1278
+ [cellContextFn],
1279
+ );
1008
1280
 
1009
1281
  const isFocusedCellFn = useCallback((row: GridRow, column: GridColumnDef): boolean => {
1010
1282
  return isGridCellPosition(focusedCellRef.current, row.id, column.name);
@@ -1018,15 +1290,18 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
1018
1290
  return gridEditorInputType(column);
1019
1291
  }, []);
1020
1292
 
1021
- const expandedContextFn = useCallback((row: GridRow): GridExpandableTemplateContext & Record<string, unknown> => {
1022
- return {
1023
- $implicit: row.entity,
1024
- row: row.entity,
1025
- rowIndex: row.index,
1026
- expanded: true,
1027
- ...(optionsRef.current.expandableRowScope ?? {}),
1028
- };
1029
- }, []);
1293
+ const expandedContextFn = useCallback(
1294
+ (row: GridRow): GridExpandableTemplateContext & Record<string, unknown> => {
1295
+ return {
1296
+ $implicit: row.entity,
1297
+ row: row.entity,
1298
+ rowIndex: row.index,
1299
+ expanded: true,
1300
+ ...(optionsRef.current.expandableRowScope ?? {}),
1301
+ };
1302
+ },
1303
+ [],
1304
+ );
1030
1305
 
1031
1306
  const columnWidthFn = useCallback((column: GridColumnDef): string => gridColumnWidth(column), []);
1032
1307
 
@@ -1042,17 +1317,23 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
1042
1317
  return gridCellIndent(optionsRef.current, visibleColumnsRef.current, row, column);
1043
1318
  }, []);
1044
1319
 
1045
- const treeToggleLabelFn = useCallback((row: GridRow): string => {
1046
- return gridTreeToggleLabelForRow(expandedTreeRowsRef.current, row, labels);
1047
- }, [labels]);
1320
+ const treeToggleLabelFn = useCallback(
1321
+ (row: GridRow): string => {
1322
+ return gridTreeToggleLabelForRow(expandedTreeRowsRef.current, row, labels);
1323
+ },
1324
+ [labels],
1325
+ );
1048
1326
 
1049
1327
  const isTreeRowExpandedFn = useCallback((row: GridRow): boolean => {
1050
1328
  return isGridTreeRowExpanded(expandedTreeRowsRef.current, row);
1051
1329
  }, []);
1052
1330
 
1053
- const expandToggleLabelFn = useCallback((row: GridRow): string => {
1054
- return gridExpandToggleLabelForRow(row, labels);
1055
- }, [labels]);
1331
+ const expandToggleLabelFn = useCallback(
1332
+ (row: GridRow): string => {
1333
+ return gridExpandToggleLabelForRow(row, labels);
1334
+ },
1335
+ [labels],
1336
+ );
1056
1337
 
1057
1338
  const isGroupedFn = useCallback((column: GridColumnDef): boolean => {
1058
1339
  return isGridColumnGrouped(groupByColumnsRef.current, column);
@@ -1072,8 +1353,7 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
1072
1353
 
1073
1354
  const paginationSummaryFn = useCallback((): string => {
1074
1355
  const ti = pipelineRef.current.totalItems;
1075
- if (ti === 0) return '0-0 of 0';
1076
- return `${getFirstRowIndexValueFn(ti) + 1}-${getLastRowIndexValueFn(ti) + 1} of ${ti}`;
1356
+ return formatPaginationSummary(ti, getFirstRowIndexValueFn(ti), getLastRowIndexValueFn(ti));
1077
1357
  }, [getFirstRowIndexValueFn, getLastRowIndexValueFn]);
1078
1358
 
1079
1359
  const pageSizeOptionsFn = useCallback((): number[] => {
@@ -1093,7 +1373,10 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
1093
1373
  const toggleSortFn = useCallback((column: GridColumnDef): void => {
1094
1374
  if (!FEATURE_SORTING || !isGridColumnSortable(optionsRef.current, column)) return;
1095
1375
 
1096
- const currentDirection = sortStateRef.current.columnName === column.name ? sortStateRef.current.direction : SORT_DIRECTIONS.none;
1376
+ const currentDirection =
1377
+ sortStateRef.current.columnName === column.name
1378
+ ? sortStateRef.current.direction
1379
+ : SORT_DIRECTIONS.none;
1097
1380
  const nextDirection =
1098
1381
  currentDirection === SORT_DIRECTIONS.none
1099
1382
  ? SORT_DIRECTIONS.asc
@@ -1113,7 +1396,7 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
1113
1396
  (updater) => setActiveFilters((current) => updater(current)),
1114
1397
  () => activeFiltersRef.current,
1115
1398
  columnName,
1116
- value
1399
+ value,
1117
1400
  );
1118
1401
  }, []);
1119
1402
 
@@ -1140,126 +1423,151 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
1140
1423
  }));
1141
1424
  }, []);
1142
1425
 
1143
- const focusCellFn = useCallback((row: GridRow, column: GridColumnDef, triggerEvent?: Event | KeyboardEvent | null): void => {
1144
- const nextFocusResult = buildGridFocusCellResult({
1145
- currentFocusedCell: focusedCellRef.current,
1146
- currentEditingCell: editingCellRef.current,
1147
- rowId: row.id,
1148
- columnName: column.name,
1149
- shouldEditOnFocus: shouldEditOnFocusFn(column),
1150
- isCellEditable: isCellEditable(row, column, triggerEvent),
1151
- });
1152
- setFocusedCell(nextFocusResult.focusedCell);
1426
+ const focusCellFn = useCallback(
1427
+ (row: GridRow, column: GridColumnDef, triggerEvent?: Event | KeyboardEvent | null): void => {
1428
+ const nextFocusResult = buildGridFocusCellResult({
1429
+ currentFocusedCell: focusedCellRef.current,
1430
+ currentEditingCell: editingCellRef.current,
1431
+ rowId: row.id,
1432
+ columnName: column.name,
1433
+ shouldEditOnFocus: shouldEditOnFocusFn(column),
1434
+ isCellEditable: isCellEditable(row, column, triggerEvent),
1435
+ });
1436
+ setFocusedCell(nextFocusResult.focusedCell);
1153
1437
 
1154
- if (nextFocusResult.shouldBeginEdit) {
1155
- startCellEditFn(row, column, triggerEvent);
1156
- }
1157
- }, [isCellEditable, shouldEditOnFocusFn, startCellEditFn]);
1438
+ if (nextFocusResult.shouldBeginEdit) {
1439
+ startCellEditFn(row, column, triggerEvent);
1440
+ }
1441
+ },
1442
+ [isCellEditable, shouldEditOnFocusFn, startCellEditFn],
1443
+ );
1158
1444
 
1159
- const handleCellKeyDownFn = useCallback((row: GridRow, column: GridColumnDef, event: React.KeyboardEvent): void => {
1160
- focusCellFn(row, column, event.nativeEvent);
1445
+ const handleCellKeyDownFn = useCallback(
1446
+ (row: GridRow, column: GridColumnDef, event: React.KeyboardEvent): void => {
1447
+ focusCellFn(row, column, event.nativeEvent);
1161
1448
 
1162
- switch (event.key) {
1163
- case 'ArrowLeft':
1164
- event.preventDefault();
1165
- moveFocusFn(row, column, 'left', event.nativeEvent);
1166
- return;
1167
- case 'ArrowRight':
1168
- event.preventDefault();
1169
- moveFocusFn(row, column, 'right', event.nativeEvent);
1170
- return;
1171
- case 'ArrowUp':
1172
- event.preventDefault();
1173
- moveFocusFn(row, column, 'up', event.nativeEvent);
1174
- return;
1175
- case 'ArrowDown':
1176
- event.preventDefault();
1177
- moveFocusFn(row, column, 'down', event.nativeEvent);
1178
- return;
1179
- case 'Tab':
1180
- event.preventDefault();
1181
- moveFocusFn(row, column, event.shiftKey ? 'left' : 'right', event.nativeEvent);
1182
- return;
1183
- case 'Enter':
1184
- event.preventDefault();
1185
- moveFocusFn(row, column, event.shiftKey ? 'up' : 'down', event.nativeEvent);
1186
- return;
1187
- case 'F2':
1188
- event.preventDefault();
1189
- if (isCellEditable(row, column, event.nativeEvent)) {
1190
- startCellEditFn(row, column, event.nativeEvent);
1191
- }
1192
- return;
1193
- case 'Backspace':
1194
- case 'Delete':
1195
- if (isCellEditable(row, column, event.nativeEvent)) {
1449
+ switch (event.key) {
1450
+ case 'ArrowLeft':
1196
1451
  event.preventDefault();
1197
- startCellEditFn(row, column, event.nativeEvent, '');
1198
- }
1199
- return;
1200
- default:
1201
- break;
1202
- }
1452
+ moveFocusFn(row, column, 'left', event.nativeEvent);
1453
+ return;
1454
+ case 'ArrowRight':
1455
+ event.preventDefault();
1456
+ moveFocusFn(row, column, 'right', event.nativeEvent);
1457
+ return;
1458
+ case 'ArrowUp':
1459
+ event.preventDefault();
1460
+ moveFocusFn(row, column, 'up', event.nativeEvent);
1461
+ return;
1462
+ case 'ArrowDown':
1463
+ event.preventDefault();
1464
+ moveFocusFn(row, column, 'down', event.nativeEvent);
1465
+ return;
1466
+ case 'Tab':
1467
+ event.preventDefault();
1468
+ moveFocusFn(row, column, event.shiftKey ? 'left' : 'right', event.nativeEvent);
1469
+ return;
1470
+ case 'Enter':
1471
+ event.preventDefault();
1472
+ moveFocusFn(row, column, event.shiftKey ? 'up' : 'down', event.nativeEvent);
1473
+ return;
1474
+ case 'F2':
1475
+ event.preventDefault();
1476
+ if (isCellEditable(row, column, event.nativeEvent)) {
1477
+ startCellEditFn(row, column, event.nativeEvent);
1478
+ }
1479
+ return;
1480
+ case 'Backspace':
1481
+ case 'Delete':
1482
+ if (isCellEditable(row, column, event.nativeEvent)) {
1483
+ event.preventDefault();
1484
+ startCellEditFn(row, column, event.nativeEvent, '');
1485
+ }
1486
+ return;
1487
+ default:
1488
+ break;
1489
+ }
1203
1490
 
1204
- if (isPrintableGridKey(event.key, event.ctrlKey, event.metaKey, event.altKey) && isCellEditable(row, column, event.nativeEvent)) {
1205
- event.preventDefault();
1206
- startCellEditFn(row, column, event.nativeEvent, event.key);
1207
- }
1208
- }, [focusCellFn, moveFocusFn, isCellEditable, startCellEditFn]);
1491
+ if (
1492
+ isPrintableGridKey(event.key, event.ctrlKey, event.metaKey, event.altKey) &&
1493
+ isCellEditable(row, column, event.nativeEvent)
1494
+ ) {
1495
+ event.preventDefault();
1496
+ startCellEditFn(row, column, event.nativeEvent, event.key);
1497
+ }
1498
+ },
1499
+ [focusCellFn, moveFocusFn, isCellEditable, startCellEditFn],
1500
+ );
1209
1501
 
1210
- const handleCellDoubleClickFn = useCallback((row: GridRow, column: GridColumnDef, event: React.MouseEvent): void => {
1211
- focusCellFn(row, column, event.nativeEvent);
1212
- if (isCellEditable(row, column, event.nativeEvent)) {
1213
- startCellEditFn(row, column, event.nativeEvent);
1214
- }
1215
- }, [focusCellFn, isCellEditable, startCellEditFn]);
1502
+ const handleCellDoubleClickFn = useCallback(
1503
+ (row: GridRow, column: GridColumnDef, event: React.MouseEvent): void => {
1504
+ focusCellFn(row, column, event.nativeEvent);
1505
+ if (isCellEditable(row, column, event.nativeEvent)) {
1506
+ startCellEditFn(row, column, event.nativeEvent);
1507
+ }
1508
+ },
1509
+ [focusCellFn, isCellEditable, startCellEditFn],
1510
+ );
1216
1511
 
1217
1512
  const updateEditingValueFn = useCallback((value: string): void => {
1218
1513
  setEditingValue(value);
1219
1514
  }, []);
1220
1515
 
1221
- const handleEditorKeyDownFn = useCallback((event: React.KeyboardEvent): void => {
1222
- if (event.key === 'Escape') {
1223
- event.preventDefault();
1224
- cancelCellEditFn();
1225
- return;
1226
- }
1227
- if (event.key === 'Enter') {
1228
- event.preventDefault();
1229
- commitCellEditFn(event.shiftKey ? 'up' : 'down');
1230
- return;
1231
- }
1232
- if (event.key === 'Tab') {
1233
- event.preventDefault();
1234
- commitCellEditFn(event.shiftKey ? 'left' : 'right');
1235
- }
1236
- }, [cancelCellEditFn, commitCellEditFn]);
1516
+ const handleEditorKeyDownFn = useCallback(
1517
+ (event: React.KeyboardEvent): void => {
1518
+ if (event.key === 'Escape') {
1519
+ event.preventDefault();
1520
+ cancelCellEditFn();
1521
+ return;
1522
+ }
1523
+ if (event.key === 'Enter') {
1524
+ event.preventDefault();
1525
+ commitCellEditFn(event.shiftKey ? 'up' : 'down');
1526
+ return;
1527
+ }
1528
+ if (event.key === 'Tab') {
1529
+ event.preventDefault();
1530
+ commitCellEditFn(event.shiftKey ? 'left' : 'right');
1531
+ }
1532
+ },
1533
+ [cancelCellEditFn, commitCellEditFn],
1534
+ );
1237
1535
 
1238
- const handleEditorBlurFn = useCallback((event: React.FocusEvent): void => {
1239
- const ec = editingCellRef.current;
1240
- const target = event.target as HTMLElement | null;
1241
- if (!ec || !target) return;
1242
- if (target.dataset['rowId'] !== ec.rowId || target.dataset['colName'] !== ec.columnName) return;
1243
- commitCellEditFn(undefined, false);
1244
- }, [commitCellEditFn]);
1536
+ const handleEditorBlurFn = useCallback(
1537
+ (event: React.FocusEvent): void => {
1538
+ const ec = editingCellRef.current;
1539
+ const target = event.target as HTMLElement | null;
1540
+ if (!ec || !target) return;
1541
+ if (target.dataset['rowId'] !== ec.rowId || target.dataset['colName'] !== ec.columnName)
1542
+ return;
1543
+ commitCellEditFn(undefined, false);
1544
+ },
1545
+ [commitCellEditFn],
1546
+ );
1245
1547
 
1246
- const toggleRowExpansionFn = useCallback((row: GridRow, event?: React.MouseEvent): void => {
1247
- event?.stopPropagation();
1248
- toggleRowExpansionByRefFn(row);
1249
- }, [toggleRowExpansionByRefFn]);
1548
+ const toggleRowExpansionFn = useCallback(
1549
+ (row: GridRow, event?: React.MouseEvent): void => {
1550
+ event?.stopPropagation();
1551
+ toggleRowExpansionByRefFn(row);
1552
+ },
1553
+ [toggleRowExpansionByRefFn],
1554
+ );
1250
1555
 
1251
- const toggleTreeRowFn = useCallback((row: GridRow, event?: React.MouseEvent): void => {
1252
- event?.stopPropagation();
1253
- toggleTreeRowByRefFn(row);
1254
- }, [toggleTreeRowByRefFn]);
1556
+ const toggleTreeRowFn = useCallback(
1557
+ (row: GridRow, event?: React.MouseEvent): void => {
1558
+ event?.stopPropagation();
1559
+ toggleTreeRowByRefFn(row);
1560
+ },
1561
+ [toggleTreeRowByRefFn],
1562
+ );
1255
1563
 
1256
1564
  const moveColumnFn = useCallback((fromIndex: number, toIndex: number): void => {
1257
1565
  moveGridColumnCommand(
1258
1566
  gridApiRef.current!,
1259
- FEATURE_COLUMN_MOVING && (optionsRef.current.enableColumnMoving === true),
1567
+ FEATURE_COLUMN_MOVING && optionsRef.current.enableColumnMoving === true,
1260
1568
  (updater) => setColumnOrder((current) => updater(current)),
1261
1569
  fromIndex,
1262
- toIndex
1570
+ toIndex,
1263
1571
  );
1264
1572
  }, []);
1265
1573
 
@@ -1271,9 +1579,12 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
1271
1579
  seekPageFn(getCurrentPageValueFn() - 1);
1272
1580
  }, [seekPageFn, getCurrentPageValueFn]);
1273
1581
 
1274
- const onPageSizeChangeFn = useCallback((value: string): void => {
1275
- setPaginationPageSizeFn(Number(value));
1276
- }, [setPaginationPageSizeFn]);
1582
+ const onPageSizeChangeFn = useCallback(
1583
+ (value: string): void => {
1584
+ setPaginationPageSizeFn(Number(value));
1585
+ },
1586
+ [setPaginationPageSizeFn],
1587
+ );
1277
1588
 
1278
1589
  const onViewportScrollFn = useCallback((startIndex: number): void => {
1279
1590
  if (!scrollingRef.current) {
@@ -1290,11 +1601,11 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
1290
1601
  raiseGridScrollEnd(gridApiRef.current!);
1291
1602
  }, 120);
1292
1603
 
1293
- const isInfiniteScrollEnabled = FEATURE_INFINITE_SCROLL && (
1294
- optionsRef.current.infiniteScrollRowsFromEnd !== undefined
1295
- || optionsRef.current.infiniteScrollUp === true
1296
- || optionsRef.current.infiniteScrollDown !== undefined
1297
- );
1604
+ const isInfiniteScrollEnabled =
1605
+ FEATURE_INFINITE_SCROLL &&
1606
+ (optionsRef.current.infiniteScrollRowsFromEnd !== undefined ||
1607
+ optionsRef.current.infiniteScrollUp === true ||
1608
+ optionsRef.current.infiniteScrollDown !== undefined);
1298
1609
 
1299
1610
  maybeRequestInfiniteScrollCommand(gridApiRef.current!, {
1300
1611
  enabled: isInfiniteScrollEnabled,
@@ -1302,7 +1613,10 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
1302
1613
  state: infiniteScrollStateRef.current,
1303
1614
  startIndex,
1304
1615
  visibleRows: pipelineRef.current.visibleRows.length,
1305
- viewportRows: Math.max(1, Math.ceil((optionsRef.current.viewportHeight ?? 560) / (optionsRef.current.rowHeight ?? 44))),
1616
+ viewportRows: computeViewportRows(
1617
+ optionsRef.current.viewportHeight,
1618
+ optionsRef.current.rowHeight,
1619
+ ),
1306
1620
  threshold: optionsRef.current.infiniteScrollRowsFromEnd ?? 20,
1307
1621
  setState: (state) => setInfiniteScrollState(state),
1308
1622
  });
@@ -1410,5 +1724,12 @@ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridA
1410
1724
  runBenchmark: runBenchmarkFn,
1411
1725
  exportCsv: exportCsvFn,
1412
1726
  onViewportScroll: onViewportScrollFn,
1727
+ // Pinning
1728
+ isPinned: isPinnedFn,
1729
+ pinnedOffset: pinnedOffsetFn,
1730
+ isPinningEnabled: isPinningEnabledFn,
1731
+ isColumnPinnable: isColumnPinnableFn,
1732
+ togglePin: togglePinFn,
1733
+ pinningFeature: FEATURE_PINNING,
1413
1734
  };
1414
1735
  }