@happyvertical/smrt-ui 0.43.4 → 0.43.6
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 +78 -4
- package/dist/components/display/CurrencyDisplay.contract.d.ts +7 -0
- package/dist/components/display/CurrencyDisplay.contract.d.ts.map +1 -0
- package/dist/components/display/CurrencyDisplay.contract.js +1 -0
- package/dist/components/display/CurrencyDisplay.svelte +148 -17
- package/dist/components/display/CurrencyDisplay.svelte.d.ts +8 -5
- package/dist/components/display/CurrencyDisplay.svelte.d.ts.map +1 -1
- package/dist/components/display/__tests__/CurrencyDisplay.test.js +193 -3
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte +20 -0
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts +19 -0
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts.map +1 -0
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte +7 -0
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts +19 -0
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts.map +1 -0
- package/dist/components/display/currency-metadata.d.ts +2 -0
- package/dist/components/display/currency-metadata.d.ts.map +1 -0
- package/dist/components/display/currency-metadata.js +14 -0
- 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.d.ts +3 -0
- package/dist/i18n/strings.d.ts.map +1 -1
- package/dist/i18n/strings.js +3 -0
- 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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CurrencyDisplayI18nHarness.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.ts"],"names":[],"mappings":"AA6BA,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IACtG,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,0BAA0B;;kBAA+E,CAAC;AAC9F,KAAK,0BAA0B,GAAG,YAAY,CAAC,OAAO,0BAA0B,CAAC,CAAC;AACpF,eAAe,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const CurrencyDisplaySsrHarness: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type CurrencyDisplaySsrHarness = InstanceType<typeof CurrencyDisplaySsrHarness>;
|
|
18
|
+
export default CurrencyDisplaySsrHarness;
|
|
19
|
+
//# sourceMappingURL=CurrencyDisplaySsrHarness.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CurrencyDisplaySsrHarness.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.ts"],"names":[],"mappings":"AAeA,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IACtG,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,yBAAyB;;kBAA+E,CAAC;AAC7F,KAAK,yBAAyB,GAAG,YAAY,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAClF,eAAe,yBAAyB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"currency-metadata.d.ts","sourceRoot":"","sources":["../../../src/components/display/currency-metadata.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,oBAAoB,4BAe/B,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function minorUnitEntries(codes, minorUnits) {
|
|
2
|
+
return codes.split(' ').map((code) => [code, minorUnits]);
|
|
3
|
+
}
|
|
4
|
+
// ISO 4217 List One, published by the ISO maintenance agency SIX on
|
|
5
|
+
// 2026-01-01. Keeping both membership and minor-unit exponents here makes SSR
|
|
6
|
+
// and browser rendering independent of their potentially different ICU data.
|
|
7
|
+
// Source: https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-one.xml
|
|
8
|
+
export const ISO_4217_MINOR_UNITS = new Map([
|
|
9
|
+
...minorUnitEntries('XOF BIF XAF CLP KMF DJF XPF GNF ISK JPY KRW PYG RWF UGX UYI VUV VND', 0),
|
|
10
|
+
...minorUnitEntries('AFN EUR ALL DZD USD AOA XCD XAD ARS AMD AWG AUD AZN BSD BDT BBD BYN BZD BMD INR BTN BOB BOV BAM BWP NOK BRL BND CVE KHR CAD KYD CNY COP COU CDF NZD CRC CUP XCG CZK DKK DOP EGP SVC ERN SZL ETB FKP FJD GMD GEL GHS GIP GTQ GBP GYD HTG HNL HKD HUF IDR IRR ILS JMD KZT KES KPW KGS LAK LBP LSL ZAR LRD CHF MOP MKD MGA MWK MYR MVR MRU MUR MXN MXV MDL MNT MAD MZN MMK NAD NPR NIO NGN PKR PAB PGK PEN PHP PLN QAR RON RUB SHP WST STN SAR RSD SCR SLE SGD SBD SOS SSP LKR SDG SRD SEK CHE CHW SYP TWD TJS TZS THB TOP TTD TRY TMT UAH AED USN UYU UZS VES VED YER ZMW ZWG', 2),
|
|
11
|
+
...minorUnitEntries('BHD IQD JOD KWD LYD OMR TND', 3),
|
|
12
|
+
...minorUnitEntries('CLF UYW', 4),
|
|
13
|
+
...minorUnitEntries('XDR XUA XSU XBA XBB XBC XBD XTS XXX XAU XPD XPT XAG', null),
|
|
14
|
+
]);
|
|
@@ -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"}
|