@happyvertical/smrt-ui 0.43.3 → 0.43.5
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/AGENTS.md +33 -0
- package/README.md +50 -4
- package/dist/components/forms/Checkbox.svelte +15 -6
- package/dist/components/forms/Checkbox.svelte.d.ts.map +1 -1
- package/dist/components/forms/Combobox.svelte +45 -10
- package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
- package/dist/components/forms/FilePicker.svelte +5 -2
- package/dist/components/forms/Form.svelte +57 -0
- package/dist/components/forms/Form.svelte.d.ts +2 -0
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/forms/Input.svelte +59 -13
- package/dist/components/forms/Input.svelte.d.ts.map +1 -1
- package/dist/components/forms/Listbox.svelte +33 -6
- package/dist/components/forms/Listbox.svelte.d.ts.map +1 -1
- package/dist/components/forms/MultiSelect.svelte +41 -19
- package/dist/components/forms/MultiSelect.svelte.d.ts.map +1 -1
- package/dist/components/forms/RadioGroup.svelte +14 -3
- package/dist/components/forms/RadioGroup.svelte.d.ts.map +1 -1
- package/dist/components/forms/RangeSlider.svelte +61 -6
- package/dist/components/forms/RangeSlider.svelte.d.ts.map +1 -1
- package/dist/components/forms/SegmentedControl.svelte +33 -6
- package/dist/components/forms/SegmentedControl.svelte.d.ts.map +1 -1
- package/dist/components/forms/Select.svelte +32 -9
- package/dist/components/forms/Select.svelte.d.ts.map +1 -1
- package/dist/components/forms/Slider.svelte +26 -6
- package/dist/components/forms/Slider.svelte.d.ts.map +1 -1
- package/dist/components/forms/StagedControlReview.svelte +637 -0
- package/dist/components/forms/StagedControlReview.svelte.d.ts +13 -0
- package/dist/components/forms/StagedControlReview.svelte.d.ts.map +1 -0
- package/dist/components/forms/Switch.svelte +16 -7
- package/dist/components/forms/Switch.svelte.d.ts.map +1 -1
- package/dist/components/forms/TagsInput.svelte +36 -8
- package/dist/components/forms/TagsInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/Textarea.svelte +20 -8
- package/dist/components/forms/Textarea.svelte.d.ts.map +1 -1
- package/dist/components/forms/ToggleButton.svelte +20 -3
- package/dist/components/forms/ToggleButton.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/Form.test.js +106 -0
- package/dist/components/forms/__tests__/StagedControlReview.test.js +668 -0
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte +22 -4
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts +6 -2
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/composite-controls.test.js +96 -1
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte +41 -0
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/control-interaction.test.js +3323 -15
- package/dist/components/forms/__tests__/core-controls.fixture.svelte +16 -5
- package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts +1 -0
- package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/core-controls.test.js +223 -2
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte +25 -0
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte +25 -0
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte +21 -0
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte +23 -0
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/staged-review.fixture.svelte +56 -0
- package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts +12 -0
- package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/control-interaction-context.d.ts +9 -1
- package/dist/components/forms/control-interaction-context.d.ts.map +1 -1
- package/dist/components/forms/control-interaction-context.js +16 -0
- package/dist/components/forms/control-interaction.d.ts +97 -7
- package/dist/components/forms/control-interaction.d.ts.map +1 -1
- package/dist/components/forms/control-interaction.js +1019 -92
- package/dist/components/forms/control-value-validation.d.ts +19 -0
- package/dist/components/forms/control-value-validation.d.ts.map +1 -0
- package/dist/components/forms/control-value-validation.js +152 -0
- package/dist/components/forms/index.d.ts +3 -1
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +2 -1
- package/dist/components/forms/staged-control-review.d.ts +21 -0
- package/dist/components/forms/staged-control-review.d.ts.map +1 -0
- package/dist/components/forms/staged-control-review.js +1 -0
- package/dist/components/forms/use-control-registration.svelte.d.ts.map +1 -1
- package/dist/components/forms/use-control-registration.svelte.js +23 -4
- package/dist/i18n/strings.ui.d.ts +18 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -1
- package/dist/i18n/strings.ui.js +19 -0
- package/dist/svelte/playground/InteractiveControlsPreview.svelte +3 -4
- package/dist/svelte/playground/InteractiveControlsPreview.svelte.d.ts.map +1 -1
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -112,6 +112,39 @@ other text pairing clears WCAG AA in both schemes.
|
|
|
112
112
|
interaction registry. Keep chat/voice transports outside this package;
|
|
113
113
|
mutations must retain sensitivity checks and the stage → confirmed apply
|
|
114
114
|
policy.
|
|
115
|
+
- Agent-originated form values remain proposals: the registry records their
|
|
116
|
+
provenance, timestamp, and revision, while `StagedControlReview` is the shared
|
|
117
|
+
review/apply surface mounted by both base and rich Forms. Agents can stage but
|
|
118
|
+
cannot self-confirm; only a local human handler submits confirmed apply,
|
|
119
|
+
discard, clear, or undo commands after the registry validates a trusted DOM
|
|
120
|
+
gesture while its event is actively dispatching. Retained events are rejected,
|
|
121
|
+
and the complete command or batch is snapshotted synchronously before gesture
|
|
122
|
+
consumption. Serialized or programmatic `source: 'user', confirmed: true`
|
|
123
|
+
input is not confirmation.
|
|
124
|
+
`StagedControlReview` marks its complete edited Apply value with
|
|
125
|
+
`reviewedValueIsCanonical: true`, but the marker grants no authority and is
|
|
126
|
+
honored only for a current staged entry under the registry's exact local
|
|
127
|
+
gesture proof. A control with proposal-relative preparation implements
|
|
128
|
+
`prepareReviewedValue` to validate/canonicalize the complete displayed value;
|
|
129
|
+
without that hook, marked edits still pass through ordinary `prepareValue`.
|
|
130
|
+
Only an unchanged value exactly equal to the stored staged canonical value can
|
|
131
|
+
reuse that trusted stored value without either hook.
|
|
132
|
+
Secret controls never accept staging, and sensitive/secret
|
|
133
|
+
values stay redacted in snapshots, events, policy callbacks, and review UI.
|
|
134
|
+
Batch execution is ordered best-effort and returns one result per command;
|
|
135
|
+
review-surface batch actions exclude stale and invalid proposals, which remain
|
|
136
|
+
visible with per-field feedback.
|
|
137
|
+
`executeBatch` is additive and optional for injected legacy registries; the
|
|
138
|
+
framework falls back to ordered `execute` calls without weakening the private
|
|
139
|
+
one-shot gesture proof on factory-created registries.
|
|
140
|
+
Rejecting async custom setters and clear handlers roll back by default;
|
|
141
|
+
controls that accept concurrent direct edits expose a monotonic revision and
|
|
142
|
+
user value from `getUserEditSnapshot()` so rollback restores newer human input;
|
|
143
|
+
fallible async setters expose an infallible `restoreValue()` path. Async hooks
|
|
144
|
+
issue nested commands through their optional final `ControlExtensionContext`;
|
|
145
|
+
same-control mutations are rejected there without timing out unrelated queued
|
|
146
|
+
commands. Context-aware setters implement the additive `setValueWithContext`
|
|
147
|
+
hook; legacy `setValue` is always invoked with exactly one argument.
|
|
115
148
|
|
|
116
149
|
## Gotchas
|
|
117
150
|
|
package/README.md
CHANGED
|
@@ -99,10 +99,56 @@ without coupling controls to a transport:
|
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
Controls publish serializable metadata, constraints, options, sensitivity, and
|
|
102
|
-
capabilities. Adapters can focus, reveal, highlight, explain, validate,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
capabilities. Adapters can focus, reveal, highlight, explain, validate, and
|
|
103
|
+
stage reviewable proposals. Agents cannot apply, discard, clear, or undo;
|
|
104
|
+
those value-changing actions require a trusted local gesture handled by the
|
|
105
|
+
framework review surface. Secret/read-only controls reject agent mutations.
|
|
106
|
+
Staging remains separate so proposals never change user state before review.
|
|
107
|
+
Custom local review controls can call `executeLocalControlCommand` or
|
|
108
|
+
`executeLocalControlBatch` synchronously from their DOM handlers; the registry
|
|
109
|
+
requires the event to still be actively dispatching, snapshots the complete
|
|
110
|
+
command, and consumes the gesture before authorizing a value-changing command.
|
|
111
|
+
Retaining an event for later use is rejected even when it remains trusted.
|
|
112
|
+
Serialized or programmatic `source: 'user', confirmed: true` input is never
|
|
113
|
+
confirmation. Sensitive and secret values, validation details, failures, and
|
|
114
|
+
events remain redacted from every public surface.
|
|
115
|
+
The shared review surface marks its complete edited Apply value with
|
|
116
|
+
`reviewedValueIsCanonical: true`. That marker carries no authority: the registry
|
|
117
|
+
honors it only for a current staged entry after validating the exact command
|
|
118
|
+
under an actively dispatching local gesture. Controls with non-idempotent
|
|
119
|
+
proposal preparation can implement `prepareReviewedValue(value)` to validate or
|
|
120
|
+
canonicalize the complete displayed value without re-applying proposal-relative
|
|
121
|
+
behavior. Controls without that hook route marked edits through their ordinary
|
|
122
|
+
`prepareValue`, so a generic marker never bypasses custom normalization or
|
|
123
|
+
rejection. An unchanged value exactly equal to the stored staged canonical value
|
|
124
|
+
uses that trusted stored value directly.
|
|
125
|
+
Registries expose optional `refresh(formId)` notification for hosts whose live
|
|
126
|
+
metadata or runtime-state getters change without a registration event; it
|
|
127
|
+
updates subscribers without discarding an internal staged proposal.
|
|
128
|
+
`executeBatch` is an additive optional registry method; Forms fall back to
|
|
129
|
+
ordered `execute` calls for older injected registries. Factory-created
|
|
130
|
+
registries retain the framework's private, one-shot gesture proof, while an
|
|
131
|
+
older custom registry remains responsible for its pre-existing execution
|
|
132
|
+
policy and accepts review actions only from a trusted browser event.
|
|
133
|
+
Custom controls whose clear operation is intentionally idempotent should return
|
|
134
|
+
`true` from `clear()` to affirm that the unchanged cleared value was accepted.
|
|
135
|
+
Async custom setters and clear handlers are rolled back when they reject. A
|
|
136
|
+
control that permits direct edits while an async mutation is pending can expose
|
|
137
|
+
`getUserEditSnapshot()` and update its revision and value only for direct user
|
|
138
|
+
edits so rollback restores newer human input even if the handler mutates again
|
|
139
|
+
before rejecting. A fallible async setter should also expose `restoreValue()` as
|
|
140
|
+
an infallible state restoration path that does not repeat the external workflow.
|
|
141
|
+
Async policy, validation, setter, clear, and restoration hooks receive an
|
|
142
|
+
optional final `ControlExtensionContext`. Hooks that need to issue another
|
|
143
|
+
control command should use `extension.execute()`; it rejects a mutation of the
|
|
144
|
+
same control immediately, while commands from independent callers remain in
|
|
145
|
+
the normal ordered queue regardless of how long the hook takes. Existing hooks
|
|
146
|
+
that omit the additional argument remain compatible. Setters retain their exact
|
|
147
|
+
legacy `setValue(value)` invocation; a setter that needs this context implements
|
|
148
|
+
the additive `setValueWithContext(value, extension)` hook instead. A hook must
|
|
149
|
+
not await a same-control mutation through a captured registry reference: that
|
|
150
|
+
call is indistinguishable from an independent caller in browser runtimes and,
|
|
151
|
+
like any hook that never settles, can hold the ordered queue indefinitely.
|
|
106
152
|
|
|
107
153
|
## DataTable controller
|
|
108
154
|
|
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
} from './control-dom.js';
|
|
8
8
|
import type { ControlInteractionOptions } from './control-interaction.js';
|
|
9
9
|
import { tryGetControlInteractionContext } from './control-interaction-context.js';
|
|
10
|
+
import {
|
|
11
|
+
booleanControlValue,
|
|
12
|
+
prepareBooleanControlValue,
|
|
13
|
+
validateNativeCheckedValue,
|
|
14
|
+
} from './control-value-validation.js';
|
|
10
15
|
import { tryGetFormGroupContext } from './form-group-context.js';
|
|
11
16
|
import { useControlRegistration } from './use-control-registration.svelte.js';
|
|
12
17
|
|
|
@@ -62,10 +67,7 @@ $effect(() => {
|
|
|
62
67
|
});
|
|
63
68
|
|
|
64
69
|
function setChecked(next: unknown) {
|
|
65
|
-
checked =
|
|
66
|
-
typeof next === 'string'
|
|
67
|
-
? ['true', '1', 'yes', 'on'].includes(next.toLowerCase())
|
|
68
|
-
: Boolean(next);
|
|
70
|
+
checked = booleanControlValue(next);
|
|
69
71
|
if (inputEl) emitControlChange(inputEl);
|
|
70
72
|
}
|
|
71
73
|
|
|
@@ -91,15 +93,20 @@ useControlRegistration(() => {
|
|
|
91
93
|
constraints: { required: required === true },
|
|
92
94
|
},
|
|
93
95
|
getValue: () => checked,
|
|
96
|
+
prepareValue: prepareBooleanControlValue,
|
|
94
97
|
setValue: setChecked,
|
|
95
|
-
clear: () =>
|
|
98
|
+
clear: () => {
|
|
99
|
+
setChecked(false);
|
|
100
|
+
return true;
|
|
101
|
+
},
|
|
96
102
|
focus: () => element.focus(),
|
|
97
103
|
reveal: () => revealControl(element),
|
|
98
104
|
highlight: (durationMs) =>
|
|
99
105
|
highlightControl(element.closest('label') ?? element, durationMs),
|
|
100
106
|
validate: () => element.reportValidity(),
|
|
107
|
+
validateValue: (next) => validateNativeCheckedValue(element, next),
|
|
101
108
|
getState: () => ({
|
|
102
|
-
disabled: element.disabled,
|
|
109
|
+
disabled: element.matches(':disabled'),
|
|
103
110
|
readonly: false,
|
|
104
111
|
valid: element.validity.valid,
|
|
105
112
|
validationMessage: element.validationMessage || undefined,
|
|
@@ -113,6 +120,8 @@ useControlRegistration(() => {
|
|
|
113
120
|
class:checkbox--disabled={disabled}
|
|
114
121
|
data-smrt-control={controlId}
|
|
115
122
|
data-smrt-form={interactionContext?.formId}
|
|
123
|
+
data-smrt-subject-type={resolvedInteraction === false ? undefined : resolvedInteraction.subject?.type}
|
|
124
|
+
data-smrt-subject-id={resolvedInteraction === false ? undefined : resolvedInteraction.subject?.id}
|
|
116
125
|
>
|
|
117
126
|
<input
|
|
118
127
|
bind:this={inputEl}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Checkbox.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Checkbox.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAM3D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"Checkbox.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Checkbox.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAM3D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAW1E,MAAM,WAAW,KACf,SAAQ,IAAI,CAAC,mBAAmB,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;IAC/D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,yBAAyB,GAAG,KAAK,CAAC;CACjD;AAkHD,QAAA,MAAM,QAAQ,kDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
emitControlChange,
|
|
4
|
+
highlightControl,
|
|
5
|
+
revealControl,
|
|
6
|
+
} from './control-dom.js';
|
|
3
7
|
import type {
|
|
4
8
|
ControlInteractionOptions,
|
|
5
9
|
ControlOption,
|
|
6
10
|
} from './control-interaction.js';
|
|
7
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
recordControlUserEdit,
|
|
13
|
+
tryGetControlInteractionContext,
|
|
14
|
+
} from './control-interaction-context.js';
|
|
15
|
+
import {
|
|
16
|
+
matchingOption,
|
|
17
|
+
prepareTextControlValue,
|
|
18
|
+
} from './control-value-validation.js';
|
|
8
19
|
import { useControlRegistration } from './use-control-registration.svelte.js';
|
|
9
20
|
export interface Props {
|
|
10
21
|
options: ControlOption[];
|
|
@@ -49,18 +60,25 @@ const filtered = $derived(
|
|
|
49
60
|
const controlId = $derived(
|
|
50
61
|
interaction === false ? undefined : (interaction?.id ?? name ?? inputId),
|
|
51
62
|
);
|
|
52
|
-
function commit(option: ControlOption) {
|
|
63
|
+
function commit(option: ControlOption, userEdit = false) {
|
|
53
64
|
if (option.disabled || disabled) return;
|
|
65
|
+
const changed = String(option.value) !== value;
|
|
54
66
|
value = String(option.value);
|
|
55
67
|
query = option.label;
|
|
56
68
|
open = false;
|
|
57
69
|
onvaluechange?.(value);
|
|
70
|
+
if (userEdit && changed) {
|
|
71
|
+
recordControlUserEdit(
|
|
72
|
+
interactionContext,
|
|
73
|
+
controlId,
|
|
74
|
+
interaction === false ? undefined : interaction?.subject,
|
|
75
|
+
);
|
|
76
|
+
if (rootEl) emitControlChange(rootEl);
|
|
77
|
+
}
|
|
58
78
|
}
|
|
59
79
|
function setValue(next: unknown) {
|
|
60
80
|
const candidate = String(next ?? '');
|
|
61
|
-
const option = options
|
|
62
|
-
(item) => String(item.value) === candidate || item.label === candidate,
|
|
63
|
-
);
|
|
81
|
+
const option = matchingOption(options, candidate, true);
|
|
64
82
|
if (option) commit(option);
|
|
65
83
|
else if (allowCustom) {
|
|
66
84
|
value = candidate;
|
|
@@ -68,6 +86,13 @@ function setValue(next: unknown) {
|
|
|
68
86
|
onvaluechange?.(candidate);
|
|
69
87
|
}
|
|
70
88
|
}
|
|
89
|
+
function prepareValue(next: unknown) {
|
|
90
|
+
const candidate = prepareTextControlValue(next);
|
|
91
|
+
const option = matchingOption(options, candidate, true);
|
|
92
|
+
if (option) return String(option.value);
|
|
93
|
+
if (allowCustom) return candidate;
|
|
94
|
+
return candidate;
|
|
95
|
+
}
|
|
71
96
|
function handleInput(event: Event & { currentTarget: HTMLInputElement }) {
|
|
72
97
|
query = event.currentTarget.value;
|
|
73
98
|
open = true;
|
|
@@ -96,7 +121,7 @@ function handleKeydown(event: KeyboardEvent) {
|
|
|
96
121
|
enabled[(position - 1 + enabled.length) % enabled.length] ?? 0;
|
|
97
122
|
} else if (event.key === 'Enter' && open && filtered[activeIndex]) {
|
|
98
123
|
event.preventDefault();
|
|
99
|
-
commit(filtered[activeIndex]);
|
|
124
|
+
commit(filtered[activeIndex], true);
|
|
100
125
|
} else if (event.key === 'Escape') {
|
|
101
126
|
open = false;
|
|
102
127
|
}
|
|
@@ -138,30 +163,40 @@ useControlRegistration(() => {
|
|
|
138
163
|
options,
|
|
139
164
|
},
|
|
140
165
|
getValue: () => value,
|
|
166
|
+
prepareValue,
|
|
141
167
|
setValue,
|
|
142
168
|
clear: () => {
|
|
143
169
|
value = '';
|
|
144
170
|
query = '';
|
|
145
171
|
onvaluechange?.('');
|
|
172
|
+
return true;
|
|
146
173
|
},
|
|
147
174
|
focus: () => input.focus(),
|
|
148
175
|
reveal: () => revealControl(root),
|
|
149
176
|
highlight: (durationMs) => highlightControl(root, durationMs),
|
|
150
177
|
validate: () => input.reportValidity(),
|
|
178
|
+
validateValue: (next) => {
|
|
179
|
+
const candidate = String(next ?? '');
|
|
180
|
+
const option = matchingOption(options, candidate, true);
|
|
181
|
+
if (option) return option.disabled !== true;
|
|
182
|
+
return allowCustom && (!required || candidate.length > 0);
|
|
183
|
+
},
|
|
151
184
|
getState: () => ({
|
|
152
|
-
disabled,
|
|
185
|
+
disabled: input.matches(':disabled'),
|
|
153
186
|
valid: input.validity.valid,
|
|
154
187
|
validationMessage: input.validationMessage || undefined,
|
|
155
188
|
}),
|
|
156
189
|
};
|
|
157
190
|
});
|
|
158
191
|
</script>
|
|
159
|
-
<div bind:this={rootEl} class="combobox {className}" data-smrt-control={controlId} data-smrt-form={interactionContext?.formId}
|
|
192
|
+
<div bind:this={rootEl} class="combobox {className}" data-smrt-control={controlId} data-smrt-form={interactionContext?.formId}
|
|
193
|
+
data-smrt-subject-type={interaction === false ? undefined : interaction?.subject?.type}
|
|
194
|
+
data-smrt-subject-id={interaction === false ? undefined : interaction?.subject?.id}>
|
|
160
195
|
<label for={inputId}>{label}</label><input bind:this={inputEl} id={inputId} {name} role="combobox" autocomplete="off" {placeholder} {disabled} {required} value={query}
|
|
161
196
|
aria-expanded={open} aria-controls={listId} aria-autocomplete="list" aria-activedescendant={open && filtered[activeIndex] ? `${listId}-${activeIndex}` : undefined}
|
|
162
197
|
onfocus={() => open = true} oninput={handleInput} onkeydown={handleKeydown} />
|
|
163
198
|
{#if open && filtered.length}<div id={listId} class="options" role="listbox">{#each filtered as option, index (option.value)}<button id={`${listId}-${index}`} type="button" role="option"
|
|
164
|
-
aria-selected={String(option.value) === value} class:active={index === activeIndex} disabled={option.disabled} onpointerdown={(event) => event.preventDefault()} onclick={() => commit(option)}>{option.label}</button>{/each}</div>{/if}
|
|
199
|
+
aria-selected={String(option.value) === value} class:active={index === activeIndex} disabled={option.disabled} onpointerdown={(event) => event.preventDefault()} onclick={() => commit(option, true)}>{option.label}</button>{/each}</div>{/if}
|
|
165
200
|
</div>
|
|
166
201
|
<style>
|
|
167
202
|
.combobox { position: relative; display: grid; gap: var(--smrt-spacing-1); color: var(--smrt-color-on-surface); }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Combobox.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Combobox.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Combobox.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Combobox.svelte.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,yBAAyB,EACzB,aAAa,EACd,MAAM,0BAA0B,CAAC;AAWlC,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,yBAAyB,GAAG,KAAK,CAAC;IAChD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+KD,QAAA,MAAM,QAAQ,gDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -92,7 +92,7 @@ useControlRegistration(() => {
|
|
|
92
92
|
highlight: (durationMs) => highlightControl(root, durationMs),
|
|
93
93
|
validate: () => input.reportValidity(),
|
|
94
94
|
getState: () => ({
|
|
95
|
-
disabled: input.disabled,
|
|
95
|
+
disabled: input.matches(':disabled'),
|
|
96
96
|
valid: input.validity.valid,
|
|
97
97
|
validationMessage: input.validationMessage || undefined,
|
|
98
98
|
}),
|
|
@@ -100,7 +100,10 @@ useControlRegistration(() => {
|
|
|
100
100
|
});
|
|
101
101
|
</script>
|
|
102
102
|
<label bind:this={rootEl} for={resolvedId} class="file-picker {className}" class:dropzone class:dragging class:disabled
|
|
103
|
-
data-smrt-control={controlId} data-smrt-form={interactionContext?.formId}
|
|
103
|
+
data-smrt-control={controlId} data-smrt-form={interactionContext?.formId}
|
|
104
|
+
data-smrt-subject-type={interaction === false ? undefined : interaction?.subject?.type}
|
|
105
|
+
data-smrt-subject-id={interaction === false ? undefined : interaction?.subject?.id}
|
|
106
|
+
ondragenter={(event) => { event.preventDefault(); dragging = true; }}
|
|
104
107
|
ondragover={(event) => event.preventDefault()} ondragleave={() => dragging = false} ondrop={handleDrop}>
|
|
105
108
|
<input bind:this={inputEl} id={resolvedId} type="file" {name} {accept} {multiple} {disabled} {required} onchange={handleChange} {...rest} />
|
|
106
109
|
<span class="label">{label}</span><span class="description">{files.length ? `${files.length} file${files.length === 1 ? '' : 's'} selected` : description}</span>
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
createControlInteractionRegistry,
|
|
26
26
|
} from './control-interaction.js';
|
|
27
27
|
import { setControlInteractionContext } from './control-interaction-context.js';
|
|
28
|
+
import StagedControlReview from './StagedControlReview.svelte';
|
|
28
29
|
|
|
29
30
|
export interface Props extends Omit<HTMLFormAttributes, 'class'> {
|
|
30
31
|
class?: string;
|
|
@@ -35,6 +36,8 @@ export interface Props extends Omit<HTMLFormAttributes, 'class'> {
|
|
|
35
36
|
interactionRegistry?: ControlInteractionRegistry;
|
|
36
37
|
/** Receives registry lifecycle and command events. */
|
|
37
38
|
oninteraction?: (event: ControlInteractionEvent) => void;
|
|
39
|
+
/** Render the built-in human review surface for staged changes. */
|
|
40
|
+
stagedReview?: boolean;
|
|
38
41
|
children: Snippet;
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -44,9 +47,13 @@ let {
|
|
|
44
47
|
formId,
|
|
45
48
|
interactionRegistry,
|
|
46
49
|
oninteraction,
|
|
50
|
+
stagedReview = true,
|
|
47
51
|
id,
|
|
48
52
|
name,
|
|
49
53
|
onsubmit,
|
|
54
|
+
onclick,
|
|
55
|
+
oninput,
|
|
56
|
+
onchange,
|
|
50
57
|
children,
|
|
51
58
|
...rest
|
|
52
59
|
}: Props = $props();
|
|
@@ -58,6 +65,7 @@ const resolvedFormId = $derived(formId ?? id ?? name ?? generatedFormId);
|
|
|
58
65
|
const resolvedInteractionRegistry = $derived(
|
|
59
66
|
interactionRegistry ?? localInteractionRegistry,
|
|
60
67
|
);
|
|
68
|
+
let formElement = $state<HTMLFormElement | null>(null);
|
|
61
69
|
|
|
62
70
|
setControlInteractionContext({
|
|
63
71
|
get formId() {
|
|
@@ -85,17 +93,66 @@ function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
|
|
|
85
93
|
if (preventDefault) event.preventDefault();
|
|
86
94
|
onsubmit?.(event);
|
|
87
95
|
}
|
|
96
|
+
|
|
97
|
+
function recordDirectUserEdit(event: Event) {
|
|
98
|
+
if (!event.isTrusted) return;
|
|
99
|
+
const target = event.target;
|
|
100
|
+
if (!(target instanceof Element)) return;
|
|
101
|
+
const control = target.closest<HTMLElement>('[data-smrt-control]');
|
|
102
|
+
const controlId = control?.dataset.smrtControl;
|
|
103
|
+
if (!controlId) return;
|
|
104
|
+
resolvedInteractionRegistry.recordUserEdit?.({
|
|
105
|
+
formId: resolvedFormId,
|
|
106
|
+
controlId,
|
|
107
|
+
subject:
|
|
108
|
+
control.dataset.smrtSubjectType && control.dataset.smrtSubjectId
|
|
109
|
+
? {
|
|
110
|
+
type: control.dataset.smrtSubjectType,
|
|
111
|
+
id: control.dataset.smrtSubjectId,
|
|
112
|
+
}
|
|
113
|
+
: undefined,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function handleInput(event: Event & { currentTarget: HTMLFormElement }) {
|
|
118
|
+
try {
|
|
119
|
+
oninput?.(event);
|
|
120
|
+
} finally {
|
|
121
|
+
// Consumer handlers may synchronously normalize the bound value. Capture
|
|
122
|
+
// that final human value even when the handler throws.
|
|
123
|
+
recordDirectUserEdit(event);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function handleChange(event: Event & { currentTarget: HTMLFormElement }) {
|
|
128
|
+
try {
|
|
129
|
+
onchange?.(event);
|
|
130
|
+
} finally {
|
|
131
|
+
// Match input semantics for controls that commit via change.
|
|
132
|
+
recordDirectUserEdit(event);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
88
135
|
</script>
|
|
89
136
|
|
|
90
137
|
<form
|
|
138
|
+
bind:this={formElement}
|
|
91
139
|
id={id}
|
|
92
140
|
name={name}
|
|
93
141
|
class="form {className}"
|
|
94
142
|
data-smrt-form={resolvedFormId}
|
|
95
143
|
onsubmit={handleSubmit}
|
|
144
|
+
{onclick}
|
|
145
|
+
oninput={handleInput}
|
|
146
|
+
onchange={handleChange}
|
|
96
147
|
{...rest}
|
|
97
148
|
>
|
|
98
149
|
{@render children()}
|
|
150
|
+
<StagedControlReview
|
|
151
|
+
registry={resolvedInteractionRegistry}
|
|
152
|
+
formId={resolvedFormId}
|
|
153
|
+
{formElement}
|
|
154
|
+
summary={stagedReview}
|
|
155
|
+
/>
|
|
99
156
|
</form>
|
|
100
157
|
|
|
101
158
|
<!--
|
|
@@ -28,6 +28,8 @@ export interface Props extends Omit<HTMLFormAttributes, 'class'> {
|
|
|
28
28
|
interactionRegistry?: ControlInteractionRegistry;
|
|
29
29
|
/** Receives registry lifecycle and command events. */
|
|
30
30
|
oninteraction?: (event: ControlInteractionEvent) => void;
|
|
31
|
+
/** Render the built-in human review surface for staged changes. */
|
|
32
|
+
stagedReview?: boolean;
|
|
31
33
|
children: Snippet;
|
|
32
34
|
}
|
|
33
35
|
declare const Form: import("svelte").Component<Props, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Form.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAEhC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Form.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAEhC,MAAM,0BAA0B,CAAC;AAKlC,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,mBAAmB,CAAC,EAAE,0BAA0B,CAAC;IACjD,sDAAsD;IACtD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAC;IACzD,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAgHD,QAAA,MAAM,IAAI;kCA9D0B,0BAA0B;qBAIvC,MAAM;MA0DqB,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
|
|
@@ -10,7 +10,12 @@ import type {
|
|
|
10
10
|
ControlKind,
|
|
11
11
|
} from './control-interaction.js';
|
|
12
12
|
import { tryGetControlInteractionContext } from './control-interaction-context.js';
|
|
13
|
+
import {
|
|
14
|
+
prepareNumberControlValue,
|
|
15
|
+
prepareTextControlValue,
|
|
16
|
+
} from './control-value-validation.js';
|
|
13
17
|
import { tryGetFormGroupContext } from './form-group-context.js';
|
|
18
|
+
import { useControlRegistration } from './use-control-registration.svelte.js';
|
|
14
19
|
|
|
15
20
|
export interface Props extends Omit<HTMLInputAttributes, 'class' | 'value'> {
|
|
16
21
|
value?: string | number;
|
|
@@ -83,23 +88,54 @@ function kindFromType(): ControlKind {
|
|
|
83
88
|
|
|
84
89
|
function setControlValue(next: unknown) {
|
|
85
90
|
value =
|
|
86
|
-
type === 'number' || type === 'range'
|
|
91
|
+
type === 'number' || type === 'range'
|
|
92
|
+
? next === '' || next === null || next === undefined
|
|
93
|
+
? ''
|
|
94
|
+
: Number(next)
|
|
95
|
+
: String(next ?? '');
|
|
87
96
|
if (inputEl) emitControlChange(inputEl);
|
|
88
97
|
}
|
|
89
98
|
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
function validateControlValue(next: unknown) {
|
|
100
|
+
if (!inputEl) return true;
|
|
101
|
+
if (
|
|
102
|
+
(type === 'number' || type === 'range') &&
|
|
103
|
+
next !== '' &&
|
|
104
|
+
next !== null &&
|
|
105
|
+
next !== undefined &&
|
|
106
|
+
!Number.isFinite(typeof next === 'number' ? next : Number(next))
|
|
107
|
+
) {
|
|
108
|
+
return { valid: false, message: 'invalid_number' };
|
|
109
|
+
}
|
|
110
|
+
const candidate = inputEl.cloneNode() as HTMLInputElement;
|
|
111
|
+
const proposedValue = String(next ?? '');
|
|
112
|
+
try {
|
|
113
|
+
candidate.value = proposedValue;
|
|
114
|
+
} catch {
|
|
115
|
+
return { valid: false, message: 'invalid_value' };
|
|
116
|
+
}
|
|
117
|
+
// Native inputs sanitize some assignments without making them invalid: an
|
|
118
|
+
// optional invalid date/time becomes empty and a range value can clamp to a
|
|
119
|
+
// bound. A staged proposal must never be marked applicable when the setter
|
|
120
|
+
// would apply a different value than the one reviewed.
|
|
121
|
+
if (candidate.value !== proposedValue) {
|
|
122
|
+
return { valid: false, message: 'invalid_value' };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
valid: candidate.checkValidity(),
|
|
126
|
+
message: candidate.validationMessage || undefined,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
useControlRegistration(() => {
|
|
92
131
|
const element = inputEl;
|
|
93
132
|
const controlId = resolvedControlId;
|
|
94
133
|
const options = resolvedInteraction;
|
|
95
|
-
if (!
|
|
134
|
+
if (!element || !controlId || options === false) return false;
|
|
96
135
|
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
controlId,
|
|
101
|
-
subject: options.subject,
|
|
102
|
-
},
|
|
136
|
+
return {
|
|
137
|
+
controlId,
|
|
138
|
+
subject: options.subject,
|
|
103
139
|
metadata: {
|
|
104
140
|
kind: kindFromType(),
|
|
105
141
|
label: formGroup?.().label ?? ariaLabel ?? undefined,
|
|
@@ -119,19 +155,27 @@ $effect(() => {
|
|
|
119
155
|
},
|
|
120
156
|
},
|
|
121
157
|
getValue: () => value,
|
|
158
|
+
prepareValue: (next) =>
|
|
159
|
+
type === 'number' || type === 'range'
|
|
160
|
+
? prepareNumberControlValue(next)
|
|
161
|
+
: prepareTextControlValue(next),
|
|
122
162
|
setValue: setControlValue,
|
|
123
|
-
clear: () =>
|
|
163
|
+
clear: () => {
|
|
164
|
+
setControlValue('');
|
|
165
|
+
return true;
|
|
166
|
+
},
|
|
124
167
|
focus: () => element.focus(),
|
|
125
168
|
reveal: () => revealControl(element),
|
|
126
169
|
highlight: (durationMs) => highlightControl(element, durationMs),
|
|
127
170
|
validate: () => element.reportValidity(),
|
|
171
|
+
validateValue: validateControlValue,
|
|
128
172
|
getState: () => ({
|
|
129
|
-
disabled: element.disabled,
|
|
173
|
+
disabled: element.matches(':disabled'),
|
|
130
174
|
readonly: element.readOnly,
|
|
131
175
|
valid: element.validity.valid,
|
|
132
176
|
validationMessage: element.validationMessage || undefined,
|
|
133
177
|
}),
|
|
134
|
-
}
|
|
178
|
+
};
|
|
135
179
|
});
|
|
136
180
|
|
|
137
181
|
export function focus(): void {
|
|
@@ -167,6 +211,8 @@ export function getElement(): HTMLInputElement | null {
|
|
|
167
211
|
class="input {className}"
|
|
168
212
|
data-smrt-control={resolvedControlId}
|
|
169
213
|
data-smrt-form={controlInteraction?.formId}
|
|
214
|
+
data-smrt-subject-type={resolvedInteraction === false ? undefined : resolvedInteraction.subject?.type}
|
|
215
|
+
data-smrt-subject-id={resolvedInteraction === false ? undefined : resolvedInteraction.subject?.id}
|
|
170
216
|
{...rest}
|
|
171
217
|
/>
|
|
172
218
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Input.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Input.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAM3D,OAAO,KAAK,EACV,yBAAyB,EAE1B,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"Input.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Input.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAM3D,OAAO,KAAK,EACV,yBAAyB,EAE1B,MAAM,0BAA0B,CAAC;AAUlC,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,mBAAmB,EAAE,OAAO,GAAG,OAAO,CAAC;IACzE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,WAAW,CAAC,EAAE,yBAAyB,GAAG,KAAK,CAAC;CACjD;AA4LD,QAAA,MAAM,KAAK;iBAvBQ,IAAI;kBAIH,IAAI;6BAIS,MAAM,KAAG,IAAI;sBAItB,gBAAgB,GAAG,IAAI;WAWI,CAAC;AACpD,KAAK,KAAK,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AACtC,eAAe,KAAK,CAAC"}
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
emitControlChange,
|
|
4
|
+
highlightControl,
|
|
5
|
+
revealControl,
|
|
6
|
+
} from './control-dom.js';
|
|
3
7
|
import type {
|
|
4
8
|
ControlInteractionOptions,
|
|
5
9
|
ControlOption,
|
|
6
10
|
} from './control-interaction.js';
|
|
7
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
recordControlUserEdit,
|
|
13
|
+
tryGetControlInteractionContext,
|
|
14
|
+
} from './control-interaction-context.js';
|
|
15
|
+
import {
|
|
16
|
+
prepareEnabledOptionValue,
|
|
17
|
+
validatesEnabledOption,
|
|
18
|
+
} from './control-value-validation.js';
|
|
8
19
|
import { useControlRegistration } from './use-control-registration.svelte.js';
|
|
9
20
|
export interface Props {
|
|
10
21
|
options: ControlOption[];
|
|
@@ -35,11 +46,20 @@ const controlId = $derived(
|
|
|
35
46
|
? undefined
|
|
36
47
|
: (interaction?.id ?? name ?? `listbox-${instanceId}`),
|
|
37
48
|
);
|
|
38
|
-
function select(next: unknown) {
|
|
49
|
+
function select(next: unknown, userEdit = false) {
|
|
39
50
|
const option = options.find((item) => String(item.value) === String(next));
|
|
40
51
|
if (!option || option.disabled || disabled) return;
|
|
52
|
+
const changed = !Object.is(value, option.value);
|
|
41
53
|
value = option.value;
|
|
42
54
|
onvaluechange?.(option.value);
|
|
55
|
+
if (userEdit && changed) {
|
|
56
|
+
recordControlUserEdit(
|
|
57
|
+
interactionContext,
|
|
58
|
+
controlId,
|
|
59
|
+
interaction === false ? undefined : interaction?.subject,
|
|
60
|
+
);
|
|
61
|
+
if (rootEl) emitControlChange(rootEl);
|
|
62
|
+
}
|
|
43
63
|
}
|
|
44
64
|
function move(event: KeyboardEvent, index: number) {
|
|
45
65
|
let next = index;
|
|
@@ -73,21 +93,28 @@ useControlRegistration(() => {
|
|
|
73
93
|
options,
|
|
74
94
|
},
|
|
75
95
|
getValue: () => value,
|
|
96
|
+
prepareValue: (next) => prepareEnabledOptionValue(options, next),
|
|
76
97
|
setValue: select,
|
|
77
98
|
clear: () => {
|
|
78
99
|
value = undefined;
|
|
100
|
+
return true;
|
|
79
101
|
},
|
|
80
102
|
focus: () => optionEls.find((item) => item && !item.disabled)?.focus(),
|
|
81
103
|
reveal: () => revealControl(root),
|
|
82
104
|
highlight: (durationMs) => highlightControl(root, durationMs),
|
|
83
|
-
|
|
105
|
+
validateValue: (next) => validatesEnabledOption(options, next),
|
|
106
|
+
getState: () => ({
|
|
107
|
+
disabled: disabled || root.closest('fieldset:disabled') !== null,
|
|
108
|
+
}),
|
|
84
109
|
};
|
|
85
110
|
});
|
|
86
111
|
</script>
|
|
87
|
-
<div bind:this={rootEl} class="listbox {className}" role="listbox" aria-label={label} aria-disabled={disabled} data-smrt-control={controlId} data-smrt-form={interactionContext?.formId}
|
|
112
|
+
<div bind:this={rootEl} class="listbox {className}" role="listbox" aria-label={label} aria-disabled={disabled} data-smrt-control={controlId} data-smrt-form={interactionContext?.formId}
|
|
113
|
+
data-smrt-subject-type={interaction === false ? undefined : interaction?.subject?.type}
|
|
114
|
+
data-smrt-subject-id={interaction === false ? undefined : interaction?.subject?.id}>
|
|
88
115
|
{#each options as option, index (option.value)}<button bind:this={optionEls[index]} type="button" role="option" aria-selected={value === option.value}
|
|
89
116
|
disabled={disabled || option.disabled} tabindex={value === option.value || (value === undefined && index === 0) ? 0 : -1}
|
|
90
|
-
onkeydown={(event) => move(event, index)} onclick={() => select(option.value)}>{option.label}</button>{/each}
|
|
117
|
+
onkeydown={(event) => move(event, index)} onclick={() => select(option.value, true)}>{option.label}</button>{/each}
|
|
91
118
|
</div>
|
|
92
119
|
<style>
|
|
93
120
|
.listbox { display: grid; padding: var(--smrt-spacing-1); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface); }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Listbox.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Listbox.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Listbox.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Listbox.svelte.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,yBAAyB,EACzB,aAAa,EACd,MAAM,0BAA0B,CAAC;AAWlC,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,yBAAyB,GAAG,KAAK,CAAC;IAChD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAiGD,QAAA,MAAM,OAAO,gDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
|