@svgrid/grid 1.2.23 → 1.2.25
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/dist/SvAutoComplete.svelte +23 -34
- package/dist/SvCalendar.svelte +74 -334
- package/dist/SvCalendar.svelte.d.ts +5 -23
- package/dist/SvCheckBox.svelte +11 -10
- package/dist/SvColorInput.svelte +31 -58
- package/dist/SvComboBox.svelte +32 -54
- package/dist/SvCountryInput.svelte +31 -29
- package/dist/SvDateTimePicker.svelte +53 -110
- package/dist/SvDateTimePicker.svelte.d.ts +4 -3
- package/dist/SvDropDownList.svelte +31 -54
- package/dist/SvGauge.svelte +21 -23
- package/dist/SvGauge.svelte.d.ts +0 -5
- package/dist/SvListBox.svelte +25 -58
- package/dist/SvListBox.svelte.d.ts +3 -0
- package/dist/SvMaskedInput.svelte +0 -0
- package/dist/SvNumberInput.svelte +0 -0
- package/dist/SvNumberInput.svelte.d.ts +2 -6
- package/dist/SvPasswordInput.svelte +18 -28
- package/dist/SvPasswordInput.svelte.d.ts +0 -4
- package/dist/SvPhoneInput.svelte +0 -0
- package/dist/SvRadioGroup.svelte +14 -37
- package/dist/SvRadioGroup.svelte.d.ts +6 -9
- package/dist/SvRating.svelte +22 -42
- package/dist/SvRating.svelte.d.ts +3 -5
- package/dist/SvSlider.svelte +30 -57
- package/dist/SvSlider.svelte.d.ts +0 -5
- package/dist/SvSwitchButton.svelte +10 -17
- package/dist/SvSwitchButton.svelte.d.ts +0 -5
- package/dist/SvTabs.svelte +26 -39
- package/dist/SvTabs.svelte.d.ts +5 -5
- package/dist/SvTagsInput.svelte +17 -28
- package/dist/SvTagsInput.svelte.d.ts +0 -5
- package/dist/SvTimePicker.svelte +52 -210
- package/dist/SvTimePicker.svelte.d.ts +11 -8
- package/dist/SvToggleButton.svelte +9 -9
- package/dist/SvTree.svelte +30 -109
- package/dist/SvTree.svelte.d.ts +9 -11
- package/dist/cdn/svgrid.js +7488 -5839
- package/dist/cdn/svgrid.svelte-external.js +4691 -3028
- package/dist/createAutocomplete.svelte.d.ts +77 -0
- package/dist/createAutocomplete.svelte.js +100 -0
- package/dist/createCalendar.svelte.d.ts +192 -0
- package/dist/createCalendar.svelte.js +402 -0
- package/dist/createCheckbox.svelte.d.ts +45 -0
- package/dist/createCheckbox.svelte.js +26 -0
- package/dist/createColorInput.svelte.d.ts +67 -0
- package/dist/createColorInput.svelte.js +100 -0
- package/dist/createCombobox.svelte.d.ts +102 -0
- package/dist/createCombobox.svelte.js +171 -0
- package/dist/createCountryInput.svelte.d.ts +86 -0
- package/dist/createCountryInput.svelte.js +126 -0
- package/dist/createDateTimePicker.svelte.d.ts +83 -0
- package/dist/createDateTimePicker.svelte.js +202 -0
- package/dist/createDropdownList.svelte.d.ts +78 -0
- package/dist/createDropdownList.svelte.js +117 -0
- package/dist/createGauge.svelte.d.ts +56 -0
- package/dist/createGauge.svelte.js +49 -0
- package/dist/createListbox.svelte.d.ts +79 -0
- package/dist/createListbox.svelte.js +117 -0
- package/dist/createMaskedInput.svelte.d.ts +30 -0
- package/dist/createMaskedInput.svelte.js +62 -0
- package/dist/createNumberInput.svelte.d.ts +72 -0
- package/dist/createNumberInput.svelte.js +167 -0
- package/dist/createPasswordInput.svelte.d.ts +60 -0
- package/dist/createPasswordInput.svelte.js +72 -0
- package/dist/createPhoneInput.svelte.d.ts +46 -0
- package/dist/createPhoneInput.svelte.js +105 -0
- package/dist/createRadioGroup.svelte.d.ts +64 -0
- package/dist/createRadioGroup.svelte.js +73 -0
- package/dist/createRating.svelte.d.ts +70 -0
- package/dist/createRating.svelte.js +85 -0
- package/dist/createSlider.svelte.d.ts +71 -0
- package/dist/createSlider.svelte.js +123 -0
- package/dist/createSwitch.svelte.d.ts +43 -0
- package/dist/createSwitch.svelte.js +41 -0
- package/dist/createTabs.svelte.d.ts +78 -0
- package/dist/createTabs.svelte.js +92 -0
- package/dist/createTagsInput.svelte.d.ts +65 -0
- package/dist/createTagsInput.svelte.js +93 -0
- package/dist/createTimePicker.svelte.d.ts +106 -0
- package/dist/createTimePicker.svelte.js +248 -0
- package/dist/createToggle.svelte.d.ts +43 -0
- package/dist/createToggle.svelte.js +39 -0
- package/dist/createTree.svelte.d.ts +96 -0
- package/dist/createTree.svelte.js +194 -0
- package/dist/editor-contract.d.ts +48 -0
- package/dist/editor-contract.js +26 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +31 -0
- package/package.json +1 -1
- package/src/SvAutoComplete.svelte +23 -34
- package/src/SvCalendar.svelte +74 -334
- package/src/SvCheckBox.svelte +11 -10
- package/src/SvColorInput.svelte +31 -58
- package/src/SvComboBox.svelte +32 -54
- package/src/SvCountryInput.svelte +31 -29
- package/src/SvDateTimePicker.svelte +53 -110
- package/src/SvDropDownList.svelte +31 -54
- package/src/SvGauge.svelte +21 -23
- package/src/SvListBox.svelte +25 -58
- package/src/SvMaskedInput.svelte +0 -0
- package/src/SvNumberInput.svelte +0 -0
- package/src/SvPasswordInput.svelte +18 -28
- package/src/SvPhoneInput.svelte +0 -0
- package/src/SvRadioGroup.svelte +14 -37
- package/src/SvRating.svelte +22 -42
- package/src/SvSlider.svelte +30 -57
- package/src/SvSwitchButton.svelte +10 -17
- package/src/SvTabs.svelte +26 -39
- package/src/SvTagsInput.svelte +17 -28
- package/src/SvTimePicker.svelte +52 -210
- package/src/SvToggleButton.svelte +9 -9
- package/src/SvTree.svelte +30 -109
- package/src/createAutocomplete.svelte.ts +104 -0
- package/src/createCalendar.svelte.ts +488 -0
- package/src/createCheckbox.svelte.ts +74 -0
- package/src/createColorInput.svelte.ts +115 -0
- package/src/createCombobox.svelte.ts +158 -0
- package/src/createCountryInput.svelte.ts +126 -0
- package/src/createDateTimePicker.svelte.ts +216 -0
- package/src/createDropdownList.svelte.ts +125 -0
- package/src/createGauge.svelte.ts +86 -0
- package/src/createListbox.svelte.ts +186 -0
- package/src/createMaskedInput.svelte.ts +81 -0
- package/src/createNumberInput.svelte.ts +193 -0
- package/src/createPasswordInput.svelte.ts +87 -0
- package/src/createPhoneInput.svelte.ts +119 -0
- package/src/createRadioGroup.svelte.ts +132 -0
- package/src/createRating.svelte.ts +145 -0
- package/src/createSlider.svelte.ts +160 -0
- package/src/createSwitch.svelte.ts +78 -0
- package/src/createTabs.svelte.ts +129 -0
- package/src/createTagsInput.svelte.ts +98 -0
- package/src/createTimePicker.svelte.ts +289 -0
- package/src/createToggle.svelte.ts +69 -0
- package/src/createTree.svelte.ts +216 -0
- package/src/editor-contract.test.ts +32 -0
- package/src/editor-contract.ts +60 -0
- package/src/index.ts +39 -0
- package/src/ui-inputs.test.ts +26 -0
- package/src/ui-selection.test.ts +13 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createTree - the HEADLESS core behind <SvTree>, in the same spirit as
|
|
3
|
+
* `createSvGrid` for the grid: a runes-based state machine (expand/collapse,
|
|
4
|
+
* single-select highlight, cascading tri-state checkboxes, full keyboard) with
|
|
5
|
+
* **prop-getters** you spread onto YOUR OWN markup. The visible tree is
|
|
6
|
+
* flattened into `rows` so roving-focus keyboard nav stays a flat index walk.
|
|
7
|
+
*
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script lang="ts">
|
|
10
|
+
* import { createTree } from '@svgrid/grid'
|
|
11
|
+
* let selected = $state<string | null>(null)
|
|
12
|
+
* const tree = createTree({ nodes: () => nodes, selected: () => selected, onSelect: (id) => (selected = id) })
|
|
13
|
+
* </script>
|
|
14
|
+
* <div {...tree.treeProps()}>
|
|
15
|
+
* {#each tree.rows as row (row.node.id)}
|
|
16
|
+
* <div {...tree.itemProps(row)}>{row.node.label}</div>
|
|
17
|
+
* {/each}
|
|
18
|
+
* </div>
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* The styled <SvTree> is just one renderer over this core. The cascade math
|
|
22
|
+
* (`treeDescendantIds`, `treeCheckState`) is exported as pure functions.
|
|
23
|
+
*/
|
|
24
|
+
export type TreeNode = {
|
|
25
|
+
id: string
|
|
26
|
+
label: string
|
|
27
|
+
children?: TreeNode[]
|
|
28
|
+
disabled?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type CheckState = 'checked' | 'indeterminate' | 'unchecked'
|
|
32
|
+
|
|
33
|
+
/** A single visible (flattened) tree row. */
|
|
34
|
+
export type TreeRow = {
|
|
35
|
+
node: TreeNode
|
|
36
|
+
depth: number
|
|
37
|
+
hasChildren: boolean
|
|
38
|
+
open: boolean
|
|
39
|
+
parentId: string | null
|
|
40
|
+
index: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** All descendant ids of `node` (pure, depth-first). */
|
|
44
|
+
export function treeDescendantIds(node: TreeNode): string[] {
|
|
45
|
+
const ids: string[] = []
|
|
46
|
+
const walk = (n: TreeNode) => { for (const c of n.children ?? []) { ids.push(c.id); walk(c) } }
|
|
47
|
+
walk(node)
|
|
48
|
+
return ids
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Tri-state check status of `node` given the set of checked ids (pure). */
|
|
52
|
+
export function treeCheckState(node: TreeNode, checked: ReadonlySet<string>): CheckState {
|
|
53
|
+
if (!node.children?.length) return checked.has(node.id) ? 'checked' : 'unchecked'
|
|
54
|
+
const desc = treeDescendantIds(node)
|
|
55
|
+
const on = desc.filter((id) => checked.has(id)).length
|
|
56
|
+
if (on === 0 && !checked.has(node.id)) return 'unchecked'
|
|
57
|
+
if (on === desc.length) return 'checked'
|
|
58
|
+
return 'indeterminate'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function findNode(list: ReadonlyArray<TreeNode>, id: string): TreeNode | null {
|
|
62
|
+
for (const n of list) {
|
|
63
|
+
if (n.id === id) return n
|
|
64
|
+
if (n.children) { const f = findNode(n.children, id); if (f) return f }
|
|
65
|
+
}
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
70
|
+
export type TreeConfig = {
|
|
71
|
+
nodes: () => ReadonlyArray<TreeNode>
|
|
72
|
+
selected?: () => string | null
|
|
73
|
+
onSelect?: (id: string) => void
|
|
74
|
+
/** Seed the internally-managed expanded set (read once). */
|
|
75
|
+
expandedIds?: () => string[] | undefined
|
|
76
|
+
onToggle?: (id: string, expanded: boolean) => void
|
|
77
|
+
checkable?: () => boolean
|
|
78
|
+
checked?: () => string[]
|
|
79
|
+
onCheck?: (ids: string[]) => void
|
|
80
|
+
ariaLabel?: () => string | undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createTree(config: TreeConfig) {
|
|
84
|
+
const nodes = () => config.nodes()
|
|
85
|
+
const checkable = () => config.checkable?.() ?? false
|
|
86
|
+
const selected = () => config.selected?.() ?? null
|
|
87
|
+
|
|
88
|
+
// Internal expanded set, seeded from the prop once (then self-managed).
|
|
89
|
+
let expanded = $state<Set<string>>(new Set())
|
|
90
|
+
let seeded = false
|
|
91
|
+
$effect(() => {
|
|
92
|
+
if (!seeded) { seeded = true; const ids = config.expandedIds?.(); if (ids) expanded = new Set(ids) }
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const rows = $derived.by<TreeRow[]>(() => {
|
|
96
|
+
const out: TreeRow[] = []
|
|
97
|
+
const walk = (list: ReadonlyArray<TreeNode>, depth: number, parentId: string | null) => {
|
|
98
|
+
for (const node of list) {
|
|
99
|
+
const hasChildren = !!node.children?.length
|
|
100
|
+
const open = expanded.has(node.id)
|
|
101
|
+
out.push({ node, depth, hasChildren, open, parentId, index: out.length })
|
|
102
|
+
if (hasChildren && open) walk(node.children!, depth + 1, node.id)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
walk(nodes(), 0, null)
|
|
106
|
+
return out
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
let active = $state(0)
|
|
110
|
+
$effect(() => { if (active >= rows.length) active = Math.max(0, rows.length - 1) })
|
|
111
|
+
|
|
112
|
+
const checkedSet = $derived(new Set(config.checked?.() ?? []))
|
|
113
|
+
|
|
114
|
+
// Roving focus target; `focusTick` bumps only on keyboard moves.
|
|
115
|
+
let focusTick = $state(0)
|
|
116
|
+
function setActive(i: number, focus = false) { active = i; if (focus) focusTick++ }
|
|
117
|
+
|
|
118
|
+
const checkStateOf = (node: TreeNode): CheckState => treeCheckState(node, checkedSet)
|
|
119
|
+
const isExpanded = (id: string) => expanded.has(id)
|
|
120
|
+
const isSelected = (id: string) => id === selected()
|
|
121
|
+
|
|
122
|
+
function toggleExpand(node: TreeNode) {
|
|
123
|
+
if (!node.children?.length) return
|
|
124
|
+
const next = new Set(expanded)
|
|
125
|
+
const willOpen = !next.has(node.id)
|
|
126
|
+
willOpen ? next.add(node.id) : next.delete(node.id)
|
|
127
|
+
expanded = next
|
|
128
|
+
config.onToggle?.(node.id, willOpen)
|
|
129
|
+
}
|
|
130
|
+
const toggle = (id: string) => { const n = findNode(nodes(), id); if (n) toggleExpand(n) }
|
|
131
|
+
|
|
132
|
+
function select(node: TreeNode) {
|
|
133
|
+
if (node.disabled) return
|
|
134
|
+
config.onSelect?.(node.id)
|
|
135
|
+
}
|
|
136
|
+
const selectId = (id: string) => { const n = findNode(nodes(), id); if (n) select(n) }
|
|
137
|
+
|
|
138
|
+
function toggleCheck(node: TreeNode) {
|
|
139
|
+
if (node.disabled) return
|
|
140
|
+
const next = new Set(checkedSet)
|
|
141
|
+
const ids = [node.id, ...treeDescendantIds(node)]
|
|
142
|
+
const currentlyOn = checkStateOf(node) === 'checked'
|
|
143
|
+
for (const id of ids) currentlyOn ? next.delete(id) : next.add(id)
|
|
144
|
+
config.onCheck?.([...next])
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function onKeydown(e: KeyboardEvent) {
|
|
148
|
+
const item = rows[active]
|
|
149
|
+
if (!item) return
|
|
150
|
+
switch (e.key) {
|
|
151
|
+
case 'ArrowDown': e.preventDefault(); setActive(Math.min(active + 1, rows.length - 1), true); break
|
|
152
|
+
case 'ArrowUp': e.preventDefault(); setActive(Math.max(active - 1, 0), true); break
|
|
153
|
+
case 'ArrowRight':
|
|
154
|
+
e.preventDefault()
|
|
155
|
+
if (item.hasChildren && !item.open) toggleExpand(item.node)
|
|
156
|
+
else if (item.hasChildren && item.open) setActive(Math.min(active + 1, rows.length - 1), true)
|
|
157
|
+
break
|
|
158
|
+
case 'ArrowLeft':
|
|
159
|
+
e.preventDefault()
|
|
160
|
+
if (item.hasChildren && item.open) toggleExpand(item.node)
|
|
161
|
+
else if (item.parentId) { const pi = rows.findIndex((r) => r.node.id === item.parentId); if (pi >= 0) setActive(pi, true) }
|
|
162
|
+
break
|
|
163
|
+
case 'Enter': e.preventDefault(); select(item.node); break
|
|
164
|
+
case ' ': e.preventDefault(); checkable() ? toggleCheck(item.node) : select(item.node); break
|
|
165
|
+
case 'Home': e.preventDefault(); setActive(0, true); break
|
|
166
|
+
case 'End': e.preventDefault(); setActive(rows.length - 1, true); break
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
/** Internally-managed set of expanded node ids. */
|
|
172
|
+
get expanded() { return expanded },
|
|
173
|
+
/** Selected node id (single-select highlight). */
|
|
174
|
+
get selected() { return selected() },
|
|
175
|
+
/** Flattened list of visible rows. */
|
|
176
|
+
get rows() { return rows },
|
|
177
|
+
/** Roving-focus row index. */
|
|
178
|
+
get activeIndex() { return active },
|
|
179
|
+
/** Monotonic counter; changes only on keyboard navigation. */
|
|
180
|
+
get focusTick() { return focusTick },
|
|
181
|
+
isExpanded,
|
|
182
|
+
isSelected,
|
|
183
|
+
checkStateOf,
|
|
184
|
+
toggle,
|
|
185
|
+
toggleExpand,
|
|
186
|
+
select: selectId,
|
|
187
|
+
toggleCheck,
|
|
188
|
+
setActive,
|
|
189
|
+
onKeydown,
|
|
190
|
+
/** Spread onto the tree container element. */
|
|
191
|
+
treeProps: () => ({
|
|
192
|
+
role: 'tree' as const,
|
|
193
|
+
'aria-label': config.ariaLabel?.(),
|
|
194
|
+
'aria-multiselectable': checkable(),
|
|
195
|
+
}),
|
|
196
|
+
/** Spread onto the treeitem element for a flattened `row`. */
|
|
197
|
+
itemProps: (row: TreeRow) => {
|
|
198
|
+
const cs = checkable() ? checkStateOf(row.node) : 'unchecked'
|
|
199
|
+
const ariaChecked: 'true' | 'mixed' | 'false' | undefined =
|
|
200
|
+
checkable() ? (cs === 'checked' ? 'true' : cs === 'indeterminate' ? 'mixed' : 'false') : undefined
|
|
201
|
+
return {
|
|
202
|
+
role: 'treeitem' as const,
|
|
203
|
+
'aria-level': row.depth + 1,
|
|
204
|
+
'aria-selected': row.node.id === selected(),
|
|
205
|
+
'aria-expanded': row.hasChildren ? row.open : undefined,
|
|
206
|
+
'aria-checked': ariaChecked,
|
|
207
|
+
'data-row': row.index,
|
|
208
|
+
tabindex: row.index === active ? 0 : -1,
|
|
209
|
+
onclick: () => { setActive(row.index); select(row.node) },
|
|
210
|
+
onkeydown: onKeydown,
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export type Tree = ReturnType<typeof createTree>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { editorAria, editorErrorId } from './editor-contract'
|
|
3
|
+
|
|
4
|
+
describe('editor-contract', () => {
|
|
5
|
+
it('emits ARIA only for the flags that are set (undefined => attribute omitted)', () => {
|
|
6
|
+
expect(editorAria({})).toEqual({
|
|
7
|
+
'aria-invalid': undefined,
|
|
8
|
+
'aria-required': undefined,
|
|
9
|
+
'aria-describedby': undefined,
|
|
10
|
+
'aria-label': undefined,
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('wires invalid + required + describedby + label', () => {
|
|
15
|
+
const a = editorAria({ id: 'qty', invalid: true, required: true, error: 'Too big', ariaLabel: 'Quantity' })
|
|
16
|
+
expect(a['aria-invalid']).toBe('true')
|
|
17
|
+
expect(a['aria-required']).toBe('true')
|
|
18
|
+
expect(a['aria-describedby']).toBe('qty__error')
|
|
19
|
+
expect(a['aria-label']).toBe('Quantity')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('describedby is only set when there is an error AND an id', () => {
|
|
23
|
+
expect(editorAria({ id: 'qty', error: '' })['aria-describedby']).toBeUndefined()
|
|
24
|
+
expect(editorAria({ error: 'x' })['aria-describedby']).toBeUndefined() // no id
|
|
25
|
+
expect(editorAria({ id: 'qty', error: 'x' })['aria-describedby']).toBe('qty__error')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('editorErrorId', () => {
|
|
29
|
+
expect(editorErrorId('a')).toBe('a__error')
|
|
30
|
+
expect(editorErrorId(undefined)).toBeUndefined()
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared "editor contract" for the SvGrid UI kit.
|
|
3
|
+
*
|
|
4
|
+
* Every value-bearing editor (inputs, selects, pickers) accepts this common set
|
|
5
|
+
* of props so forms, validation and accessibility behave IDENTICALLY across the
|
|
6
|
+
* kit - the same spirit as the grid's headless core, applied to the editors.
|
|
7
|
+
* Components spread their own `value` / options / behavior on top of this base.
|
|
8
|
+
*
|
|
9
|
+
* Framework-free (no Svelte, no DOM) so it can be unit-tested and reused.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type EditorSize = 'sm' | 'md' | 'lg'
|
|
13
|
+
|
|
14
|
+
/** Props shared by every value-bearing editor in the kit. */
|
|
15
|
+
export type SvEditorProps = {
|
|
16
|
+
/** Disable interaction + form submission. */
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
/** Read-only: the value is shown but not editable. */
|
|
19
|
+
readonly?: boolean
|
|
20
|
+
/** Required for validation (adds `aria-required` + participates in validity). */
|
|
21
|
+
required?: boolean
|
|
22
|
+
/** Marks the control invalid (`aria-invalid` + error styling). */
|
|
23
|
+
invalid?: boolean
|
|
24
|
+
/** Error message; when set it is announced via `aria-describedby` and shown. */
|
|
25
|
+
error?: string
|
|
26
|
+
/** Control size / density. */
|
|
27
|
+
size?: EditorSize
|
|
28
|
+
/** Form field name; the editor emits a hidden input carrying its value. */
|
|
29
|
+
name?: string
|
|
30
|
+
/** Root/control element id (the error-text id derives from it). */
|
|
31
|
+
id?: string
|
|
32
|
+
/** Accessible name when there is no visible `<label>`. */
|
|
33
|
+
ariaLabel?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Stable DOM id for an editor's error text, for `aria-describedby` wiring. */
|
|
37
|
+
export const editorErrorId = (id?: string): string | undefined => (id ? `${id}__error` : undefined)
|
|
38
|
+
|
|
39
|
+
/** A subset of {@link SvEditorProps} that drives ARIA on the focusable control. */
|
|
40
|
+
export type EditorAriaState = Pick<SvEditorProps, 'id' | 'invalid' | 'required' | 'error' | 'ariaLabel'>
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* ARIA attributes for the focusable control of an editor, derived from its state.
|
|
44
|
+
* Spread onto the input/button/combobox:
|
|
45
|
+
* `<input {...editorAria({ id, invalid, required, error, ariaLabel })} />`
|
|
46
|
+
* Undefined values are omitted by Svelte, so unset props add no attributes.
|
|
47
|
+
*/
|
|
48
|
+
export function editorAria(state: EditorAriaState): {
|
|
49
|
+
'aria-invalid': 'true' | undefined
|
|
50
|
+
'aria-required': 'true' | undefined
|
|
51
|
+
'aria-describedby': string | undefined
|
|
52
|
+
'aria-label': string | undefined
|
|
53
|
+
} {
|
|
54
|
+
return {
|
|
55
|
+
'aria-invalid': state.invalid ? 'true' : undefined,
|
|
56
|
+
'aria-required': state.required ? 'true' : undefined,
|
|
57
|
+
'aria-describedby': state.error ? editorErrorId(state.id) : undefined,
|
|
58
|
+
'aria-label': state.ariaLabel,
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -73,6 +73,45 @@ export { default as SvGridDropdown } from './SvGridDropdown.svelte'
|
|
|
73
73
|
export { default as SvCalendar, type CalendarValue, type CalendarPreset, type CalendarAnimation } from './SvCalendar.svelte'
|
|
74
74
|
export { default as SvTimePicker, type TimeValue } from './SvTimePicker.svelte'
|
|
75
75
|
export { default as SvDateTimePicker, type DateTimeValue } from './SvDateTimePicker.svelte'
|
|
76
|
+
// UI kit - shared editor contract (common props + a11y wiring for every editor)
|
|
77
|
+
export { editorAria, editorErrorId, type SvEditorProps, type EditorSize, type EditorAriaState } from './editor-contract'
|
|
78
|
+
// UI kit - headless cores (state + prop-getters you render yourself, like createSvGrid)
|
|
79
|
+
export {
|
|
80
|
+
createListbox,
|
|
81
|
+
toSelectedArray,
|
|
82
|
+
type Listbox,
|
|
83
|
+
type ListboxConfig,
|
|
84
|
+
type ListboxValue,
|
|
85
|
+
type ListboxRootProps,
|
|
86
|
+
type OptionProps,
|
|
87
|
+
} from './createListbox.svelte'
|
|
88
|
+
// Selection family cores
|
|
89
|
+
export { createCombobox, type Combobox, type ComboboxConfig, type ComboboxValue } from './createCombobox.svelte'
|
|
90
|
+
export { createDropdownList, type DropdownList, type DropdownListConfig, type DropdownValue } from './createDropdownList.svelte'
|
|
91
|
+
export { createAutocomplete, type Autocomplete, type AutocompleteConfig } from './createAutocomplete.svelte'
|
|
92
|
+
export { createTagsInput, type TagsInput, type TagsInputConfig } from './createTagsInput.svelte'
|
|
93
|
+
export { createCountryInput, type CountryInput, type CountryInputConfig } from './createCountryInput.svelte'
|
|
94
|
+
// Text-input family cores
|
|
95
|
+
export { createNumberInput, type NumberInputConfig, type NumberInputCore } from './createNumberInput.svelte'
|
|
96
|
+
export { createMaskedInput, type MaskedInputConfig, type MaskedInputCore } from './createMaskedInput.svelte'
|
|
97
|
+
export { createPhoneInput, type PhoneInputConfig, type PhoneInputCore } from './createPhoneInput.svelte'
|
|
98
|
+
export { createColorInput, normalizeHex, type ColorInputConfig, type ColorInputCore } from './createColorInput.svelte'
|
|
99
|
+
export { createPasswordInput, passwordStrength, STRENGTH_LABELS, type PasswordInputConfig, type PasswordInputCore } from './createPasswordInput.svelte'
|
|
100
|
+
// Buttons & toggles family cores
|
|
101
|
+
export { createToggle, type Toggle, type ToggleConfig, type ToggleButtonProps } from './createToggle.svelte'
|
|
102
|
+
export { createSwitch, type Switch, type SwitchConfig, type SwitchProps } from './createSwitch.svelte'
|
|
103
|
+
export { createCheckbox, type Checkbox, type CheckboxConfig, type CheckboxBoxProps } from './createCheckbox.svelte'
|
|
104
|
+
export { createRadioGroup, type RadioGroup, type RadioGroupConfig, type RadioProps, type RadioGroupProps } from './createRadioGroup.svelte'
|
|
105
|
+
export { createRating, type Rating, type RatingConfig, type RatingFill, type RatingRootProps, type RatingStarProps } from './createRating.svelte'
|
|
106
|
+
// Date & time family cores
|
|
107
|
+
export { createCalendar, normalizeCalendarValue, type Calendar, type CalendarConfig, type CalendarDayState, type CalendarNavDir, type CalendarNameFormat, type DisplayMode } from './createCalendar.svelte'
|
|
108
|
+
export { createTimePicker, parseTimeValue, type TimePicker, type TimePickerConfig, type TimeSelection, type TimeFormat, type DialTick } from './createTimePicker.svelte'
|
|
109
|
+
export { createDateTimePicker, type DateTimePicker, type DateTimePickerConfig, type DropDownDisplayMode, type DateTimeTab } from './createDateTimePicker.svelte'
|
|
110
|
+
// Layout & range family cores
|
|
111
|
+
export { createTabs, type Tabs, type TabsConfig, type TabsOrientation, type TabsActivation } from './createTabs.svelte'
|
|
112
|
+
export { createTree, treeDescendantIds, treeCheckState, type Tree, type TreeConfig, type TreeRow, type CheckState } from './createTree.svelte'
|
|
113
|
+
export { createSlider, type Slider, type SliderConfig, type SliderValue, type SliderOrientation, type SliderThumb, type SliderPoint } from './createSlider.svelte'
|
|
114
|
+
export { createGauge, type Gauge, type GaugeConfig, type GaugePoint } from './createGauge.svelte'
|
|
76
115
|
// UI kit - Group B (buttons & toggles)
|
|
77
116
|
export { default as SvButton } from './SvButton.svelte'
|
|
78
117
|
export { default as SvRepeatButton } from './SvRepeatButton.svelte'
|
package/src/ui-inputs.test.ts
CHANGED
|
@@ -14,6 +14,32 @@ function mnt(Comp: any, props: Record<string, unknown>) {
|
|
|
14
14
|
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
describe('SvNumberInput validation a11y (editor contract)', () => {
|
|
18
|
+
it('wires aria-invalid + aria-required + aria-describedby -> error text', () => {
|
|
19
|
+
const { target, destroy } = mnt(SvNumberInput, { value: 5, id: 'qty', invalid: true, required: true, error: 'Too big' })
|
|
20
|
+
try {
|
|
21
|
+
const input = target.querySelector<HTMLInputElement>('.sv-num__input')!
|
|
22
|
+
expect(input.getAttribute('aria-invalid')).toBe('true')
|
|
23
|
+
expect(input.getAttribute('aria-required')).toBe('true')
|
|
24
|
+
expect(input.getAttribute('aria-describedby')).toBe('qty__error')
|
|
25
|
+
const err = target.querySelector('#qty__error')!
|
|
26
|
+
expect(err.getAttribute('role')).toBe('alert')
|
|
27
|
+
expect(err.textContent).toBe('Too big')
|
|
28
|
+
expect(target.querySelector('.sv-num.is-invalid')).toBeTruthy()
|
|
29
|
+
} finally { destroy() }
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('adds no validation attributes when the props are unset', () => {
|
|
33
|
+
const { target, destroy } = mnt(SvNumberInput, { value: 1 })
|
|
34
|
+
try {
|
|
35
|
+
const input = target.querySelector<HTMLInputElement>('.sv-num__input')!
|
|
36
|
+
expect(input.getAttribute('aria-invalid')).toBeNull()
|
|
37
|
+
expect(input.getAttribute('aria-describedby')).toBeNull()
|
|
38
|
+
expect(target.querySelector('.sv-num.is-invalid')).toBeNull()
|
|
39
|
+
} finally { destroy() }
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
17
43
|
describe('SvNumberInput', () => {
|
|
18
44
|
it('clamps to max on commit', () => {
|
|
19
45
|
let got: number | null | undefined
|
package/src/ui-selection.test.ts
CHANGED
|
@@ -39,6 +39,19 @@ describe('SvListBox', () => {
|
|
|
39
39
|
expect(got).toEqual(['a', 'b'])
|
|
40
40
|
} finally { b.destroy() }
|
|
41
41
|
})
|
|
42
|
+
|
|
43
|
+
it('posts its value(s) via hidden inputs when `name` is set (multi -> repeated name)', () => {
|
|
44
|
+
const m = mnt(SvListBox, { options: OPTS, value: ['a', 'c'], multiple: true, name: 'tags' })
|
|
45
|
+
try {
|
|
46
|
+
const hidden = Array.from(m.target.querySelectorAll<HTMLInputElement>('input[type="hidden"][name="tags"]'))
|
|
47
|
+
expect(hidden.map((h) => h.value)).toEqual(['a', 'c'])
|
|
48
|
+
} finally { m.destroy() }
|
|
49
|
+
const s = mnt(SvListBox, { options: OPTS, value: 'b', name: 'choice' })
|
|
50
|
+
try {
|
|
51
|
+
const hidden = Array.from(s.target.querySelectorAll<HTMLInputElement>('input[type="hidden"][name="choice"]'))
|
|
52
|
+
expect(hidden.map((h) => h.value)).toEqual(['b'])
|
|
53
|
+
} finally { s.destroy() }
|
|
54
|
+
})
|
|
42
55
|
})
|
|
43
56
|
|
|
44
57
|
describe('SvDropDownList', () => {
|