@svgrid/grid 2.2.27 → 2.2.29
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.
- package/README.md +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +179 -179
- package/dist/GridMenus.svelte +782 -734
- package/dist/SvAutoComplete.svelte +165 -165
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +101 -101
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +275 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +160 -160
- package/dist/SvDrawer.svelte +254 -250
- package/dist/SvDropDownList.svelte +374 -374
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.controller.svelte.d.ts +14 -1
- package/dist/SvGrid.controller.svelte.js +112 -28
- package/dist/SvGrid.css +2704 -2723
- package/dist/SvGrid.svelte +3361 -3333
- package/dist/SvGrid.types.d.ts +20 -5
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +696 -696
- package/dist/SvGridSelect.svelte +259 -259
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +326 -287
- package/dist/SvListBox.svelte.d.ts +10 -1
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +281 -281
- package/dist/SvNumberInput.svelte +168 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -131
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +200 -200
- package/dist/SvSwitchButton.svelte +107 -107
- package/dist/SvTagsInput.svelte +112 -112
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +204 -204
- package/dist/SvTree.svelte +437 -437
- package/dist/SvTreeSelect.svelte +233 -233
- package/dist/cdn/GridMenus-Da_p7SKS.js +486 -0
- package/dist/cdn/GridMenus-Ds3OpzZT.js +490 -0
- package/dist/cdn/{SvDateTimePicker-B1sDc9Pz.js → SvDateTimePicker-B8GopjhC.js} +2 -2
- package/dist/cdn/{SvDateTimePicker-Bh0FTs1E.js → SvDateTimePicker-DB0aIEk8.js} +2 -2
- package/dist/cdn/{SvGridChart-Bgk4hDs8.js → SvGridChart-BcLCYvw9.js} +1 -1
- package/dist/cdn/{SvGridChart-BKQsO5fZ.js → SvGridChart-BhnFdLd0.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DkCc2Z7b.js → SvGridChartPanel-Cmx9DHAS.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DSkHIQDd.js → SvGridChartPanel-DvjwxLvK.js} +1 -1
- package/dist/cdn/{SvGridChartView-BrbVaELm.js → SvGridChartView-B40DTJSX.js} +1 -1
- package/dist/cdn/{SvGridChartView-i9rWvSKg.js → SvGridChartView-PwqI4s02.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D08sVmAy.js → SvGridDropdown-DjALc_4f.js} +1 -1
- package/dist/cdn/{SvGridDropdown-CGxxopQ4.js → SvGridDropdown-hpnHvOGt.js} +1 -1
- package/dist/cdn/{src-cgs1wcUS.js → src-BLJfdyL0.js} +4034 -3889
- package/dist/cdn/{src-Cw8d9U8j.js → src-DYDCiC0D.js} +7053 -6908
- package/dist/cdn/svgrid.js +9 -9
- package/dist/cdn/svgrid.svelte-external.js +9 -9
- package/dist/chart-export.js +8 -8
- package/dist/column-groups.js +8 -10
- package/dist/column-id.d.ts +9 -0
- package/dist/column-id.js +25 -0
- package/dist/core.js +2 -1
- package/dist/createListbox.svelte.d.ts +15 -2
- package/dist/createListbox.svelte.js +27 -6
- package/dist/editing.d.ts +1 -0
- package/dist/editing.js +45 -10
- package/dist/filtering/excel-filters.d.ts +30 -10
- package/dist/filtering/excel-filters.js +96 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/menus.d.ts +2 -0
- package/dist/menus.js +25 -0
- package/dist/row-resize.js +40 -0
- package/dist/selection.js +1 -1
- package/package.json +10 -10
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +179 -179
- package/src/GridMenus.svelte +782 -734
- package/src/SvAutoComplete.svelte +165 -165
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +101 -101
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +275 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +160 -160
- package/src/SvDrawer.svelte +254 -250
- package/src/SvDrawer.test.ts +18 -1
- package/src/SvDropDownList.svelte +374 -374
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.controller.svelte.ts +3335 -3239
- package/src/SvGrid.css +2704 -2723
- package/src/SvGrid.svelte +3361 -3333
- package/src/SvGrid.types.ts +1628 -1613
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +696 -696
- package/src/SvGridSelect.svelte +259 -259
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +326 -287
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +281 -281
- package/src/SvNavPane.test.ts +15 -4
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -131
- package/src/SvPopover.svelte +197 -197
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +200 -200
- package/src/SvSwitchButton.svelte +107 -107
- package/src/SvTagsInput.svelte +112 -112
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +204 -204
- package/src/SvTree.svelte +437 -437
- package/src/SvTreeSelect.svelte +233 -233
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1391 -1391
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +895 -895
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/column-groups.test.ts +48 -0
- package/src/column-groups.ts +8 -10
- package/src/column-id.ts +30 -0
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1112 -1111
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createListbox.svelte.ts +39 -7
- package/src/createMenu.svelte.ts +225 -225
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +319 -319
- package/src/editing.test.ts +949 -859
- package/src/editing.ts +709 -675
- package/src/editor-contract.ts +157 -157
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/filter-operators.ts +157 -157
- package/src/filtering/excel-filters.test.ts +44 -1
- package/src/filtering/excel-filters.ts +91 -14
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/index.ts +715 -713
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +172 -172
- package/src/menus.test.ts +736 -700
- package/src/menus.ts +679 -652
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/row-resize.test.ts +100 -0
- package/src/row-resize.ts +36 -0
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +872 -872
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +524 -524
- package/src/svgrid.api-extensions.test.ts +8 -3
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.cell-dom-ids.test.ts +96 -0
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.conditional-stat-scope.test.ts +111 -0
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +308 -0
- package/src/svgrid.filter-menu-scroll.test.ts +3 -1
- package/src/svgrid.group-header-ids.test.ts +121 -0
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +215 -215
- package/src/toast-store.svelte.ts +246 -246
- package/src/toast-store.test.ts +147 -147
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
- package/themes/ember.css +71 -71
- package/dist/cdn/GridMenus-BzxEYNtW.js +0 -421
- package/dist/cdn/GridMenus-Dr-nGEXn.js +0 -417
|
@@ -1,213 +1,213 @@
|
|
|
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
|
-
/** Read-only: value shown, trigger focusable, but the panel will not open. */
|
|
27
|
-
readonly?: () => boolean
|
|
28
|
-
/** Commit the item at `index` (Enter or a click the component forwards). */
|
|
29
|
-
onSelect: (index: number) => void
|
|
30
|
-
/** Notified whenever the open state changes. */
|
|
31
|
-
onOpenChange?: (open: boolean) => void
|
|
32
|
-
/** The trigger element (component owns `bind:this`, passes a getter). */
|
|
33
|
-
getTrigger: () => HTMLElement | null
|
|
34
|
-
/** The panel element. */
|
|
35
|
-
getPanel: () => HTMLElement | null
|
|
36
|
-
/** Element focused when the panel opens (e.g. a search input). Defaults to the
|
|
37
|
-
* panel itself. Its id-holder also carries `aria-activedescendant`. */
|
|
38
|
-
getInitialFocus?: () => HTMLElement | null
|
|
39
|
-
/** Estimated panel height, for the flip-up decision. Default 280. */
|
|
40
|
-
estimatedHeight?: () => number
|
|
41
|
-
/** Minimum panel width. Default 200. */
|
|
42
|
-
minWidth?: () => number
|
|
43
|
-
/** Close after a selection (single-select). Default true. */
|
|
44
|
-
closeOnSelect?: () => boolean
|
|
45
|
-
/** Active index to start on when opening (e.g. the selected row). Default 0. */
|
|
46
|
-
initialActive?: () => number
|
|
47
|
-
/** Handle a key the core doesn't (e.g. tree ArrowLeft/Right). Return true if
|
|
48
|
-
* handled to stop the core's default handling. */
|
|
49
|
-
onExtraKey?: (event: KeyboardEvent, active: number) => boolean
|
|
50
|
-
idPrefix?: string
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function createPopoverSelect(config: PopoverSelectConfig) {
|
|
54
|
-
const id = `${config.idPrefix ?? 'sv-pop'}-${uidSeq++}`
|
|
55
|
-
const panelId = `${id}-panel`
|
|
56
|
-
const optionId = (i: number) => `${id}-opt-${i}`
|
|
57
|
-
|
|
58
|
-
let open = $state(false)
|
|
59
|
-
let active = $state(0)
|
|
60
|
-
let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
|
|
61
|
-
|
|
62
|
-
const count = () => config.itemCount()
|
|
63
|
-
const isDisabled = (i: number) => config.disabled?.(i) ?? false
|
|
64
|
-
const readonly = () => config.readonly?.() ?? false
|
|
65
|
-
|
|
66
|
-
function enabledIndices(): number[] {
|
|
67
|
-
return enabledIndicesOf(count(), isDisabled)
|
|
68
|
-
}
|
|
69
|
-
function clampActive(i: number): number {
|
|
70
|
-
const n = count()
|
|
71
|
-
return n <= 0 ? 0 : Math.max(0, Math.min(i, n - 1))
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function setOpen(v: boolean) {
|
|
75
|
-
if (v === open) return
|
|
76
|
-
open = v
|
|
77
|
-
config.onOpenChange?.(v)
|
|
78
|
-
}
|
|
79
|
-
function openPanel() {
|
|
80
|
-
if (readonly()) return
|
|
81
|
-
active = clampActive(config.initialActive?.() ?? 0)
|
|
82
|
-
setOpen(true)
|
|
83
|
-
}
|
|
84
|
-
function closePanel() {
|
|
85
|
-
setOpen(false)
|
|
86
|
-
config.getTrigger()?.focus()
|
|
87
|
-
}
|
|
88
|
-
function toggle() {
|
|
89
|
-
if (readonly()) return
|
|
90
|
-
open ? setOpen(false) : openPanel()
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function move(delta: number) {
|
|
94
|
-
const next = wrapMove(enabledIndices(), active, delta)
|
|
95
|
-
if (next >= 0) active = next
|
|
96
|
-
}
|
|
97
|
-
function first() { active = enabledIndices()[0] ?? 0 }
|
|
98
|
-
function last() { active = enabledIndices().at(-1) ?? 0 }
|
|
99
|
-
function selectActive() {
|
|
100
|
-
if (readonly() || count() === 0 || isDisabled(active)) return
|
|
101
|
-
config.onSelect(active)
|
|
102
|
-
if (config.closeOnSelect?.() ?? true) closePanel()
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function updatePos() {
|
|
106
|
-
const t = config.getTrigger()
|
|
107
|
-
if (!t) return
|
|
108
|
-
rect = anchoredRect(t.getBoundingClientRect(), {
|
|
109
|
-
estimatedHeight: config.estimatedHeight?.() ?? 280,
|
|
110
|
-
minWidth: config.minWidth?.() ?? 200,
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// While open: position, focus into the panel, reposition on scroll/resize, and
|
|
115
|
-
// register a dismissable layer (Escape / outside-click, top-of-stack first).
|
|
116
|
-
$effect(() => {
|
|
117
|
-
if (!open) return
|
|
118
|
-
updatePos()
|
|
119
|
-
const initial = config.getInitialFocus?.() ?? config.getPanel()
|
|
120
|
-
queueMicrotask(() => initial?.focus())
|
|
121
|
-
const rp = () => updatePos()
|
|
122
|
-
window.addEventListener('scroll', rp, true)
|
|
123
|
-
window.addEventListener('resize', rp)
|
|
124
|
-
const layer = createDismissableLayer({
|
|
125
|
-
element: () => [config.getTrigger(), config.getPanel()],
|
|
126
|
-
onDismiss: () => setOpen(false),
|
|
127
|
-
})
|
|
128
|
-
layer.activate()
|
|
129
|
-
return () => {
|
|
130
|
-
window.removeEventListener('scroll', rp, true)
|
|
131
|
-
window.removeEventListener('resize', rp)
|
|
132
|
-
layer.release()
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
function onTriggerKeydown(e: KeyboardEvent) {
|
|
137
|
-
if (readonly()) return
|
|
138
|
-
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
|
|
139
|
-
e.preventDefault()
|
|
140
|
-
openPanel()
|
|
141
|
-
} else if (e.key === 'ArrowUp') {
|
|
142
|
-
e.preventDefault()
|
|
143
|
-
openPanel()
|
|
144
|
-
last()
|
|
145
|
-
} else if (e.key === 'Escape') {
|
|
146
|
-
setOpen(false)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function onPanelKeydown(e: KeyboardEvent) {
|
|
150
|
-
if (config.onExtraKey?.(e, active)) return
|
|
151
|
-
switch (e.key) {
|
|
152
|
-
case 'ArrowDown': e.preventDefault(); move(1); break
|
|
153
|
-
case 'ArrowUp': e.preventDefault(); move(-1); break
|
|
154
|
-
case 'Home': e.preventDefault(); first(); break
|
|
155
|
-
case 'End': e.preventDefault(); last(); break
|
|
156
|
-
case 'Enter': e.preventDefault(); selectActive(); break
|
|
157
|
-
case 'Escape': e.preventDefault(); closePanel(); break
|
|
158
|
-
case 'Tab': setOpen(false); break
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** The current active-descendant id (for the focused element), or undefined. */
|
|
163
|
-
const activeDescendant = () => (open && count() > 0 ? optionId(active) : undefined)
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
get open() { return open },
|
|
167
|
-
set open(v: boolean) { setOpen(v) },
|
|
168
|
-
get active() { return active },
|
|
169
|
-
set active(i: number) { active = clampActive(i) },
|
|
170
|
-
get rect() { return rect },
|
|
171
|
-
/** Stable id of the panel element (for aria-controls / labelling). */
|
|
172
|
-
panelId,
|
|
173
|
-
/** Stable id of the option at `i` (for aria-activedescendant + item id). */
|
|
174
|
-
optionId,
|
|
175
|
-
activeDescendant,
|
|
176
|
-
openPanel,
|
|
177
|
-
closePanel,
|
|
178
|
-
toggle,
|
|
179
|
-
move,
|
|
180
|
-
first,
|
|
181
|
-
last,
|
|
182
|
-
selectActive,
|
|
183
|
-
updatePos,
|
|
184
|
-
onPanelKeydown,
|
|
185
|
-
/** True when item `i` is the roving-active one. */
|
|
186
|
-
isActive: (i: number) => i === active,
|
|
187
|
-
/** Spread onto the trigger button. Pass the ARIA popup type the panel
|
|
188
|
-
* exposes (mirrors `focusOwnerProps(role)`); defaults to 'listbox' for
|
|
189
|
-
* the classic select panels. */
|
|
190
|
-
triggerProps: (haspopup: 'listbox' | 'tree' | 'grid' = 'listbox') => ({
|
|
191
|
-
'aria-haspopup': haspopup,
|
|
192
|
-
'aria-expanded': open,
|
|
193
|
-
'aria-controls': open ? panelId : undefined,
|
|
194
|
-
'aria-readonly': readonly() || undefined,
|
|
195
|
-
onkeydown: onTriggerKeydown,
|
|
196
|
-
}),
|
|
197
|
-
/** Attributes for the element that OWNS focus + activedescendant (the panel,
|
|
198
|
-
* or a search input). Pass the ARIA role the panel should expose. */
|
|
199
|
-
focusOwnerProps: (role: 'listbox' | 'tree' | 'grid' = 'listbox') => ({
|
|
200
|
-
role,
|
|
201
|
-
'aria-activedescendant': activeDescendant(),
|
|
202
|
-
onkeydown: onPanelKeydown,
|
|
203
|
-
}),
|
|
204
|
-
/** Spread onto the item element at `i`. */
|
|
205
|
-
itemProps: (i: number) => ({
|
|
206
|
-
id: optionId(i),
|
|
207
|
-
'data-active': i === active ? '' : undefined,
|
|
208
|
-
onpointermove: () => { if (!isDisabled(i)) active = i },
|
|
209
|
-
}),
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
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
|
+
/** Read-only: value shown, trigger focusable, but the panel will not open. */
|
|
27
|
+
readonly?: () => boolean
|
|
28
|
+
/** Commit the item at `index` (Enter or a click the component forwards). */
|
|
29
|
+
onSelect: (index: number) => void
|
|
30
|
+
/** Notified whenever the open state changes. */
|
|
31
|
+
onOpenChange?: (open: boolean) => void
|
|
32
|
+
/** The trigger element (component owns `bind:this`, passes a getter). */
|
|
33
|
+
getTrigger: () => HTMLElement | null
|
|
34
|
+
/** The panel element. */
|
|
35
|
+
getPanel: () => HTMLElement | null
|
|
36
|
+
/** Element focused when the panel opens (e.g. a search input). Defaults to the
|
|
37
|
+
* panel itself. Its id-holder also carries `aria-activedescendant`. */
|
|
38
|
+
getInitialFocus?: () => HTMLElement | null
|
|
39
|
+
/** Estimated panel height, for the flip-up decision. Default 280. */
|
|
40
|
+
estimatedHeight?: () => number
|
|
41
|
+
/** Minimum panel width. Default 200. */
|
|
42
|
+
minWidth?: () => number
|
|
43
|
+
/** Close after a selection (single-select). Default true. */
|
|
44
|
+
closeOnSelect?: () => boolean
|
|
45
|
+
/** Active index to start on when opening (e.g. the selected row). Default 0. */
|
|
46
|
+
initialActive?: () => number
|
|
47
|
+
/** Handle a key the core doesn't (e.g. tree ArrowLeft/Right). Return true if
|
|
48
|
+
* handled to stop the core's default handling. */
|
|
49
|
+
onExtraKey?: (event: KeyboardEvent, active: number) => boolean
|
|
50
|
+
idPrefix?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createPopoverSelect(config: PopoverSelectConfig) {
|
|
54
|
+
const id = `${config.idPrefix ?? 'sv-pop'}-${uidSeq++}`
|
|
55
|
+
const panelId = `${id}-panel`
|
|
56
|
+
const optionId = (i: number) => `${id}-opt-${i}`
|
|
57
|
+
|
|
58
|
+
let open = $state(false)
|
|
59
|
+
let active = $state(0)
|
|
60
|
+
let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
|
|
61
|
+
|
|
62
|
+
const count = () => config.itemCount()
|
|
63
|
+
const isDisabled = (i: number) => config.disabled?.(i) ?? false
|
|
64
|
+
const readonly = () => config.readonly?.() ?? false
|
|
65
|
+
|
|
66
|
+
function enabledIndices(): number[] {
|
|
67
|
+
return enabledIndicesOf(count(), isDisabled)
|
|
68
|
+
}
|
|
69
|
+
function clampActive(i: number): number {
|
|
70
|
+
const n = count()
|
|
71
|
+
return n <= 0 ? 0 : Math.max(0, Math.min(i, n - 1))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function setOpen(v: boolean) {
|
|
75
|
+
if (v === open) return
|
|
76
|
+
open = v
|
|
77
|
+
config.onOpenChange?.(v)
|
|
78
|
+
}
|
|
79
|
+
function openPanel() {
|
|
80
|
+
if (readonly()) return
|
|
81
|
+
active = clampActive(config.initialActive?.() ?? 0)
|
|
82
|
+
setOpen(true)
|
|
83
|
+
}
|
|
84
|
+
function closePanel() {
|
|
85
|
+
setOpen(false)
|
|
86
|
+
config.getTrigger()?.focus()
|
|
87
|
+
}
|
|
88
|
+
function toggle() {
|
|
89
|
+
if (readonly()) return
|
|
90
|
+
open ? setOpen(false) : openPanel()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function move(delta: number) {
|
|
94
|
+
const next = wrapMove(enabledIndices(), active, delta)
|
|
95
|
+
if (next >= 0) active = next
|
|
96
|
+
}
|
|
97
|
+
function first() { active = enabledIndices()[0] ?? 0 }
|
|
98
|
+
function last() { active = enabledIndices().at(-1) ?? 0 }
|
|
99
|
+
function selectActive() {
|
|
100
|
+
if (readonly() || count() === 0 || isDisabled(active)) return
|
|
101
|
+
config.onSelect(active)
|
|
102
|
+
if (config.closeOnSelect?.() ?? true) closePanel()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function updatePos() {
|
|
106
|
+
const t = config.getTrigger()
|
|
107
|
+
if (!t) return
|
|
108
|
+
rect = anchoredRect(t.getBoundingClientRect(), {
|
|
109
|
+
estimatedHeight: config.estimatedHeight?.() ?? 280,
|
|
110
|
+
minWidth: config.minWidth?.() ?? 200,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// While open: position, focus into the panel, reposition on scroll/resize, and
|
|
115
|
+
// register a dismissable layer (Escape / outside-click, top-of-stack first).
|
|
116
|
+
$effect(() => {
|
|
117
|
+
if (!open) return
|
|
118
|
+
updatePos()
|
|
119
|
+
const initial = config.getInitialFocus?.() ?? config.getPanel()
|
|
120
|
+
queueMicrotask(() => initial?.focus())
|
|
121
|
+
const rp = () => updatePos()
|
|
122
|
+
window.addEventListener('scroll', rp, true)
|
|
123
|
+
window.addEventListener('resize', rp)
|
|
124
|
+
const layer = createDismissableLayer({
|
|
125
|
+
element: () => [config.getTrigger(), config.getPanel()],
|
|
126
|
+
onDismiss: () => setOpen(false),
|
|
127
|
+
})
|
|
128
|
+
layer.activate()
|
|
129
|
+
return () => {
|
|
130
|
+
window.removeEventListener('scroll', rp, true)
|
|
131
|
+
window.removeEventListener('resize', rp)
|
|
132
|
+
layer.release()
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
function onTriggerKeydown(e: KeyboardEvent) {
|
|
137
|
+
if (readonly()) return
|
|
138
|
+
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
|
|
139
|
+
e.preventDefault()
|
|
140
|
+
openPanel()
|
|
141
|
+
} else if (e.key === 'ArrowUp') {
|
|
142
|
+
e.preventDefault()
|
|
143
|
+
openPanel()
|
|
144
|
+
last()
|
|
145
|
+
} else if (e.key === 'Escape') {
|
|
146
|
+
setOpen(false)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function onPanelKeydown(e: KeyboardEvent) {
|
|
150
|
+
if (config.onExtraKey?.(e, active)) return
|
|
151
|
+
switch (e.key) {
|
|
152
|
+
case 'ArrowDown': e.preventDefault(); move(1); break
|
|
153
|
+
case 'ArrowUp': e.preventDefault(); move(-1); break
|
|
154
|
+
case 'Home': e.preventDefault(); first(); break
|
|
155
|
+
case 'End': e.preventDefault(); last(); break
|
|
156
|
+
case 'Enter': e.preventDefault(); selectActive(); break
|
|
157
|
+
case 'Escape': e.preventDefault(); closePanel(); break
|
|
158
|
+
case 'Tab': setOpen(false); break
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The current active-descendant id (for the focused element), or undefined. */
|
|
163
|
+
const activeDescendant = () => (open && count() > 0 ? optionId(active) : undefined)
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
get open() { return open },
|
|
167
|
+
set open(v: boolean) { setOpen(v) },
|
|
168
|
+
get active() { return active },
|
|
169
|
+
set active(i: number) { active = clampActive(i) },
|
|
170
|
+
get rect() { return rect },
|
|
171
|
+
/** Stable id of the panel element (for aria-controls / labelling). */
|
|
172
|
+
panelId,
|
|
173
|
+
/** Stable id of the option at `i` (for aria-activedescendant + item id). */
|
|
174
|
+
optionId,
|
|
175
|
+
activeDescendant,
|
|
176
|
+
openPanel,
|
|
177
|
+
closePanel,
|
|
178
|
+
toggle,
|
|
179
|
+
move,
|
|
180
|
+
first,
|
|
181
|
+
last,
|
|
182
|
+
selectActive,
|
|
183
|
+
updatePos,
|
|
184
|
+
onPanelKeydown,
|
|
185
|
+
/** True when item `i` is the roving-active one. */
|
|
186
|
+
isActive: (i: number) => i === active,
|
|
187
|
+
/** Spread onto the trigger button. Pass the ARIA popup type the panel
|
|
188
|
+
* exposes (mirrors `focusOwnerProps(role)`); defaults to 'listbox' for
|
|
189
|
+
* the classic select panels. */
|
|
190
|
+
triggerProps: (haspopup: 'listbox' | 'tree' | 'grid' = 'listbox') => ({
|
|
191
|
+
'aria-haspopup': haspopup,
|
|
192
|
+
'aria-expanded': open,
|
|
193
|
+
'aria-controls': open ? panelId : undefined,
|
|
194
|
+
'aria-readonly': readonly() || undefined,
|
|
195
|
+
onkeydown: onTriggerKeydown,
|
|
196
|
+
}),
|
|
197
|
+
/** Attributes for the element that OWNS focus + activedescendant (the panel,
|
|
198
|
+
* or a search input). Pass the ARIA role the panel should expose. */
|
|
199
|
+
focusOwnerProps: (role: 'listbox' | 'tree' | 'grid' = 'listbox') => ({
|
|
200
|
+
role,
|
|
201
|
+
'aria-activedescendant': activeDescendant(),
|
|
202
|
+
onkeydown: onPanelKeydown,
|
|
203
|
+
}),
|
|
204
|
+
/** Spread onto the item element at `i`. */
|
|
205
|
+
itemProps: (i: number) => ({
|
|
206
|
+
id: optionId(i),
|
|
207
|
+
'data-active': i === active ? '' : undefined,
|
|
208
|
+
onpointermove: () => { if (!isDisabled(i)) active = i },
|
|
209
|
+
}),
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export type PopoverSelect = ReturnType<typeof createPopoverSelect>
|