@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,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createToggle - the HEADLESS core behind <SvToggleButton>, in the same spirit
|
|
3
|
+
* as `createSvGrid`: a tiny controlled state machine (a single pressed on/off
|
|
4
|
+
* bit + ARIA) exposed as **prop-getters** you spread onto YOUR OWN markup. No
|
|
5
|
+
* styles, no DOM assumptions.
|
|
6
|
+
*
|
|
7
|
+
* ```svelte
|
|
8
|
+
* <script lang="ts">
|
|
9
|
+
* import { createToggle } from '@svgrid/grid'
|
|
10
|
+
* let pressed = $state(false)
|
|
11
|
+
* const t = createToggle({ pressed: () => pressed, onChange: (v) => (pressed = v) })
|
|
12
|
+
* </script>
|
|
13
|
+
* <button {...t.buttonProps()}>Bold</button>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The styled <SvToggleButton> is just one renderer over this core.
|
|
17
|
+
*/
|
|
18
|
+
/** Reactive inputs are passed as getters so the core tracks live prop changes
|
|
19
|
+
* (the same controlled pattern used across the SvGrid UI kit). */
|
|
20
|
+
export type ToggleConfig = {
|
|
21
|
+
pressed: () => boolean;
|
|
22
|
+
onChange?: (pressed: boolean) => void;
|
|
23
|
+
disabled?: () => boolean;
|
|
24
|
+
ariaLabel?: () => string | undefined;
|
|
25
|
+
};
|
|
26
|
+
export type ToggleButtonProps = {
|
|
27
|
+
type: 'button';
|
|
28
|
+
'aria-pressed': boolean;
|
|
29
|
+
'aria-label': string | undefined;
|
|
30
|
+
disabled: boolean;
|
|
31
|
+
'data-pressed': '' | undefined;
|
|
32
|
+
onclick: () => void;
|
|
33
|
+
};
|
|
34
|
+
export type Toggle = {
|
|
35
|
+
/** Current pressed state. */
|
|
36
|
+
readonly pressed: boolean;
|
|
37
|
+
readonly disabled: boolean;
|
|
38
|
+
/** Flip pressed (no-op when disabled). */
|
|
39
|
+
toggle: () => void;
|
|
40
|
+
/** Spread onto the button element. */
|
|
41
|
+
buttonProps: () => ToggleButtonProps;
|
|
42
|
+
};
|
|
43
|
+
export declare function createToggle(config: ToggleConfig): Toggle;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createToggle - the HEADLESS core behind <SvToggleButton>, in the same spirit
|
|
3
|
+
* as `createSvGrid`: a tiny controlled state machine (a single pressed on/off
|
|
4
|
+
* bit + ARIA) exposed as **prop-getters** you spread onto YOUR OWN markup. No
|
|
5
|
+
* styles, no DOM assumptions.
|
|
6
|
+
*
|
|
7
|
+
* ```svelte
|
|
8
|
+
* <script lang="ts">
|
|
9
|
+
* import { createToggle } from '@svgrid/grid'
|
|
10
|
+
* let pressed = $state(false)
|
|
11
|
+
* const t = createToggle({ pressed: () => pressed, onChange: (v) => (pressed = v) })
|
|
12
|
+
* </script>
|
|
13
|
+
* <button {...t.buttonProps()}>Bold</button>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The styled <SvToggleButton> is just one renderer over this core.
|
|
17
|
+
*/
|
|
18
|
+
export function createToggle(config) {
|
|
19
|
+
const pressed = () => config.pressed();
|
|
20
|
+
const disabled = () => config.disabled?.() ?? false;
|
|
21
|
+
function toggle() {
|
|
22
|
+
if (disabled())
|
|
23
|
+
return;
|
|
24
|
+
config.onChange?.(!pressed());
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
get pressed() { return pressed(); },
|
|
28
|
+
get disabled() { return disabled(); },
|
|
29
|
+
toggle,
|
|
30
|
+
buttonProps: () => ({
|
|
31
|
+
type: 'button',
|
|
32
|
+
'aria-pressed': pressed(),
|
|
33
|
+
'aria-label': config.ariaLabel?.(),
|
|
34
|
+
disabled: disabled(),
|
|
35
|
+
'data-pressed': pressed() ? '' : undefined,
|
|
36
|
+
onclick: toggle,
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
export type CheckState = 'checked' | 'indeterminate' | 'unchecked';
|
|
31
|
+
/** A single visible (flattened) tree row. */
|
|
32
|
+
export type TreeRow = {
|
|
33
|
+
node: TreeNode;
|
|
34
|
+
depth: number;
|
|
35
|
+
hasChildren: boolean;
|
|
36
|
+
open: boolean;
|
|
37
|
+
parentId: string | null;
|
|
38
|
+
index: number;
|
|
39
|
+
};
|
|
40
|
+
/** All descendant ids of `node` (pure, depth-first). */
|
|
41
|
+
export declare function treeDescendantIds(node: TreeNode): string[];
|
|
42
|
+
/** Tri-state check status of `node` given the set of checked ids (pure). */
|
|
43
|
+
export declare function treeCheckState(node: TreeNode, checked: ReadonlySet<string>): CheckState;
|
|
44
|
+
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
45
|
+
export type TreeConfig = {
|
|
46
|
+
nodes: () => ReadonlyArray<TreeNode>;
|
|
47
|
+
selected?: () => string | null;
|
|
48
|
+
onSelect?: (id: string) => void;
|
|
49
|
+
/** Seed the internally-managed expanded set (read once). */
|
|
50
|
+
expandedIds?: () => string[] | undefined;
|
|
51
|
+
onToggle?: (id: string, expanded: boolean) => void;
|
|
52
|
+
checkable?: () => boolean;
|
|
53
|
+
checked?: () => string[];
|
|
54
|
+
onCheck?: (ids: string[]) => void;
|
|
55
|
+
ariaLabel?: () => string | undefined;
|
|
56
|
+
};
|
|
57
|
+
export declare function createTree(config: TreeConfig): {
|
|
58
|
+
/** Internally-managed set of expanded node ids. */
|
|
59
|
+
readonly expanded: Set<string>;
|
|
60
|
+
/** Selected node id (single-select highlight). */
|
|
61
|
+
readonly selected: string | null;
|
|
62
|
+
/** Flattened list of visible rows. */
|
|
63
|
+
readonly rows: TreeRow[];
|
|
64
|
+
/** Roving-focus row index. */
|
|
65
|
+
readonly activeIndex: number;
|
|
66
|
+
/** Monotonic counter; changes only on keyboard navigation. */
|
|
67
|
+
readonly focusTick: number;
|
|
68
|
+
isExpanded: (id: string) => boolean;
|
|
69
|
+
isSelected: (id: string) => boolean;
|
|
70
|
+
checkStateOf: (node: TreeNode) => CheckState;
|
|
71
|
+
toggle: (id: string) => void;
|
|
72
|
+
toggleExpand: (node: TreeNode) => void;
|
|
73
|
+
select: (id: string) => void;
|
|
74
|
+
toggleCheck: (node: TreeNode) => void;
|
|
75
|
+
setActive: (i: number, focus?: boolean) => void;
|
|
76
|
+
onKeydown: (e: KeyboardEvent) => void;
|
|
77
|
+
/** Spread onto the tree container element. */
|
|
78
|
+
treeProps: () => {
|
|
79
|
+
role: "tree";
|
|
80
|
+
'aria-label': string | undefined;
|
|
81
|
+
'aria-multiselectable': boolean;
|
|
82
|
+
};
|
|
83
|
+
/** Spread onto the treeitem element for a flattened `row`. */
|
|
84
|
+
itemProps: (row: TreeRow) => {
|
|
85
|
+
role: "treeitem";
|
|
86
|
+
'aria-level': number;
|
|
87
|
+
'aria-selected': boolean;
|
|
88
|
+
'aria-expanded': boolean | undefined;
|
|
89
|
+
'aria-checked': "true" | "mixed" | "false" | undefined;
|
|
90
|
+
'data-row': number;
|
|
91
|
+
tabindex: number;
|
|
92
|
+
onclick: () => void;
|
|
93
|
+
onkeydown: (e: KeyboardEvent) => void;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
export type Tree = ReturnType<typeof createTree>;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/** All descendant ids of `node` (pure, depth-first). */
|
|
2
|
+
export function treeDescendantIds(node) {
|
|
3
|
+
const ids = [];
|
|
4
|
+
const walk = (n) => { for (const c of n.children ?? []) {
|
|
5
|
+
ids.push(c.id);
|
|
6
|
+
walk(c);
|
|
7
|
+
} };
|
|
8
|
+
walk(node);
|
|
9
|
+
return ids;
|
|
10
|
+
}
|
|
11
|
+
/** Tri-state check status of `node` given the set of checked ids (pure). */
|
|
12
|
+
export function treeCheckState(node, checked) {
|
|
13
|
+
if (!node.children?.length)
|
|
14
|
+
return checked.has(node.id) ? 'checked' : 'unchecked';
|
|
15
|
+
const desc = treeDescendantIds(node);
|
|
16
|
+
const on = desc.filter((id) => checked.has(id)).length;
|
|
17
|
+
if (on === 0 && !checked.has(node.id))
|
|
18
|
+
return 'unchecked';
|
|
19
|
+
if (on === desc.length)
|
|
20
|
+
return 'checked';
|
|
21
|
+
return 'indeterminate';
|
|
22
|
+
}
|
|
23
|
+
function findNode(list, id) {
|
|
24
|
+
for (const n of list) {
|
|
25
|
+
if (n.id === id)
|
|
26
|
+
return n;
|
|
27
|
+
if (n.children) {
|
|
28
|
+
const f = findNode(n.children, id);
|
|
29
|
+
if (f)
|
|
30
|
+
return f;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
export function createTree(config) {
|
|
36
|
+
const nodes = () => config.nodes();
|
|
37
|
+
const checkable = () => config.checkable?.() ?? false;
|
|
38
|
+
const selected = () => config.selected?.() ?? null;
|
|
39
|
+
// Internal expanded set, seeded from the prop once (then self-managed).
|
|
40
|
+
let expanded = $state(new Set());
|
|
41
|
+
let seeded = false;
|
|
42
|
+
$effect(() => {
|
|
43
|
+
if (!seeded) {
|
|
44
|
+
seeded = true;
|
|
45
|
+
const ids = config.expandedIds?.();
|
|
46
|
+
if (ids)
|
|
47
|
+
expanded = new Set(ids);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const rows = $derived.by(() => {
|
|
51
|
+
const out = [];
|
|
52
|
+
const walk = (list, depth, parentId) => {
|
|
53
|
+
for (const node of list) {
|
|
54
|
+
const hasChildren = !!node.children?.length;
|
|
55
|
+
const open = expanded.has(node.id);
|
|
56
|
+
out.push({ node, depth, hasChildren, open, parentId, index: out.length });
|
|
57
|
+
if (hasChildren && open)
|
|
58
|
+
walk(node.children, depth + 1, node.id);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
walk(nodes(), 0, null);
|
|
62
|
+
return out;
|
|
63
|
+
});
|
|
64
|
+
let active = $state(0);
|
|
65
|
+
$effect(() => { if (active >= rows.length)
|
|
66
|
+
active = Math.max(0, rows.length - 1); });
|
|
67
|
+
const checkedSet = $derived(new Set(config.checked?.() ?? []));
|
|
68
|
+
// Roving focus target; `focusTick` bumps only on keyboard moves.
|
|
69
|
+
let focusTick = $state(0);
|
|
70
|
+
function setActive(i, focus = false) { active = i; if (focus)
|
|
71
|
+
focusTick++; }
|
|
72
|
+
const checkStateOf = (node) => treeCheckState(node, checkedSet);
|
|
73
|
+
const isExpanded = (id) => expanded.has(id);
|
|
74
|
+
const isSelected = (id) => id === selected();
|
|
75
|
+
function toggleExpand(node) {
|
|
76
|
+
if (!node.children?.length)
|
|
77
|
+
return;
|
|
78
|
+
const next = new Set(expanded);
|
|
79
|
+
const willOpen = !next.has(node.id);
|
|
80
|
+
willOpen ? next.add(node.id) : next.delete(node.id);
|
|
81
|
+
expanded = next;
|
|
82
|
+
config.onToggle?.(node.id, willOpen);
|
|
83
|
+
}
|
|
84
|
+
const toggle = (id) => { const n = findNode(nodes(), id); if (n)
|
|
85
|
+
toggleExpand(n); };
|
|
86
|
+
function select(node) {
|
|
87
|
+
if (node.disabled)
|
|
88
|
+
return;
|
|
89
|
+
config.onSelect?.(node.id);
|
|
90
|
+
}
|
|
91
|
+
const selectId = (id) => { const n = findNode(nodes(), id); if (n)
|
|
92
|
+
select(n); };
|
|
93
|
+
function toggleCheck(node) {
|
|
94
|
+
if (node.disabled)
|
|
95
|
+
return;
|
|
96
|
+
const next = new Set(checkedSet);
|
|
97
|
+
const ids = [node.id, ...treeDescendantIds(node)];
|
|
98
|
+
const currentlyOn = checkStateOf(node) === 'checked';
|
|
99
|
+
for (const id of ids)
|
|
100
|
+
currentlyOn ? next.delete(id) : next.add(id);
|
|
101
|
+
config.onCheck?.([...next]);
|
|
102
|
+
}
|
|
103
|
+
function onKeydown(e) {
|
|
104
|
+
const item = rows[active];
|
|
105
|
+
if (!item)
|
|
106
|
+
return;
|
|
107
|
+
switch (e.key) {
|
|
108
|
+
case 'ArrowDown':
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
setActive(Math.min(active + 1, rows.length - 1), true);
|
|
111
|
+
break;
|
|
112
|
+
case 'ArrowUp':
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
setActive(Math.max(active - 1, 0), true);
|
|
115
|
+
break;
|
|
116
|
+
case 'ArrowRight':
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
if (item.hasChildren && !item.open)
|
|
119
|
+
toggleExpand(item.node);
|
|
120
|
+
else if (item.hasChildren && item.open)
|
|
121
|
+
setActive(Math.min(active + 1, rows.length - 1), true);
|
|
122
|
+
break;
|
|
123
|
+
case 'ArrowLeft':
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
if (item.hasChildren && item.open)
|
|
126
|
+
toggleExpand(item.node);
|
|
127
|
+
else if (item.parentId) {
|
|
128
|
+
const pi = rows.findIndex((r) => r.node.id === item.parentId);
|
|
129
|
+
if (pi >= 0)
|
|
130
|
+
setActive(pi, true);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
case 'Enter':
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
select(item.node);
|
|
136
|
+
break;
|
|
137
|
+
case ' ':
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
checkable() ? toggleCheck(item.node) : select(item.node);
|
|
140
|
+
break;
|
|
141
|
+
case 'Home':
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
setActive(0, true);
|
|
144
|
+
break;
|
|
145
|
+
case 'End':
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
setActive(rows.length - 1, true);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
/** Internally-managed set of expanded node ids. */
|
|
153
|
+
get expanded() { return expanded; },
|
|
154
|
+
/** Selected node id (single-select highlight). */
|
|
155
|
+
get selected() { return selected(); },
|
|
156
|
+
/** Flattened list of visible rows. */
|
|
157
|
+
get rows() { return rows; },
|
|
158
|
+
/** Roving-focus row index. */
|
|
159
|
+
get activeIndex() { return active; },
|
|
160
|
+
/** Monotonic counter; changes only on keyboard navigation. */
|
|
161
|
+
get focusTick() { return focusTick; },
|
|
162
|
+
isExpanded,
|
|
163
|
+
isSelected,
|
|
164
|
+
checkStateOf,
|
|
165
|
+
toggle,
|
|
166
|
+
toggleExpand,
|
|
167
|
+
select: selectId,
|
|
168
|
+
toggleCheck,
|
|
169
|
+
setActive,
|
|
170
|
+
onKeydown,
|
|
171
|
+
/** Spread onto the tree container element. */
|
|
172
|
+
treeProps: () => ({
|
|
173
|
+
role: 'tree',
|
|
174
|
+
'aria-label': config.ariaLabel?.(),
|
|
175
|
+
'aria-multiselectable': checkable(),
|
|
176
|
+
}),
|
|
177
|
+
/** Spread onto the treeitem element for a flattened `row`. */
|
|
178
|
+
itemProps: (row) => {
|
|
179
|
+
const cs = checkable() ? checkStateOf(row.node) : 'unchecked';
|
|
180
|
+
const ariaChecked = checkable() ? (cs === 'checked' ? 'true' : cs === 'indeterminate' ? 'mixed' : 'false') : undefined;
|
|
181
|
+
return {
|
|
182
|
+
role: 'treeitem',
|
|
183
|
+
'aria-level': row.depth + 1,
|
|
184
|
+
'aria-selected': row.node.id === selected(),
|
|
185
|
+
'aria-expanded': row.hasChildren ? row.open : undefined,
|
|
186
|
+
'aria-checked': ariaChecked,
|
|
187
|
+
'data-row': row.index,
|
|
188
|
+
tabindex: row.index === active ? 0 : -1,
|
|
189
|
+
onclick: () => { setActive(row.index); select(row.node); },
|
|
190
|
+
onkeydown: onKeydown,
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
export type EditorSize = 'sm' | 'md' | 'lg';
|
|
12
|
+
/** Props shared by every value-bearing editor in the kit. */
|
|
13
|
+
export type SvEditorProps = {
|
|
14
|
+
/** Disable interaction + form submission. */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Read-only: the value is shown but not editable. */
|
|
17
|
+
readonly?: boolean;
|
|
18
|
+
/** Required for validation (adds `aria-required` + participates in validity). */
|
|
19
|
+
required?: boolean;
|
|
20
|
+
/** Marks the control invalid (`aria-invalid` + error styling). */
|
|
21
|
+
invalid?: boolean;
|
|
22
|
+
/** Error message; when set it is announced via `aria-describedby` and shown. */
|
|
23
|
+
error?: string;
|
|
24
|
+
/** Control size / density. */
|
|
25
|
+
size?: EditorSize;
|
|
26
|
+
/** Form field name; the editor emits a hidden input carrying its value. */
|
|
27
|
+
name?: string;
|
|
28
|
+
/** Root/control element id (the error-text id derives from it). */
|
|
29
|
+
id?: string;
|
|
30
|
+
/** Accessible name when there is no visible `<label>`. */
|
|
31
|
+
ariaLabel?: string;
|
|
32
|
+
};
|
|
33
|
+
/** Stable DOM id for an editor's error text, for `aria-describedby` wiring. */
|
|
34
|
+
export declare const editorErrorId: (id?: string) => string | undefined;
|
|
35
|
+
/** A subset of {@link SvEditorProps} that drives ARIA on the focusable control. */
|
|
36
|
+
export type EditorAriaState = Pick<SvEditorProps, 'id' | 'invalid' | 'required' | 'error' | 'ariaLabel'>;
|
|
37
|
+
/**
|
|
38
|
+
* ARIA attributes for the focusable control of an editor, derived from its state.
|
|
39
|
+
* Spread onto the input/button/combobox:
|
|
40
|
+
* `<input {...editorAria({ id, invalid, required, error, ariaLabel })} />`
|
|
41
|
+
* Undefined values are omitted by Svelte, so unset props add no attributes.
|
|
42
|
+
*/
|
|
43
|
+
export declare function editorAria(state: EditorAriaState): {
|
|
44
|
+
'aria-invalid': 'true' | undefined;
|
|
45
|
+
'aria-required': 'true' | undefined;
|
|
46
|
+
'aria-describedby': string | undefined;
|
|
47
|
+
'aria-label': string | undefined;
|
|
48
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
/** Stable DOM id for an editor's error text, for `aria-describedby` wiring. */
|
|
12
|
+
export const editorErrorId = (id) => (id ? `${id}__error` : undefined);
|
|
13
|
+
/**
|
|
14
|
+
* ARIA attributes for the focusable control of an editor, derived from its state.
|
|
15
|
+
* Spread onto the input/button/combobox:
|
|
16
|
+
* `<input {...editorAria({ id, invalid, required, error, ariaLabel })} />`
|
|
17
|
+
* Undefined values are omitted by Svelte, so unset props add no attributes.
|
|
18
|
+
*/
|
|
19
|
+
export function editorAria(state) {
|
|
20
|
+
return {
|
|
21
|
+
'aria-invalid': state.invalid ? 'true' : undefined,
|
|
22
|
+
'aria-required': state.required ? 'true' : undefined,
|
|
23
|
+
'aria-describedby': state.error ? editorErrorId(state.id) : undefined,
|
|
24
|
+
'aria-label': state.ariaLabel,
|
|
25
|
+
};
|
|
26
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,30 @@ export { default as SvGridDropdown } from './SvGridDropdown.svelte';
|
|
|
9
9
|
export { default as SvCalendar, type CalendarValue, type CalendarPreset, type CalendarAnimation } from './SvCalendar.svelte';
|
|
10
10
|
export { default as SvTimePicker, type TimeValue } from './SvTimePicker.svelte';
|
|
11
11
|
export { default as SvDateTimePicker, type DateTimeValue } from './SvDateTimePicker.svelte';
|
|
12
|
+
export { editorAria, editorErrorId, type SvEditorProps, type EditorSize, type EditorAriaState } from './editor-contract';
|
|
13
|
+
export { createListbox, toSelectedArray, type Listbox, type ListboxConfig, type ListboxValue, type ListboxRootProps, type OptionProps, } from './createListbox.svelte';
|
|
14
|
+
export { createCombobox, type Combobox, type ComboboxConfig, type ComboboxValue } from './createCombobox.svelte';
|
|
15
|
+
export { createDropdownList, type DropdownList, type DropdownListConfig, type DropdownValue } from './createDropdownList.svelte';
|
|
16
|
+
export { createAutocomplete, type Autocomplete, type AutocompleteConfig } from './createAutocomplete.svelte';
|
|
17
|
+
export { createTagsInput, type TagsInput, type TagsInputConfig } from './createTagsInput.svelte';
|
|
18
|
+
export { createCountryInput, type CountryInput, type CountryInputConfig } from './createCountryInput.svelte';
|
|
19
|
+
export { createNumberInput, type NumberInputConfig, type NumberInputCore } from './createNumberInput.svelte';
|
|
20
|
+
export { createMaskedInput, type MaskedInputConfig, type MaskedInputCore } from './createMaskedInput.svelte';
|
|
21
|
+
export { createPhoneInput, type PhoneInputConfig, type PhoneInputCore } from './createPhoneInput.svelte';
|
|
22
|
+
export { createColorInput, normalizeHex, type ColorInputConfig, type ColorInputCore } from './createColorInput.svelte';
|
|
23
|
+
export { createPasswordInput, passwordStrength, STRENGTH_LABELS, type PasswordInputConfig, type PasswordInputCore } from './createPasswordInput.svelte';
|
|
24
|
+
export { createToggle, type Toggle, type ToggleConfig, type ToggleButtonProps } from './createToggle.svelte';
|
|
25
|
+
export { createSwitch, type Switch, type SwitchConfig, type SwitchProps } from './createSwitch.svelte';
|
|
26
|
+
export { createCheckbox, type Checkbox, type CheckboxConfig, type CheckboxBoxProps } from './createCheckbox.svelte';
|
|
27
|
+
export { createRadioGroup, type RadioGroup, type RadioGroupConfig, type RadioProps, type RadioGroupProps } from './createRadioGroup.svelte';
|
|
28
|
+
export { createRating, type Rating, type RatingConfig, type RatingFill, type RatingRootProps, type RatingStarProps } from './createRating.svelte';
|
|
29
|
+
export { createCalendar, normalizeCalendarValue, type Calendar, type CalendarConfig, type CalendarDayState, type CalendarNavDir, type CalendarNameFormat, type DisplayMode } from './createCalendar.svelte';
|
|
30
|
+
export { createTimePicker, parseTimeValue, type TimePicker, type TimePickerConfig, type TimeSelection, type TimeFormat, type DialTick } from './createTimePicker.svelte';
|
|
31
|
+
export { createDateTimePicker, type DateTimePicker, type DateTimePickerConfig, type DropDownDisplayMode, type DateTimeTab } from './createDateTimePicker.svelte';
|
|
32
|
+
export { createTabs, type Tabs, type TabsConfig, type TabsOrientation, type TabsActivation } from './createTabs.svelte';
|
|
33
|
+
export { createTree, treeDescendantIds, treeCheckState, type Tree, type TreeConfig, type TreeRow, type CheckState } from './createTree.svelte';
|
|
34
|
+
export { createSlider, type Slider, type SliderConfig, type SliderValue, type SliderOrientation, type SliderThumb, type SliderPoint } from './createSlider.svelte';
|
|
35
|
+
export { createGauge, type Gauge, type GaugeConfig, type GaugePoint } from './createGauge.svelte';
|
|
12
36
|
export { default as SvButton } from './SvButton.svelte';
|
|
13
37
|
export { default as SvRepeatButton } from './SvRepeatButton.svelte';
|
|
14
38
|
export { default as SvToggleButton } from './SvToggleButton.svelte';
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,37 @@ export { default as SvGridDropdown } from './SvGridDropdown.svelte';
|
|
|
9
9
|
export { default as SvCalendar } from './SvCalendar.svelte';
|
|
10
10
|
export { default as SvTimePicker } from './SvTimePicker.svelte';
|
|
11
11
|
export { default as SvDateTimePicker } from './SvDateTimePicker.svelte';
|
|
12
|
+
// UI kit - shared editor contract (common props + a11y wiring for every editor)
|
|
13
|
+
export { editorAria, editorErrorId } from './editor-contract';
|
|
14
|
+
// UI kit - headless cores (state + prop-getters you render yourself, like createSvGrid)
|
|
15
|
+
export { createListbox, toSelectedArray, } from './createListbox.svelte';
|
|
16
|
+
// Selection family cores
|
|
17
|
+
export { createCombobox } from './createCombobox.svelte';
|
|
18
|
+
export { createDropdownList } from './createDropdownList.svelte';
|
|
19
|
+
export { createAutocomplete } from './createAutocomplete.svelte';
|
|
20
|
+
export { createTagsInput } from './createTagsInput.svelte';
|
|
21
|
+
export { createCountryInput } from './createCountryInput.svelte';
|
|
22
|
+
// Text-input family cores
|
|
23
|
+
export { createNumberInput } from './createNumberInput.svelte';
|
|
24
|
+
export { createMaskedInput } from './createMaskedInput.svelte';
|
|
25
|
+
export { createPhoneInput } from './createPhoneInput.svelte';
|
|
26
|
+
export { createColorInput, normalizeHex } from './createColorInput.svelte';
|
|
27
|
+
export { createPasswordInput, passwordStrength, STRENGTH_LABELS } from './createPasswordInput.svelte';
|
|
28
|
+
// Buttons & toggles family cores
|
|
29
|
+
export { createToggle } from './createToggle.svelte';
|
|
30
|
+
export { createSwitch } from './createSwitch.svelte';
|
|
31
|
+
export { createCheckbox } from './createCheckbox.svelte';
|
|
32
|
+
export { createRadioGroup } from './createRadioGroup.svelte';
|
|
33
|
+
export { createRating } from './createRating.svelte';
|
|
34
|
+
// Date & time family cores
|
|
35
|
+
export { createCalendar, normalizeCalendarValue } from './createCalendar.svelte';
|
|
36
|
+
export { createTimePicker, parseTimeValue } from './createTimePicker.svelte';
|
|
37
|
+
export { createDateTimePicker } from './createDateTimePicker.svelte';
|
|
38
|
+
// Layout & range family cores
|
|
39
|
+
export { createTabs } from './createTabs.svelte';
|
|
40
|
+
export { createTree, treeDescendantIds, treeCheckState } from './createTree.svelte';
|
|
41
|
+
export { createSlider } from './createSlider.svelte';
|
|
42
|
+
export { createGauge } from './createGauge.svelte';
|
|
12
43
|
// UI kit - Group B (buttons & toggles)
|
|
13
44
|
export { default as SvButton } from './SvButton.svelte';
|
|
14
45
|
export { default as SvRepeatButton } from './SvRepeatButton.svelte';
|
package/package.json
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
* SvAutoComplete - a free-text input with a live-filtered suggestion list.
|
|
4
4
|
* Unlike SvComboBox, it accepts ANY value (the text), suggestions are just
|
|
5
5
|
* shortcuts. Parity: Smart `smart-input` (autocomplete). Emits the text string.
|
|
6
|
+
*
|
|
7
|
+
* One styled renderer over the headless `createAutocomplete` core; only
|
|
8
|
+
* portal/measure render concerns live here.
|
|
6
9
|
*/
|
|
7
10
|
import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
|
|
8
|
-
import {
|
|
11
|
+
import { type ListOption } from './list-option'
|
|
12
|
+
import { createAutocomplete } from './createAutocomplete.svelte'
|
|
9
13
|
|
|
10
14
|
type Props = {
|
|
11
15
|
value?: string
|
|
@@ -22,38 +26,31 @@
|
|
|
22
26
|
|
|
23
27
|
let { value = '', onChange, suggestions = [], minChars = 1, placeholder, disabled = false, name, size = 'md', ariaLabel }: Props = $props()
|
|
24
28
|
|
|
25
|
-
const asOptions = $derived<ListOption[]>(
|
|
26
|
-
suggestions.map((s) => (typeof s === 'string' ? { value: s, label: s } : s)),
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
let open = $state(false)
|
|
30
|
-
let active = $state(0)
|
|
31
29
|
let fieldEl = $state<HTMLInputElement | null>(null)
|
|
32
30
|
let panelEl = $state<HTMLDivElement | null>(null)
|
|
33
31
|
let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
|
|
34
32
|
|
|
35
|
-
const
|
|
33
|
+
const ac = createAutocomplete({
|
|
34
|
+
value: () => value,
|
|
35
|
+
onChange: (v) => onChange?.(v),
|
|
36
|
+
suggestions: () => suggestions,
|
|
37
|
+
minChars: () => minChars,
|
|
38
|
+
disabled: () => disabled,
|
|
39
|
+
ariaLabel: () => ariaLabel,
|
|
40
|
+
focusInput: () => fieldEl?.focus(),
|
|
41
|
+
})
|
|
36
42
|
|
|
37
43
|
function updatePos() {
|
|
38
44
|
if (!fieldEl) return
|
|
39
|
-
rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: Math.min(filtered.length, 8) * 34 + 8 })
|
|
40
|
-
}
|
|
41
|
-
function maybeOpen() { open = filtered.length > 0; if (open) { active = 0; updatePos() } }
|
|
42
|
-
function pick(o: ListOption | undefined) { if (!o) return; onChange?.(String(o.value)); open = false; fieldEl?.focus() }
|
|
43
|
-
function onInput(e: Event) { onChange?.((e.currentTarget as HTMLInputElement).value); queueMicrotask(maybeOpen) }
|
|
44
|
-
function onKeydown(e: KeyboardEvent) {
|
|
45
|
-
if (!open) { if (e.key === 'ArrowDown') { queueMicrotask(maybeOpen) } return }
|
|
46
|
-
if (e.key === 'ArrowDown') { e.preventDefault(); active = Math.min(active + 1, filtered.length - 1) }
|
|
47
|
-
else if (e.key === 'ArrowUp') { e.preventDefault(); active = Math.max(active - 1, 0) }
|
|
48
|
-
else if (e.key === 'Enter') { e.preventDefault(); pick(filtered[active]) }
|
|
49
|
-
else if (e.key === 'Escape') { open = false }
|
|
45
|
+
rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: Math.min(ac.filtered.length, 8) * 34 + 8 })
|
|
50
46
|
}
|
|
51
47
|
|
|
52
48
|
$effect(() => {
|
|
53
|
-
if (!open) return
|
|
49
|
+
if (!ac.open) return
|
|
50
|
+
updatePos()
|
|
54
51
|
const rp = () => updatePos()
|
|
55
52
|
window.addEventListener('scroll', rp, true); window.addEventListener('resize', rp)
|
|
56
|
-
const od = (e: PointerEvent) => { const t = e.target as Node | null; if (t && (fieldEl?.contains(t) || panelEl?.contains(t))) return;
|
|
53
|
+
const od = (e: PointerEvent) => { const t = e.target as Node | null; if (t && (fieldEl?.contains(t) || panelEl?.contains(t))) return; ac.close() }
|
|
57
54
|
document.addEventListener('pointerdown', od, true)
|
|
58
55
|
return () => { window.removeEventListener('scroll', rp, true); window.removeEventListener('resize', rp); document.removeEventListener('pointerdown', od, true) }
|
|
59
56
|
})
|
|
@@ -63,23 +60,15 @@
|
|
|
63
60
|
bind:this={fieldEl}
|
|
64
61
|
class="sv-ac sv-ac--{size}"
|
|
65
62
|
type="text"
|
|
66
|
-
role="combobox"
|
|
67
|
-
aria-expanded={open}
|
|
68
|
-
aria-autocomplete="list"
|
|
69
|
-
value={value}
|
|
70
63
|
{placeholder}
|
|
71
|
-
{
|
|
72
|
-
aria-label={ariaLabel}
|
|
73
|
-
oninput={onInput}
|
|
74
|
-
onfocus={maybeOpen}
|
|
75
|
-
onkeydown={onKeydown}
|
|
64
|
+
{...ac.inputProps()}
|
|
76
65
|
/>
|
|
77
66
|
|
|
78
|
-
{#if open}
|
|
79
|
-
<div bind:this={panelEl} class="sv-ddl__panel" use:portalToBody style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`}
|
|
80
|
-
{#each filtered as opt, i (opt.value)}
|
|
67
|
+
{#if ac.open}
|
|
68
|
+
<div bind:this={panelEl} class="sv-ddl__panel" use:portalToBody style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} {...ac.listboxProps()}>
|
|
69
|
+
{#each ac.filtered as opt, i (opt.value)}
|
|
81
70
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
|
|
82
|
-
<div class="sv-ddl__opt" class:is-active={i
|
|
71
|
+
<div class="sv-ddl__opt" class:is-active={ac.isActive(i)} {...ac.optionProps(i)}>{opt.label}</div>
|
|
83
72
|
{/each}
|
|
84
73
|
</div>
|
|
85
74
|
{/if}
|