@keenmate/web-grid 1.0.0-rc15 → 1.0.0

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.
@@ -0,0 +1,498 @@
1
+ TYPESCRIPT TYPES
2
+ ================
3
+ @keenmate/web-grid - Key exported types reference.
4
+
5
+ All types are exported from the package entry point:
6
+ import type { Column, EditorOptions, SortState } from '@keenmate/web-grid'
7
+
8
+
9
+ ----------------------------------------------------------------------
10
+ COLUMN<T>
11
+ ----------------------------------------------------------------------
12
+ Defines how a column renders, edits, and behaves.
13
+
14
+ type Column<T> = {
15
+ field: keyof T | string REQUIRED. Data field name.
16
+ title: string Column header text.
17
+ headerInfo?: string Info tooltip (shows i icon in header).
18
+ width?: string e.g., '100px', '20%'
19
+ minWidth?: string Minimum width during resize.
20
+ maxWidth?: string Maximum width during resize.
21
+ horizontalAlign?: 'left' | 'center' | 'right' | 'justify'
22
+ verticalAlign?: 'top' | 'middle' | 'bottom'
23
+ headerHorizontalAlign?: 'left' | 'center' | 'right' | 'justify'
24
+ headerVerticalAlign?: 'top' | 'middle' | 'bottom'
25
+ textOverflow?: 'wrap' | 'ellipsis'
26
+ maxLines?: number Line-clamp when textOverflow is 'wrap'.
27
+ cellClass?: string Static CSS class for all cells.
28
+ cellClassCallback?: (value: unknown, row: T) => string | null
29
+ formatCallback?: (value: unknown, row: T) => string
30
+ templateCallback?: (row: T) => string
31
+ renderCallback?: (row: T, element: HTMLElement) => void
32
+ isEditable?: boolean
33
+ editor?: EditorType
34
+ editTrigger?: EditTrigger
35
+ editorOptions?: EditorOptions<T>
36
+ dropdownToggleVisibility?: ToggleVisibility
37
+ shouldOpenDropdownOnEnter?: boolean
38
+ cellEditCallback?: (context: CustomEditorContext<T>) => void
39
+ isEditButtonVisible?: boolean
40
+ validateCallback?: (value: unknown, row: T) => string | null | Promise<string | null>
41
+ beforeCommitCallback?: (context: BeforeCommitContext<T>) => BeforeCommitResult | Promise<BeforeCommitResult>
42
+ validationTooltipCallback?: (context: ValidationTooltipContext<T>) => string | null
43
+ tooltipMember?: string
44
+ tooltipCallback?: (value: unknown, row: T) => string | null
45
+ beforeCopyCallback?: (value: unknown, row: T) => string
46
+ beforePasteCallback?: (value: string, row: T) => unknown
47
+ isFrozen?: boolean
48
+ isResizable?: boolean Default: true
49
+ isMovable?: boolean Default: true
50
+ isHidden?: boolean
51
+ isSortable?: boolean
52
+ isFilterable?: boolean
53
+ fillDirection?: FillDirection
54
+ }
55
+
56
+
57
+ ----------------------------------------------------------------------
58
+ EDITOROPTIONS<T>
59
+ ----------------------------------------------------------------------
60
+ Configuration for all editor types. Properties are grouped by editor type.
61
+
62
+ type EditorOptions<T> = {
63
+ // Shared (select/combobox/autocomplete)
64
+ options?: EditorOption[]
65
+ loadOptions?: (row: T, field: string) => Promise<EditorOption[]>
66
+ optionsLoadTrigger?: OptionsLoadTrigger
67
+ valueMember?: string
68
+ displayMember?: string
69
+ searchMember?: string
70
+ iconMember?: string
71
+ subtitleMember?: string
72
+ disabledMember?: string
73
+ groupMember?: string
74
+ getValueCallback?: (option: EditorOption) => string | number
75
+ getDisplayCallback?: (option: EditorOption) => string
76
+ getSearchCallback?: (option: EditorOption) => string
77
+ getIconCallback?: (option: EditorOption) => string
78
+ getSubtitleCallback?: (option: EditorOption) => string
79
+ getDisabledCallback?: (option: EditorOption) => boolean
80
+ getGroupCallback?: (option: EditorOption) => string
81
+ renderOptionCallback?: (option: EditorOption, context: OptionRenderContext) => string
82
+ onselect?: (option: EditorOption, row: T) => void
83
+ allowEmpty?: boolean
84
+ emptyLabel?: string
85
+ noOptionsText?: string
86
+ searchingText?: string
87
+ dropdownMinWidth?: string
88
+ placeholder?: string
89
+ // Text
90
+ maxLength?: number
91
+ pattern?: string
92
+ inputMode?: 'text' | 'numeric' | 'email' | 'tel' | 'url'
93
+ editStartSelection?: EditStartSelection
94
+ // Number
95
+ min?: number
96
+ max?: number
97
+ step?: number
98
+ decimalPlaces?: number
99
+ allowNegative?: boolean
100
+ // Checkbox
101
+ trueValue?: unknown
102
+ falseValue?: unknown
103
+ // Date
104
+ minDate?: Date | string
105
+ maxDate?: Date | string
106
+ dateFormat?: string
107
+ outputFormat?: DateOutputFormat
108
+ // Autocomplete
109
+ initialOptions?: EditorOption[]
110
+ searchCallback?: (query: string, row: T, signal?: AbortSignal) => Promise<EditorOption[]>
111
+ minSearchLength?: number
112
+ debounceMs?: number
113
+ multiple?: boolean
114
+ maxSelections?: number
115
+ }
116
+
117
+
118
+ ----------------------------------------------------------------------
119
+ SIMPLE TYPE ALIASES
120
+ ----------------------------------------------------------------------
121
+
122
+ type EditorType = 'text' | 'number' | 'checkbox' | 'select' | 'combobox' | 'date' | 'autocomplete' | 'custom'
123
+
124
+ type EditTrigger = 'click' | 'dblclick' | 'button' | 'always' | 'navigate'
125
+
126
+ type GridMode = 'read-only' | 'excel' | 'input-matrix'
127
+
128
+ type SortMode = 'none' | 'single' | 'multi'
129
+
130
+ type SortDirection = 'asc' | 'desc'
131
+
132
+ type CellSelectionMode = 'disabled' | 'click' | 'shift'
133
+
134
+ type ToggleVisibility = 'always' | 'on-focus'
135
+
136
+ type OptionsLoadTrigger = 'immediate' | 'oneditstart' | 'ondropdownopen'
137
+
138
+ type DateOutputFormat = 'date' | 'iso' | 'timestamp'
139
+
140
+ type EditStartSelection = 'mousePosition' | 'selectAll' | 'cursorAtStart' | 'cursorAtEnd'
141
+
142
+ type FillDirection = 'vertical' | 'all'
143
+
144
+ type ToolbarPosition = 'auto' | 'left' | 'right' | 'top' | 'inline'
145
+
146
+ type LockedRowEditBehavior = 'block' | 'allow' | 'callback'
147
+
148
+ type DataRequestTrigger = 'sort' | 'page' | 'pageSize' | 'init' | 'loadMore'
149
+
150
+ type DataRequestMode = 'replace' | 'append'
151
+
152
+
153
+ ----------------------------------------------------------------------
154
+ SORTSTATE
155
+ ----------------------------------------------------------------------
156
+
157
+ type SortState = {
158
+ column: string
159
+ direction: SortDirection 'asc' | 'desc'
160
+ }
161
+
162
+
163
+ ----------------------------------------------------------------------
164
+ ROWTOOLBARCONFIG<T>
165
+ ----------------------------------------------------------------------
166
+
167
+ type RowToolbarConfig<T> = PredefinedToolbarItemType | RowToolbarItem<T>
168
+
169
+ type PredefinedToolbarItemType = 'add' | 'delete' | 'duplicate' | 'moveUp' | 'moveDown'
170
+
171
+ type RowToolbarItem<T> = {
172
+ id: string
173
+ icon: string
174
+ title: string
175
+ label?: string
176
+ row?: number Row in multi-row toolbar (1 = closest)
177
+ group?: number Group number for divider placement
178
+ minWidth?: string
179
+ type?: PredefinedToolbarItemType
180
+ danger?: boolean
181
+ disabled?: boolean | ((row: T, rowIndex: number) => boolean)
182
+ hidden?: boolean | ((row: T, rowIndex: number) => boolean)
183
+ tooltip?: ToolbarTooltip
184
+ tooltipCallback?: (row: T, rowIndex: number) => string
185
+ onclick?: (detail: { row: T, rowIndex: number }) => void | Promise<void>
186
+ }
187
+
188
+
189
+ ----------------------------------------------------------------------
190
+ CONTEXTMENUITEM<T>
191
+ ----------------------------------------------------------------------
192
+
193
+ type ContextMenuItem<T> = {
194
+ id: string
195
+ label: string | ((context: ContextMenuContext<T>) => string)
196
+ icon?: string | ((context: ContextMenuContext<T>) => string)
197
+ shortcut?: string
198
+ disabled?: boolean | ((context: ContextMenuContext<T>) => boolean)
199
+ visible?: boolean | ((context: ContextMenuContext<T>) => boolean)
200
+ danger?: boolean
201
+ dividerBefore?: boolean
202
+ onclick?: (context: ContextMenuContext<T>) => void | Promise<void>
203
+ }
204
+
205
+ type ContextMenuContext<T> = {
206
+ row: T
207
+ rowIndex: number
208
+ colIndex: number
209
+ column: Column<T>
210
+ cellValue: unknown
211
+ }
212
+
213
+
214
+ ----------------------------------------------------------------------
215
+ HEADERMENUCONFIG<T>
216
+ ----------------------------------------------------------------------
217
+
218
+ type HeaderMenuConfig<T> = PredefinedHeaderMenuItemType | HeaderMenuItem<T>
219
+
220
+ type PredefinedHeaderMenuItemType = 'sortAsc' | 'sortDesc' | 'clearSort' | 'hideColumn' | 'freezeColumn' | 'unfreezeColumn' | 'columnVisibility'
221
+
222
+ type HeaderMenuItem<T> = {
223
+ id: string
224
+ label: string | ((context: HeaderMenuContext<T>) => string)
225
+ icon?: string | ((context: HeaderMenuContext<T>) => string)
226
+ shortcut?: string
227
+ disabled?: boolean | ((context: HeaderMenuContext<T>) => boolean)
228
+ visible?: boolean | ((context: HeaderMenuContext<T>) => boolean)
229
+ danger?: boolean
230
+ dividerBefore?: boolean
231
+ onclick?: (context: HeaderMenuContext<T>) => void | Promise<void>
232
+ children?: HeaderMenuItem<T>[]
233
+ submenu?: (context: HeaderMenuContext<T>) => HeaderMenuItem<T>[]
234
+ }
235
+
236
+ type HeaderMenuContext<T> = {
237
+ column: Column<T>
238
+ field: string
239
+ columnIndex: number
240
+ sortDirection: 'asc' | 'desc' | null
241
+ isFrozen: boolean
242
+ allColumns: Column<T>[]
243
+ labels: GridLabels
244
+ }
245
+
246
+
247
+ ----------------------------------------------------------------------
248
+ ROWSHORTCUT<T> AND RANGESHORTCUT<T>
249
+ ----------------------------------------------------------------------
250
+
251
+ type RowShortcut<T> = {
252
+ key: string e.g., 'Delete', 'Ctrl+D', 'F3'
253
+ id: string
254
+ label: string
255
+ action: (ctx: ShortcutContext<T>) => void | Promise<void>
256
+ disabled?: boolean | ((ctx: ShortcutContext<T>) => boolean)
257
+ }
258
+
259
+ type ShortcutContext<T> = {
260
+ row: T
261
+ rowIndex: number
262
+ colIndex: number
263
+ column: Column<T>
264
+ cellValue: unknown
265
+ }
266
+
267
+ type RangeShortcut<T> = {
268
+ key: string
269
+ id: string
270
+ label: string
271
+ action: (ctx: RangeShortcutContext<T>) => void | Promise<void>
272
+ disabled?: boolean | ((ctx: RangeShortcutContext<T>) => boolean)
273
+ }
274
+
275
+ type RangeShortcutContext<T> = {
276
+ rows: T[]
277
+ rowIndices: number[]
278
+ cellRange?: CellRange
279
+ cells?: Array<{ row: T, rowIndex: number, colIndex: number, field: string, value: unknown }>
280
+ }
281
+
282
+
283
+ ----------------------------------------------------------------------
284
+ FILLDRAGDETAIL
285
+ ----------------------------------------------------------------------
286
+
287
+ type FillDragDetail = {
288
+ sourceCell: {
289
+ rowIndex: number
290
+ colIndex: number
291
+ field: string
292
+ value: unknown
293
+ }
294
+ targetCells: Array<{
295
+ rowIndex: number
296
+ colIndex: number
297
+ field: string
298
+ }>
299
+ direction: 'up' | 'down' | 'left' | 'right'
300
+ }
301
+
302
+
303
+ ----------------------------------------------------------------------
304
+ ROWLOCKINGOPTIONS<T> AND ROWLOCKINFO
305
+ ----------------------------------------------------------------------
306
+
307
+ type RowLockingOptions<T> = {
308
+ lockedMember?: keyof T
309
+ lockInfoMember?: keyof T
310
+ isLockedCallback?: (row: T, rowIndex: number) => boolean
311
+ getLockInfoCallback?: (row: T, rowIndex: number) => RowLockInfo | null
312
+ lockedEditBehavior?: LockedRowEditBehavior
313
+ canEditLockedCallback?: (row: T, lockInfo: RowLockInfo) => boolean
314
+ lockTooltipCallback?: (lockInfo: RowLockInfo, row: T) => string | null
315
+ }
316
+
317
+ type RowLockInfo = {
318
+ isLocked: boolean
319
+ lockedBy?: string
320
+ lockedAt?: Date | string
321
+ reason?: string
322
+ [key: string]: unknown
323
+ }
324
+
325
+ type RowLockChangeDetail<T> = {
326
+ rowId: unknown
327
+ row: T | null
328
+ rowIndex: number
329
+ lockInfo: RowLockInfo | null
330
+ source: 'property' | 'callback' | 'external'
331
+ }
332
+
333
+
334
+ ----------------------------------------------------------------------
335
+ BEFORECOMMITCONTEXT<T> AND BEFORECOMMITRESULT
336
+ ----------------------------------------------------------------------
337
+
338
+ type BeforeCommitContext<T> = {
339
+ value: unknown
340
+ oldValue: unknown
341
+ row: T
342
+ rowIndex: number
343
+ field: string
344
+ }
345
+
346
+ type BeforeCommitResult = ValidationResult | boolean | string | null | undefined
347
+
348
+ type ValidationResult = {
349
+ valid: boolean
350
+ message?: string
351
+ transformedValue?: unknown
352
+ }
353
+
354
+
355
+ ----------------------------------------------------------------------
356
+ ROWCHANGEDETAIL<T>
357
+ ----------------------------------------------------------------------
358
+
359
+ type RowChangeDetail<T> = {
360
+ row: T Original row (unchanged)
361
+ draftRow: T Draft with user's changes (including invalid)
362
+ rowIndex: number
363
+ field: string
364
+ oldValue: unknown
365
+ newValue: unknown
366
+ isValid: boolean
367
+ validationError?: string | null
368
+ }
369
+
370
+
371
+ ----------------------------------------------------------------------
372
+ DATAREQUESTDETAIL
373
+ ----------------------------------------------------------------------
374
+
375
+ type DataRequestDetail = {
376
+ sort: SortState[]
377
+ page: number
378
+ pageSize: number
379
+ trigger: DataRequestTrigger
380
+ mode: DataRequestMode
381
+ skip: number
382
+ }
383
+
384
+
385
+ ----------------------------------------------------------------------
386
+ PAGINATIONLABELSCALLBACK
387
+ ----------------------------------------------------------------------
388
+
389
+ type PaginationLabelsCallback = (context: PaginationLabelsContext) => Partial<PaginationLabels>
390
+
391
+ type PaginationLabelsContext = {
392
+ currentPage: number
393
+ totalPages: number
394
+ totalItems: number
395
+ pageSize: number
396
+ }
397
+
398
+ type PaginationLabels = {
399
+ first: string
400
+ previous: string
401
+ next: string
402
+ last: string
403
+ pageInfo: string
404
+ itemCount: string
405
+ perPage: string
406
+ }
407
+
408
+
409
+ ----------------------------------------------------------------------
410
+ GRIDLABELS
411
+ ----------------------------------------------------------------------
412
+
413
+ type GridLabels = {
414
+ rowActions: string
415
+ inlineActionsHeader: string
416
+ keyboardShortcuts: string
417
+ paginationFirst: string
418
+ paginationPrevious: string
419
+ paginationNext: string
420
+ paginationLast: string
421
+ paginationPageInfo: string
422
+ paginationItemCount: string
423
+ paginationPerPage: string
424
+ dropdownNoOptions: string
425
+ dropdownSearching: string
426
+ contextMenu: {
427
+ sortAsc: string
428
+ sortDesc: string
429
+ clearSort: string
430
+ hideColumn: string
431
+ freezeColumn: string
432
+ unfreezeColumn: string
433
+ columnVisibility: string
434
+ showAll: string
435
+ }
436
+ }
437
+
438
+
439
+ ----------------------------------------------------------------------
440
+ CELLVALIDATIONSTATE
441
+ ----------------------------------------------------------------------
442
+
443
+ type CellValidationState = {
444
+ rowIndex: number
445
+ field: string
446
+ error: string
447
+ }
448
+
449
+ Used by the invalidCells grid property for external validation state.
450
+
451
+
452
+ ----------------------------------------------------------------------
453
+ CELLRANGE
454
+ ----------------------------------------------------------------------
455
+
456
+ type CellRange = {
457
+ startRowIndex: number
458
+ startColIndex: number
459
+ endRowIndex: number
460
+ endColIndex: number
461
+ startField: string
462
+ endField: string
463
+ }
464
+
465
+
466
+ ----------------------------------------------------------------------
467
+ ADDITIONAL EXPORTED TYPES
468
+ ----------------------------------------------------------------------
469
+
470
+ type EditorOption = { value: string | number | boolean, label: string, [key: string]: unknown }
471
+ type OptionRenderContext = { index: number, isHighlighted: boolean, isSelected: boolean, isDisabled: boolean }
472
+ type CustomEditorContext<T> = { value: unknown, row: T, rowIndex: number, field: string, commit: (newValue: unknown) => void, cancel: () => void }
473
+ type ValidationTooltipContext<T> = { field: string, error: string, value: unknown, row: T, rowIndex: number }
474
+ type RowFocusDetail<T> = { rowIndex: number, row: T, previousRowIndex: number | null }
475
+ type CellSelectionChangeDetail = { range: CellRange | null, cellCount: number }
476
+ type ColumnWidthState = { field: string, width: string }
477
+ type ColumnOrderState = { field: string, order: number }
478
+ type ColumnResizeDetail = { field: string, oldWidth: string, newWidth: string, allWidths: ColumnWidthState[] }
479
+ type ColumnReorderDetail = { field: string, fromIndex: number, toIndex: number, allOrder: ColumnOrderState[] }
480
+ type ToolbarClickDetail<T> = { item: NormalizedToolbarItem<T>, rowIndex: number, row: T }
481
+ type ToolbarTooltip = { description?: string, shortcut?: string }
482
+ type SummaryContext<T> = { items: T[], allItems: T[], totalItems: number, currentPage: number, pageSize: number, metadata: unknown }
483
+ type SummaryContentCallback<T> = (context: SummaryContext<T>) => string
484
+ type NewRowPosition = 'top' | 'bottom' (experimental)
485
+
486
+ type BeforePasteDetail<T> = {
487
+ rawText: string, parsedRows: string[][], hasHeaders: boolean,
488
+ headerMapping: PasteColumnMapping[] | null,
489
+ targetRowIndex: number, targetColIndex: number, newRowsCount: number,
490
+ cancel: boolean, skipCells: Set<string>
491
+ }
492
+ type PasteCellResult = { rowIndex: number, field: string, oldValue: unknown, newValue: unknown, success: boolean, error?: string }
493
+ type PasteDetail<T> = {
494
+ totalCells: number, successfulCells: number, failedCells: number,
495
+ skippedCells: number, newRowsCreated: number,
496
+ cellResults: PasteCellResult[], hadHeaders: boolean
497
+ }
498
+ type CreateRowCallback<T> = (pastedData: Record<string, unknown>, rowIndex: number) => T
@@ -0,0 +1,146 @@
1
+ VIRTUAL SCROLL AND INFINITE SCROLL
2
+ ===================================
3
+ @keenmate/web-grid - Performance features for large datasets.
4
+
5
+
6
+ VIRTUAL SCROLL
7
+ --------------
8
+ Virtual scroll renders only the rows visible in the viewport plus a small
9
+ buffer, dramatically reducing DOM nodes for large datasets.
10
+
11
+
12
+ PROPERTIES
13
+ ----------
14
+ isVirtualScrollEnabled (boolean, default: false)
15
+ Explicitly enable virtual scroll.
16
+
17
+ virtualScrollThreshold (number, default: 100)
18
+ Auto-enable virtual scroll when items.length >= this threshold. The grid
19
+ checks this automatically, so you often do not need to set
20
+ isVirtualScrollEnabled manually.
21
+
22
+ virtualScrollRowHeight (number, default: 38)
23
+ Fixed row height in pixels. All rows MUST be the same height for virtual
24
+ scroll to calculate positions correctly. This value is used to compute
25
+ total scroll height, visible row range, and scroll offsets.
26
+
27
+ virtualScrollBuffer (number, default: 10)
28
+ Number of extra rows rendered above and below the visible viewport. This
29
+ prevents blank flashes during fast scrolling. Higher values render more
30
+ DOM nodes but provide smoother scrolling.
31
+
32
+
33
+ HOW IT WORKS
34
+ ------------
35
+ The grid calculates which rows are visible based on:
36
+ - scrollTop of the scroll container
37
+ - viewportHeight (clientHeight of container minus header height)
38
+ - virtualScrollRowHeight (fixed height per row)
39
+
40
+ A spacer element with the total height (rowCount * rowHeight) is placed in
41
+ the table to create the correct scrollbar. Only rows in the visible range
42
+ (plus buffer) are rendered as actual DOM elements. When the user scrolls,
43
+ the visible range is recalculated and rows are added/removed.
44
+
45
+ The grid uses shouldUseVirtualScroll() internally which returns true when
46
+ isVirtualScrollEnabled is set OR when items.length >= virtualScrollThreshold.
47
+
48
+
49
+ KEYBOARD NAVIGATION WITH VIRTUAL SCROLL
50
+ ----------------------------------------
51
+ Arrow keys, PageUp, PageDown, Home, and End all work with virtual scroll.
52
+ When navigating to a row outside the current viewport:
53
+ - The grid scrolls to make the target row visible with minimal movement
54
+ - For PageUp/PageDown, the target row is positioned as the second visible
55
+ row (one row of context above)
56
+ - Focus is set after the scroll-triggered re-render completes
57
+
58
+
59
+ FIXED ROW HEIGHT REQUIREMENT
60
+ -----------------------------
61
+ Virtual scroll requires all rows to be exactly virtualScrollRowHeight pixels
62
+ tall. Variable row heights are NOT supported. If rows have different content
63
+ heights, set a fixed height via CSS:
64
+
65
+ web-grid {
66
+ --wg-row-min-height: 38px;
67
+ }
68
+
69
+ Or use textOverflow: 'ellipsis' on columns to prevent text wrapping.
70
+
71
+
72
+ EXAMPLE
73
+ -------
74
+ grid.items = largeDataset // 10,000 rows
75
+ grid.virtualScrollRowHeight = 40 // Each row is 40px
76
+ grid.virtualScrollBuffer = 15 // Render 15 extra rows each side
77
+ // virtualScrollThreshold defaults to 100, so virtual scroll activates
78
+ // automatically for datasets >= 100 rows
79
+
80
+
81
+ INFINITE SCROLL
82
+ ---------------
83
+ Infinite scroll triggers a data load when the user scrolls near the bottom.
84
+ It is designed for "load more" patterns with server-side data.
85
+
86
+
87
+ PROPERTIES
88
+ ----------
89
+ isInfiniteScrollEnabled (boolean, default: false)
90
+ Enable infinite scroll behavior.
91
+
92
+ infiniteScrollThreshold (number, default: 100)
93
+ Distance from the bottom of the scroll container (in pixels) at which
94
+ the ondatarequest event fires to load more data.
95
+
96
+ hasMoreItems (boolean, default: true)
97
+ Set to false when there is no more data to load. This prevents further
98
+ ondatarequest events from firing.
99
+
100
+
101
+ HOW IT WORKS
102
+ ------------
103
+ When the user scrolls within infiniteScrollThreshold pixels of the bottom,
104
+ the grid fires the ondatarequest event with:
105
+ trigger: 'loadMore'
106
+ mode: 'append'
107
+ skip: current items.length
108
+
109
+ The consumer should:
110
+ 1. Fetch the next batch of data from the server
111
+ 2. Append it to the existing items array
112
+ 3. Set hasMoreItems = false when the server returns no more data
113
+
114
+ Example:
115
+ grid.isInfiniteScrollEnabled = true
116
+ grid.infiniteScrollThreshold = 200
117
+ grid.ondatarequest = async (detail) => {
118
+ if (detail.trigger === 'loadMore') {
119
+ const newItems = await fetchItems(detail.skip, detail.pageSize)
120
+ grid.items = [...grid.items, ...newItems]
121
+ if (newItems.length < detail.pageSize) {
122
+ grid.hasMoreItems = false
123
+ }
124
+ }
125
+ }
126
+
127
+
128
+ COMBINING VIRTUAL AND INFINITE SCROLL
129
+ --------------------------------------
130
+ Virtual scroll and infinite scroll can be used together. The grid renders
131
+ only visible rows (virtual scroll) while loading more data as the user
132
+ scrolls toward the bottom (infinite scroll). This is the recommended
133
+ pattern for very large server-side datasets.
134
+
135
+
136
+ PERFORMANCE CONSIDERATIONS
137
+ --------------------------
138
+ - Virtual scroll reduces DOM nodes from N rows to roughly
139
+ (viewportHeight / rowHeight) + (2 * buffer) rows
140
+ - For 10,000 rows with buffer=10, about 30-40 DOM rows exist at any time
141
+ - Column count still affects performance (each row has N cells)
142
+ - beforeCommitCallback, formatCallback, and other per-cell callbacks still
143
+ run only for rendered cells
144
+ - Cell selection and clipboard operations work across the full dataset,
145
+ not just visible rows
146
+ - Sorting and filtering operate on the full items array
package/dist/grid.d.ts CHANGED
@@ -123,6 +123,7 @@ export declare class WebGrid<T = unknown> {
123
123
  protected _focusedCell: FocusedCell;
124
124
  protected _isCommittingFromKeyboard: boolean;
125
125
  protected _skipNextDropdownAutoEdit: boolean;
126
+ protected _tabTraversalStartColIndex: number | null;
126
127
  protected _hoveredRowIndex: number | null;
127
128
  protected _focusedRowIndex: number | null;
128
129
  protected _onrowfocus: ((detail: RowFocusDetail<T>) => void) | undefined;
@@ -627,6 +628,9 @@ export declare class WebGrid<T = unknown> {
627
628
  * NOTE: Does NOT call requestUpdate() - GridElement handles DOM updates surgically
628
629
  */
629
630
  clearFocusedCell(): void;
631
+ /** Tab traversal start column for Excel-like Enter behavior */
632
+ get tabTraversalStartColIndex(): number | null;
633
+ set tabTraversalStartColIndex(value: number | null);
630
634
  /**
631
635
  * Set the hovered row index (for toolbar/shortcuts)
632
636
  * NOTE: Does NOT call requestUpdate() - GridElement handles UI updates
@@ -5,6 +5,10 @@ import type { GridContext } from '../types.js';
5
5
  * In virtual scroll mode, only scroll if cell is outside viewport (minimal scroll)
6
6
  */
7
7
  export declare function focusCellElement<T>(ctx: GridContext<T>, rowIndex: number, colIndex: number): void;
8
+ /**
9
+ * If cell is behind frozen columns, scroll horizontally to reveal it
10
+ */
11
+ export declare function ensureCellNotBehindFrozen<T>(ctx: GridContext<T>, cell: HTMLElement, rowIndex: number): void;
8
12
  /**
9
13
  * Scroll to position a row as the second visible row (for PageUp/PageDown)
10
14
  * Exported so keyboard handlers can use it directly
@@ -1 +1 @@
1
- export { focusCellElement, updateFocusVisual, clearEditingVisual, cleanupEditState, restoreEditingCellToDisplayMode, handleCellFocus, moveFocus, tryStartEdit, getCursorPositionFromClick, handleTableFocusOut, scrollToRowPosition } from './focus.js';
1
+ export { focusCellElement, ensureCellNotBehindFrozen, updateFocusVisual, clearEditingVisual, cleanupEditState, restoreEditingCellToDisplayMode, handleCellFocus, moveFocus, tryStartEdit, getCursorPositionFromClick, handleTableFocusOut, scrollToRowPosition } from './focus.js';