@svgrid/grid 2.1.22 → 2.2.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.
Files changed (295) hide show
  1. package/README.md +9 -2
  2. package/dist/DockNodeView.svelte +306 -0
  3. package/dist/DockNodeView.svelte.d.ts +7 -0
  4. package/dist/DockPopoutHost.svelte +17 -0
  5. package/dist/DockPopoutHost.svelte.d.ts +14 -0
  6. package/dist/FlexRender.svelte +96 -96
  7. package/dist/GridFooter.svelte +178 -178
  8. package/dist/GridMenus.svelte +83 -3
  9. package/dist/SvAutoComplete.svelte +5 -3
  10. package/dist/SvCarousel.svelte +141 -163
  11. package/dist/SvColorInput.svelte +187 -186
  12. package/dist/SvColorInput.svelte.d.ts +1 -1
  13. package/dist/SvComboBox.svelte +7 -3
  14. package/dist/SvCommand.svelte +28 -85
  15. package/dist/SvContextMenu.svelte +116 -116
  16. package/dist/SvCountryInput.svelte +1 -1
  17. package/dist/SvDateRangeInput.svelte +1 -1
  18. package/dist/SvDateTimePicker.svelte +9 -3
  19. package/dist/SvDateTimePicker.svelte.d.ts +3 -0
  20. package/dist/SvDockLayout.svelte +244 -0
  21. package/dist/SvDockLayout.svelte.d.ts +77 -0
  22. package/dist/SvDockManager.svelte +922 -0
  23. package/dist/SvDockManager.svelte.d.ts +106 -0
  24. package/dist/SvDrawer.svelte +250 -228
  25. package/dist/SvDrawer.svelte.d.ts +4 -0
  26. package/dist/SvDropDownList.svelte +85 -13
  27. package/dist/SvDropDownList.svelte.d.ts +3 -0
  28. package/dist/SvField.svelte +21 -4
  29. package/dist/SvField.svelte.d.ts +1 -0
  30. package/dist/SvForm.svelte +201 -91
  31. package/dist/SvForm.svelte.d.ts +15 -22
  32. package/dist/SvGrid.controller.svelte.d.ts +32 -1
  33. package/dist/SvGrid.controller.svelte.js +156 -27
  34. package/dist/SvGrid.css +193 -5
  35. package/dist/SvGrid.svelte +3131 -2786
  36. package/dist/SvGrid.types.d.ts +452 -1
  37. package/dist/SvGridBoard.svelte +2339 -2339
  38. package/dist/SvGridChart.svelte +1724 -1724
  39. package/dist/SvGridChartPanel.svelte +485 -485
  40. package/dist/SvGridDropdown.svelte +689 -682
  41. package/dist/SvGridSelect.svelte +1 -0
  42. package/dist/SvGroupCell.svelte +109 -109
  43. package/dist/SvListBox.svelte +36 -14
  44. package/dist/SvMaskedInput.svelte +4 -3
  45. package/dist/SvMaskedInput.svelte.d.ts +1 -1
  46. package/dist/SvMenu.svelte +124 -124
  47. package/dist/SvMenuList.svelte +117 -165
  48. package/dist/SvMenuList.svelte.d.ts +2 -13
  49. package/dist/SvModal.svelte +12 -29
  50. package/dist/SvMultiSelect.svelte +1 -0
  51. package/dist/SvNumberInput.svelte +131 -130
  52. package/dist/SvNumberInput.svelte.d.ts +1 -1
  53. package/dist/SvOtpInput.svelte +158 -158
  54. package/dist/SvPagination.svelte +18 -18
  55. package/dist/SvPasswordInput.svelte +3 -2
  56. package/dist/SvPasswordInput.svelte.d.ts +1 -1
  57. package/dist/SvPhoneInput.svelte +3 -2
  58. package/dist/SvPhoneInput.svelte.d.ts +1 -1
  59. package/dist/SvPopover.svelte +139 -139
  60. package/dist/SvRowGroupPanel.svelte +170 -170
  61. package/dist/SvScrollArea.svelte +61 -61
  62. package/dist/SvStepper.svelte +6 -9
  63. package/dist/SvTagsInput.svelte +3 -2
  64. package/dist/SvTagsInput.svelte.d.ts +1 -1
  65. package/dist/SvTooltip.svelte +127 -138
  66. package/dist/SvTour.svelte +204 -176
  67. package/dist/SvTree.svelte +377 -377
  68. package/dist/SvTreeSelect.svelte +230 -229
  69. package/dist/build-api.js +76 -54
  70. package/dist/cdn/svgrid.js +10913 -6450
  71. package/dist/cdn/svgrid.svelte-external.js +9628 -5481
  72. package/dist/chart-export.js +8 -8
  73. package/dist/columns.js +6 -0
  74. package/dist/conditional-formatting.d.ts +67 -3
  75. package/dist/conditional-formatting.js +124 -17
  76. package/dist/createCarousel.svelte.d.ts +72 -0
  77. package/dist/createCarousel.svelte.js +80 -0
  78. package/dist/createCommand.svelte.d.ts +62 -0
  79. package/dist/createCommand.svelte.js +109 -0
  80. package/dist/createForm.svelte.d.ts +62 -0
  81. package/dist/createForm.svelte.js +374 -0
  82. package/dist/createMenu.svelte.d.ts +88 -0
  83. package/dist/createMenu.svelte.js +153 -0
  84. package/dist/createOverlay.svelte.d.ts +38 -0
  85. package/dist/createOverlay.svelte.js +68 -0
  86. package/dist/createPagination.d.ts +74 -0
  87. package/dist/createPagination.js +71 -0
  88. package/dist/createPopoverSelect.svelte.js +6 -12
  89. package/dist/createStepper.d.ts +44 -0
  90. package/dist/createStepper.js +26 -0
  91. package/dist/createTooltip.svelte.d.ts +32 -0
  92. package/dist/createTooltip.svelte.js +61 -0
  93. package/dist/datetime/timezone.d.ts +36 -0
  94. package/dist/datetime/timezone.js +110 -0
  95. package/dist/dock-context.d.ts +52 -0
  96. package/dist/dock-context.js +1 -0
  97. package/dist/dock-manager-model.d.ts +136 -0
  98. package/dist/dock-manager-model.js +514 -0
  99. package/dist/dock-model.d.ts +105 -0
  100. package/dist/dock-model.js +288 -0
  101. package/dist/dock-popout.d.ts +30 -0
  102. package/dist/dock-popout.js +81 -0
  103. package/dist/editor-contract.d.ts +2 -0
  104. package/dist/filter-operators.d.ts +6 -0
  105. package/dist/filter-operators.js +35 -2
  106. package/dist/filtering/excel-filters.d.ts +18 -1
  107. package/dist/filtering/excel-filters.js +56 -0
  108. package/dist/form-field.d.ts +73 -0
  109. package/dist/form-field.js +15 -0
  110. package/dist/index.d.ts +25 -5
  111. package/dist/index.js +40 -4
  112. package/dist/list-nav.d.ts +18 -0
  113. package/dist/list-nav.js +30 -0
  114. package/dist/list-option.d.ts +2 -0
  115. package/dist/menu-item.d.ts +20 -0
  116. package/dist/menu-item.js +1 -0
  117. package/dist/menus.d.ts +5 -0
  118. package/dist/menus.js +68 -16
  119. package/dist/panel-resize.d.ts +25 -0
  120. package/dist/panel-resize.js +40 -0
  121. package/dist/popover.d.ts +29 -3
  122. package/dist/popover.js +22 -6
  123. package/dist/recurrence.d.ts +33 -7
  124. package/dist/recurrence.js +151 -13
  125. package/dist/row-drag.d.ts +8 -0
  126. package/dist/row-drag.js +11 -0
  127. package/dist/scheduler-ical.d.ts +30 -0
  128. package/dist/scheduler-ical.js +180 -0
  129. package/dist/scheduler-model.d.ts +269 -0
  130. package/dist/scheduler-model.js +602 -0
  131. package/dist/scheduler-view.svelte.d.ts +22 -0
  132. package/dist/scheduler-view.svelte.js +13 -0
  133. package/dist/spreadsheet.js +20 -12
  134. package/dist/ui-app.types.d.ts +3 -0
  135. package/package.json +14 -10
  136. package/src/DockNodeView.svelte +306 -0
  137. package/src/DockPopoutHost.svelte +17 -0
  138. package/src/FlexRender.svelte +96 -96
  139. package/src/GridFooter.svelte +178 -178
  140. package/src/GridMenus.svelte +83 -3
  141. package/src/SvAutoComplete.svelte +5 -3
  142. package/src/SvCarousel.svelte +141 -163
  143. package/src/SvCarousel.test.ts +68 -0
  144. package/src/SvColorInput.svelte +187 -186
  145. package/src/SvComboBox.svelte +7 -3
  146. package/src/SvCommand.svelte +28 -85
  147. package/src/SvCommand.test.ts +78 -0
  148. package/src/SvContextMenu.svelte +116 -116
  149. package/src/SvCountryInput.svelte +1 -1
  150. package/src/SvDateRangeInput.svelte +1 -1
  151. package/src/SvDateTimePicker.svelte +9 -3
  152. package/src/SvDockLayout.svelte +244 -0
  153. package/src/SvDockManager.svelte +922 -0
  154. package/src/SvDrawer.svelte +250 -228
  155. package/src/SvDropDownList.svelte +85 -13
  156. package/src/SvField.svelte +21 -4
  157. package/src/SvForm.svelte +201 -91
  158. package/src/SvForm.test.ts +299 -0
  159. package/src/SvGrid.controller.svelte.ts +3158 -3034
  160. package/src/SvGrid.css +193 -5
  161. package/src/SvGrid.svelte +3131 -2786
  162. package/src/SvGrid.types.ts +1489 -1027
  163. package/src/SvGridBoard.svelte +2339 -2339
  164. package/src/SvGridChart.svelte +1724 -1724
  165. package/src/SvGridChartPanel.svelte +485 -485
  166. package/src/SvGridDropdown.svelte +689 -682
  167. package/src/SvGridSelect.svelte +1 -0
  168. package/src/SvGroupCell.svelte +109 -109
  169. package/src/SvListBox.svelte +36 -14
  170. package/src/SvMaskedInput.svelte +4 -3
  171. package/src/SvMenu.svelte +124 -124
  172. package/src/SvMenu.test.ts +97 -73
  173. package/src/SvMenuList.svelte +117 -165
  174. package/src/SvModal.svelte +12 -29
  175. package/src/SvModal.test.ts +82 -0
  176. package/src/SvMultiSelect.svelte +1 -0
  177. package/src/SvNumberInput.svelte +131 -130
  178. package/src/SvOtpInput.svelte +158 -158
  179. package/src/SvPagination.svelte +18 -18
  180. package/src/SvPasswordInput.svelte +3 -2
  181. package/src/SvPhoneInput.svelte +3 -2
  182. package/src/SvPopover.svelte +139 -139
  183. package/src/SvRowGroupPanel.svelte +170 -170
  184. package/src/SvScrollArea.svelte +61 -61
  185. package/src/SvStepper.svelte +6 -9
  186. package/src/SvTagsInput.svelte +3 -2
  187. package/src/SvTooltip.svelte +127 -138
  188. package/src/SvTooltip.test.ts +73 -0
  189. package/src/SvTour.svelte +204 -176
  190. package/src/SvTree.svelte +377 -377
  191. package/src/SvTreeSelect.svelte +230 -229
  192. package/src/a11y.contract.test.ts +49 -49
  193. package/src/a11y.test.ts +59 -59
  194. package/src/a11y.ts +61 -61
  195. package/src/build-api.ts +870 -847
  196. package/src/cell-formatting.ts +169 -169
  197. package/src/cell-render.ts +469 -469
  198. package/src/chart-export.ts +201 -201
  199. package/src/chart.ts +2296 -2296
  200. package/src/collaboration.test.ts +104 -104
  201. package/src/collaboration.ts +167 -167
  202. package/src/columns.ts +5 -0
  203. package/src/conditional-formatting.test.ts +104 -0
  204. package/src/conditional-formatting.ts +176 -19
  205. package/src/core.performance.test.ts +30 -30
  206. package/src/core.ts +1111 -1111
  207. package/src/createAutocomplete.svelte.ts +132 -132
  208. package/src/createCarousel.svelte.ts +104 -0
  209. package/src/createCombobox.svelte.ts +179 -179
  210. package/src/createCommand.svelte.ts +125 -0
  211. package/src/createCountryInput.svelte.ts +157 -157
  212. package/src/createDropdownList.svelte.ts +162 -162
  213. package/src/createForm.svelte.ts +333 -0
  214. package/src/createGrid.svelte.ts +42 -42
  215. package/src/createGrid.test.ts +10 -10
  216. package/src/createGridState.svelte.ts +17 -17
  217. package/src/createMenu.svelte.ts +202 -0
  218. package/src/createOverlay.svelte.ts +101 -0
  219. package/src/createPagination.test.ts +65 -0
  220. package/src/createPagination.ts +84 -0
  221. package/src/createPopoverSelect.svelte.ts +206 -210
  222. package/src/createStepper.test.ts +62 -0
  223. package/src/createStepper.ts +61 -0
  224. package/src/createTooltip.svelte.ts +86 -0
  225. package/src/createTree.svelte.ts +319 -319
  226. package/src/datetime/timezone.test.ts +69 -0
  227. package/src/datetime/timezone.ts +134 -0
  228. package/src/dock-context.ts +47 -0
  229. package/src/dock-manager-model.test.ts +275 -0
  230. package/src/dock-manager-model.ts +593 -0
  231. package/src/dock-model.test.ts +163 -0
  232. package/src/dock-model.ts +368 -0
  233. package/src/dock-popout.ts +94 -0
  234. package/src/editing.test.ts +859 -859
  235. package/src/editing.ts +675 -675
  236. package/src/editor-contract.ts +2 -0
  237. package/src/export-data-api.test.ts +126 -126
  238. package/src/export-format.test.ts +107 -107
  239. package/src/export-format.ts +598 -598
  240. package/src/filter-operators.test.ts +59 -0
  241. package/src/filter-operators.ts +35 -2
  242. package/src/filtering/excel-filters.test.ts +86 -1
  243. package/src/filtering/excel-filters.ts +65 -0
  244. package/src/flex-render.ts +3 -3
  245. package/src/form-field.ts +87 -0
  246. package/src/index.ts +623 -469
  247. package/src/keyboard.test.ts +59 -59
  248. package/src/keyboard.ts +97 -97
  249. package/src/list-nav.test.ts +49 -0
  250. package/src/list-nav.ts +29 -0
  251. package/src/list-option.ts +2 -0
  252. package/src/menu-item.ts +21 -0
  253. package/src/menus.test.ts +48 -0
  254. package/src/menus.ts +639 -582
  255. package/src/merge-objects.ts +48 -48
  256. package/src/overlays.test.ts +90 -90
  257. package/src/panel-resize.ts +52 -0
  258. package/src/popover.bounds.test.ts +93 -0
  259. package/src/popover.ts +51 -7
  260. package/src/recurrence.test.ts +88 -1
  261. package/src/recurrence.ts +166 -15
  262. package/src/render-component.ts +28 -28
  263. package/src/row-drag.ts +10 -0
  264. package/src/scheduler-ical.test.ts +68 -0
  265. package/src/scheduler-ical.ts +180 -0
  266. package/src/scheduler-model.test.ts +546 -0
  267. package/src/scheduler-model.ts +853 -0
  268. package/src/scheduler-view.svelte.ts +33 -0
  269. package/src/selection.test.ts +754 -754
  270. package/src/selection.ts +600 -600
  271. package/src/server-data-source.test.ts +289 -289
  272. package/src/server-data-source.ts +413 -413
  273. package/src/sparkline.test.ts +68 -68
  274. package/src/sparkline.ts +169 -169
  275. package/src/spreadsheet.test.ts +489 -489
  276. package/src/spreadsheet.ts +312 -304
  277. package/src/static-functions.ts +11 -11
  278. package/src/subscribe.ts +38 -38
  279. package/src/svgrid-wrapper.types.ts +501 -501
  280. package/src/svgrid.behavior.test.ts +706 -706
  281. package/src/svgrid.charting.test.ts +527 -527
  282. package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
  283. package/src/svgrid.features.test.ts +157 -157
  284. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  285. package/src/svgrid.wrapper.test.ts +40 -40
  286. package/src/test-setup.ts +62 -62
  287. package/src/themes/index.ts +159 -159
  288. package/src/ui-app.types.ts +3 -0
  289. package/src/virtual.test.ts +6 -4
  290. package/src/virtualization/column-virtualizer.test.ts +27 -27
  291. package/src/virtualization/column-virtualizer.ts +30 -30
  292. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  293. package/src/virtualization/types.ts +30 -30
  294. package/src/virtualization/virtualizer.test.ts +47 -47
  295. package/src/virtualization/virtualizer.ts +296 -296
@@ -1,59 +1,59 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { getKeyboardIntent, getNextActiveCell } from './keyboard'
3
-
4
- describe('keyboard navigation engine', () => {
5
- it('maps keys to navigation intents', () => {
6
- expect(getKeyboardIntent(new KeyboardEvent('keydown', { key: 'ArrowLeft' }))).toBe(
7
- 'moveLeft',
8
- )
9
- expect(getKeyboardIntent(new KeyboardEvent('keydown', { key: 'Home', ctrlKey: true }))).toBe(
10
- 'gridStart',
11
- )
12
- expect(getKeyboardIntent(new KeyboardEvent('keydown', { key: ' ' }))).toBe(
13
- 'activate',
14
- )
15
- })
16
-
17
- it('computes next active cell within bounds', () => {
18
- const next = getNextActiveCell(
19
- { rowIndex: 1, colIndex: 1, cellId: null },
20
- 'moveDown',
21
- { maxRow: 2, maxCol: 3, pageSize: 10 },
22
- )
23
- expect(next.rowIndex).toBe(2)
24
- expect(next.colIndex).toBe(1)
25
- })
26
-
27
- it('handles row and grid boundary intents', () => {
28
- const rowEnd = getNextActiveCell(
29
- { rowIndex: 2, colIndex: 1, cellId: null },
30
- 'rowEnd',
31
- { maxRow: 2, maxCol: 4, pageSize: 2 },
32
- )
33
- expect(rowEnd.colIndex).toBe(4)
34
-
35
- const gridStart = getNextActiveCell(
36
- { rowIndex: 2, colIndex: 4, cellId: null },
37
- 'gridStart',
38
- { maxRow: 2, maxCol: 4, pageSize: 2 },
39
- )
40
- expect(gridStart.rowIndex).toBe(0)
41
- expect(gridStart.colIndex).toBe(0)
42
- })
43
-
44
- it('clamps page navigation to valid bounds', () => {
45
- const pageUp = getNextActiveCell(
46
- { rowIndex: 1, colIndex: 0, cellId: null },
47
- 'pageUp',
48
- { maxRow: 20, maxCol: 2, pageSize: 10 },
49
- )
50
- const pageDown = getNextActiveCell(
51
- { rowIndex: 18, colIndex: 0, cellId: null },
52
- 'pageDown',
53
- { maxRow: 20, maxCol: 2, pageSize: 10 },
54
- )
55
-
56
- expect(pageUp.rowIndex).toBe(0)
57
- expect(pageDown.rowIndex).toBe(20)
58
- })
59
- })
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getKeyboardIntent, getNextActiveCell } from './keyboard'
3
+
4
+ describe('keyboard navigation engine', () => {
5
+ it('maps keys to navigation intents', () => {
6
+ expect(getKeyboardIntent(new KeyboardEvent('keydown', { key: 'ArrowLeft' }))).toBe(
7
+ 'moveLeft',
8
+ )
9
+ expect(getKeyboardIntent(new KeyboardEvent('keydown', { key: 'Home', ctrlKey: true }))).toBe(
10
+ 'gridStart',
11
+ )
12
+ expect(getKeyboardIntent(new KeyboardEvent('keydown', { key: ' ' }))).toBe(
13
+ 'activate',
14
+ )
15
+ })
16
+
17
+ it('computes next active cell within bounds', () => {
18
+ const next = getNextActiveCell(
19
+ { rowIndex: 1, colIndex: 1, cellId: null },
20
+ 'moveDown',
21
+ { maxRow: 2, maxCol: 3, pageSize: 10 },
22
+ )
23
+ expect(next.rowIndex).toBe(2)
24
+ expect(next.colIndex).toBe(1)
25
+ })
26
+
27
+ it('handles row and grid boundary intents', () => {
28
+ const rowEnd = getNextActiveCell(
29
+ { rowIndex: 2, colIndex: 1, cellId: null },
30
+ 'rowEnd',
31
+ { maxRow: 2, maxCol: 4, pageSize: 2 },
32
+ )
33
+ expect(rowEnd.colIndex).toBe(4)
34
+
35
+ const gridStart = getNextActiveCell(
36
+ { rowIndex: 2, colIndex: 4, cellId: null },
37
+ 'gridStart',
38
+ { maxRow: 2, maxCol: 4, pageSize: 2 },
39
+ )
40
+ expect(gridStart.rowIndex).toBe(0)
41
+ expect(gridStart.colIndex).toBe(0)
42
+ })
43
+
44
+ it('clamps page navigation to valid bounds', () => {
45
+ const pageUp = getNextActiveCell(
46
+ { rowIndex: 1, colIndex: 0, cellId: null },
47
+ 'pageUp',
48
+ { maxRow: 20, maxCol: 2, pageSize: 10 },
49
+ )
50
+ const pageDown = getNextActiveCell(
51
+ { rowIndex: 18, colIndex: 0, cellId: null },
52
+ 'pageDown',
53
+ { maxRow: 20, maxCol: 2, pageSize: 10 },
54
+ )
55
+
56
+ expect(pageUp.rowIndex).toBe(0)
57
+ expect(pageDown.rowIndex).toBe(20)
58
+ })
59
+ })
package/src/keyboard.ts CHANGED
@@ -1,97 +1,97 @@
1
- import type { ActiveCellState } from './core'
2
-
3
- export type GridKeyboardIntent =
4
- | 'moveLeft'
5
- | 'moveRight'
6
- | 'moveUp'
7
- | 'moveDown'
8
- | 'tabNext'
9
- | 'tabPrev'
10
- | 'rowStart'
11
- | 'rowEnd'
12
- | 'gridStart'
13
- | 'gridEnd'
14
- | 'pageUp'
15
- | 'pageDown'
16
- | 'activate'
17
- | 'clearCells'
18
- | 'noop'
19
-
20
- export function getKeyboardIntent(event: KeyboardEvent): GridKeyboardIntent {
21
- // Ctrl+Home / Ctrl+End come before plain Home / End so the modifier wins.
22
- if (event.ctrlKey && event.key === 'Home') return 'gridStart'
23
- if (event.ctrlKey && event.key === 'End') return 'gridEnd'
24
-
25
- if (event.key === 'ArrowLeft') return 'moveLeft'
26
- if (event.key === 'ArrowRight') return 'moveRight'
27
- if (event.key === 'ArrowUp') return 'moveUp'
28
- if (event.key === 'ArrowDown') return 'moveDown'
29
- if (event.key === 'Home') return 'rowStart'
30
- if (event.key === 'End') return 'rowEnd'
31
- if (event.key === 'PageUp') return 'pageUp'
32
- if (event.key === 'PageDown') return 'pageDown'
33
-
34
- // Excel-style data-entry navigation:
35
- // Enter → move down (Shift+Enter → up)
36
- // Tab → move right (Shift+Tab → left), wraps at row boundaries
37
- // Delete → clear contents of the selected cells
38
- // F2 / Space → start editing the active cell
39
- if (event.key === 'Enter') return event.shiftKey ? 'moveUp' : 'moveDown'
40
- if (event.key === 'Tab') return event.shiftKey ? 'tabPrev' : 'tabNext'
41
- if (event.key === 'F2') return 'activate'
42
- if (event.key === ' ') return 'activate'
43
- if (event.key === 'Delete') return 'clearCells'
44
-
45
- return 'noop'
46
- }
47
-
48
- export function getNextActiveCell(
49
- current: ActiveCellState,
50
- intent: GridKeyboardIntent,
51
- bounds: { maxRow: number; maxCol: number; pageSize?: number },
52
- ): ActiveCellState {
53
- const pageSize = bounds.pageSize ?? 10
54
- let rowIndex = current.rowIndex
55
- let colIndex = current.colIndex
56
-
57
- if (intent === 'moveLeft') colIndex -= 1
58
- if (intent === 'moveRight') colIndex += 1
59
- if (intent === 'moveUp') rowIndex -= 1
60
- if (intent === 'moveDown') rowIndex += 1
61
- if (intent === 'tabNext') {
62
- // Right one column; if we fall off the right edge, wrap to the
63
- // first column of the next row - Excel's "data entry" behavior.
64
- colIndex += 1
65
- if (colIndex > bounds.maxCol) {
66
- colIndex = 0
67
- rowIndex += 1
68
- }
69
- }
70
- if (intent === 'tabPrev') {
71
- colIndex -= 1
72
- if (colIndex < 0) {
73
- colIndex = bounds.maxCol
74
- rowIndex -= 1
75
- }
76
- }
77
- if (intent === 'rowStart') colIndex = 0
78
- if (intent === 'rowEnd') colIndex = bounds.maxCol
79
- if (intent === 'gridStart') {
80
- rowIndex = 0
81
- colIndex = 0
82
- }
83
- if (intent === 'gridEnd') {
84
- rowIndex = bounds.maxRow
85
- colIndex = bounds.maxCol
86
- }
87
- if (intent === 'pageUp') rowIndex -= pageSize
88
- if (intent === 'pageDown') rowIndex += pageSize
89
-
90
- rowIndex = Math.min(Math.max(rowIndex, 0), Math.max(bounds.maxRow, 0))
91
- colIndex = Math.min(Math.max(colIndex, 0), Math.max(bounds.maxCol, 0))
92
- return {
93
- rowIndex,
94
- colIndex,
95
- cellId: current.cellId,
96
- }
97
- }
1
+ import type { ActiveCellState } from './core'
2
+
3
+ export type GridKeyboardIntent =
4
+ | 'moveLeft'
5
+ | 'moveRight'
6
+ | 'moveUp'
7
+ | 'moveDown'
8
+ | 'tabNext'
9
+ | 'tabPrev'
10
+ | 'rowStart'
11
+ | 'rowEnd'
12
+ | 'gridStart'
13
+ | 'gridEnd'
14
+ | 'pageUp'
15
+ | 'pageDown'
16
+ | 'activate'
17
+ | 'clearCells'
18
+ | 'noop'
19
+
20
+ export function getKeyboardIntent(event: KeyboardEvent): GridKeyboardIntent {
21
+ // Ctrl+Home / Ctrl+End come before plain Home / End so the modifier wins.
22
+ if (event.ctrlKey && event.key === 'Home') return 'gridStart'
23
+ if (event.ctrlKey && event.key === 'End') return 'gridEnd'
24
+
25
+ if (event.key === 'ArrowLeft') return 'moveLeft'
26
+ if (event.key === 'ArrowRight') return 'moveRight'
27
+ if (event.key === 'ArrowUp') return 'moveUp'
28
+ if (event.key === 'ArrowDown') return 'moveDown'
29
+ if (event.key === 'Home') return 'rowStart'
30
+ if (event.key === 'End') return 'rowEnd'
31
+ if (event.key === 'PageUp') return 'pageUp'
32
+ if (event.key === 'PageDown') return 'pageDown'
33
+
34
+ // Excel-style data-entry navigation:
35
+ // Enter → move down (Shift+Enter → up)
36
+ // Tab → move right (Shift+Tab → left), wraps at row boundaries
37
+ // Delete → clear contents of the selected cells
38
+ // F2 / Space → start editing the active cell
39
+ if (event.key === 'Enter') return event.shiftKey ? 'moveUp' : 'moveDown'
40
+ if (event.key === 'Tab') return event.shiftKey ? 'tabPrev' : 'tabNext'
41
+ if (event.key === 'F2') return 'activate'
42
+ if (event.key === ' ') return 'activate'
43
+ if (event.key === 'Delete') return 'clearCells'
44
+
45
+ return 'noop'
46
+ }
47
+
48
+ export function getNextActiveCell(
49
+ current: ActiveCellState,
50
+ intent: GridKeyboardIntent,
51
+ bounds: { maxRow: number; maxCol: number; pageSize?: number },
52
+ ): ActiveCellState {
53
+ const pageSize = bounds.pageSize ?? 10
54
+ let rowIndex = current.rowIndex
55
+ let colIndex = current.colIndex
56
+
57
+ if (intent === 'moveLeft') colIndex -= 1
58
+ if (intent === 'moveRight') colIndex += 1
59
+ if (intent === 'moveUp') rowIndex -= 1
60
+ if (intent === 'moveDown') rowIndex += 1
61
+ if (intent === 'tabNext') {
62
+ // Right one column; if we fall off the right edge, wrap to the
63
+ // first column of the next row - Excel's "data entry" behavior.
64
+ colIndex += 1
65
+ if (colIndex > bounds.maxCol) {
66
+ colIndex = 0
67
+ rowIndex += 1
68
+ }
69
+ }
70
+ if (intent === 'tabPrev') {
71
+ colIndex -= 1
72
+ if (colIndex < 0) {
73
+ colIndex = bounds.maxCol
74
+ rowIndex -= 1
75
+ }
76
+ }
77
+ if (intent === 'rowStart') colIndex = 0
78
+ if (intent === 'rowEnd') colIndex = bounds.maxCol
79
+ if (intent === 'gridStart') {
80
+ rowIndex = 0
81
+ colIndex = 0
82
+ }
83
+ if (intent === 'gridEnd') {
84
+ rowIndex = bounds.maxRow
85
+ colIndex = bounds.maxCol
86
+ }
87
+ if (intent === 'pageUp') rowIndex -= pageSize
88
+ if (intent === 'pageDown') rowIndex += pageSize
89
+
90
+ rowIndex = Math.min(Math.max(rowIndex, 0), Math.max(bounds.maxRow, 0))
91
+ colIndex = Math.min(Math.max(colIndex, 0), Math.max(bounds.maxCol, 0))
92
+ return {
93
+ rowIndex,
94
+ colIndex,
95
+ cellId: current.cellId,
96
+ }
97
+ }
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { enabledIndices, wrapMove } from './list-nav'
3
+
4
+ describe('enabledIndices', () => {
5
+ it('lists every index when nothing is disabled', () => {
6
+ expect(enabledIndices(3)).toEqual([0, 1, 2])
7
+ })
8
+
9
+ it('skips indices the predicate marks disabled', () => {
10
+ expect(enabledIndices(5, (i) => i === 1 || i === 3)).toEqual([0, 2, 4])
11
+ })
12
+
13
+ it('returns an empty array for a zero count', () => {
14
+ expect(enabledIndices(0)).toEqual([])
15
+ })
16
+ })
17
+
18
+ describe('wrapMove', () => {
19
+ const list = [0, 2, 4] // e.g. indices 1 and 3 are disabled
20
+
21
+ it('moves forward to the next enabled index', () => {
22
+ expect(wrapMove(list, 2, 1)).toBe(4)
23
+ })
24
+
25
+ it('wraps past the end back to the head', () => {
26
+ expect(wrapMove(list, 4, 1)).toBe(0)
27
+ })
28
+
29
+ it('wraps before the head to the tail', () => {
30
+ expect(wrapMove(list, 0, -1)).toBe(4)
31
+ })
32
+
33
+ it('from an absent current, delta 1 lands on the first entry', () => {
34
+ expect(wrapMove(list, -1, 1)).toBe(0)
35
+ })
36
+
37
+ it('from an absent current, delta -1 lands on the last-but-one', () => {
38
+ expect(wrapMove(list, -1, -1)).toBe(2)
39
+ })
40
+
41
+ it('stays put for a single-entry list', () => {
42
+ expect(wrapMove([5], 5, 1)).toBe(5)
43
+ expect(wrapMove([5], 5, -1)).toBe(5)
44
+ })
45
+
46
+ it('returns -1 for an empty list', () => {
47
+ expect(wrapMove([], 0, 1)).toBe(-1)
48
+ })
49
+ })
@@ -0,0 +1,29 @@
1
+ /**
2
+ * list-nav - the pure, framework-free navigation math shared by the roving-focus
3
+ * cores (createListbox, createPopoverSelect, createMenu). No runes, no DOM - just
4
+ * index arithmetic, so it is unit-testable on its own and behaves identically
5
+ * everywhere. The cores wrap these in `$state`.
6
+ */
7
+
8
+ /** Indices of items that can take roving focus / selection - i.e. the ones the
9
+ * caller does not mark disabled. `count` is the total item count; `isDisabled`
10
+ * is consulted per index (a separator counts as disabled to the caller). */
11
+ export function enabledIndices(count: number, isDisabled?: (index: number) => boolean): number[] {
12
+ const out: number[] = []
13
+ for (let i = 0; i < count; i++) if (!isDisabled?.(i)) out.push(i)
14
+ return out
15
+ }
16
+
17
+ /**
18
+ * The next index when moving `delta` steps through `list`, wrapping around the
19
+ * ends. `current` need not be in `list` (it may sit on a disabled item): when it
20
+ * is absent we start just before the head so `delta: 1` lands on the first entry
21
+ * and `delta: -1` on the last-but-one - the same behaviour every existing core
22
+ * already had. Returns `-1` for an empty list.
23
+ */
24
+ export function wrapMove(list: readonly number[], current: number, delta: number): number {
25
+ const len = list.length
26
+ if (len === 0) return -1
27
+ const pos = list.indexOf(current)
28
+ return list[(pos + delta + len) % len]!
29
+ }
@@ -5,6 +5,8 @@ export type ListOption = {
5
5
  disabled?: boolean
6
6
  /** Optional group heading this option belongs under. */
7
7
  group?: string
8
+ /** Optional color swatch (any CSS color) shown before the label. */
9
+ color?: string
8
10
  }
9
11
 
10
12
  /** Case-insensitive substring filter over option labels. */
@@ -0,0 +1,21 @@
1
+ import type { Snippet } from 'svelte'
2
+
3
+ /**
4
+ * A single item in a menu (SvMenu / SvMenuList / SvContextMenu) and the headless
5
+ * `createMenu` core. Kept in a plain `.ts` module so the core can import the type
6
+ * without pulling in the `.svelte` component - which would create a
7
+ * `.svelte.ts` -> `.svelte` -> `.svelte.ts` import cycle that Vite's dev-time
8
+ * import-analysis fails to resolve.
9
+ */
10
+ export type MenuItem = {
11
+ /** Item text. Omit + set `separator` for a divider. */
12
+ label?: string
13
+ separator?: boolean
14
+ icon?: Snippet
15
+ /** Right-aligned hint, e.g. a keyboard shortcut. */
16
+ shortcut?: string
17
+ disabled?: boolean
18
+ /** Nested submenu items (renders a flyout). */
19
+ children?: MenuItem[]
20
+ onSelect?: () => void
21
+ }
package/src/menus.test.ts CHANGED
@@ -98,6 +98,54 @@ describe('createMenus - filter row/menu value updates', () => {
98
98
  expect(ctx.filterRowValues.name).toBe('foo')
99
99
  })
100
100
 
101
+ it('addFilterToken builds a newline-serialised in/notIn list, ignoring dupes/blanks', () => {
102
+ const ctx = makeCtx()
103
+ const m = createMenus(ctx)
104
+ m.addFilterToken('symbol', 'TSM')
105
+ m.addFilterToken('symbol', 'BP')
106
+ m.addFilterToken('symbol', 'TSM') // dupe - ignored
107
+ m.addFilterToken('symbol', ' ') // blank - ignored
108
+ expect(ctx.filterRowValues.symbol).toBe('TSM\nBP')
109
+ // mirrored into the menu model too, so both filter surfaces agree
110
+ expect(ctx.filterMenuValues.symbol.value).toBe('TSM\nBP')
111
+ })
112
+
113
+ it('removeFilterToken drops a value from the list', () => {
114
+ const ctx = makeCtx({ filterRowValues: { symbol: 'TSM\nBP\nBABA' } })
115
+ const m = createMenus(ctx)
116
+ m.removeFilterToken('symbol', 'BP')
117
+ expect(ctx.filterRowValues.symbol).toBe('TSM\nBABA')
118
+ })
119
+
120
+ it('toggleFilterToken adds when absent and removes when present', () => {
121
+ const ctx = makeCtx()
122
+ const m = createMenus(ctx)
123
+ m.toggleFilterToken('symbol', 'BP')
124
+ expect(ctx.filterRowValues.symbol).toBe('BP')
125
+ m.toggleFilterToken('symbol', 'BP')
126
+ expect(ctx.filterRowValues.symbol).toBe('')
127
+ })
128
+
129
+ it('openInSuggest anchors the dropdown and targets the column; closeInSuggest clears it', () => {
130
+ const ctx = makeCtx()
131
+ const m = createMenus(ctx)
132
+ const anchor = {
133
+ getBoundingClientRect: () => ({ left: 40, bottom: 80 }),
134
+ } as unknown as HTMLElement
135
+ m.openInSuggest(anchor, 'symbol')
136
+ expect(ctx.inSuggestFor).toBe('symbol')
137
+ expect(ctx.inSuggestPos).toMatchObject({ y: 84 })
138
+ m.closeInSuggest()
139
+ expect(ctx.inSuggestFor).toBe(null)
140
+ })
141
+
142
+ it('closeMenus also dismisses the in-suggest dropdown', () => {
143
+ const ctx = makeCtx({ inSuggestFor: 'symbol' })
144
+ const m = createMenus(ctx)
145
+ m.closeMenus()
146
+ expect(ctx.inSuggestFor).toBe(null)
147
+ })
148
+
101
149
  it('updateFilterMenuValueTo sets the upper bound for between', () => {
102
150
  const ctx = makeCtx({ filterMenuValues: { age: { operator: 'between', value: '5' } } })
103
151
  const m = createMenus(ctx)