@happyvertical/smrt-ui 0.43.4 → 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
|
@@ -8,32 +8,43 @@ import RangeSlider from '../RangeSlider.svelte';
|
|
|
8
8
|
import SegmentedControl from '../SegmentedControl.svelte';
|
|
9
9
|
import Slider from '../Slider.svelte';
|
|
10
10
|
import Switch from '../Switch.svelte';
|
|
11
|
+
import Textarea from '../Textarea.svelte';
|
|
12
|
+
import ToggleButton from '../ToggleButton.svelte';
|
|
11
13
|
|
|
12
14
|
interface Props {
|
|
13
15
|
registry: ControlInteractionRegistry;
|
|
16
|
+
validInitial?: boolean;
|
|
14
17
|
}
|
|
15
|
-
let { registry }: Props = $props();
|
|
18
|
+
let { registry, validInitial = false }: Props = $props();
|
|
16
19
|
let accepted = $state(false);
|
|
17
20
|
let notifications = $state(false);
|
|
18
21
|
let role = $state('editor');
|
|
19
22
|
let volume = $state(30);
|
|
20
23
|
let range = $state({ min: 20, max: 80 });
|
|
21
24
|
let view = $state<string | number>('grid');
|
|
25
|
+
let notes = $state('valid notes');
|
|
26
|
+
let pinned = $state(false);
|
|
27
|
+
$effect(() => {
|
|
28
|
+
accepted = validInitial;
|
|
29
|
+
notifications = validInitial;
|
|
30
|
+
});
|
|
22
31
|
</script>
|
|
23
32
|
|
|
24
33
|
<Form formId="settings" interactionRegistry={registry} aria-label="Settings">
|
|
25
|
-
<Checkbox name="accepted" label="Accept terms" bind:checked={accepted} />
|
|
26
|
-
<Switch name="notifications" label="Notifications" bind:checked={notifications} />
|
|
34
|
+
<Checkbox name="accepted" label="Accept terms" required bind:checked={accepted} />
|
|
35
|
+
<Switch name="notifications" label="Notifications" required bind:checked={notifications} />
|
|
27
36
|
<RadioGroup name="role" label="Role" bind:value={role}>
|
|
28
37
|
<Radio value="editor" label="Editor" />
|
|
29
38
|
<Radio value="viewer" label="Viewer" />
|
|
30
39
|
</RadioGroup>
|
|
31
|
-
<Slider name="volume" label="Volume" bind:value={volume} />
|
|
32
|
-
<RangeSlider name="price" label="Price" bind:value={range} />
|
|
40
|
+
<Slider name="volume" label="Volume" step={5} bind:value={volume} />
|
|
41
|
+
<RangeSlider name="price" label="Price" step={5} bind:value={range} />
|
|
33
42
|
<SegmentedControl
|
|
34
43
|
name="view"
|
|
35
44
|
label="View"
|
|
36
45
|
options={[{ value: 'grid', label: 'Grid' }, { value: 'list', label: 'List' }]}
|
|
37
46
|
bind:value={view}
|
|
38
47
|
/>
|
|
48
|
+
<Textarea name="notes" aria-label="Notes" required minlength={3} bind:value={notes} />
|
|
49
|
+
<ToggleButton name="pinned" aria-label="Pinned" bind:pressed={pinned}>Pinned</ToggleButton>
|
|
39
50
|
</Form>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ControlInteractionRegistry } from '../control-interaction.js';
|
|
2
2
|
interface Props {
|
|
3
3
|
registry: ControlInteractionRegistry;
|
|
4
|
+
validInitial?: boolean;
|
|
4
5
|
}
|
|
5
6
|
declare const CoreControls: import("svelte").Component<Props, {}, "">;
|
|
6
7
|
type CoreControls = ReturnType<typeof CoreControls>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core-controls.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/core-controls.fixture.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"core-controls.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/core-controls.fixture.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAY5E,UAAU,KAAK;IACb,QAAQ,EAAE,0BAA0B,CAAC;IACrC,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AA6CD,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -5,8 +5,10 @@ import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
|
5
5
|
import Progress from '../../feedback/Progress.svelte';
|
|
6
6
|
import Spinner from '../../feedback/Spinner.svelte';
|
|
7
7
|
import { createControlInteractionRegistry } from '../control-interaction.js';
|
|
8
|
+
import { snapSteppedNumber, validatesSteppedNumber, } from '../control-value-validation.js';
|
|
8
9
|
import ErrorSummary from '../ErrorSummary.svelte';
|
|
9
10
|
import Fixture from './core-controls.fixture.svelte';
|
|
11
|
+
import DecimalSlidersFixture from './decimal-sliders.fixture.svelte';
|
|
10
12
|
describe('core controls', () => {
|
|
11
13
|
it('provides distinct checkbox, switch, radio, slider, range, and segmented semantics', async () => {
|
|
12
14
|
const registry = createControlInteractionRegistry();
|
|
@@ -21,8 +23,10 @@ describe('core controls', () => {
|
|
|
21
23
|
await userEvent.click(screen.getByRole('radio', { name: 'List' }));
|
|
22
24
|
expect(screen.getByRole('radio', { name: 'List' })).toHaveAttribute('aria-checked', 'true');
|
|
23
25
|
});
|
|
24
|
-
it('registers all controls and
|
|
25
|
-
const registry = createControlInteractionRegistry(
|
|
26
|
+
it('registers all controls and keeps agent staging separate from human apply', async () => {
|
|
27
|
+
const registry = createControlInteractionRegistry({
|
|
28
|
+
isLocalGesture: () => true,
|
|
29
|
+
});
|
|
26
30
|
render(Fixture, { props: { registry } });
|
|
27
31
|
expect(registry.list('settings').map((item) => item.identity.controlId)).toEqual(expect.arrayContaining([
|
|
28
32
|
'accepted',
|
|
@@ -42,7 +46,224 @@ describe('core controls', () => {
|
|
|
42
46
|
action: 'apply',
|
|
43
47
|
identity: { formId: 'settings', controlId: 'volume' },
|
|
44
48
|
}, { source: 'agent', confirmed: true });
|
|
49
|
+
expect(screen.getByRole('slider', { name: 'Volume' })).toHaveValue('30');
|
|
50
|
+
await userEvent.click(screen.getByRole('button', { name: 'Apply Volume' }));
|
|
51
|
+
expect(screen.getByRole('slider', { name: 'Volume' })).toHaveValue('55');
|
|
52
|
+
await registry.execute({
|
|
53
|
+
action: 'stage',
|
|
54
|
+
identity: { formId: 'settings', controlId: 'volume' },
|
|
55
|
+
value: '',
|
|
56
|
+
}, { source: 'agent' });
|
|
57
|
+
expect(registry.get({ formId: 'settings', controlId: 'volume' })?.state.staged
|
|
58
|
+
?.value).toBe(0);
|
|
59
|
+
await userEvent.click(screen.getByRole('button', { name: 'Apply Volume' }));
|
|
60
|
+
expect(screen.getByRole('slider', { name: 'Volume' })).toHaveValue('0');
|
|
61
|
+
expect(registry.get({ formId: 'settings', controlId: 'volume' })?.state.staged).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
it('canonicalizes scalar proposals before exposing them for review', async () => {
|
|
64
|
+
const registry = createControlInteractionRegistry({
|
|
65
|
+
isLocalGesture: () => true,
|
|
66
|
+
});
|
|
67
|
+
render(Fixture, { props: { registry } });
|
|
68
|
+
for (const controlId of ['accepted', 'notes']) {
|
|
69
|
+
expect(await registry.execute({
|
|
70
|
+
action: 'stage',
|
|
71
|
+
identity: { formId: 'settings', controlId },
|
|
72
|
+
value: { malformed: true },
|
|
73
|
+
}, { source: 'agent' })).toMatchObject({ ok: false, reason: 'staged_value_invalid' });
|
|
74
|
+
expect(registry.get({ formId: 'settings', controlId })?.state.staged).toBeUndefined();
|
|
75
|
+
}
|
|
76
|
+
await registry.execute({
|
|
77
|
+
action: 'stage',
|
|
78
|
+
identity: { formId: 'settings', controlId: 'volume' },
|
|
79
|
+
value: '55',
|
|
80
|
+
}, { source: 'agent' });
|
|
81
|
+
expect(registry.get({ formId: 'settings', controlId: 'volume' })?.state.staged
|
|
82
|
+
?.value).toBe(55);
|
|
83
|
+
await userEvent.click(screen.getByRole('button', { name: 'Apply Volume' }));
|
|
84
|
+
expect(screen.getByRole('slider', { name: 'Volume' })).toHaveValue('55');
|
|
85
|
+
});
|
|
86
|
+
it('applies decimal-step slider proposals without floating-point drift', async () => {
|
|
87
|
+
const registry = createControlInteractionRegistry({
|
|
88
|
+
isLocalGesture: () => true,
|
|
89
|
+
});
|
|
90
|
+
render(DecimalSlidersFixture, { props: { registry } });
|
|
91
|
+
const valueIdentity = { formId: 'decimal', controlId: 'value' };
|
|
92
|
+
const rangeIdentity = { formId: 'decimal', controlId: 'range' };
|
|
93
|
+
const tinyIdentity = { formId: 'decimal', controlId: 'tiny' };
|
|
94
|
+
const microscopicIdentity = {
|
|
95
|
+
formId: 'decimal',
|
|
96
|
+
controlId: 'microscopic',
|
|
97
|
+
};
|
|
98
|
+
const microscopicRangeIdentity = {
|
|
99
|
+
formId: 'decimal',
|
|
100
|
+
controlId: 'microscopic-range',
|
|
101
|
+
};
|
|
102
|
+
const decimalDrift = 0.1 + 0.2;
|
|
103
|
+
const validCases = [
|
|
104
|
+
{ next: 0.3, expected: 0.3, min: 0.1, max: 1, step: 0.2 },
|
|
105
|
+
{ next: decimalDrift, expected: 0.3, min: 0.1, max: 1, step: 0.2 },
|
|
106
|
+
{
|
|
107
|
+
next: 3e-16,
|
|
108
|
+
expected: 3e-16,
|
|
109
|
+
min: 1e-16,
|
|
110
|
+
max: 9e-16,
|
|
111
|
+
step: 2e-16,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
next: 3e-101,
|
|
115
|
+
expected: 3e-101,
|
|
116
|
+
min: 1e-101,
|
|
117
|
+
max: 9e-101,
|
|
118
|
+
step: 2e-101,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
next: 1.12e-101,
|
|
122
|
+
expected: 1.12e-101,
|
|
123
|
+
min: 1e-101,
|
|
124
|
+
max: 1.9e-101,
|
|
125
|
+
step: 3e-103,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
next: Number.MIN_VALUE * 3,
|
|
129
|
+
expected: Number.MIN_VALUE * 3,
|
|
130
|
+
min: Number.MIN_VALUE,
|
|
131
|
+
max: Number.MIN_VALUE * 5,
|
|
132
|
+
step: Number.MIN_VALUE,
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
for (const { next, expected, min, max, step } of validCases) {
|
|
136
|
+
expect(validatesSteppedNumber(next, min, max, step)).toBe(true);
|
|
137
|
+
const snapped = snapSteppedNumber(next, min, max, step);
|
|
138
|
+
expect(snapped).toBe(expected);
|
|
139
|
+
expect(validatesSteppedNumber(snapped, min, max, step)).toBe(true);
|
|
140
|
+
expect(snapSteppedNumber(snapped, min, max, step)).toBe(snapped);
|
|
141
|
+
}
|
|
142
|
+
for (const { min, max, step } of [
|
|
143
|
+
{ min: 0.1, max: 0.9, step: 0.2 },
|
|
144
|
+
{ min: 1e-16, max: 9e-16, step: 2e-16 },
|
|
145
|
+
{ min: 1e-101, max: 1.9e-101, step: 3e-103 },
|
|
146
|
+
{
|
|
147
|
+
min: Number.MIN_VALUE,
|
|
148
|
+
max: Number.MIN_VALUE * 5,
|
|
149
|
+
step: Number.MIN_VALUE,
|
|
150
|
+
},
|
|
151
|
+
]) {
|
|
152
|
+
const lastIndex = Math.round((max - min) / step);
|
|
153
|
+
for (let index = 0; index <= lastIndex; index += 1) {
|
|
154
|
+
const validValue = min + index * step;
|
|
155
|
+
expect(validatesSteppedNumber(validValue, min, max, step)).toBe(true);
|
|
156
|
+
const canonical = snapSteppedNumber(validValue, min, max, step);
|
|
157
|
+
expect(validatesSteppedNumber(canonical, min, max, step)).toBe(true);
|
|
158
|
+
expect(snapSteppedNumber(canonical, min, max, step)).toBe(canonical);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await registry.execute({ action: 'stage', identity: valueIdentity, value: decimalDrift }, { source: 'agent' });
|
|
162
|
+
await registry.execute({ action: 'stage', identity: tinyIdentity, value: 3e-16 }, { source: 'agent' });
|
|
163
|
+
await registry.execute({ action: 'stage', identity: microscopicIdentity, value: 1.12e-101 }, { source: 'agent' });
|
|
164
|
+
await registry.execute({
|
|
165
|
+
action: 'stage',
|
|
166
|
+
identity: microscopicRangeIdentity,
|
|
167
|
+
value: { min: 1.12e-101, max: 1.9e-101 },
|
|
168
|
+
}, { source: 'agent' });
|
|
169
|
+
await registry.execute({
|
|
170
|
+
action: 'stage',
|
|
171
|
+
identity: rangeIdentity,
|
|
172
|
+
value: { min: decimalDrift, max: 0.5 },
|
|
173
|
+
}, { source: 'agent' });
|
|
174
|
+
expect(registry.get(valueIdentity)?.state.staged).toMatchObject({
|
|
175
|
+
value: 0.3,
|
|
176
|
+
valid: true,
|
|
177
|
+
});
|
|
178
|
+
expect(registry.get(rangeIdentity)?.state.staged).toMatchObject({
|
|
179
|
+
value: { min: 0.3, max: 0.5 },
|
|
180
|
+
valid: true,
|
|
181
|
+
});
|
|
182
|
+
expect(registry.get(tinyIdentity)?.state.staged).toMatchObject({
|
|
183
|
+
value: 3e-16,
|
|
184
|
+
valid: true,
|
|
185
|
+
});
|
|
186
|
+
expect(registry.get(microscopicIdentity)?.state.staged).toMatchObject({
|
|
187
|
+
value: 1.12e-101,
|
|
188
|
+
valid: true,
|
|
189
|
+
});
|
|
190
|
+
expect(registry.get(microscopicRangeIdentity)?.state.staged).toMatchObject({
|
|
191
|
+
value: { min: 1.12e-101, max: 1.9e-101 },
|
|
192
|
+
valid: true,
|
|
193
|
+
});
|
|
194
|
+
await userEvent.click(screen.getByRole('button', { name: 'Apply Value' }));
|
|
195
|
+
await userEvent.click(screen.getByRole('button', { name: 'Apply Range' }));
|
|
196
|
+
await userEvent.click(screen.getByRole('button', { name: 'Apply Tiny value' }));
|
|
197
|
+
await userEvent.click(screen.getByRole('button', { name: 'Apply Microscopic value' }));
|
|
198
|
+
await userEvent.click(screen.getByRole('button', { name: 'Apply Microscopic range' }));
|
|
199
|
+
expect(registry.get(valueIdentity)?.state).toMatchObject({ value: 0.3 });
|
|
200
|
+
expect(registry.get(rangeIdentity)?.state).toMatchObject({
|
|
201
|
+
value: { min: 0.3, max: 0.5 },
|
|
202
|
+
});
|
|
203
|
+
expect(registry.get(tinyIdentity)?.state).toMatchObject({ value: 3e-16 });
|
|
204
|
+
expect(registry.get(microscopicIdentity)?.state).toMatchObject({
|
|
205
|
+
value: 1.12e-101,
|
|
206
|
+
});
|
|
207
|
+
expect(registry.get(microscopicRangeIdentity)?.state).toMatchObject({
|
|
208
|
+
value: { min: 1.12e-101, max: 1.9e-101 },
|
|
209
|
+
});
|
|
210
|
+
expect(registry.get(valueIdentity)?.state.staged).toBeUndefined();
|
|
211
|
+
expect(registry.get(rangeIdentity)?.state.staged).toBeUndefined();
|
|
212
|
+
expect(registry.get(tinyIdentity)?.state.staged).toBeUndefined();
|
|
213
|
+
expect(registry.get(microscopicIdentity)?.state.staged).toBeUndefined();
|
|
214
|
+
expect(registry.get(microscopicRangeIdentity)?.state.staged).toBeUndefined();
|
|
215
|
+
expect(screen.getByRole('slider', { name: 'Value' })).toHaveValue('0.3');
|
|
216
|
+
expect(screen.getByRole('spinbutton', { name: 'Value value' })).toHaveValue(0.3);
|
|
217
|
+
expect(screen.getByRole('slider', { name: 'Minimum' })).toHaveValue('0.3');
|
|
218
|
+
expect(screen.getByRole('slider', { name: 'Maximum' })).toHaveValue('0.5');
|
|
219
|
+
expect(screen.getByRole('slider', { name: 'Tiny value' })).toHaveValue('3e-16');
|
|
220
|
+
expect(screen.getByRole('slider', { name: 'Microscopic value' })).toHaveValue('1.12e-101');
|
|
221
|
+
expect(screen.getByRole('slider', { name: 'Microscopic minimum' })).toHaveValue('1.12e-101');
|
|
222
|
+
expect(screen.getByRole('slider', { name: 'Microscopic maximum' })).toHaveValue('1.9e-101');
|
|
223
|
+
});
|
|
224
|
+
it('marks constrained proposals invalid before the first valid-only batch', async () => {
|
|
225
|
+
const registry = createControlInteractionRegistry({
|
|
226
|
+
isLocalGesture: () => true,
|
|
227
|
+
});
|
|
228
|
+
render(Fixture, { props: { registry, validInitial: true } });
|
|
229
|
+
const invalid = new Map([
|
|
230
|
+
['accepted', false],
|
|
231
|
+
['notifications', false],
|
|
232
|
+
['role', 'administrator'],
|
|
233
|
+
['price', { min: 20, max: 83 }],
|
|
234
|
+
['view', 'cards'],
|
|
235
|
+
['notes', 'x'],
|
|
236
|
+
['pinned', 'yes'],
|
|
237
|
+
]);
|
|
238
|
+
for (const [controlId, value] of invalid) {
|
|
239
|
+
await registry.execute({
|
|
240
|
+
action: 'stage',
|
|
241
|
+
identity: { formId: 'settings', controlId },
|
|
242
|
+
value,
|
|
243
|
+
}, { source: 'agent' });
|
|
244
|
+
expect(registry.get({ formId: 'settings', controlId })?.state.staged?.valid).toBe(false);
|
|
245
|
+
}
|
|
246
|
+
await registry.execute({
|
|
247
|
+
action: 'stage',
|
|
248
|
+
identity: { formId: 'settings', controlId: 'volume' },
|
|
249
|
+
value: 12,
|
|
250
|
+
}, { source: 'agent' });
|
|
251
|
+
expect(registry.get({ formId: 'settings', controlId: 'volume' })?.state.staged
|
|
252
|
+
?.valid).toBe(false);
|
|
253
|
+
await registry.execute({
|
|
254
|
+
action: 'stage',
|
|
255
|
+
identity: { formId: 'settings', controlId: 'volume' },
|
|
256
|
+
value: 55,
|
|
257
|
+
}, { source: 'agent' });
|
|
258
|
+
expect(registry.get({ formId: 'settings', controlId: 'volume' })?.state.staged
|
|
259
|
+
?.valid).toBe(true);
|
|
260
|
+
expect(await screen.findAllByRole('alert')).toHaveLength(invalid.size);
|
|
261
|
+
await userEvent.click(screen.getByRole('button', { name: 'Apply valid changes' }));
|
|
45
262
|
expect(screen.getByRole('slider', { name: 'Volume' })).toHaveValue('55');
|
|
263
|
+
expect(registry.get({ formId: 'settings', controlId: 'volume' })?.state.staged).toBeUndefined();
|
|
264
|
+
for (const controlId of invalid.keys()) {
|
|
265
|
+
expect(registry.get({ formId: 'settings', controlId })?.state.staged?.valid).toBe(false);
|
|
266
|
+
}
|
|
46
267
|
});
|
|
47
268
|
it('is axe-clean as a composed form', async () => {
|
|
48
269
|
const registry = createControlInteractionRegistry();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ControlInteractionRegistry } from '../control-interaction.js';
|
|
3
|
+
import Form from '../Form.svelte';
|
|
4
|
+
import RangeSlider from '../RangeSlider.svelte';
|
|
5
|
+
import Slider from '../Slider.svelte';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
registry: ControlInteractionRegistry;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { registry }: Props = $props();
|
|
12
|
+
let value = $state(0.1);
|
|
13
|
+
let range = $state({ min: 0.1, max: 0.5 });
|
|
14
|
+
let tinyValue = $state(1e-16);
|
|
15
|
+
let microscopicValue = $state(1e-101);
|
|
16
|
+
let microscopicRange = $state({ min: 1e-101, max: 1.9e-101 });
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<Form formId="decimal" interactionRegistry={registry} aria-label="Decimal controls">
|
|
20
|
+
<Slider name="value" label="Value" min={0.1} max={1} step={0.2} bind:value />
|
|
21
|
+
<RangeSlider name="range" label="Range" min={0.1} max={1} step={0.2} bind:value={range} />
|
|
22
|
+
<Slider name="tiny" label="Tiny value" min={1e-16} max={9e-16} step={2e-16} bind:value={tinyValue} />
|
|
23
|
+
<Slider name="microscopic" label="Microscopic value" min={1e-101} max={1.9e-101} step={3e-103} bind:value={microscopicValue} />
|
|
24
|
+
<RangeSlider name="microscopic-range" label="Microscopic range" minLabel="Microscopic minimum" maxLabel="Microscopic maximum" min={1e-101} max={1.9e-101} step={3e-103} bind:value={microscopicRange} />
|
|
25
|
+
</Form>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ControlInteractionRegistry } from '../control-interaction.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
registry: ControlInteractionRegistry;
|
|
4
|
+
}
|
|
5
|
+
declare const DecimalSliders: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type DecimalSliders = ReturnType<typeof DecimalSliders>;
|
|
7
|
+
export default DecimalSliders;
|
|
8
|
+
//# sourceMappingURL=decimal-sliders.fixture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decimal-sliders.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/decimal-sliders.fixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAM5E,UAAU,KAAK;IACb,QAAQ,EAAE,0BAA0B,CAAC;CACtC;AA0BD,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ControlInteractionRegistry } from '../control-interaction.js';
|
|
3
|
+
import Form from '../Form.svelte';
|
|
4
|
+
import Select from '../Select.svelte';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
registry,
|
|
8
|
+
required = false,
|
|
9
|
+
}: {
|
|
10
|
+
registry: ControlInteractionRegistry;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
} = $props();
|
|
13
|
+
let role = $state('user');
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<Form formId="account" interactionRegistry={registry} aria-label="Account form">
|
|
17
|
+
<Select name="role" aria-label="Role" {required} bind:value={role}>
|
|
18
|
+
<option value="">Choose a role</option>
|
|
19
|
+
<option value="user">User</option>
|
|
20
|
+
<option value="admin" disabled>Administrator</option>
|
|
21
|
+
<optgroup label="Unavailable roles" disabled>
|
|
22
|
+
<option value="owner">Owner</option>
|
|
23
|
+
</optgroup>
|
|
24
|
+
</Select>
|
|
25
|
+
</Form>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ControlInteractionRegistry } from '../control-interaction.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
registry: ControlInteractionRegistry;
|
|
4
|
+
required?: boolean;
|
|
5
|
+
};
|
|
6
|
+
declare const SelectInteraction: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type SelectInteraction = ReturnType<typeof SelectInteraction>;
|
|
8
|
+
export default SelectInteraction;
|
|
9
|
+
//# sourceMappingURL=select-interaction.fixture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select-interaction.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/select-interaction.fixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAI3E,KAAK,gBAAgB,GAAI;IACxB,QAAQ,EAAE,0BAA0B,CAAC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AA0BF,QAAA,MAAM,iBAAiB,sDAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ControlInteractionRegistry } from '../control-interaction.js';
|
|
3
|
+
import Form from '../Form.svelte';
|
|
4
|
+
import FormGroup from '../FormGroup.svelte';
|
|
5
|
+
import Input from '../Input.svelte';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
registry,
|
|
9
|
+
}: {
|
|
10
|
+
registry: ControlInteractionRegistry;
|
|
11
|
+
} = $props();
|
|
12
|
+
let displayName = $state('Ada');
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<Form formId="profile" interactionRegistry={registry} aria-label="Profile form">
|
|
16
|
+
<fieldset>
|
|
17
|
+
<FormGroup label="Display name">
|
|
18
|
+
<Input name="display-name" bind:value={displayName} required minlength={3} />
|
|
19
|
+
</FormGroup>
|
|
20
|
+
</fieldset>
|
|
21
|
+
</Form>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ControlInteractionRegistry } from '../control-interaction.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
registry: ControlInteractionRegistry;
|
|
4
|
+
};
|
|
5
|
+
declare const StagedReviewFieldset: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type StagedReviewFieldset = ReturnType<typeof StagedReviewFieldset>;
|
|
7
|
+
export default StagedReviewFieldset;
|
|
8
|
+
//# sourceMappingURL=staged-review-fieldset.fixture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staged-review-fieldset.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/staged-review-fieldset.fixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAK3E,KAAK,gBAAgB,GAAI;IACxB,QAAQ,EAAE,0BAA0B,CAAC;CACtC,CAAC;AAuBF,QAAA,MAAM,oBAAoB,sDAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ControlInteractionRegistry } from '../control-interaction.js';
|
|
3
|
+
import Form from '../Form.svelte';
|
|
4
|
+
import FormGroup from '../FormGroup.svelte';
|
|
5
|
+
import Input from '../Input.svelte';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
registry,
|
|
9
|
+
}: {
|
|
10
|
+
registry: ControlInteractionRegistry;
|
|
11
|
+
} = $props();
|
|
12
|
+
let displayName = $state('Ada');
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<fieldset>
|
|
16
|
+
<fieldset>
|
|
17
|
+
<Form formId="profile" interactionRegistry={registry} aria-label="Profile form">
|
|
18
|
+
<FormGroup label="Display name">
|
|
19
|
+
<Input name="display-name" bind:value={displayName} required minlength={3} />
|
|
20
|
+
</FormGroup>
|
|
21
|
+
</Form>
|
|
22
|
+
</fieldset>
|
|
23
|
+
</fieldset>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ControlInteractionRegistry } from '../control-interaction.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
registry: ControlInteractionRegistry;
|
|
4
|
+
};
|
|
5
|
+
declare const StagedReviewOuterFieldset: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type StagedReviewOuterFieldset = ReturnType<typeof StagedReviewOuterFieldset>;
|
|
7
|
+
export default StagedReviewOuterFieldset;
|
|
8
|
+
//# sourceMappingURL=staged-review-outer-fieldset.fixture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staged-review-outer-fieldset.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAK3E,KAAK,gBAAgB,GAAI;IACxB,QAAQ,EAAE,0BAA0B,CAAC;CACtC,CAAC;AAyBF,QAAA,MAAM,yBAAyB,sDAAwC,CAAC;AACxE,KAAK,yBAAyB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC9E,eAAe,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
ControlInteractionRegistry,
|
|
4
|
+
ControlSubject,
|
|
5
|
+
} from '../control-interaction.js';
|
|
6
|
+
import Form from '../Form.svelte';
|
|
7
|
+
import FormGroup from '../FormGroup.svelte';
|
|
8
|
+
import Input from '../Input.svelte';
|
|
9
|
+
import Slider from '../Slider.svelte';
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
registry,
|
|
13
|
+
subject,
|
|
14
|
+
displayNameLabel = 'Display name',
|
|
15
|
+
formOnInput,
|
|
16
|
+
formId = 'profile',
|
|
17
|
+
}: {
|
|
18
|
+
registry: ControlInteractionRegistry;
|
|
19
|
+
subject?: ControlSubject;
|
|
20
|
+
displayNameLabel?: string;
|
|
21
|
+
formOnInput?: (event: Event) => void;
|
|
22
|
+
formId?: string;
|
|
23
|
+
} = $props();
|
|
24
|
+
let displayName = $state('Ada');
|
|
25
|
+
let apiToken = $state('token');
|
|
26
|
+
let volume = $state(25);
|
|
27
|
+
let score = $state<string | number>(1);
|
|
28
|
+
let birthday = $state('');
|
|
29
|
+
let meetingTime = $state('');
|
|
30
|
+
let intensity = $state(50);
|
|
31
|
+
let mutableSubject = $state<ControlSubject | undefined>();
|
|
32
|
+
$effect(() => {
|
|
33
|
+
mutableSubject = subject ? { ...subject } : undefined;
|
|
34
|
+
});
|
|
35
|
+
function mutateSubject() {
|
|
36
|
+
if (mutableSubject) mutableSubject.id = 'two';
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<Form {formId} interactionRegistry={registry} aria-label="Profile form" oninput={formOnInput}>
|
|
41
|
+
<FormGroup label={displayNameLabel}>
|
|
42
|
+
<Input name="display-name" interaction={mutableSubject ? { subject: mutableSubject } : undefined} bind:value={displayName} required minlength={3} />
|
|
43
|
+
</FormGroup>
|
|
44
|
+
<FormGroup label="API token" interaction={{ sensitivity: 'secret' }}>
|
|
45
|
+
<Input name="api-token" type="password" bind:value={apiToken} />
|
|
46
|
+
</FormGroup>
|
|
47
|
+
<Slider name="volume" label="Volume" bind:value={volume} />
|
|
48
|
+
<Input name="score" aria-label="Score" type="number" bind:value={score} />
|
|
49
|
+
<Input name="birthday" aria-label="Birthday" type="date" bind:value={birthday} />
|
|
50
|
+
<Input name="meeting-time" aria-label="Meeting time" type="time" bind:value={meetingTime} />
|
|
51
|
+
<Input name="intensity" aria-label="Intensity" type="range" min="10" max="90" bind:value={intensity} />
|
|
52
|
+
<button type="reset">Reset</button>
|
|
53
|
+
{#if mutableSubject}
|
|
54
|
+
<button type="button" onclick={mutateSubject}>Mutate subject</button>
|
|
55
|
+
{/if}
|
|
56
|
+
</Form>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ControlInteractionRegistry, ControlSubject } from '../control-interaction.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
registry: ControlInteractionRegistry;
|
|
4
|
+
subject?: ControlSubject;
|
|
5
|
+
displayNameLabel?: string;
|
|
6
|
+
formOnInput?: (event: Event) => void;
|
|
7
|
+
formId?: string;
|
|
8
|
+
};
|
|
9
|
+
declare const StagedReview: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type StagedReview = ReturnType<typeof StagedReview>;
|
|
11
|
+
export default StagedReview;
|
|
12
|
+
//# sourceMappingURL=staged-review.fixture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staged-review.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/staged-review.fixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,0BAA0B,EAC1B,cAAc,EACf,MAAM,2BAA2B,CAAC;AAMlC,KAAK,gBAAgB,GAAI;IACxB,QAAQ,EAAE,0BAA0B,CAAC;IACrC,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAmDF,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ControlInteractionRegistry } from './control-interaction.js';
|
|
1
|
+
import type { ControlInteractionRegistry, ControlSubject } from './control-interaction.js';
|
|
2
2
|
export interface ControlInteractionContextValue {
|
|
3
3
|
readonly formId: string;
|
|
4
4
|
readonly registry: ControlInteractionRegistry;
|
|
@@ -6,4 +6,12 @@ export interface ControlInteractionContextValue {
|
|
|
6
6
|
export declare function setControlInteractionContext(context: ControlInteractionContextValue): void;
|
|
7
7
|
export declare function tryGetControlInteractionContext(): ControlInteractionContextValue | undefined;
|
|
8
8
|
export declare function getControlInteractionContext(): ControlInteractionContextValue;
|
|
9
|
+
/**
|
|
10
|
+
* Records an edit made by a composite control's own user-event handler.
|
|
11
|
+
*
|
|
12
|
+
* Native inputs bubble trusted `input`/`change` events to their Form host.
|
|
13
|
+
* Composite controls must use this only after a user action has actually
|
|
14
|
+
* changed their value; programmatic setters deliberately do not call it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function recordControlUserEdit(context: ControlInteractionContextValue | undefined, controlId: string | undefined, subject: ControlSubject | undefined): void;
|
|
9
17
|
//# sourceMappingURL=control-interaction-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control-interaction-context.d.ts","sourceRoot":"","sources":["../../../src/components/forms/control-interaction-context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"control-interaction-context.d.ts","sourceRoot":"","sources":["../../../src/components/forms/control-interaction-context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,0BAA0B,EAC1B,cAAc,EACf,MAAM,0BAA0B,CAAC;AAIlC,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,0BAA0B,CAAC;CAC/C;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,8BAA8B,GACtC,IAAI,CAEN;AAED,wBAAgB,+BAA+B,IAC3C,8BAA8B,GAC9B,SAAS,CAIZ;AAED,wBAAgB,4BAA4B,IAAI,8BAA8B,CAQ7E;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,8BAA8B,GAAG,SAAS,EACnD,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,OAAO,EAAE,cAAc,GAAG,SAAS,GAClC,IAAI,CAON"}
|
|
@@ -13,3 +13,19 @@ export function getControlInteractionContext() {
|
|
|
13
13
|
}
|
|
14
14
|
return context;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Records an edit made by a composite control's own user-event handler.
|
|
18
|
+
*
|
|
19
|
+
* Native inputs bubble trusted `input`/`change` events to their Form host.
|
|
20
|
+
* Composite controls must use this only after a user action has actually
|
|
21
|
+
* changed their value; programmatic setters deliberately do not call it.
|
|
22
|
+
*/
|
|
23
|
+
export function recordControlUserEdit(context, controlId, subject) {
|
|
24
|
+
if (!context || !controlId)
|
|
25
|
+
return;
|
|
26
|
+
context.registry.recordUserEdit?.({
|
|
27
|
+
formId: context.formId,
|
|
28
|
+
controlId,
|
|
29
|
+
subject,
|
|
30
|
+
});
|
|
31
|
+
}
|