@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.
Files changed (108) hide show
  1. package/AGENTS.md +33 -0
  2. package/README.md +78 -4
  3. package/dist/components/display/CurrencyDisplay.contract.d.ts +7 -0
  4. package/dist/components/display/CurrencyDisplay.contract.d.ts.map +1 -0
  5. package/dist/components/display/CurrencyDisplay.contract.js +1 -0
  6. package/dist/components/display/CurrencyDisplay.svelte +148 -17
  7. package/dist/components/display/CurrencyDisplay.svelte.d.ts +8 -5
  8. package/dist/components/display/CurrencyDisplay.svelte.d.ts.map +1 -1
  9. package/dist/components/display/__tests__/CurrencyDisplay.test.js +193 -3
  10. package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte +20 -0
  11. package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts +19 -0
  12. package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts.map +1 -0
  13. package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte +7 -0
  14. package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts +19 -0
  15. package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts.map +1 -0
  16. package/dist/components/display/currency-metadata.d.ts +2 -0
  17. package/dist/components/display/currency-metadata.d.ts.map +1 -0
  18. package/dist/components/display/currency-metadata.js +14 -0
  19. package/dist/components/forms/Checkbox.svelte +15 -6
  20. package/dist/components/forms/Checkbox.svelte.d.ts.map +1 -1
  21. package/dist/components/forms/Combobox.svelte +45 -10
  22. package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
  23. package/dist/components/forms/FilePicker.svelte +5 -2
  24. package/dist/components/forms/Form.svelte +57 -0
  25. package/dist/components/forms/Form.svelte.d.ts +2 -0
  26. package/dist/components/forms/Form.svelte.d.ts.map +1 -1
  27. package/dist/components/forms/Input.svelte +59 -13
  28. package/dist/components/forms/Input.svelte.d.ts.map +1 -1
  29. package/dist/components/forms/Listbox.svelte +33 -6
  30. package/dist/components/forms/Listbox.svelte.d.ts.map +1 -1
  31. package/dist/components/forms/MultiSelect.svelte +41 -19
  32. package/dist/components/forms/MultiSelect.svelte.d.ts.map +1 -1
  33. package/dist/components/forms/RadioGroup.svelte +14 -3
  34. package/dist/components/forms/RadioGroup.svelte.d.ts.map +1 -1
  35. package/dist/components/forms/RangeSlider.svelte +61 -6
  36. package/dist/components/forms/RangeSlider.svelte.d.ts.map +1 -1
  37. package/dist/components/forms/SegmentedControl.svelte +33 -6
  38. package/dist/components/forms/SegmentedControl.svelte.d.ts.map +1 -1
  39. package/dist/components/forms/Select.svelte +32 -9
  40. package/dist/components/forms/Select.svelte.d.ts.map +1 -1
  41. package/dist/components/forms/Slider.svelte +26 -6
  42. package/dist/components/forms/Slider.svelte.d.ts.map +1 -1
  43. package/dist/components/forms/StagedControlReview.svelte +637 -0
  44. package/dist/components/forms/StagedControlReview.svelte.d.ts +13 -0
  45. package/dist/components/forms/StagedControlReview.svelte.d.ts.map +1 -0
  46. package/dist/components/forms/Switch.svelte +16 -7
  47. package/dist/components/forms/Switch.svelte.d.ts.map +1 -1
  48. package/dist/components/forms/TagsInput.svelte +36 -8
  49. package/dist/components/forms/TagsInput.svelte.d.ts.map +1 -1
  50. package/dist/components/forms/Textarea.svelte +20 -8
  51. package/dist/components/forms/Textarea.svelte.d.ts.map +1 -1
  52. package/dist/components/forms/ToggleButton.svelte +20 -3
  53. package/dist/components/forms/ToggleButton.svelte.d.ts.map +1 -1
  54. package/dist/components/forms/__tests__/Form.test.js +106 -0
  55. package/dist/components/forms/__tests__/StagedControlReview.test.js +668 -0
  56. package/dist/components/forms/__tests__/composite-controls.fixture.svelte +22 -4
  57. package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts +6 -2
  58. package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts.map +1 -1
  59. package/dist/components/forms/__tests__/composite-controls.test.js +96 -1
  60. package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte +41 -0
  61. package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts +13 -0
  62. package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts.map +1 -0
  63. package/dist/components/forms/__tests__/control-interaction.test.js +3323 -15
  64. package/dist/components/forms/__tests__/core-controls.fixture.svelte +16 -5
  65. package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts +1 -0
  66. package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts.map +1 -1
  67. package/dist/components/forms/__tests__/core-controls.test.js +223 -2
  68. package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte +25 -0
  69. package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts +8 -0
  70. package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts.map +1 -0
  71. package/dist/components/forms/__tests__/select-interaction.fixture.svelte +25 -0
  72. package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts +9 -0
  73. package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts.map +1 -0
  74. package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte +21 -0
  75. package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts +8 -0
  76. package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts.map +1 -0
  77. package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte +23 -0
  78. package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts +8 -0
  79. package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts.map +1 -0
  80. package/dist/components/forms/__tests__/staged-review.fixture.svelte +56 -0
  81. package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts +12 -0
  82. package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts.map +1 -0
  83. package/dist/components/forms/control-interaction-context.d.ts +9 -1
  84. package/dist/components/forms/control-interaction-context.d.ts.map +1 -1
  85. package/dist/components/forms/control-interaction-context.js +16 -0
  86. package/dist/components/forms/control-interaction.d.ts +97 -7
  87. package/dist/components/forms/control-interaction.d.ts.map +1 -1
  88. package/dist/components/forms/control-interaction.js +1019 -92
  89. package/dist/components/forms/control-value-validation.d.ts +19 -0
  90. package/dist/components/forms/control-value-validation.d.ts.map +1 -0
  91. package/dist/components/forms/control-value-validation.js +152 -0
  92. package/dist/components/forms/index.d.ts +3 -1
  93. package/dist/components/forms/index.d.ts.map +1 -1
  94. package/dist/components/forms/index.js +2 -1
  95. package/dist/components/forms/staged-control-review.d.ts +21 -0
  96. package/dist/components/forms/staged-control-review.d.ts.map +1 -0
  97. package/dist/components/forms/staged-control-review.js +1 -0
  98. package/dist/components/forms/use-control-registration.svelte.d.ts.map +1 -1
  99. package/dist/components/forms/use-control-registration.svelte.js +23 -4
  100. package/dist/i18n/strings.d.ts +3 -0
  101. package/dist/i18n/strings.d.ts.map +1 -1
  102. package/dist/i18n/strings.js +3 -0
  103. package/dist/i18n/strings.ui.d.ts +18 -0
  104. package/dist/i18n/strings.ui.d.ts.map +1 -1
  105. package/dist/i18n/strings.ui.js +19 -0
  106. package/dist/svelte/playground/InteractiveControlsPreview.svelte +3 -4
  107. package/dist/svelte/playground/InteractiveControlsPreview.svelte.d.ts.map +1 -1
  108. 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,7 @@
1
+ <script lang="ts">
2
+ import CurrencyDisplay from '../CurrencyDisplay.svelte';
3
+ </script>
4
+
5
+ <CurrencyDisplay amount={12345} currency="EUR" />
6
+ <CurrencyDisplay amount={12345} currency="ZZZ" />
7
+ <CurrencyDisplay amount={12345} currency="XAU" />
@@ -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,2 @@
1
+ export declare const ISO_4217_MINOR_UNITS: Map<string, number | null>;
2
+ //# sourceMappingURL=currency-metadata.d.ts.map
@@ -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: () => setChecked(false),
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;AAM1E,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;AA+GD,QAAA,MAAM,QAAQ,kDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,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 { highlightControl, revealControl } from './control-dom.js';
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 { tryGetControlInteractionContext } from './control-interaction-context.js';
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.find(
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":"AAIA,OAAO,KAAK,EACV,yBAAyB,EACzB,aAAa,EACd,MAAM,0BAA0B,CAAC;AAIlC,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;AAwJD,QAAA,MAAM,QAAQ,gDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
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} ondragenter={(event) => { event.preventDefault(); dragging = true; }}
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;AAIlC,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,QAAQ,EAAE,OAAO,CAAC;CACnB;AAkED,QAAA,MAAM,IAAI;kCAtB0B,0BAA0B;qBAIvC,MAAM;MAkBqB,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,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' ? Number(next) : String(next ?? '');
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
- $effect(() => {
91
- const context = controlInteraction;
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 (!context || !element || !controlId || options === false) return;
134
+ if (!element || !controlId || options === false) return false;
96
135
 
97
- return context.registry.register({
98
- identity: {
99
- formId: context.formId,
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: () => setControlValue(''),
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;AAKlC,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;AAmJD,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
+ {"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 { highlightControl, revealControl } from './control-dom.js';
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 { tryGetControlInteractionContext } from './control-interaction-context.js';
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
- getState: () => ({ disabled }),
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":"AAIA,OAAO,KAAK,EACV,yBAAyB,EACzB,aAAa,EACd,MAAM,0BAA0B,CAAC;AAIlC,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;AAkFD,QAAA,MAAM,OAAO,gDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
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"}