@ornery/ui-grid-react 0.1.4 → 0.1.5

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