@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,210 +1,206 @@
1
- /**
2
- * createPopoverSelect - the shared HEADLESS engine behind the kit's dropdown
3
- * selects (SvMultiSelect, SvTreeSelect, SvGridSelect and any future one). It
4
- * owns the parts every anchored-panel select repeats: open/close state, the
5
- * `anchoredRect` positioning (reposition on scroll/resize), outside/Escape
6
- * dismissal via the shared layer stack, a roving `active` index over the visible
7
- * items, the WAI-ARIA combobox wiring (`aria-expanded` / `aria-controls` /
8
- * `aria-activedescendant` + stable option ids), and focus management (focus
9
- * moves into the panel on open, back to the trigger on close).
10
- *
11
- * The component owns its own `bind:this` refs and item rendering; it passes the
12
- * refs in as getters and spreads the returned prop-getters onto its trigger,
13
- * panel and items. Runes-based, like `createListbox`.
14
- */
15
- import { anchoredRect, type AnchoredRect } from './popover'
16
- import { createDismissableLayer } from './a11y/dismissable'
17
-
18
- let uidSeq = 0
19
-
20
- export type PopoverSelectConfig = {
21
- /** Number of currently-visible/navigable items (post-filter). */
22
- itemCount: () => number
23
- /** Whether item `i` is disabled (skipped by roving, not selectable). */
24
- disabled?: (index: number) => boolean
25
- /** Commit the item at `index` (Enter or a click the component forwards). */
26
- onSelect: (index: number) => void
27
- /** Notified whenever the open state changes. */
28
- onOpenChange?: (open: boolean) => void
29
- /** The trigger element (component owns `bind:this`, passes a getter). */
30
- getTrigger: () => HTMLElement | null
31
- /** The panel element. */
32
- getPanel: () => HTMLElement | null
33
- /** Element focused when the panel opens (e.g. a search input). Defaults to the
34
- * panel itself. Its id-holder also carries `aria-activedescendant`. */
35
- getInitialFocus?: () => HTMLElement | null
36
- /** Estimated panel height, for the flip-up decision. Default 280. */
37
- estimatedHeight?: () => number
38
- /** Minimum panel width. Default 200. */
39
- minWidth?: () => number
40
- /** Close after a selection (single-select). Default true. */
41
- closeOnSelect?: () => boolean
42
- /** Active index to start on when opening (e.g. the selected row). Default 0. */
43
- initialActive?: () => number
44
- /** Handle a key the core doesn't (e.g. tree ArrowLeft/Right). Return true if
45
- * handled to stop the core's default handling. */
46
- onExtraKey?: (event: KeyboardEvent, active: number) => boolean
47
- idPrefix?: string
48
- }
49
-
50
- export function createPopoverSelect(config: PopoverSelectConfig) {
51
- const id = `${config.idPrefix ?? 'sv-pop'}-${uidSeq++}`
52
- const panelId = `${id}-panel`
53
- const optionId = (i: number) => `${id}-opt-${i}`
54
-
55
- let open = $state(false)
56
- let active = $state(0)
57
- let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
58
-
59
- const count = () => config.itemCount()
60
- const isDisabled = (i: number) => config.disabled?.(i) ?? false
61
-
62
- function enabledIndices(): number[] {
63
- const out: number[] = []
64
- const n = count()
65
- for (let i = 0; i < n; i++) if (!isDisabled(i)) out.push(i)
66
- return out
67
- }
68
- function clampActive(i: number): number {
69
- const n = count()
70
- return n <= 0 ? 0 : Math.max(0, Math.min(i, n - 1))
71
- }
72
-
73
- function setOpen(v: boolean) {
74
- if (v === open) return
75
- open = v
76
- config.onOpenChange?.(v)
77
- }
78
- function openPanel() {
79
- active = clampActive(config.initialActive?.() ?? 0)
80
- setOpen(true)
81
- }
82
- function closePanel() {
83
- setOpen(false)
84
- config.getTrigger()?.focus()
85
- }
86
- function toggle() {
87
- open ? setOpen(false) : openPanel()
88
- }
89
-
90
- function move(delta: number) {
91
- const list = enabledIndices()
92
- if (!list.length) return
93
- const pos = list.indexOf(active)
94
- active = list[(pos + delta + list.length) % list.length] ?? list[0]!
95
- }
96
- function first() { active = enabledIndices()[0] ?? 0 }
97
- function last() { active = enabledIndices().at(-1) ?? 0 }
98
- function selectActive() {
99
- if (count() === 0 || isDisabled(active)) return
100
- config.onSelect(active)
101
- if (config.closeOnSelect?.() ?? true) closePanel()
102
- }
103
-
104
- function updatePos() {
105
- const t = config.getTrigger()
106
- if (!t) return
107
- rect = anchoredRect(t.getBoundingClientRect(), {
108
- estimatedHeight: config.estimatedHeight?.() ?? 280,
109
- minWidth: config.minWidth?.() ?? 200,
110
- })
111
- }
112
-
113
- // While open: position, focus into the panel, reposition on scroll/resize, and
114
- // register a dismissable layer (Escape / outside-click, top-of-stack first).
115
- $effect(() => {
116
- if (!open) return
117
- updatePos()
118
- const initial = config.getInitialFocus?.() ?? config.getPanel()
119
- queueMicrotask(() => initial?.focus())
120
- const rp = () => updatePos()
121
- window.addEventListener('scroll', rp, true)
122
- window.addEventListener('resize', rp)
123
- const layer = createDismissableLayer({
124
- element: () => [config.getTrigger(), config.getPanel()],
125
- onDismiss: () => setOpen(false),
126
- })
127
- layer.activate()
128
- return () => {
129
- window.removeEventListener('scroll', rp, true)
130
- window.removeEventListener('resize', rp)
131
- layer.release()
132
- }
133
- })
134
-
135
- function onTriggerKeydown(e: KeyboardEvent) {
136
- if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
137
- e.preventDefault()
138
- openPanel()
139
- } else if (e.key === 'ArrowUp') {
140
- e.preventDefault()
141
- openPanel()
142
- last()
143
- } else if (e.key === 'Escape') {
144
- setOpen(false)
145
- }
146
- }
147
- function onPanelKeydown(e: KeyboardEvent) {
148
- if (config.onExtraKey?.(e, active)) return
149
- switch (e.key) {
150
- case 'ArrowDown': e.preventDefault(); move(1); break
151
- case 'ArrowUp': e.preventDefault(); move(-1); break
152
- case 'Home': e.preventDefault(); first(); break
153
- case 'End': e.preventDefault(); last(); break
154
- case 'Enter': e.preventDefault(); selectActive(); break
155
- case 'Escape': e.preventDefault(); closePanel(); break
156
- case 'Tab': setOpen(false); break
157
- }
158
- }
159
-
160
- /** The current active-descendant id (for the focused element), or undefined. */
161
- const activeDescendant = () => (open && count() > 0 ? optionId(active) : undefined)
162
-
163
- return {
164
- get open() { return open },
165
- set open(v: boolean) { setOpen(v) },
166
- get active() { return active },
167
- set active(i: number) { active = clampActive(i) },
168
- get rect() { return rect },
169
- /** Stable id of the panel element (for aria-controls / labelling). */
170
- panelId,
171
- /** Stable id of the option at `i` (for aria-activedescendant + item id). */
172
- optionId,
173
- activeDescendant,
174
- openPanel,
175
- closePanel,
176
- toggle,
177
- move,
178
- first,
179
- last,
180
- selectActive,
181
- updatePos,
182
- onPanelKeydown,
183
- /** True when item `i` is the roving-active one. */
184
- isActive: (i: number) => i === active,
185
- /** Spread onto the trigger button. Pass the ARIA popup type the panel
186
- * exposes (mirrors `focusOwnerProps(role)`); defaults to 'listbox' for
187
- * the classic select panels. */
188
- triggerProps: (haspopup: 'listbox' | 'tree' | 'grid' = 'listbox') => ({
189
- 'aria-haspopup': haspopup,
190
- 'aria-expanded': open,
191
- 'aria-controls': open ? panelId : undefined,
192
- onkeydown: onTriggerKeydown,
193
- }),
194
- /** Attributes for the element that OWNS focus + activedescendant (the panel,
195
- * or a search input). Pass the ARIA role the panel should expose. */
196
- focusOwnerProps: (role: 'listbox' | 'tree' | 'grid' = 'listbox') => ({
197
- role,
198
- 'aria-activedescendant': activeDescendant(),
199
- onkeydown: onPanelKeydown,
200
- }),
201
- /** Spread onto the item element at `i`. */
202
- itemProps: (i: number) => ({
203
- id: optionId(i),
204
- 'data-active': i === active ? '' : undefined,
205
- onpointermove: () => { if (!isDisabled(i)) active = i },
206
- }),
207
- }
208
- }
209
-
210
- export type PopoverSelect = ReturnType<typeof createPopoverSelect>
1
+ /**
2
+ * createPopoverSelect - the shared HEADLESS engine behind the kit's dropdown
3
+ * selects (SvMultiSelect, SvTreeSelect, SvGridSelect and any future one). It
4
+ * owns the parts every anchored-panel select repeats: open/close state, the
5
+ * `anchoredRect` positioning (reposition on scroll/resize), outside/Escape
6
+ * dismissal via the shared layer stack, a roving `active` index over the visible
7
+ * items, the WAI-ARIA combobox wiring (`aria-expanded` / `aria-controls` /
8
+ * `aria-activedescendant` + stable option ids), and focus management (focus
9
+ * moves into the panel on open, back to the trigger on close).
10
+ *
11
+ * The component owns its own `bind:this` refs and item rendering; it passes the
12
+ * refs in as getters and spreads the returned prop-getters onto its trigger,
13
+ * panel and items. Runes-based, like `createListbox`.
14
+ */
15
+ import { anchoredRect, type AnchoredRect } from './popover'
16
+ import { createDismissableLayer } from './a11y/dismissable'
17
+ import { enabledIndices as enabledIndicesOf, wrapMove } from './list-nav'
18
+
19
+ let uidSeq = 0
20
+
21
+ export type PopoverSelectConfig = {
22
+ /** Number of currently-visible/navigable items (post-filter). */
23
+ itemCount: () => number
24
+ /** Whether item `i` is disabled (skipped by roving, not selectable). */
25
+ disabled?: (index: number) => boolean
26
+ /** Commit the item at `index` (Enter or a click the component forwards). */
27
+ onSelect: (index: number) => void
28
+ /** Notified whenever the open state changes. */
29
+ onOpenChange?: (open: boolean) => void
30
+ /** The trigger element (component owns `bind:this`, passes a getter). */
31
+ getTrigger: () => HTMLElement | null
32
+ /** The panel element. */
33
+ getPanel: () => HTMLElement | null
34
+ /** Element focused when the panel opens (e.g. a search input). Defaults to the
35
+ * panel itself. Its id-holder also carries `aria-activedescendant`. */
36
+ getInitialFocus?: () => HTMLElement | null
37
+ /** Estimated panel height, for the flip-up decision. Default 280. */
38
+ estimatedHeight?: () => number
39
+ /** Minimum panel width. Default 200. */
40
+ minWidth?: () => number
41
+ /** Close after a selection (single-select). Default true. */
42
+ closeOnSelect?: () => boolean
43
+ /** Active index to start on when opening (e.g. the selected row). Default 0. */
44
+ initialActive?: () => number
45
+ /** Handle a key the core doesn't (e.g. tree ArrowLeft/Right). Return true if
46
+ * handled to stop the core's default handling. */
47
+ onExtraKey?: (event: KeyboardEvent, active: number) => boolean
48
+ idPrefix?: string
49
+ }
50
+
51
+ export function createPopoverSelect(config: PopoverSelectConfig) {
52
+ const id = `${config.idPrefix ?? 'sv-pop'}-${uidSeq++}`
53
+ const panelId = `${id}-panel`
54
+ const optionId = (i: number) => `${id}-opt-${i}`
55
+
56
+ let open = $state(false)
57
+ let active = $state(0)
58
+ let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
59
+
60
+ const count = () => config.itemCount()
61
+ const isDisabled = (i: number) => config.disabled?.(i) ?? false
62
+
63
+ function enabledIndices(): number[] {
64
+ return enabledIndicesOf(count(), isDisabled)
65
+ }
66
+ function clampActive(i: number): number {
67
+ const n = count()
68
+ return n <= 0 ? 0 : Math.max(0, Math.min(i, n - 1))
69
+ }
70
+
71
+ function setOpen(v: boolean) {
72
+ if (v === open) return
73
+ open = v
74
+ config.onOpenChange?.(v)
75
+ }
76
+ function openPanel() {
77
+ active = clampActive(config.initialActive?.() ?? 0)
78
+ setOpen(true)
79
+ }
80
+ function closePanel() {
81
+ setOpen(false)
82
+ config.getTrigger()?.focus()
83
+ }
84
+ function toggle() {
85
+ open ? setOpen(false) : openPanel()
86
+ }
87
+
88
+ function move(delta: number) {
89
+ const next = wrapMove(enabledIndices(), active, delta)
90
+ if (next >= 0) active = next
91
+ }
92
+ function first() { active = enabledIndices()[0] ?? 0 }
93
+ function last() { active = enabledIndices().at(-1) ?? 0 }
94
+ function selectActive() {
95
+ if (count() === 0 || isDisabled(active)) return
96
+ config.onSelect(active)
97
+ if (config.closeOnSelect?.() ?? true) closePanel()
98
+ }
99
+
100
+ function updatePos() {
101
+ const t = config.getTrigger()
102
+ if (!t) return
103
+ rect = anchoredRect(t.getBoundingClientRect(), {
104
+ estimatedHeight: config.estimatedHeight?.() ?? 280,
105
+ minWidth: config.minWidth?.() ?? 200,
106
+ })
107
+ }
108
+
109
+ // While open: position, focus into the panel, reposition on scroll/resize, and
110
+ // register a dismissable layer (Escape / outside-click, top-of-stack first).
111
+ $effect(() => {
112
+ if (!open) return
113
+ updatePos()
114
+ const initial = config.getInitialFocus?.() ?? config.getPanel()
115
+ queueMicrotask(() => initial?.focus())
116
+ const rp = () => updatePos()
117
+ window.addEventListener('scroll', rp, true)
118
+ window.addEventListener('resize', rp)
119
+ const layer = createDismissableLayer({
120
+ element: () => [config.getTrigger(), config.getPanel()],
121
+ onDismiss: () => setOpen(false),
122
+ })
123
+ layer.activate()
124
+ return () => {
125
+ window.removeEventListener('scroll', rp, true)
126
+ window.removeEventListener('resize', rp)
127
+ layer.release()
128
+ }
129
+ })
130
+
131
+ function onTriggerKeydown(e: KeyboardEvent) {
132
+ if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
133
+ e.preventDefault()
134
+ openPanel()
135
+ } else if (e.key === 'ArrowUp') {
136
+ e.preventDefault()
137
+ openPanel()
138
+ last()
139
+ } else if (e.key === 'Escape') {
140
+ setOpen(false)
141
+ }
142
+ }
143
+ function onPanelKeydown(e: KeyboardEvent) {
144
+ if (config.onExtraKey?.(e, active)) return
145
+ switch (e.key) {
146
+ case 'ArrowDown': e.preventDefault(); move(1); break
147
+ case 'ArrowUp': e.preventDefault(); move(-1); break
148
+ case 'Home': e.preventDefault(); first(); break
149
+ case 'End': e.preventDefault(); last(); break
150
+ case 'Enter': e.preventDefault(); selectActive(); break
151
+ case 'Escape': e.preventDefault(); closePanel(); break
152
+ case 'Tab': setOpen(false); break
153
+ }
154
+ }
155
+
156
+ /** The current active-descendant id (for the focused element), or undefined. */
157
+ const activeDescendant = () => (open && count() > 0 ? optionId(active) : undefined)
158
+
159
+ return {
160
+ get open() { return open },
161
+ set open(v: boolean) { setOpen(v) },
162
+ get active() { return active },
163
+ set active(i: number) { active = clampActive(i) },
164
+ get rect() { return rect },
165
+ /** Stable id of the panel element (for aria-controls / labelling). */
166
+ panelId,
167
+ /** Stable id of the option at `i` (for aria-activedescendant + item id). */
168
+ optionId,
169
+ activeDescendant,
170
+ openPanel,
171
+ closePanel,
172
+ toggle,
173
+ move,
174
+ first,
175
+ last,
176
+ selectActive,
177
+ updatePos,
178
+ onPanelKeydown,
179
+ /** True when item `i` is the roving-active one. */
180
+ isActive: (i: number) => i === active,
181
+ /** Spread onto the trigger button. Pass the ARIA popup type the panel
182
+ * exposes (mirrors `focusOwnerProps(role)`); defaults to 'listbox' for
183
+ * the classic select panels. */
184
+ triggerProps: (haspopup: 'listbox' | 'tree' | 'grid' = 'listbox') => ({
185
+ 'aria-haspopup': haspopup,
186
+ 'aria-expanded': open,
187
+ 'aria-controls': open ? panelId : undefined,
188
+ onkeydown: onTriggerKeydown,
189
+ }),
190
+ /** Attributes for the element that OWNS focus + activedescendant (the panel,
191
+ * or a search input). Pass the ARIA role the panel should expose. */
192
+ focusOwnerProps: (role: 'listbox' | 'tree' | 'grid' = 'listbox') => ({
193
+ role,
194
+ 'aria-activedescendant': activeDescendant(),
195
+ onkeydown: onPanelKeydown,
196
+ }),
197
+ /** Spread onto the item element at `i`. */
198
+ itemProps: (i: number) => ({
199
+ id: optionId(i),
200
+ 'data-active': i === active ? '' : undefined,
201
+ onpointermove: () => { if (!isDisabled(i)) active = i },
202
+ }),
203
+ }
204
+ }
205
+
206
+ export type PopoverSelect = ReturnType<typeof createPopoverSelect>
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { createStepper } from './createStepper'
3
+
4
+ function make(over: Partial<{ current: number; count: number; linear: boolean }> = {}) {
5
+ let current = over.current ?? 2
6
+ const onChange = vi.fn((i: number) => (current = i))
7
+ const st = createStepper({
8
+ count: () => over.count ?? 5,
9
+ current: () => current,
10
+ onChange,
11
+ linear: () => over.linear ?? true,
12
+ })
13
+ return { st, onChange }
14
+ }
15
+
16
+ describe('createStepper', () => {
17
+ it('reports per-step status relative to the current step', () => {
18
+ const { st } = make({ current: 2 })
19
+ expect(st.statusOf(0)).toBe('complete')
20
+ expect(st.statusOf(1)).toBe('complete')
21
+ expect(st.statusOf(2)).toBe('active')
22
+ expect(st.statusOf(3)).toBe('upcoming')
23
+ })
24
+
25
+ it('in linear mode only reached steps are clickable', () => {
26
+ const { st } = make({ current: 2, linear: true })
27
+ expect(st.clickable(1)).toBe(true) // complete
28
+ expect(st.clickable(2)).toBe(true) // active
29
+ expect(st.clickable(3)).toBe(false) // upcoming
30
+ })
31
+
32
+ it('in free mode every step is clickable', () => {
33
+ const { st } = make({ current: 2, linear: false })
34
+ expect(st.clickable(4)).toBe(true)
35
+ })
36
+
37
+ it('go() navigates only to clickable, different steps', () => {
38
+ const { st, onChange } = make({ current: 2, linear: true })
39
+ st.go(4) // upcoming, blocked in linear mode
40
+ expect(onChange).not.toHaveBeenCalled()
41
+ st.go(0) // reached
42
+ expect(onChange).toHaveBeenLastCalledWith(0)
43
+ st.go(2) // same as current
44
+ expect(onChange).toHaveBeenCalledTimes(1)
45
+ })
46
+
47
+ it('prev/next step through (relative to current)', () => {
48
+ const fwd = make({ current: 2, linear: false })
49
+ fwd.st.next()
50
+ expect(fwd.onChange).toHaveBeenLastCalledWith(3)
51
+ const back = make({ current: 2, linear: false })
52
+ back.st.prev()
53
+ expect(back.onChange).toHaveBeenLastCalledWith(1)
54
+ })
55
+
56
+ it('stepButtonProps marks the active step and disables unreachable ones', () => {
57
+ const { st } = make({ current: 2, linear: true })
58
+ expect(st.stepButtonProps(2)['aria-current']).toBe('step')
59
+ expect(st.stepButtonProps(0)['aria-current']).toBeUndefined()
60
+ expect(st.stepButtonProps(3).disabled).toBe(true)
61
+ })
62
+ })
@@ -0,0 +1,61 @@
1
+ /**
2
+ * createStepper - the HEADLESS core behind <SvStepper>: per-step status
3
+ * (complete / active / upcoming), the linear-vs-free clickability rule, and
4
+ * navigation, as pure getters + prop-getters. No runes, framework-free, directly
5
+ * unit-testable.
6
+ *
7
+ * ```svelte
8
+ * <script lang="ts">
9
+ * import { createStepper } from '@svgrid/grid'
10
+ * const st = createStepper({ count: () => steps.length, current: () => i, onChange: (n) => (i = n) })
11
+ * </script>
12
+ * <ol>
13
+ * {#each steps as step, idx}
14
+ * <li class="is-{st.statusOf(idx)}">
15
+ * <button {...st.stepButtonProps(idx)}>{step.label}</button>
16
+ * </li>
17
+ * {/each}
18
+ * </ol>
19
+ * ```
20
+ */
21
+ export type StepStatus = 'complete' | 'active' | 'upcoming'
22
+
23
+ export type StepperConfig = {
24
+ count: () => number
25
+ /** 0-based index of the active step. */
26
+ current: () => number
27
+ onChange?: (index: number) => void
28
+ /** Only allow navigating to already-reached steps. Default true. */
29
+ linear?: () => boolean
30
+ }
31
+
32
+ export function createStepper(config: StepperConfig) {
33
+ const current = () => config.current()
34
+ const linear = () => config.linear?.() ?? true
35
+
36
+ const statusOf = (i: number): StepStatus =>
37
+ i < current() ? 'complete' : i === current() ? 'active' : 'upcoming'
38
+ const clickable = (i: number) => (linear() ? i <= current() : true)
39
+ const go = (i: number) => {
40
+ if (clickable(i) && i !== current()) config.onChange?.(i)
41
+ }
42
+ const next = () => go(current() + 1)
43
+ const prev = () => go(current() - 1)
44
+
45
+ return {
46
+ statusOf,
47
+ clickable,
48
+ go,
49
+ next,
50
+ prev,
51
+ isActive: (i: number) => i === current(),
52
+ /** Spread onto a step's button. */
53
+ stepButtonProps: (i: number) => ({
54
+ 'aria-current': (statusOf(i) === 'active' ? 'step' : undefined) as 'step' | undefined,
55
+ disabled: !clickable(i),
56
+ onclick: () => go(i),
57
+ }),
58
+ }
59
+ }
60
+
61
+ export type Stepper = ReturnType<typeof createStepper>
@@ -0,0 +1,86 @@
1
+ /**
2
+ * createTooltip - the HEADLESS core behind <SvTooltip>: the hover/focus open
3
+ * state machine with a show delay, Escape-to-hide, and the `aria-describedby` /
4
+ * `role="tooltip"` wiring - as prop-getters you spread onto your own anchor and
5
+ * tooltip elements. No styles, no positioning (that is a DOM concern the renderer
6
+ * owns), like `createListbox`.
7
+ *
8
+ * ```svelte
9
+ * <script lang="ts">
10
+ * import { createTooltip } from '@svgrid/grid'
11
+ * const tip = createTooltip({ text: () => label })
12
+ * </script>
13
+ * <span {...tip.anchorProps()}>{@render children?.()}</span>
14
+ * {#if tip.open}
15
+ * <div {...tip.tooltipProps()}>{label}</div>
16
+ * {/if}
17
+ * ```
18
+ */
19
+ import { nextEditorId } from './editor-contract'
20
+
21
+ export type TooltipConfig = {
22
+ /** The tooltip text; the tooltip never opens when it is empty. */
23
+ text?: () => string | undefined
24
+ /** Suppress the tooltip entirely. */
25
+ disabled?: () => boolean
26
+ /** Show delay in ms after hover/focus. Default 300. */
27
+ delay?: () => number
28
+ }
29
+
30
+ export type TooltipAnchorProps = {
31
+ 'aria-describedby': string | undefined
32
+ onpointerenter: () => void
33
+ onpointerleave: () => void
34
+ onfocusin: () => void
35
+ onfocusout: () => void
36
+ }
37
+
38
+ export type TooltipProps = {
39
+ id: string
40
+ role: 'tooltip'
41
+ }
42
+
43
+ export function createTooltip(config: TooltipConfig = {}) {
44
+ const id = nextEditorId('sv-tip')
45
+ let open = $state(false)
46
+ let showTimer: ReturnType<typeof setTimeout> | undefined
47
+
48
+ function show() {
49
+ if ((config.disabled?.() ?? false) || !(config.text?.() ?? '')) return
50
+ clearTimeout(showTimer)
51
+ showTimer = setTimeout(() => { open = true }, config.delay?.() ?? 300)
52
+ }
53
+ function hide() {
54
+ clearTimeout(showTimer)
55
+ open = false
56
+ }
57
+
58
+ // Escape hides the tooltip while it is open (WAI-ARIA tooltip pattern).
59
+ $effect(() => {
60
+ if (!open) return
61
+ const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') hide() }
62
+ document.addEventListener('keydown', onKey)
63
+ return () => document.removeEventListener('keydown', onKey)
64
+ })
65
+
66
+ return {
67
+ get open() { return open },
68
+ /** Stable id of the tooltip element (for `aria-describedby` + the tip's id). */
69
+ tipId: id,
70
+ show,
71
+ hide,
72
+ /** Spread onto the anchor/trigger wrapper. Also mirror `tipId` onto the real
73
+ * focusable child's `aria-describedby` so AT announces it on focus. */
74
+ anchorProps: (): TooltipAnchorProps => ({
75
+ 'aria-describedby': open ? id : undefined,
76
+ onpointerenter: show,
77
+ onpointerleave: hide,
78
+ onfocusin: show,
79
+ onfocusout: hide,
80
+ }),
81
+ /** Spread onto the tooltip element. */
82
+ tooltipProps: (): TooltipProps => ({ id, role: 'tooltip' }),
83
+ }
84
+ }
85
+
86
+ export type Tooltip = ReturnType<typeof createTooltip>