@svgrid/grid 2.2.25 → 2.2.27
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 -145
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +179 -179
- package/dist/GridMenus.svelte +734 -728
- 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 +250 -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.css +2723 -2719
- package/dist/SvGrid.svelte +3333 -3333
- 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 +287 -287
- 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 +131 -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/a11y/dismissable.d.ts +11 -0
- package/dist/a11y/dismissable.js +21 -0
- package/dist/ai.js +3 -3
- package/dist/cdn/GridMenus-BzxEYNtW.js +421 -0
- package/dist/cdn/GridMenus-Dr-nGEXn.js +417 -0
- package/dist/cdn/{SvDateTimePicker-CYC7gRER.js → SvDateTimePicker-B1sDc9Pz.js} +451 -444
- package/dist/cdn/{SvDateTimePicker-DtQKd4Ns.js → SvDateTimePicker-Bh0FTs1E.js} +114 -107
- package/dist/cdn/{SvGridChart-BhnFdLd0.js → SvGridChart-BKQsO5fZ.js} +1 -1
- package/dist/cdn/{SvGridChart-BcLCYvw9.js → SvGridChart-Bgk4hDs8.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DvjwxLvK.js → SvGridChartPanel-DSkHIQDd.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-Cmx9DHAS.js → SvGridChartPanel-DkCc2Z7b.js} +1 -1
- package/dist/cdn/{SvGridChartView-B40DTJSX.js → SvGridChartView-BrbVaELm.js} +1 -1
- package/dist/cdn/{SvGridChartView-PwqI4s02.js → SvGridChartView-i9rWvSKg.js} +1 -1
- package/dist/cdn/{SvGridDropdown-hpnHvOGt.js → SvGridDropdown-CGxxopQ4.js} +1 -1
- package/dist/cdn/{SvGridDropdown-DjALc_4f.js → SvGridDropdown-D08sVmAy.js} +1 -1
- package/dist/cdn/{src-DysW9qbk.js → src-Cw8d9U8j.js} +5753 -5753
- package/dist/cdn/{src-bYfr_7aR.js → src-cgs1wcUS.js} +8102 -8102
- 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/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +30 -13
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +179 -179
- package/src/GridMenus.svelte +734 -728
- 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 +250 -250
- 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 +3239 -3239
- package/src/SvGrid.css +2723 -2719
- package/src/SvGrid.svelte +3333 -3333
- package/src/SvGrid.types.ts +1613 -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 +287 -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/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +131 -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 -95
- package/src/a11y/dismissable.ts +114 -94
- 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/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -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/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 +859 -859
- package/src/editing.ts +675 -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/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/index.ts +713 -712
- 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 +700 -700
- package/src/menus.ts +652 -652
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- 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.behavior.test.ts +706 -706
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-menu-scroll.test.ts +102 -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-GfCAHgkZ.js +0 -420
- package/dist/cdn/GridMenus-GiVNSeDU.js +0 -416
|
@@ -1,157 +1,157 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* createCountryInput - the HEADLESS core behind <SvCountryInput>: a searchable
|
|
3
|
-
* country picker (search by name, dial code, or ISO code; roving active index +
|
|
4
|
-
* keyboard) that emits the ISO 3166-1 alpha-2 code. Exposed as **prop-getters**
|
|
5
|
-
* you spread onto YOUR OWN markup. No styles/DOM/portal - those stay in the
|
|
6
|
-
* styled component.
|
|
7
|
-
*
|
|
8
|
-
* ```svelte
|
|
9
|
-
* <script lang="ts">
|
|
10
|
-
* import { createCountryInput, flagEmoji } from '@svgrid/grid'
|
|
11
|
-
* let value = $state<string | null>(null)
|
|
12
|
-
* const ci = createCountryInput({ value: () => value, onChange: (c) => (value = c) })
|
|
13
|
-
* </script>
|
|
14
|
-
* <button {...ci.triggerProps()}>{ci.selected?.name ?? 'Select'}</button>
|
|
15
|
-
* {#if ci.open}
|
|
16
|
-
* <input {...ci.searchProps()} />
|
|
17
|
-
* <ul {...ci.listboxProps()}>
|
|
18
|
-
* {#each ci.filtered as c, i (c.code)}<li {...ci.optionProps(i)}>{c.name}</li>{/each}
|
|
19
|
-
* </ul>
|
|
20
|
-
* {/if}
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
23
|
-
import { COUNTRIES, COUNTRY_BY_CODE, type Country } from './countries'
|
|
24
|
-
import { editorAria, type EditorAriaState } from './editor-contract'
|
|
25
|
-
|
|
26
|
-
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
27
|
-
export type CountryInputConfig = {
|
|
28
|
-
value: () => string | null
|
|
29
|
-
onChange?: (code: string) => void
|
|
30
|
-
disabled?: () => boolean
|
|
31
|
-
ariaLabel?: () => string | undefined
|
|
32
|
-
/** Accessible label for the in-panel search field (localizable). */
|
|
33
|
-
searchLabel?: () => string | undefined
|
|
34
|
-
/** DOM focus hook provided by the renderer; the core never touches the DOM. */
|
|
35
|
-
focusTrigger?: () => void
|
|
36
|
-
// Editor contract (ARIA + validation) - folded into triggerProps().
|
|
37
|
-
id?: () => string | undefined
|
|
38
|
-
invalid?: () => boolean
|
|
39
|
-
required?: () => boolean
|
|
40
|
-
error?: () => string | undefined
|
|
41
|
-
hint?: () => string | undefined
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let uid = 0
|
|
45
|
-
|
|
46
|
-
export function createCountryInput(config: CountryInputConfig) {
|
|
47
|
-
const id = `sv-country-${uid++}`
|
|
48
|
-
const listId = `${id}-list`
|
|
49
|
-
const optionId = (i: number) => `${listId}-opt-${i}`
|
|
50
|
-
const disabled = () => config.disabled?.() ?? false
|
|
51
|
-
|
|
52
|
-
const ariaState = (): EditorAriaState => ({
|
|
53
|
-
id: config.id?.(),
|
|
54
|
-
invalid: config.invalid?.(),
|
|
55
|
-
required: config.required?.(),
|
|
56
|
-
error: config.error?.(),
|
|
57
|
-
hint: config.hint?.(),
|
|
58
|
-
ariaLabel: config.ariaLabel?.(),
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
let open = $state(false)
|
|
62
|
-
let query = $state('')
|
|
63
|
-
let active = $state(0)
|
|
64
|
-
|
|
65
|
-
const selected = $derived(config.value() ? COUNTRY_BY_CODE.get(config.value()!) ?? null : null)
|
|
66
|
-
const filtered = $derived<Country[]>(
|
|
67
|
-
query.trim() === ''
|
|
68
|
-
? COUNTRIES
|
|
69
|
-
: COUNTRIES.filter(
|
|
70
|
-
(c) =>
|
|
71
|
-
c.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
72
|
-
c.dial.includes(query) ||
|
|
73
|
-
c.code.toLowerCase() === query.toLowerCase(),
|
|
74
|
-
),
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
function openPanel() { if (disabled() || open) return; open = true; query = ''; active = 0 }
|
|
78
|
-
function close() { open = false; config.focusTrigger?.() }
|
|
79
|
-
function toggle() { if (open) { open = false } else { openPanel() } }
|
|
80
|
-
function pick(code: string) { config.onChange?.(code); open = false; config.focusTrigger?.() }
|
|
81
|
-
function setQuery(v: string) { query = v; active = 0 }
|
|
82
|
-
function onSearchInput(e: Event) { setQuery((e.currentTarget as HTMLInputElement).value) }
|
|
83
|
-
/** The current active-descendant id (for the focused search input), or undefined. */
|
|
84
|
-
const activeDescendant = () => (open && filtered.length > 0 ? optionId(active) : undefined)
|
|
85
|
-
function onKeydown(e: KeyboardEvent) {
|
|
86
|
-
if (!open) return
|
|
87
|
-
if (e.key === 'ArrowDown') { e.preventDefault(); active = Math.min(active + 1, filtered.length - 1) }
|
|
88
|
-
else if (e.key === 'ArrowUp') { e.preventDefault(); active = Math.max(active - 1, 0) }
|
|
89
|
-
else if (e.key === 'Enter') { e.preventDefault(); const c = filtered[active]; if (c) pick(c.code) }
|
|
90
|
-
else if (e.key === 'Escape') { e.preventDefault(); close() }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
/** Panel open state. */
|
|
95
|
-
get open() { return open },
|
|
96
|
-
/** Highlighted country index into `filtered`. */
|
|
97
|
-
get activeIndex() { return active },
|
|
98
|
-
/** Current search query. */
|
|
99
|
-
get query() { return query },
|
|
100
|
-
/** Countries after the current search filter. */
|
|
101
|
-
get filtered() { return filtered },
|
|
102
|
-
/** The selected country, if any. */
|
|
103
|
-
get selected() { return selected },
|
|
104
|
-
/** Controlled value (ISO alpha-2 code). */
|
|
105
|
-
get value() { return config.value() },
|
|
106
|
-
isActive: (i: number) => i === active,
|
|
107
|
-
isSelected: (code: string) => code === config.value(),
|
|
108
|
-
setActive: (i: number) => { active = i },
|
|
109
|
-
setQuery,
|
|
110
|
-
openPanel,
|
|
111
|
-
close,
|
|
112
|
-
toggle,
|
|
113
|
-
pick,
|
|
114
|
-
onKeydown,
|
|
115
|
-
/** Spread onto the trigger <button>. */
|
|
116
|
-
triggerProps: () => ({
|
|
117
|
-
id: config.id?.(),
|
|
118
|
-
type: 'button' as const,
|
|
119
|
-
'aria-haspopup': 'listbox' as const,
|
|
120
|
-
'aria-expanded': open,
|
|
121
|
-
'aria-controls': listId,
|
|
122
|
-
...editorAria(ariaState()),
|
|
123
|
-
disabled: disabled(),
|
|
124
|
-
onclick: toggle,
|
|
125
|
-
}),
|
|
126
|
-
/** Spread onto the search <input> inside the panel. */
|
|
127
|
-
searchProps: () => ({
|
|
128
|
-
type: 'text' as const,
|
|
129
|
-
value: query,
|
|
130
|
-
'aria-label': config.searchLabel?.() ?? 'Search countries',
|
|
131
|
-
'aria-controls': listId,
|
|
132
|
-
'aria-activedescendant': activeDescendant(),
|
|
133
|
-
oninput: onSearchInput,
|
|
134
|
-
onkeydown: onKeydown,
|
|
135
|
-
}),
|
|
136
|
-
/** Spread onto the listbox container. */
|
|
137
|
-
listboxProps: () => ({
|
|
138
|
-
id: listId,
|
|
139
|
-
role: 'listbox' as const,
|
|
140
|
-
}),
|
|
141
|
-
/** Spread onto the country option at `index` (index into `filtered`). */
|
|
142
|
-
optionProps: (index: number) => {
|
|
143
|
-
const c = filtered[index]
|
|
144
|
-
return {
|
|
145
|
-
id: optionId(index),
|
|
146
|
-
role: 'option' as const,
|
|
147
|
-
tabindex: -1,
|
|
148
|
-
'aria-selected': c ? c.code === config.value() : false,
|
|
149
|
-
'data-idx': index,
|
|
150
|
-
onclick: () => { if (c) pick(c.code) },
|
|
151
|
-
onpointermove: () => { active = index },
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export type CountryInput = ReturnType<typeof createCountryInput>
|
|
1
|
+
/**
|
|
2
|
+
* createCountryInput - the HEADLESS core behind <SvCountryInput>: a searchable
|
|
3
|
+
* country picker (search by name, dial code, or ISO code; roving active index +
|
|
4
|
+
* keyboard) that emits the ISO 3166-1 alpha-2 code. Exposed as **prop-getters**
|
|
5
|
+
* you spread onto YOUR OWN markup. No styles/DOM/portal - those stay in the
|
|
6
|
+
* styled component.
|
|
7
|
+
*
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script lang="ts">
|
|
10
|
+
* import { createCountryInput, flagEmoji } from '@svgrid/grid'
|
|
11
|
+
* let value = $state<string | null>(null)
|
|
12
|
+
* const ci = createCountryInput({ value: () => value, onChange: (c) => (value = c) })
|
|
13
|
+
* </script>
|
|
14
|
+
* <button {...ci.triggerProps()}>{ci.selected?.name ?? 'Select'}</button>
|
|
15
|
+
* {#if ci.open}
|
|
16
|
+
* <input {...ci.searchProps()} />
|
|
17
|
+
* <ul {...ci.listboxProps()}>
|
|
18
|
+
* {#each ci.filtered as c, i (c.code)}<li {...ci.optionProps(i)}>{c.name}</li>{/each}
|
|
19
|
+
* </ul>
|
|
20
|
+
* {/if}
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { COUNTRIES, COUNTRY_BY_CODE, type Country } from './countries'
|
|
24
|
+
import { editorAria, type EditorAriaState } from './editor-contract'
|
|
25
|
+
|
|
26
|
+
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
27
|
+
export type CountryInputConfig = {
|
|
28
|
+
value: () => string | null
|
|
29
|
+
onChange?: (code: string) => void
|
|
30
|
+
disabled?: () => boolean
|
|
31
|
+
ariaLabel?: () => string | undefined
|
|
32
|
+
/** Accessible label for the in-panel search field (localizable). */
|
|
33
|
+
searchLabel?: () => string | undefined
|
|
34
|
+
/** DOM focus hook provided by the renderer; the core never touches the DOM. */
|
|
35
|
+
focusTrigger?: () => void
|
|
36
|
+
// Editor contract (ARIA + validation) - folded into triggerProps().
|
|
37
|
+
id?: () => string | undefined
|
|
38
|
+
invalid?: () => boolean
|
|
39
|
+
required?: () => boolean
|
|
40
|
+
error?: () => string | undefined
|
|
41
|
+
hint?: () => string | undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let uid = 0
|
|
45
|
+
|
|
46
|
+
export function createCountryInput(config: CountryInputConfig) {
|
|
47
|
+
const id = `sv-country-${uid++}`
|
|
48
|
+
const listId = `${id}-list`
|
|
49
|
+
const optionId = (i: number) => `${listId}-opt-${i}`
|
|
50
|
+
const disabled = () => config.disabled?.() ?? false
|
|
51
|
+
|
|
52
|
+
const ariaState = (): EditorAriaState => ({
|
|
53
|
+
id: config.id?.(),
|
|
54
|
+
invalid: config.invalid?.(),
|
|
55
|
+
required: config.required?.(),
|
|
56
|
+
error: config.error?.(),
|
|
57
|
+
hint: config.hint?.(),
|
|
58
|
+
ariaLabel: config.ariaLabel?.(),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
let open = $state(false)
|
|
62
|
+
let query = $state('')
|
|
63
|
+
let active = $state(0)
|
|
64
|
+
|
|
65
|
+
const selected = $derived(config.value() ? COUNTRY_BY_CODE.get(config.value()!) ?? null : null)
|
|
66
|
+
const filtered = $derived<Country[]>(
|
|
67
|
+
query.trim() === ''
|
|
68
|
+
? COUNTRIES
|
|
69
|
+
: COUNTRIES.filter(
|
|
70
|
+
(c) =>
|
|
71
|
+
c.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
72
|
+
c.dial.includes(query) ||
|
|
73
|
+
c.code.toLowerCase() === query.toLowerCase(),
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
function openPanel() { if (disabled() || open) return; open = true; query = ''; active = 0 }
|
|
78
|
+
function close() { open = false; config.focusTrigger?.() }
|
|
79
|
+
function toggle() { if (open) { open = false } else { openPanel() } }
|
|
80
|
+
function pick(code: string) { config.onChange?.(code); open = false; config.focusTrigger?.() }
|
|
81
|
+
function setQuery(v: string) { query = v; active = 0 }
|
|
82
|
+
function onSearchInput(e: Event) { setQuery((e.currentTarget as HTMLInputElement).value) }
|
|
83
|
+
/** The current active-descendant id (for the focused search input), or undefined. */
|
|
84
|
+
const activeDescendant = () => (open && filtered.length > 0 ? optionId(active) : undefined)
|
|
85
|
+
function onKeydown(e: KeyboardEvent) {
|
|
86
|
+
if (!open) return
|
|
87
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); active = Math.min(active + 1, filtered.length - 1) }
|
|
88
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); active = Math.max(active - 1, 0) }
|
|
89
|
+
else if (e.key === 'Enter') { e.preventDefault(); const c = filtered[active]; if (c) pick(c.code) }
|
|
90
|
+
else if (e.key === 'Escape') { e.preventDefault(); close() }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
/** Panel open state. */
|
|
95
|
+
get open() { return open },
|
|
96
|
+
/** Highlighted country index into `filtered`. */
|
|
97
|
+
get activeIndex() { return active },
|
|
98
|
+
/** Current search query. */
|
|
99
|
+
get query() { return query },
|
|
100
|
+
/** Countries after the current search filter. */
|
|
101
|
+
get filtered() { return filtered },
|
|
102
|
+
/** The selected country, if any. */
|
|
103
|
+
get selected() { return selected },
|
|
104
|
+
/** Controlled value (ISO alpha-2 code). */
|
|
105
|
+
get value() { return config.value() },
|
|
106
|
+
isActive: (i: number) => i === active,
|
|
107
|
+
isSelected: (code: string) => code === config.value(),
|
|
108
|
+
setActive: (i: number) => { active = i },
|
|
109
|
+
setQuery,
|
|
110
|
+
openPanel,
|
|
111
|
+
close,
|
|
112
|
+
toggle,
|
|
113
|
+
pick,
|
|
114
|
+
onKeydown,
|
|
115
|
+
/** Spread onto the trigger <button>. */
|
|
116
|
+
triggerProps: () => ({
|
|
117
|
+
id: config.id?.(),
|
|
118
|
+
type: 'button' as const,
|
|
119
|
+
'aria-haspopup': 'listbox' as const,
|
|
120
|
+
'aria-expanded': open,
|
|
121
|
+
'aria-controls': listId,
|
|
122
|
+
...editorAria(ariaState()),
|
|
123
|
+
disabled: disabled(),
|
|
124
|
+
onclick: toggle,
|
|
125
|
+
}),
|
|
126
|
+
/** Spread onto the search <input> inside the panel. */
|
|
127
|
+
searchProps: () => ({
|
|
128
|
+
type: 'text' as const,
|
|
129
|
+
value: query,
|
|
130
|
+
'aria-label': config.searchLabel?.() ?? 'Search countries',
|
|
131
|
+
'aria-controls': listId,
|
|
132
|
+
'aria-activedescendant': activeDescendant(),
|
|
133
|
+
oninput: onSearchInput,
|
|
134
|
+
onkeydown: onKeydown,
|
|
135
|
+
}),
|
|
136
|
+
/** Spread onto the listbox container. */
|
|
137
|
+
listboxProps: () => ({
|
|
138
|
+
id: listId,
|
|
139
|
+
role: 'listbox' as const,
|
|
140
|
+
}),
|
|
141
|
+
/** Spread onto the country option at `index` (index into `filtered`). */
|
|
142
|
+
optionProps: (index: number) => {
|
|
143
|
+
const c = filtered[index]
|
|
144
|
+
return {
|
|
145
|
+
id: optionId(index),
|
|
146
|
+
role: 'option' as const,
|
|
147
|
+
tabindex: -1,
|
|
148
|
+
'aria-selected': c ? c.code === config.value() : false,
|
|
149
|
+
'data-idx': index,
|
|
150
|
+
onclick: () => { if (c) pick(c.code) },
|
|
151
|
+
onpointermove: () => { active = index },
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type CountryInput = ReturnType<typeof createCountryInput>
|
|
@@ -1,168 +1,168 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* createDropdownList - the HEADLESS core behind <SvDropDownList>: a single-select
|
|
3
|
-
* dropdown state machine (trigger open/close, roving active index over enabled
|
|
4
|
-
* options, full keyboard) exposed as **prop-getters** you spread onto YOUR OWN
|
|
5
|
-
* markup. No styles, no DOM, no portal/measurement - those stay in the styled
|
|
6
|
-
* component.
|
|
7
|
-
*
|
|
8
|
-
* ```svelte
|
|
9
|
-
* <script lang="ts">
|
|
10
|
-
* import { createDropdownList } from '@svgrid/grid'
|
|
11
|
-
* let value = $state<string | null>(null)
|
|
12
|
-
* const dd = createDropdownList({ options: () => options, value: () => value, onChange: (v) => (value = v) })
|
|
13
|
-
* </script>
|
|
14
|
-
* <button {...dd.triggerProps()}>{dd.selected?.label ?? 'Select'}</button>
|
|
15
|
-
* {#if dd.open}
|
|
16
|
-
* <ul {...dd.listboxProps()}>
|
|
17
|
-
* {#each options as opt, i (opt.value)}<li {...dd.optionProps(i)}>{opt.label}</li>{/each}
|
|
18
|
-
* </ul>
|
|
19
|
-
* {/if}
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
import { createTypeaheadBuffer, isTypeaheadKey, nextTypeaheadIndex, type ListOption } from './list-option'
|
|
23
|
-
import { editorAria, type EditorAriaState } from './editor-contract'
|
|
24
|
-
|
|
25
|
-
export type DropdownValue = string | number | null
|
|
26
|
-
|
|
27
|
-
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
28
|
-
export type DropdownListConfig = {
|
|
29
|
-
options: () => ReadonlyArray<ListOption>
|
|
30
|
-
value: () => DropdownValue
|
|
31
|
-
onChange?: (value: string | number) => void
|
|
32
|
-
disabled?: () => boolean
|
|
33
|
-
/** Read-only: value shown, trigger focusable, but the panel will not open. */
|
|
34
|
-
readonly?: () => boolean
|
|
35
|
-
ariaLabel?: () => string | undefined
|
|
36
|
-
/** DOM focus hook provided by the renderer; the core never touches the DOM. */
|
|
37
|
-
focusTrigger?: () => void
|
|
38
|
-
// Editor contract (ARIA + validation) - folded into triggerProps().
|
|
39
|
-
id?: () => string | undefined
|
|
40
|
-
invalid?: () => boolean
|
|
41
|
-
required?: () => boolean
|
|
42
|
-
error?: () => string | undefined
|
|
43
|
-
hint?: () => string | undefined
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let uid = 0
|
|
47
|
-
|
|
48
|
-
export function createDropdownList(config: DropdownListConfig) {
|
|
49
|
-
const listId = `sv-ddl-${uid++}`
|
|
50
|
-
const optionId = (index: number) => `${listId}-opt-${index}`
|
|
51
|
-
const opts = () => config.options()
|
|
52
|
-
const disabled = () => config.disabled?.() ?? false
|
|
53
|
-
const readonly = () => config.readonly?.() ?? false
|
|
54
|
-
|
|
55
|
-
const ariaState = (): EditorAriaState => ({
|
|
56
|
-
id: config.id?.(),
|
|
57
|
-
invalid: config.invalid?.(),
|
|
58
|
-
required: config.required?.(),
|
|
59
|
-
error: config.error?.(),
|
|
60
|
-
hint: config.hint?.(),
|
|
61
|
-
ariaLabel: config.ariaLabel?.(),
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
let open = $state(false)
|
|
65
|
-
let active = $state(-1)
|
|
66
|
-
|
|
67
|
-
const selected = $derived(opts().find((o) => o.value === config.value()) ?? null)
|
|
68
|
-
const enabledIdx = $derived(opts().map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0))
|
|
69
|
-
|
|
70
|
-
function openPanel() {
|
|
71
|
-
if (disabled() || readonly() || open) return
|
|
72
|
-
open = true
|
|
73
|
-
active = Math.max(0, opts().findIndex((o) => o.value === config.value()))
|
|
74
|
-
}
|
|
75
|
-
function close() { open = false }
|
|
76
|
-
function toggle() { if (open) { close() } else { openPanel() } }
|
|
77
|
-
function pick(i: number) {
|
|
78
|
-
if (readonly()) return
|
|
79
|
-
const o = opts()[i]
|
|
80
|
-
if (!o || o.disabled) return
|
|
81
|
-
config.onChange?.(o.value); close(); config.focusTrigger?.()
|
|
82
|
-
}
|
|
83
|
-
function setActive(i: number) { const o = opts()[i]; if (o && !o.disabled) active = i }
|
|
84
|
-
function move(d: number) {
|
|
85
|
-
const pos = enabledIdx.indexOf(active)
|
|
86
|
-
active = enabledIdx[(pos + d + enabledIdx.length) % enabledIdx.length] ?? active
|
|
87
|
-
}
|
|
88
|
-
// Type-ahead: typing a label prefix jumps the active option (and selects it
|
|
89
|
-
// when the list is closed, matching a native <select>).
|
|
90
|
-
const typeahead = createTypeaheadBuffer()
|
|
91
|
-
function onTypeahead(char: string) {
|
|
92
|
-
if (readonly()) return
|
|
93
|
-
const buffer = typeahead.push(char.toLowerCase())
|
|
94
|
-
const from = active >= 0 ? active : opts().findIndex((o) => o.value === config.value())
|
|
95
|
-
const next = nextTypeaheadIndex(opts(), buffer, from)
|
|
96
|
-
if (next < 0) return
|
|
97
|
-
active = next
|
|
98
|
-
if (!open) config.onChange?.(opts()[next]!.value)
|
|
99
|
-
}
|
|
100
|
-
function onKeydown(e: KeyboardEvent) {
|
|
101
|
-
if (disabled() || readonly()) return
|
|
102
|
-
if (isTypeaheadKey(e)) { onTypeahead(e.key); return }
|
|
103
|
-
if (!open && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); openPanel(); return }
|
|
104
|
-
if (!open) return
|
|
105
|
-
if (e.key === 'ArrowDown') { e.preventDefault(); move(1) }
|
|
106
|
-
else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1) }
|
|
107
|
-
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(active) }
|
|
108
|
-
else if (e.key === 'Escape') { e.preventDefault(); close(); config.focusTrigger?.() }
|
|
109
|
-
else if (e.key === 'Home') { active = enabledIdx[0] ?? 0 }
|
|
110
|
-
else if (e.key === 'End') { active = enabledIdx.at(-1) ?? 0 }
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
/** Panel open state. */
|
|
115
|
-
get open() { return open },
|
|
116
|
-
/** Highlighted option index into `options`. */
|
|
117
|
-
get activeIndex() { return active },
|
|
118
|
-
/** The option matching the controlled value, if any. */
|
|
119
|
-
get selected() { return selected },
|
|
120
|
-
/** Controlled value. */
|
|
121
|
-
get value() { return config.value() },
|
|
122
|
-
isActive: (i: number) => i === active,
|
|
123
|
-
isSelected: (o: ListOption) => o.value === config.value(),
|
|
124
|
-
setActive,
|
|
125
|
-
openPanel,
|
|
126
|
-
close,
|
|
127
|
-
toggle,
|
|
128
|
-
pick,
|
|
129
|
-
move,
|
|
130
|
-
onKeydown,
|
|
131
|
-
/** Spread onto the trigger <button>. */
|
|
132
|
-
triggerProps: () => ({
|
|
133
|
-
id: config.id?.(),
|
|
134
|
-
type: 'button' as const,
|
|
135
|
-
'aria-haspopup': 'listbox' as const,
|
|
136
|
-
'aria-expanded': open,
|
|
137
|
-
'aria-controls': listId,
|
|
138
|
-
'aria-activedescendant': open && opts()[active] ? optionId(active) : undefined,
|
|
139
|
-
...editorAria(ariaState()),
|
|
140
|
-
'aria-readonly': readonly() || undefined,
|
|
141
|
-
disabled: disabled(),
|
|
142
|
-
onclick: toggle,
|
|
143
|
-
onkeydown: onKeydown,
|
|
144
|
-
}),
|
|
145
|
-
/** Spread onto the listbox container. */
|
|
146
|
-
listboxProps: () => ({
|
|
147
|
-
id: listId,
|
|
148
|
-
role: 'listbox' as const,
|
|
149
|
-
tabindex: -1,
|
|
150
|
-
}),
|
|
151
|
-
/** Spread onto the option at `index` (index into `options`). */
|
|
152
|
-
optionProps: (index: number) => {
|
|
153
|
-
const o = opts()[index]
|
|
154
|
-
return {
|
|
155
|
-
role: 'option' as const,
|
|
156
|
-
id: optionId(index),
|
|
157
|
-
tabindex: -1,
|
|
158
|
-
'aria-selected': o ? o.value === config.value() : false,
|
|
159
|
-
'aria-disabled': o?.disabled,
|
|
160
|
-
'data-idx': index,
|
|
161
|
-
onclick: () => pick(index),
|
|
162
|
-
onpointermove: () => { if (o && !o.disabled) active = index },
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export type DropdownList = ReturnType<typeof createDropdownList>
|
|
1
|
+
/**
|
|
2
|
+
* createDropdownList - the HEADLESS core behind <SvDropDownList>: a single-select
|
|
3
|
+
* dropdown state machine (trigger open/close, roving active index over enabled
|
|
4
|
+
* options, full keyboard) exposed as **prop-getters** you spread onto YOUR OWN
|
|
5
|
+
* markup. No styles, no DOM, no portal/measurement - those stay in the styled
|
|
6
|
+
* component.
|
|
7
|
+
*
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script lang="ts">
|
|
10
|
+
* import { createDropdownList } from '@svgrid/grid'
|
|
11
|
+
* let value = $state<string | null>(null)
|
|
12
|
+
* const dd = createDropdownList({ options: () => options, value: () => value, onChange: (v) => (value = v) })
|
|
13
|
+
* </script>
|
|
14
|
+
* <button {...dd.triggerProps()}>{dd.selected?.label ?? 'Select'}</button>
|
|
15
|
+
* {#if dd.open}
|
|
16
|
+
* <ul {...dd.listboxProps()}>
|
|
17
|
+
* {#each options as opt, i (opt.value)}<li {...dd.optionProps(i)}>{opt.label}</li>{/each}
|
|
18
|
+
* </ul>
|
|
19
|
+
* {/if}
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { createTypeaheadBuffer, isTypeaheadKey, nextTypeaheadIndex, type ListOption } from './list-option'
|
|
23
|
+
import { editorAria, type EditorAriaState } from './editor-contract'
|
|
24
|
+
|
|
25
|
+
export type DropdownValue = string | number | null
|
|
26
|
+
|
|
27
|
+
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
28
|
+
export type DropdownListConfig = {
|
|
29
|
+
options: () => ReadonlyArray<ListOption>
|
|
30
|
+
value: () => DropdownValue
|
|
31
|
+
onChange?: (value: string | number) => void
|
|
32
|
+
disabled?: () => boolean
|
|
33
|
+
/** Read-only: value shown, trigger focusable, but the panel will not open. */
|
|
34
|
+
readonly?: () => boolean
|
|
35
|
+
ariaLabel?: () => string | undefined
|
|
36
|
+
/** DOM focus hook provided by the renderer; the core never touches the DOM. */
|
|
37
|
+
focusTrigger?: () => void
|
|
38
|
+
// Editor contract (ARIA + validation) - folded into triggerProps().
|
|
39
|
+
id?: () => string | undefined
|
|
40
|
+
invalid?: () => boolean
|
|
41
|
+
required?: () => boolean
|
|
42
|
+
error?: () => string | undefined
|
|
43
|
+
hint?: () => string | undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let uid = 0
|
|
47
|
+
|
|
48
|
+
export function createDropdownList(config: DropdownListConfig) {
|
|
49
|
+
const listId = `sv-ddl-${uid++}`
|
|
50
|
+
const optionId = (index: number) => `${listId}-opt-${index}`
|
|
51
|
+
const opts = () => config.options()
|
|
52
|
+
const disabled = () => config.disabled?.() ?? false
|
|
53
|
+
const readonly = () => config.readonly?.() ?? false
|
|
54
|
+
|
|
55
|
+
const ariaState = (): EditorAriaState => ({
|
|
56
|
+
id: config.id?.(),
|
|
57
|
+
invalid: config.invalid?.(),
|
|
58
|
+
required: config.required?.(),
|
|
59
|
+
error: config.error?.(),
|
|
60
|
+
hint: config.hint?.(),
|
|
61
|
+
ariaLabel: config.ariaLabel?.(),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
let open = $state(false)
|
|
65
|
+
let active = $state(-1)
|
|
66
|
+
|
|
67
|
+
const selected = $derived(opts().find((o) => o.value === config.value()) ?? null)
|
|
68
|
+
const enabledIdx = $derived(opts().map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0))
|
|
69
|
+
|
|
70
|
+
function openPanel() {
|
|
71
|
+
if (disabled() || readonly() || open) return
|
|
72
|
+
open = true
|
|
73
|
+
active = Math.max(0, opts().findIndex((o) => o.value === config.value()))
|
|
74
|
+
}
|
|
75
|
+
function close() { open = false }
|
|
76
|
+
function toggle() { if (open) { close() } else { openPanel() } }
|
|
77
|
+
function pick(i: number) {
|
|
78
|
+
if (readonly()) return
|
|
79
|
+
const o = opts()[i]
|
|
80
|
+
if (!o || o.disabled) return
|
|
81
|
+
config.onChange?.(o.value); close(); config.focusTrigger?.()
|
|
82
|
+
}
|
|
83
|
+
function setActive(i: number) { const o = opts()[i]; if (o && !o.disabled) active = i }
|
|
84
|
+
function move(d: number) {
|
|
85
|
+
const pos = enabledIdx.indexOf(active)
|
|
86
|
+
active = enabledIdx[(pos + d + enabledIdx.length) % enabledIdx.length] ?? active
|
|
87
|
+
}
|
|
88
|
+
// Type-ahead: typing a label prefix jumps the active option (and selects it
|
|
89
|
+
// when the list is closed, matching a native <select>).
|
|
90
|
+
const typeahead = createTypeaheadBuffer()
|
|
91
|
+
function onTypeahead(char: string) {
|
|
92
|
+
if (readonly()) return
|
|
93
|
+
const buffer = typeahead.push(char.toLowerCase())
|
|
94
|
+
const from = active >= 0 ? active : opts().findIndex((o) => o.value === config.value())
|
|
95
|
+
const next = nextTypeaheadIndex(opts(), buffer, from)
|
|
96
|
+
if (next < 0) return
|
|
97
|
+
active = next
|
|
98
|
+
if (!open) config.onChange?.(opts()[next]!.value)
|
|
99
|
+
}
|
|
100
|
+
function onKeydown(e: KeyboardEvent) {
|
|
101
|
+
if (disabled() || readonly()) return
|
|
102
|
+
if (isTypeaheadKey(e)) { onTypeahead(e.key); return }
|
|
103
|
+
if (!open && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); openPanel(); return }
|
|
104
|
+
if (!open) return
|
|
105
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); move(1) }
|
|
106
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1) }
|
|
107
|
+
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(active) }
|
|
108
|
+
else if (e.key === 'Escape') { e.preventDefault(); close(); config.focusTrigger?.() }
|
|
109
|
+
else if (e.key === 'Home') { active = enabledIdx[0] ?? 0 }
|
|
110
|
+
else if (e.key === 'End') { active = enabledIdx.at(-1) ?? 0 }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
/** Panel open state. */
|
|
115
|
+
get open() { return open },
|
|
116
|
+
/** Highlighted option index into `options`. */
|
|
117
|
+
get activeIndex() { return active },
|
|
118
|
+
/** The option matching the controlled value, if any. */
|
|
119
|
+
get selected() { return selected },
|
|
120
|
+
/** Controlled value. */
|
|
121
|
+
get value() { return config.value() },
|
|
122
|
+
isActive: (i: number) => i === active,
|
|
123
|
+
isSelected: (o: ListOption) => o.value === config.value(),
|
|
124
|
+
setActive,
|
|
125
|
+
openPanel,
|
|
126
|
+
close,
|
|
127
|
+
toggle,
|
|
128
|
+
pick,
|
|
129
|
+
move,
|
|
130
|
+
onKeydown,
|
|
131
|
+
/** Spread onto the trigger <button>. */
|
|
132
|
+
triggerProps: () => ({
|
|
133
|
+
id: config.id?.(),
|
|
134
|
+
type: 'button' as const,
|
|
135
|
+
'aria-haspopup': 'listbox' as const,
|
|
136
|
+
'aria-expanded': open,
|
|
137
|
+
'aria-controls': listId,
|
|
138
|
+
'aria-activedescendant': open && opts()[active] ? optionId(active) : undefined,
|
|
139
|
+
...editorAria(ariaState()),
|
|
140
|
+
'aria-readonly': readonly() || undefined,
|
|
141
|
+
disabled: disabled(),
|
|
142
|
+
onclick: toggle,
|
|
143
|
+
onkeydown: onKeydown,
|
|
144
|
+
}),
|
|
145
|
+
/** Spread onto the listbox container. */
|
|
146
|
+
listboxProps: () => ({
|
|
147
|
+
id: listId,
|
|
148
|
+
role: 'listbox' as const,
|
|
149
|
+
tabindex: -1,
|
|
150
|
+
}),
|
|
151
|
+
/** Spread onto the option at `index` (index into `options`). */
|
|
152
|
+
optionProps: (index: number) => {
|
|
153
|
+
const o = opts()[index]
|
|
154
|
+
return {
|
|
155
|
+
role: 'option' as const,
|
|
156
|
+
id: optionId(index),
|
|
157
|
+
tabindex: -1,
|
|
158
|
+
'aria-selected': o ? o.value === config.value() : false,
|
|
159
|
+
'aria-disabled': o?.disabled,
|
|
160
|
+
'data-idx': index,
|
|
161
|
+
onclick: () => pick(index),
|
|
162
|
+
onpointermove: () => { if (o && !o.disabled) active = index },
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export type DropdownList = ReturnType<typeof createDropdownList>
|