@svgrid/grid 1.1.1 → 1.2.1

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.
Files changed (188) hide show
  1. package/dist/FlexRender.svelte +96 -96
  2. package/dist/SvGrid.controller.svelte.js +17 -6
  3. package/dist/SvGrid.css +2012 -2012
  4. package/dist/SvGrid.svelte +2571 -2346
  5. package/dist/SvGridDropdown.svelte +7 -3
  6. package/dist/build-api.js +2 -2
  7. package/dist/cdn/svgrid.js +13504 -0
  8. package/dist/cdn/svgrid.svelte-external.js +11188 -0
  9. package/dist/cell-values.d.ts +1 -1
  10. package/dist/cell-values.js +7 -7
  11. package/dist/column-types.js +1 -1
  12. package/dist/core.d.ts +1 -1
  13. package/dist/core.js +2 -2
  14. package/dist/summaries.js +4 -4
  15. package/package.json +12 -2
  16. package/src/FlexRender.svelte +96 -96
  17. package/src/SvGrid.controller.svelte.ts +2363 -2352
  18. package/src/SvGrid.css +2012 -2012
  19. package/src/SvGrid.svelte +2571 -2346
  20. package/src/SvGrid.types.ts +537 -537
  21. package/src/SvGridDropdown.svelte +7 -3
  22. package/src/a11y.contract.test.ts +49 -49
  23. package/src/a11y.test.ts +59 -59
  24. package/src/a11y.ts +59 -59
  25. package/src/build-api.ts +683 -683
  26. package/src/cell-formatting.ts +169 -169
  27. package/src/cell-values.ts +4 -4
  28. package/src/column-types.ts +1 -1
  29. package/src/core.performance.test.ts +30 -30
  30. package/src/core.ts +1077 -1077
  31. package/src/createGrid.svelte.ts +42 -42
  32. package/src/createGrid.test.ts +10 -10
  33. package/src/createGridState.svelte.ts +17 -17
  34. package/src/editing.ts +669 -669
  35. package/src/flex-render.ts +3 -3
  36. package/src/index.ts +208 -208
  37. package/src/keyboard.test.ts +59 -59
  38. package/src/keyboard.ts +97 -97
  39. package/src/merge-objects.ts +48 -48
  40. package/src/render-component.ts +28 -28
  41. package/src/spreadsheet.test.ts +489 -489
  42. package/src/spreadsheet.ts +304 -304
  43. package/src/static-functions.ts +11 -11
  44. package/src/subscribe.ts +38 -38
  45. package/src/summaries.ts +4 -4
  46. package/src/svgrid-wrapper.types.ts +412 -412
  47. package/src/svgrid.features.test.ts +157 -157
  48. package/src/svgrid.wrapper.test.ts +40 -40
  49. package/src/virtualization/column-virtualizer.test.ts +27 -27
  50. package/src/virtualization/column-virtualizer.ts +30 -30
  51. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  52. package/src/virtualization/types.ts +30 -30
  53. package/src/virtualization/virtualizer.test.ts +47 -47
  54. package/src/virtualization/virtualizer.ts +296 -296
  55. package/dist/SvGrid.helpers.test.d.ts +0 -1
  56. package/dist/SvGrid.helpers.test.js +0 -298
  57. package/dist/SvGridChart.test.d.ts +0 -1
  58. package/dist/SvGridChart.test.js +0 -62
  59. package/dist/a11y.contract.test.d.ts +0 -1
  60. package/dist/a11y.contract.test.js +0 -39
  61. package/dist/a11y.test.d.ts +0 -1
  62. package/dist/a11y.test.js +0 -44
  63. package/dist/aligned-grids.test.d.ts +0 -1
  64. package/dist/aligned-grids.test.js +0 -75
  65. package/dist/build-api.coverage.test.d.ts +0 -20
  66. package/dist/build-api.coverage.test.js +0 -505
  67. package/dist/cell-formatting.test.d.ts +0 -1
  68. package/dist/cell-formatting.test.js +0 -234
  69. package/dist/cell-render.test.d.ts +0 -1
  70. package/dist/cell-render.test.js +0 -338
  71. package/dist/chart-export.test.d.ts +0 -1
  72. package/dist/chart-export.test.js +0 -302
  73. package/dist/chart.coverage.test.d.ts +0 -1
  74. package/dist/chart.coverage.test.js +0 -748
  75. package/dist/chart.test.d.ts +0 -1
  76. package/dist/chart.test.js +0 -289
  77. package/dist/clipboard.test.d.ts +0 -1
  78. package/dist/clipboard.test.js +0 -700
  79. package/dist/collaboration.coverage.test.d.ts +0 -1
  80. package/dist/collaboration.coverage.test.js +0 -200
  81. package/dist/collaboration.test.d.ts +0 -1
  82. package/dist/collaboration.test.js +0 -65
  83. package/dist/column-groups.test.d.ts +0 -1
  84. package/dist/column-groups.test.js +0 -56
  85. package/dist/column-types.test.d.ts +0 -1
  86. package/dist/column-types.test.js +0 -62
  87. package/dist/columns.test.d.ts +0 -1
  88. package/dist/columns.test.js +0 -625
  89. package/dist/conditional-formatting.test.d.ts +0 -1
  90. package/dist/conditional-formatting.test.js +0 -87
  91. package/dist/core.coverage.test.d.ts +0 -1
  92. package/dist/core.coverage.test.js +0 -186
  93. package/dist/core.performance.test.d.ts +0 -1
  94. package/dist/core.performance.test.js +0 -29
  95. package/dist/createGrid.test.d.ts +0 -1
  96. package/dist/createGrid.test.js +0 -9
  97. package/dist/editing.test.d.ts +0 -1
  98. package/dist/editing.test.js +0 -732
  99. package/dist/editors/cell-editors.coverage.test.d.ts +0 -1
  100. package/dist/editors/cell-editors.coverage.test.js +0 -139
  101. package/dist/editors/cell-editors.test.d.ts +0 -1
  102. package/dist/editors/cell-editors.test.js +0 -75
  103. package/dist/facet-buckets.test.d.ts +0 -1
  104. package/dist/facet-buckets.test.js +0 -296
  105. package/dist/fill-patterns.test.d.ts +0 -1
  106. package/dist/fill-patterns.test.js +0 -81
  107. package/dist/filter-operators.test.d.ts +0 -1
  108. package/dist/filter-operators.test.js +0 -135
  109. package/dist/filtering/excel-filters.test.d.ts +0 -1
  110. package/dist/filtering/excel-filters.test.js +0 -116
  111. package/dist/filtering/locale-filter.test.d.ts +0 -13
  112. package/dist/filtering/locale-filter.test.js +0 -189
  113. package/dist/flex-render.test.d.ts +0 -1
  114. package/dist/flex-render.test.js +0 -142
  115. package/dist/group-aggregate.test.d.ts +0 -1
  116. package/dist/group-aggregate.test.js +0 -32
  117. package/dist/hyperformula-adapter.test.d.ts +0 -1
  118. package/dist/hyperformula-adapter.test.js +0 -205
  119. package/dist/keyboard-handlers.coverage.test.d.ts +0 -1
  120. package/dist/keyboard-handlers.coverage.test.js +0 -495
  121. package/dist/keyboard.test.d.ts +0 -1
  122. package/dist/keyboard.test.js +0 -27
  123. package/dist/menus.test.d.ts +0 -1
  124. package/dist/menus.test.js +0 -560
  125. package/dist/named-views.coverage.test.d.ts +0 -1
  126. package/dist/named-views.coverage.test.js +0 -180
  127. package/dist/named-views.test.d.ts +0 -1
  128. package/dist/named-views.test.js +0 -57
  129. package/dist/new-features.test.d.ts +0 -1
  130. package/dist/new-features.test.js +0 -183
  131. package/dist/render-component.test.d.ts +0 -1
  132. package/dist/render-component.test.js +0 -41
  133. package/dist/row-drag.test.d.ts +0 -1
  134. package/dist/row-drag.test.js +0 -142
  135. package/dist/row-resize.test.d.ts +0 -1
  136. package/dist/row-resize.test.js +0 -329
  137. package/dist/scroll-sync.test.d.ts +0 -1
  138. package/dist/scroll-sync.test.js +0 -290
  139. package/dist/selection.multi-range.test.d.ts +0 -1
  140. package/dist/selection.multi-range.test.js +0 -55
  141. package/dist/selection.test.d.ts +0 -1
  142. package/dist/selection.test.js +0 -647
  143. package/dist/server-data-source.coverage.test.d.ts +0 -1
  144. package/dist/server-data-source.coverage.test.js +0 -154
  145. package/dist/server-data-source.test.d.ts +0 -1
  146. package/dist/server-data-source.test.js +0 -81
  147. package/dist/sparkline.test.d.ts +0 -1
  148. package/dist/sparkline.test.js +0 -52
  149. package/dist/spreadsheet.test.d.ts +0 -1
  150. package/dist/spreadsheet.test.js +0 -446
  151. package/dist/subscribe.test.d.ts +0 -1
  152. package/dist/subscribe.test.js +0 -93
  153. package/dist/svgrid.api-extensions.test.d.ts +0 -14
  154. package/dist/svgrid.api-extensions.test.js +0 -334
  155. package/dist/svgrid.api.test.d.ts +0 -15
  156. package/dist/svgrid.api.test.js +0 -169
  157. package/dist/svgrid.behavior.test.d.ts +0 -15
  158. package/dist/svgrid.behavior.test.js +0 -625
  159. package/dist/svgrid.column-reorder.test.d.ts +0 -15
  160. package/dist/svgrid.column-reorder.test.js +0 -224
  161. package/dist/svgrid.comments-autocomplete.test.d.ts +0 -1
  162. package/dist/svgrid.comments-autocomplete.test.js +0 -96
  163. package/dist/svgrid.context-menu.test.d.ts +0 -1
  164. package/dist/svgrid.context-menu.test.js +0 -102
  165. package/dist/svgrid.features.test.d.ts +0 -1
  166. package/dist/svgrid.features.test.js +0 -129
  167. package/dist/svgrid.interaction.test.d.ts +0 -9
  168. package/dist/svgrid.interaction.test.js +0 -349
  169. package/dist/svgrid.locale-filtering.test.d.ts +0 -15
  170. package/dist/svgrid.locale-filtering.test.js +0 -261
  171. package/dist/svgrid.new-features.wrapper.test.d.ts +0 -1
  172. package/dist/svgrid.new-features.wrapper.test.js +0 -194
  173. package/dist/svgrid.row-pinning.test.d.ts +0 -14
  174. package/dist/svgrid.row-pinning.test.js +0 -250
  175. package/dist/svgrid.set-filter.test.d.ts +0 -17
  176. package/dist/svgrid.set-filter.test.js +0 -268
  177. package/dist/svgrid.wrapper.test.d.ts +0 -1
  178. package/dist/svgrid.wrapper.test.js +0 -59
  179. package/dist/test-setup.d.ts +0 -1
  180. package/dist/test-setup.js +0 -35
  181. package/dist/transaction.test.d.ts +0 -1
  182. package/dist/transaction.test.js +0 -89
  183. package/dist/virtualization/column-virtualizer.test.d.ts +0 -1
  184. package/dist/virtualization/column-virtualizer.test.js +0 -25
  185. package/dist/virtualization/scroll-scaling.test.d.ts +0 -1
  186. package/dist/virtualization/scroll-scaling.test.js +0 -127
  187. package/dist/virtualization/virtualizer.test.d.ts +0 -1
  188. package/dist/virtualization/virtualizer.test.js +0 -41
@@ -1,495 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { createKeyboard } from './keyboard-handlers';
3
- function makeCtx(overrides = {}) {
4
- const state = {
5
- activeCell: { rowIndex: 1, colIndex: 1, cellId: null },
6
- sorting: [],
7
- ...overrides.state,
8
- };
9
- const setState = vi.fn((updater) => {
10
- Object.assign(state, typeof updater === 'function' ? updater(state) : updater);
11
- });
12
- const ctx = {
13
- editingCell: null,
14
- findOpen: false,
15
- columnMenuFor: null,
16
- operatorMenuFor: null,
17
- history: [],
18
- historyPtr: -1,
19
- historyVersion: 0,
20
- headerHeight: 30,
21
- scrollContainer: { clientHeight: 330 },
22
- props: { rowHeight: 30 },
23
- allRows: [],
24
- allColumns: [],
25
- grid: {
26
- getState: () => state,
27
- store: { setState },
28
- },
29
- // recorded side effects
30
- copySelectionToClipboard: vi.fn(),
31
- pasteFromClipboard: vi.fn(() => Promise.resolve()),
32
- cutSelectionToClipboard: vi.fn(() => Promise.resolve()),
33
- applyHistoryStep: vi.fn(),
34
- clearSelectedCells: vi.fn(() => false),
35
- clearSelectedCellValues: vi.fn(),
36
- onCellDoubleClick: vi.fn(),
37
- startEditingWithChar: vi.fn(() => true),
38
- toggleBooleanCell: vi.fn(),
39
- setActiveCell: vi.fn(),
40
- scrollActiveCellIntoView: vi.fn(),
41
- extendSelection: vi.fn(),
42
- setSelection: vi.fn(),
43
- closeMenus: vi.fn(),
44
- ...overrides,
45
- };
46
- ctx._state = state;
47
- ctx._setState = setState;
48
- return ctx;
49
- }
50
- // A keydown event whose target === currentTarget (so the grid root owns it).
51
- function rootKeyEvent(init) {
52
- const el = document.createElement('div');
53
- const event = new KeyboardEvent('keydown', { cancelable: true, ...init });
54
- Object.defineProperty(event, 'target', { value: el });
55
- Object.defineProperty(event, 'currentTarget', { value: el });
56
- vi.spyOn(event, 'preventDefault');
57
- return event;
58
- }
59
- describe('onGridKeyDown / guards', () => {
60
- it('ignores events that did not originate on the grid root', () => {
61
- const ctx = makeCtx();
62
- const { onGridKeyDown } = createKeyboard(ctx);
63
- const el = document.createElement('div');
64
- const other = document.createElement('div');
65
- const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
66
- Object.defineProperty(event, 'target', { value: other });
67
- Object.defineProperty(event, 'currentTarget', { value: el });
68
- onGridKeyDown(event);
69
- expect(ctx.setActiveCell).not.toHaveBeenCalled();
70
- });
71
- it('ignores all keys while a cell is being edited', () => {
72
- const ctx = makeCtx({ editingCell: { rowIndex: 0, colIndex: 0 } });
73
- const { onGridKeyDown } = createKeyboard(ctx);
74
- onGridKeyDown(rootKeyEvent({ key: 'ArrowDown' }));
75
- expect(ctx.setActiveCell).not.toHaveBeenCalled();
76
- });
77
- });
78
- describe('onGridKeyDown / clipboard shortcuts', () => {
79
- it('Ctrl+C copies the selection', () => {
80
- const ctx = makeCtx();
81
- const { onGridKeyDown } = createKeyboard(ctx);
82
- const e = rootKeyEvent({ key: 'c', ctrlKey: true });
83
- onGridKeyDown(e);
84
- expect(ctx.copySelectionToClipboard).toHaveBeenCalledTimes(1);
85
- expect(e.preventDefault).toHaveBeenCalled();
86
- });
87
- it('Cmd+C (metaKey) also copies', () => {
88
- const ctx = makeCtx();
89
- const { onGridKeyDown } = createKeyboard(ctx);
90
- onGridKeyDown(rootKeyEvent({ key: 'C', metaKey: true }));
91
- expect(ctx.copySelectionToClipboard).toHaveBeenCalledTimes(1);
92
- });
93
- it('Ctrl+V in a secure context reads via the Clipboard API', () => {
94
- const original = navigator.clipboard;
95
- Object.defineProperty(navigator, 'clipboard', {
96
- value: { readText: () => Promise.resolve('') },
97
- configurable: true,
98
- });
99
- const ctx = makeCtx();
100
- const { onGridKeyDown } = createKeyboard(ctx);
101
- const e = rootKeyEvent({ key: 'v', ctrlKey: true });
102
- onGridKeyDown(e);
103
- expect(ctx.pasteFromClipboard).toHaveBeenCalledTimes(1);
104
- expect(e.preventDefault).toHaveBeenCalled();
105
- Object.defineProperty(navigator, 'clipboard', { value: original, configurable: true });
106
- });
107
- it('Ctrl+V in an insecure context does NOT preventDefault (lets native paste through)', () => {
108
- const original = navigator.clipboard;
109
- Object.defineProperty(navigator, 'clipboard', { value: undefined, configurable: true });
110
- const ctx = makeCtx();
111
- const { onGridKeyDown } = createKeyboard(ctx);
112
- const e = rootKeyEvent({ key: 'v', ctrlKey: true });
113
- onGridKeyDown(e);
114
- expect(ctx.pasteFromClipboard).not.toHaveBeenCalled();
115
- expect(e.preventDefault).not.toHaveBeenCalled();
116
- Object.defineProperty(navigator, 'clipboard', { value: original, configurable: true });
117
- });
118
- it('Ctrl+X cuts the selection', () => {
119
- const ctx = makeCtx();
120
- const { onGridKeyDown } = createKeyboard(ctx);
121
- const e = rootKeyEvent({ key: 'x', ctrlKey: true });
122
- onGridKeyDown(e);
123
- expect(ctx.cutSelectionToClipboard).toHaveBeenCalledTimes(1);
124
- expect(e.preventDefault).toHaveBeenCalled();
125
- });
126
- it('does not treat Ctrl+Alt+C as a copy shortcut', () => {
127
- const ctx = makeCtx();
128
- const { onGridKeyDown } = createKeyboard(ctx);
129
- onGridKeyDown(rootKeyEvent({ key: 'c', ctrlKey: true, altKey: true }));
130
- expect(ctx.copySelectionToClipboard).not.toHaveBeenCalled();
131
- });
132
- });
133
- describe('onGridKeyDown / undo & redo', () => {
134
- it('Ctrl+Z undoes when there is history to undo', () => {
135
- const step = { kind: 'edit' };
136
- const ctx = makeCtx({ history: [step], historyPtr: 0 });
137
- const { onGridKeyDown } = createKeyboard(ctx);
138
- onGridKeyDown(rootKeyEvent({ key: 'z', ctrlKey: true }));
139
- expect(ctx.applyHistoryStep).toHaveBeenCalledWith(step, 'undo');
140
- expect(ctx.historyPtr).toBe(-1);
141
- expect(ctx.historyVersion).toBe(1);
142
- });
143
- it('Ctrl+Z is a no-op when nothing to undo', () => {
144
- const ctx = makeCtx({ history: [], historyPtr: -1 });
145
- const { onGridKeyDown } = createKeyboard(ctx);
146
- onGridKeyDown(rootKeyEvent({ key: 'z', ctrlKey: true }));
147
- expect(ctx.applyHistoryStep).not.toHaveBeenCalled();
148
- });
149
- it('Ctrl+Shift+Z redoes when there is a future step', () => {
150
- const step = { kind: 'edit' };
151
- const ctx = makeCtx({ history: [{}, step], historyPtr: 0 });
152
- const { onGridKeyDown } = createKeyboard(ctx);
153
- onGridKeyDown(rootKeyEvent({ key: 'z', ctrlKey: true, shiftKey: true }));
154
- expect(ctx.applyHistoryStep).toHaveBeenCalledWith(step, 'redo');
155
- expect(ctx.historyPtr).toBe(1);
156
- expect(ctx.historyVersion).toBe(1);
157
- });
158
- it('Ctrl+Y also redoes', () => {
159
- const step = { kind: 'edit' };
160
- const ctx = makeCtx({ history: [{}, step], historyPtr: 0 });
161
- const { onGridKeyDown } = createKeyboard(ctx);
162
- onGridKeyDown(rootKeyEvent({ key: 'y', ctrlKey: true }));
163
- expect(ctx.applyHistoryStep).toHaveBeenCalledWith(step, 'redo');
164
- });
165
- it('redo is a no-op when already at the end of history', () => {
166
- const ctx = makeCtx({ history: [{}], historyPtr: 0 });
167
- const { onGridKeyDown } = createKeyboard(ctx);
168
- onGridKeyDown(rootKeyEvent({ key: 'y', ctrlKey: true }));
169
- expect(ctx.applyHistoryStep).not.toHaveBeenCalled();
170
- });
171
- });
172
- describe('onGridKeyDown / find overlay', () => {
173
- it('Ctrl+F opens the find overlay', () => {
174
- const ctx = makeCtx();
175
- const { onGridKeyDown } = createKeyboard(ctx);
176
- onGridKeyDown(rootKeyEvent({ key: 'f', ctrlKey: true }));
177
- expect(ctx.findOpen).toBe(true);
178
- });
179
- it('Escape closes the find overlay when it is open', () => {
180
- const ctx = makeCtx({ findOpen: true });
181
- const { onGridKeyDown } = createKeyboard(ctx);
182
- const e = rootKeyEvent({ key: 'Escape' });
183
- onGridKeyDown(e);
184
- expect(ctx.findOpen).toBe(false);
185
- expect(e.preventDefault).toHaveBeenCalled();
186
- });
187
- });
188
- describe('onGridKeyDown / delete & backspace clearing', () => {
189
- it('Delete clears the selection when cells were cleared', () => {
190
- const ctx = makeCtx({ clearSelectedCells: vi.fn(() => true), allRows: [{}], allColumns: [{}] });
191
- const { onGridKeyDown } = createKeyboard(ctx);
192
- const e = rootKeyEvent({ key: 'Delete' });
193
- onGridKeyDown(e);
194
- expect(ctx.clearSelectedCells).toHaveBeenCalled();
195
- expect(e.preventDefault).toHaveBeenCalled();
196
- });
197
- it('Backspace clears the selection too', () => {
198
- const ctx = makeCtx({ clearSelectedCells: vi.fn(() => true), allRows: [{}], allColumns: [{}] });
199
- const { onGridKeyDown } = createKeyboard(ctx);
200
- onGridKeyDown(rootKeyEvent({ key: 'Backspace' }));
201
- expect(ctx.clearSelectedCells).toHaveBeenCalled();
202
- });
203
- it('Delete falls through to clearCells intent when nothing was cleared', () => {
204
- const ctx = makeCtx({
205
- clearSelectedCells: vi.fn(() => false),
206
- allRows: [{}, {}],
207
- allColumns: [{}, {}],
208
- });
209
- const { onGridKeyDown } = createKeyboard(ctx);
210
- onGridKeyDown(rootKeyEvent({ key: 'Delete' }));
211
- expect(ctx.clearSelectedCellValues).toHaveBeenCalledTimes(1);
212
- });
213
- });
214
- describe('onGridKeyDown / F2 + activation', () => {
215
- it('F2 starts editing the active cell', () => {
216
- const ctx = makeCtx({ allRows: [{}, {}], allColumns: [{}, {}] });
217
- const { onGridKeyDown } = createKeyboard(ctx);
218
- onGridKeyDown(rootKeyEvent({ key: 'F2' }));
219
- expect(ctx.onCellDoubleClick).toHaveBeenCalledWith(1, 1);
220
- });
221
- it('Space toggles row selection', () => {
222
- const toggleSelected = vi.fn();
223
- const ctx = makeCtx({
224
- allRows: [{}, { toggleSelected }],
225
- allColumns: [{}, {}],
226
- });
227
- const { onGridKeyDown } = createKeyboard(ctx);
228
- onGridKeyDown(rootKeyEvent({ key: ' ' }));
229
- expect(toggleSelected).toHaveBeenCalled();
230
- });
231
- it('Space on an expandable row toggles selection (space branch wins over expand)', () => {
232
- // getKeyboardIntent(" ") === "activate". The handler checks `event.key === " "`
233
- // FIRST and toggles row selection, returning before the ctrl/expand branch.
234
- const toggleSelected = vi.fn();
235
- const toggleExpanded = vi.fn();
236
- const row = { toggleSelected, getCanExpand: () => true, toggleExpanded };
237
- const ctx = makeCtx({ allRows: [{}, row], allColumns: [{}, {}] });
238
- const { onGridKeyDown } = createKeyboard(ctx);
239
- onGridKeyDown(rootKeyEvent({ key: ' ', ctrlKey: true }));
240
- expect(toggleSelected).toHaveBeenCalled();
241
- expect(toggleExpanded).not.toHaveBeenCalled();
242
- });
243
- it('F2 on a checkbox column opens editing via onCellDoubleClick', () => {
244
- // F2 is intercepted before intent resolution and always calls
245
- // onCellDoubleClick regardless of column editorType.
246
- const ctx = makeCtx({
247
- allRows: [{}, {}],
248
- allColumns: [{}, { columnDef: { editorType: 'checkbox' } }],
249
- });
250
- const { onGridKeyDown } = createKeyboard(ctx);
251
- onGridKeyDown(rootKeyEvent({ key: 'F2' }));
252
- expect(ctx.onCellDoubleClick).toHaveBeenCalledWith(1, 1);
253
- expect(ctx.toggleBooleanCell).not.toHaveBeenCalled();
254
- });
255
- });
256
- describe('onGridKeyDown / typing to edit', () => {
257
- it('a printable character starts editing seeded with that char', () => {
258
- const ctx = makeCtx({
259
- allRows: [{}, {}],
260
- allColumns: [{}, {}],
261
- startEditingWithChar: vi.fn(() => true),
262
- });
263
- const { onGridKeyDown } = createKeyboard(ctx);
264
- const e = rootKeyEvent({ key: 'a' });
265
- onGridKeyDown(e);
266
- expect(ctx.startEditingWithChar).toHaveBeenCalledWith(1, 1, 'a');
267
- expect(e.preventDefault).toHaveBeenCalled();
268
- });
269
- it('does not preventDefault when editing cannot start', () => {
270
- const ctx = makeCtx({
271
- allRows: [{}, {}],
272
- allColumns: [{}, {}],
273
- startEditingWithChar: vi.fn(() => false),
274
- });
275
- const { onGridKeyDown } = createKeyboard(ctx);
276
- const e = rootKeyEvent({ key: 'a' });
277
- onGridKeyDown(e);
278
- expect(e.preventDefault).not.toHaveBeenCalled();
279
- });
280
- it('ignores a multi-character / modified key as a noop', () => {
281
- const ctx = makeCtx({ allRows: [{}, {}], allColumns: [{}, {}] });
282
- const { onGridKeyDown } = createKeyboard(ctx);
283
- onGridKeyDown(rootKeyEvent({ key: 'Shift' }));
284
- expect(ctx.startEditingWithChar).not.toHaveBeenCalled();
285
- });
286
- });
287
- describe('onGridKeyDown / navigation', () => {
288
- function navCtx(extra = {}) {
289
- return makeCtx({
290
- state: { activeCell: { rowIndex: 1, colIndex: 1, cellId: null } },
291
- allRows: [{}, {}, {}, {}, {}],
292
- allColumns: [{}, {}, {}, {}],
293
- ...extra,
294
- });
295
- }
296
- it('ArrowDown moves the active cell down and updates selection', () => {
297
- const ctx = navCtx();
298
- const { onGridKeyDown } = createKeyboard(ctx);
299
- onGridKeyDown(rootKeyEvent({ key: 'ArrowDown' }));
300
- expect(ctx.setActiveCell).toHaveBeenCalledWith(2, 1);
301
- expect(ctx.scrollActiveCellIntoView).toHaveBeenCalledWith(2, 1);
302
- expect(ctx.setSelection).toHaveBeenCalledWith(2, 1);
303
- expect(ctx.extendSelection).not.toHaveBeenCalled();
304
- });
305
- it('Shift+ArrowRight extends the selection rectangle', () => {
306
- const ctx = navCtx();
307
- const { onGridKeyDown } = createKeyboard(ctx);
308
- onGridKeyDown(rootKeyEvent({ key: 'ArrowRight', shiftKey: true }));
309
- expect(ctx.setActiveCell).toHaveBeenCalledWith(1, 2);
310
- expect(ctx.extendSelection).toHaveBeenCalledWith(1, 2);
311
- expect(ctx.setSelection).not.toHaveBeenCalled();
312
- });
313
- it('Shift+Tab changes direction without extending the range', () => {
314
- // tabPrev is NOT in the arrow-family extend set, so even with shiftKey the
315
- // selection is replaced rather than grown.
316
- const ctx = navCtx({ state: { activeCell: { rowIndex: 1, colIndex: 1, cellId: null } } });
317
- const { onGridKeyDown } = createKeyboard(ctx);
318
- onGridKeyDown(rootKeyEvent({ key: 'Tab', shiftKey: true }));
319
- expect(ctx.setActiveCell).toHaveBeenCalledWith(1, 0);
320
- expect(ctx.extendSelection).not.toHaveBeenCalled();
321
- expect(ctx.setSelection).toHaveBeenCalledWith(1, 0);
322
- });
323
- it('Shift+Enter (moveUp) DOES extend the selection (arrow-family intent)', () => {
324
- const ctx = navCtx();
325
- const { onGridKeyDown } = createKeyboard(ctx);
326
- onGridKeyDown(rootKeyEvent({ key: 'Enter', shiftKey: true }));
327
- // Enter+shift -> moveUp, which is in the extend set.
328
- expect(ctx.setActiveCell).toHaveBeenCalledWith(0, 1);
329
- expect(ctx.extendSelection).toHaveBeenCalledWith(0, 1);
330
- });
331
- it('Tab moves right and wraps using getNextActiveCell', () => {
332
- const ctx = navCtx({ state: { activeCell: { rowIndex: 1, colIndex: 3, cellId: null } } });
333
- const { onGridKeyDown } = createKeyboard(ctx);
334
- onGridKeyDown(rootKeyEvent({ key: 'Tab' }));
335
- // colIndex 3 is maxCol -> wraps to col 0 row 2
336
- expect(ctx.setActiveCell).toHaveBeenCalledWith(2, 0);
337
- });
338
- it('defaults the active cell to (0,0) when state has none', () => {
339
- const ctx = navCtx({ state: { activeCell: undefined } });
340
- const { onGridKeyDown } = createKeyboard(ctx);
341
- onGridKeyDown(rootKeyEvent({ key: 'ArrowRight' }));
342
- expect(ctx.setActiveCell).toHaveBeenCalledWith(0, 1);
343
- });
344
- it('PageDown uses the pagination pageSize when present', () => {
345
- const ctx = navCtx({
346
- state: {
347
- activeCell: { rowIndex: 0, colIndex: 0, cellId: null },
348
- pagination: { pageSize: 2 },
349
- },
350
- });
351
- const { onGridKeyDown } = createKeyboard(ctx);
352
- onGridKeyDown(rootKeyEvent({ key: 'PageDown' }));
353
- expect(ctx.setActiveCell).toHaveBeenCalledWith(2, 0);
354
- });
355
- it('PageDown derives a page step from container height when no pagination', () => {
356
- // clientHeight 330 - headerHeight 30 = 300 usable / 30 row - 1 = 9 step.
357
- const ctx = navCtx({
358
- state: { activeCell: { rowIndex: 0, colIndex: 0, cellId: null } },
359
- allRows: new Array(40).fill({}),
360
- });
361
- const { onGridKeyDown } = createKeyboard(ctx);
362
- onGridKeyDown(rootKeyEvent({ key: 'PageDown' }));
363
- expect(ctx.setActiveCell).toHaveBeenCalledWith(9, 0);
364
- });
365
- it('PageDown handles a functional rowHeight prop', () => {
366
- const ctx = navCtx({
367
- state: { activeCell: { rowIndex: 0, colIndex: 0, cellId: null } },
368
- allRows: new Array(40).fill({}),
369
- props: { rowHeight: () => 60 },
370
- });
371
- const { onGridKeyDown } = createKeyboard(ctx);
372
- onGridKeyDown(rootKeyEvent({ key: 'PageDown' }));
373
- // 300 usable / 60 - 1 = 4
374
- expect(ctx.setActiveCell).toHaveBeenCalledWith(4, 0);
375
- });
376
- it('Home / Ctrl+Home move to row start and grid start', () => {
377
- const ctx = navCtx({ state: { activeCell: { rowIndex: 2, colIndex: 3, cellId: null } } });
378
- const { onGridKeyDown } = createKeyboard(ctx);
379
- onGridKeyDown(rootKeyEvent({ key: 'Home' }));
380
- expect(ctx.setActiveCell).toHaveBeenLastCalledWith(2, 0);
381
- onGridKeyDown(rootKeyEvent({ key: 'Home', ctrlKey: true }));
382
- expect(ctx.setActiveCell).toHaveBeenLastCalledWith(0, 0);
383
- });
384
- it('clearCells intent (Delete with non-empty range) wipes values', () => {
385
- const ctx = navCtx({ clearSelectedCells: vi.fn(() => false) });
386
- const { onGridKeyDown } = createKeyboard(ctx);
387
- onGridKeyDown(rootKeyEvent({ key: 'Delete' }));
388
- expect(ctx.clearSelectedCellValues).toHaveBeenCalled();
389
- expect(ctx.setActiveCell).not.toHaveBeenCalled();
390
- });
391
- });
392
- describe('onWindowKeydown', () => {
393
- it('Escape closes menus when a column menu is open', () => {
394
- const ctx = makeCtx({ columnMenuFor: 'col1' });
395
- const { onWindowKeydown } = createKeyboard(ctx);
396
- onWindowKeydown(new KeyboardEvent('keydown', { key: 'Escape' }));
397
- expect(ctx.closeMenus).toHaveBeenCalled();
398
- });
399
- it('Escape closes menus when an operator menu is open', () => {
400
- const ctx = makeCtx({ operatorMenuFor: 'op1' });
401
- const { onWindowKeydown } = createKeyboard(ctx);
402
- onWindowKeydown(new KeyboardEvent('keydown', { key: 'Escape' }));
403
- expect(ctx.closeMenus).toHaveBeenCalled();
404
- });
405
- it('does nothing when no menu is open', () => {
406
- const ctx = makeCtx();
407
- const { onWindowKeydown } = createKeyboard(ctx);
408
- onWindowKeydown(new KeyboardEvent('keydown', { key: 'Escape' }));
409
- expect(ctx.closeMenus).not.toHaveBeenCalled();
410
- });
411
- it('ignores non-Escape keys', () => {
412
- const ctx = makeCtx({ columnMenuFor: 'col1' });
413
- const { onWindowKeydown } = createKeyboard(ctx);
414
- onWindowKeydown(new KeyboardEvent('keydown', { key: 'Enter' }));
415
- expect(ctx.closeMenus).not.toHaveBeenCalled();
416
- });
417
- });
418
- describe('onHeaderSortClick', () => {
419
- function sortCtx(sorting = [], canSort = true) {
420
- const columns = [
421
- { id: 'name', getCanSort: () => canSort },
422
- { id: 'age', getCanSort: () => true },
423
- ];
424
- return makeCtx({ allColumns: columns, state: { sorting } });
425
- }
426
- it('ignores a click on a non-sortable column', () => {
427
- const ctx = sortCtx([], false);
428
- const { onHeaderSortClick } = createKeyboard(ctx);
429
- onHeaderSortClick(new MouseEvent('click'), 'name');
430
- expect(ctx._setState).not.toHaveBeenCalled();
431
- });
432
- it('ignores a click on an unknown column id', () => {
433
- const ctx = sortCtx();
434
- const { onHeaderSortClick } = createKeyboard(ctx);
435
- onHeaderSortClick(new MouseEvent('click'), 'nope');
436
- expect(ctx._setState).not.toHaveBeenCalled();
437
- });
438
- it('single-click on an unsorted column sorts ascending', () => {
439
- const ctx = sortCtx([]);
440
- const { onHeaderSortClick } = createKeyboard(ctx);
441
- onHeaderSortClick(new MouseEvent('click'), 'name');
442
- expect(ctx._state.sorting).toEqual([{ id: 'name', desc: false }]);
443
- });
444
- it('single-click cycles asc -> desc', () => {
445
- const ctx = sortCtx([{ id: 'name', desc: false }]);
446
- const { onHeaderSortClick } = createKeyboard(ctx);
447
- onHeaderSortClick(new MouseEvent('click'), 'name');
448
- expect(ctx._state.sorting).toEqual([{ id: 'name', desc: true }]);
449
- });
450
- it('single-click cycles desc -> none', () => {
451
- const ctx = sortCtx([{ id: 'name', desc: true }]);
452
- const { onHeaderSortClick } = createKeyboard(ctx);
453
- onHeaderSortClick(new MouseEvent('click'), 'name');
454
- expect(ctx._state.sorting).toEqual([]);
455
- });
456
- it('shift-click appends a new column to a multi-sort', () => {
457
- const ctx = sortCtx([{ id: 'age', desc: false }]);
458
- const { onHeaderSortClick } = createKeyboard(ctx);
459
- onHeaderSortClick(new MouseEvent('click', { shiftKey: true }), 'name');
460
- expect(ctx._state.sorting).toEqual([
461
- { id: 'age', desc: false },
462
- { id: 'name', desc: false },
463
- ]);
464
- });
465
- it('shift-click toggles an existing asc clause to desc', () => {
466
- const ctx = sortCtx([
467
- { id: 'age', desc: false },
468
- { id: 'name', desc: false },
469
- ]);
470
- const { onHeaderSortClick } = createKeyboard(ctx);
471
- onHeaderSortClick(new MouseEvent('click', { shiftKey: true }), 'name');
472
- expect(ctx._state.sorting).toEqual([
473
- { id: 'age', desc: false },
474
- { id: 'name', desc: true },
475
- ]);
476
- });
477
- it('shift-click removes a desc clause from the multi-sort', () => {
478
- const ctx = sortCtx([
479
- { id: 'age', desc: false },
480
- { id: 'name', desc: true },
481
- ]);
482
- const { onHeaderSortClick } = createKeyboard(ctx);
483
- onHeaderSortClick(new MouseEvent('click', { shiftKey: true }), 'name');
484
- expect(ctx._state.sorting).toEqual([{ id: 'age', desc: false }]);
485
- });
486
- it('handles a missing sorting array (defaults to [])', () => {
487
- const ctx = makeCtx({
488
- allColumns: [{ id: 'name', getCanSort: () => true }],
489
- state: { sorting: undefined },
490
- });
491
- const { onHeaderSortClick } = createKeyboard(ctx);
492
- onHeaderSortClick(new MouseEvent('click'), 'name');
493
- expect(ctx._state.sorting).toEqual([{ id: 'name', desc: false }]);
494
- });
495
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,27 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { getKeyboardIntent, getNextActiveCell } from './keyboard';
3
- describe('keyboard navigation engine', () => {
4
- it('maps keys to navigation intents', () => {
5
- expect(getKeyboardIntent(new KeyboardEvent('keydown', { key: 'ArrowLeft' }))).toBe('moveLeft');
6
- expect(getKeyboardIntent(new KeyboardEvent('keydown', { key: 'Home', ctrlKey: true }))).toBe('gridStart');
7
- expect(getKeyboardIntent(new KeyboardEvent('keydown', { key: ' ' }))).toBe('activate');
8
- });
9
- it('computes next active cell within bounds', () => {
10
- const next = getNextActiveCell({ rowIndex: 1, colIndex: 1, cellId: null }, 'moveDown', { maxRow: 2, maxCol: 3, pageSize: 10 });
11
- expect(next.rowIndex).toBe(2);
12
- expect(next.colIndex).toBe(1);
13
- });
14
- it('handles row and grid boundary intents', () => {
15
- const rowEnd = getNextActiveCell({ rowIndex: 2, colIndex: 1, cellId: null }, 'rowEnd', { maxRow: 2, maxCol: 4, pageSize: 2 });
16
- expect(rowEnd.colIndex).toBe(4);
17
- const gridStart = getNextActiveCell({ rowIndex: 2, colIndex: 4, cellId: null }, 'gridStart', { maxRow: 2, maxCol: 4, pageSize: 2 });
18
- expect(gridStart.rowIndex).toBe(0);
19
- expect(gridStart.colIndex).toBe(0);
20
- });
21
- it('clamps page navigation to valid bounds', () => {
22
- const pageUp = getNextActiveCell({ rowIndex: 1, colIndex: 0, cellId: null }, 'pageUp', { maxRow: 20, maxCol: 2, pageSize: 10 });
23
- const pageDown = getNextActiveCell({ rowIndex: 18, colIndex: 0, cellId: null }, 'pageDown', { maxRow: 20, maxCol: 2, pageSize: 10 });
24
- expect(pageUp.rowIndex).toBe(0);
25
- expect(pageDown.rowIndex).toBe(20);
26
- });
27
- });
@@ -1 +0,0 @@
1
- export {};