@happyvertical/smrt-ui 0.52.0 → 0.53.1

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 (208) hide show
  1. package/AGENTS.md +11 -1
  2. package/README.md +300 -2
  3. package/dist/components/data/DataTable.svelte +60 -15
  4. package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
  5. package/dist/components/data/DataTableResponsive.d.ts +4 -0
  6. package/dist/components/data/DataTableResponsive.d.ts.map +1 -0
  7. package/dist/components/data/DataTableResponsive.js +22 -0
  8. package/dist/components/data/__tests__/DataTableResponsive.test.js +80 -0
  9. package/dist/components/data/__tests__/responsive-table.fixture.svelte +29 -0
  10. package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +7 -0
  11. package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +1 -0
  12. package/dist/components/data/types.d.ts +8 -0
  13. package/dist/components/data/types.d.ts.map +1 -1
  14. package/dist/components/display/DateDisplay.svelte +76 -14
  15. package/dist/components/display/DateDisplay.svelte.d.ts +2 -0
  16. package/dist/components/display/DateDisplay.svelte.d.ts.map +1 -1
  17. package/dist/components/display/Icon.svelte +3 -16
  18. package/dist/components/display/Icon.svelte.d.ts +0 -9
  19. package/dist/components/display/Icon.svelte.d.ts.map +1 -1
  20. package/dist/components/display/StatusBadge.svelte +29 -2
  21. package/dist/components/display/StatusBadge.svelte.d.ts +4 -2
  22. package/dist/components/display/StatusBadge.svelte.d.ts.map +1 -1
  23. package/dist/components/display/__tests__/DateDisplay.test.js +82 -1
  24. package/dist/components/display/__tests__/Icon.test.js +132 -2
  25. package/dist/components/display/__tests__/StatusBadge.test.js +78 -0
  26. package/dist/components/display/icons.svelte.d.ts +11 -0
  27. package/dist/components/display/icons.svelte.d.ts.map +1 -0
  28. package/dist/components/display/icons.svelte.js +60 -0
  29. package/dist/components/display/index.d.ts +3 -1
  30. package/dist/components/display/index.d.ts.map +1 -1
  31. package/dist/components/display/index.js +1 -0
  32. package/dist/components/display/types.d.ts +2 -0
  33. package/dist/components/display/types.d.ts.map +1 -1
  34. package/dist/components/feedback/ConfirmDialog.svelte +59 -48
  35. package/dist/components/feedback/ConfirmDialog.svelte.d.ts +14 -2
  36. package/dist/components/feedback/ConfirmDialog.svelte.d.ts.map +1 -1
  37. package/dist/components/feedback/ToastViewport.svelte +156 -8
  38. package/dist/components/feedback/ToastViewport.svelte.d.ts +5 -1
  39. package/dist/components/feedback/ToastViewport.svelte.d.ts.map +1 -1
  40. package/dist/components/feedback/__tests__/ConfirmDialog.test.js +38 -20
  41. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +9 -0
  42. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +19 -0
  43. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +1 -0
  44. package/dist/components/feedback/__tests__/toast.test.js +142 -1
  45. package/dist/components/feedback/index.d.ts +1 -1
  46. package/dist/components/feedback/index.d.ts.map +1 -1
  47. package/dist/components/feedback/toast.d.ts +13 -1
  48. package/dist/components/feedback/toast.d.ts.map +1 -1
  49. package/dist/components/feedback/toast.js +51 -10
  50. package/dist/components/forms/CameraCapture.svelte +619 -0
  51. package/dist/components/forms/CameraCapture.svelte.d.ts +42 -0
  52. package/dist/components/forms/CameraCapture.svelte.d.ts.map +1 -0
  53. package/dist/components/forms/Checkbox.svelte +8 -1
  54. package/dist/components/forms/Checkbox.svelte.d.ts +2 -0
  55. package/dist/components/forms/Checkbox.svelte.d.ts.map +1 -1
  56. package/dist/components/forms/Combobox.svelte +40 -0
  57. package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
  58. package/dist/components/forms/Form.svelte +5 -0
  59. package/dist/components/forms/Form.svelte.d.ts +1 -0
  60. package/dist/components/forms/Form.svelte.d.ts.map +1 -1
  61. package/dist/components/forms/Input.svelte +8 -1
  62. package/dist/components/forms/Input.svelte.d.ts +2 -0
  63. package/dist/components/forms/Input.svelte.d.ts.map +1 -1
  64. package/dist/components/forms/Listbox.svelte +39 -1
  65. package/dist/components/forms/Listbox.svelte.d.ts.map +1 -1
  66. package/dist/components/forms/MultiSelect.svelte +41 -1
  67. package/dist/components/forms/MultiSelect.svelte.d.ts.map +1 -1
  68. package/dist/components/forms/Radio.svelte +1 -0
  69. package/dist/components/forms/RadioGroup.svelte +7 -1
  70. package/dist/components/forms/RadioGroup.svelte.d.ts +2 -0
  71. package/dist/components/forms/RadioGroup.svelte.d.ts.map +1 -1
  72. package/dist/components/forms/SegmentedControl.svelte +152 -11
  73. package/dist/components/forms/SegmentedControl.svelte.d.ts +4 -0
  74. package/dist/components/forms/SegmentedControl.svelte.d.ts.map +1 -1
  75. package/dist/components/forms/Select.svelte +8 -1
  76. package/dist/components/forms/Select.svelte.d.ts +2 -0
  77. package/dist/components/forms/Select.svelte.d.ts.map +1 -1
  78. package/dist/components/forms/SignaturePad.svelte +436 -0
  79. package/dist/components/forms/SignaturePad.svelte.d.ts +32 -0
  80. package/dist/components/forms/SignaturePad.svelte.d.ts.map +1 -0
  81. package/dist/components/forms/Switch.svelte +8 -1
  82. package/dist/components/forms/Switch.svelte.d.ts +2 -0
  83. package/dist/components/forms/Switch.svelte.d.ts.map +1 -1
  84. package/dist/components/forms/Textarea.svelte +8 -1
  85. package/dist/components/forms/Textarea.svelte.d.ts +2 -0
  86. package/dist/components/forms/Textarea.svelte.d.ts.map +1 -1
  87. package/dist/components/forms/__tests__/CameraCapture.test.js +504 -0
  88. package/dist/components/forms/__tests__/Combobox.test.js +279 -12
  89. package/dist/components/forms/__tests__/Form.test.js +46 -0
  90. package/dist/components/forms/__tests__/Listbox.test.js +303 -0
  91. package/dist/components/forms/__tests__/MultiSelect.test.js +281 -0
  92. package/dist/components/forms/__tests__/SignaturePad.test.js +338 -0
  93. package/dist/components/forms/__tests__/camera-capture-session.test.js +207 -0
  94. package/dist/components/forms/__tests__/capture-form-retry.test.js +166 -0
  95. package/dist/components/forms/__tests__/capture-form.fixture.svelte +38 -0
  96. package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +10 -0
  97. package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +1 -0
  98. package/dist/components/forms/__tests__/capture-test-env.d.ts +70 -0
  99. package/dist/components/forms/__tests__/capture-test-env.d.ts.map +1 -0
  100. package/dist/components/forms/__tests__/capture-test-env.js +122 -0
  101. package/dist/components/forms/__tests__/capture-touch.fixture.svelte +15 -0
  102. package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +8 -0
  103. package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +1 -0
  104. package/dist/components/forms/__tests__/combobox-form.fixture.svelte +29 -0
  105. package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +14 -0
  106. package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +1 -0
  107. package/dist/components/forms/__tests__/form-attachment.fixture.svelte +28 -0
  108. package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +11 -0
  109. package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +1 -0
  110. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +31 -0
  111. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +13 -0
  112. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +1 -0
  113. package/dist/components/forms/__tests__/form-retry-form.test.js +107 -0
  114. package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +69 -0
  115. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +29 -0
  116. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +9 -0
  117. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +1 -0
  118. package/dist/components/forms/__tests__/listbox-form.fixture.svelte +27 -0
  119. package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +13 -0
  120. package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +1 -0
  121. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +27 -0
  122. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +13 -0
  123. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +1 -0
  124. package/dist/components/forms/__tests__/native-file-field.test.js +177 -0
  125. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +28 -0
  126. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +12 -0
  127. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +1 -0
  128. package/dist/components/forms/__tests__/segmented-posting.test.js +92 -0
  129. package/dist/components/forms/__tests__/signature-pad-logic.test.js +54 -0
  130. package/dist/components/forms/__tests__/touch-density.fixture.svelte +34 -0
  131. package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +8 -0
  132. package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +1 -0
  133. package/dist/components/forms/__tests__/touch-density.test.js +66 -0
  134. package/dist/components/forms/camera-capture-session.d.ts +81 -0
  135. package/dist/components/forms/camera-capture-session.d.ts.map +1 -0
  136. package/dist/components/forms/camera-capture-session.js +130 -0
  137. package/dist/components/forms/form-retry/__tests__/boundaries.test.js +23 -0
  138. package/dist/components/forms/form-retry/__tests__/content-digest.test.js +70 -0
  139. package/dist/components/forms/form-retry/__tests__/controller.test.js +802 -0
  140. package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +82 -0
  141. package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +1 -0
  142. package/dist/components/forms/form-retry/__tests__/fake-kit.js +180 -0
  143. package/dist/components/forms/form-retry/__tests__/form-draft.test.js +169 -0
  144. package/dist/components/forms/form-retry/__tests__/ssr.test.js +37 -0
  145. package/dist/components/forms/form-retry/__tests__/storage.d.ts +26 -0
  146. package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +1 -0
  147. package/dist/components/forms/form-retry/__tests__/storage.js +63 -0
  148. package/dist/components/forms/form-retry/__tests__/submission-key.test.js +236 -0
  149. package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +334 -0
  150. package/dist/components/forms/form-retry/content-digest.d.ts +54 -0
  151. package/dist/components/forms/form-retry/content-digest.d.ts.map +1 -0
  152. package/dist/components/forms/form-retry/content-digest.js +169 -0
  153. package/dist/components/forms/form-retry/controller.d.ts +214 -0
  154. package/dist/components/forms/form-retry/controller.d.ts.map +1 -0
  155. package/dist/components/forms/form-retry/controller.js +404 -0
  156. package/dist/components/forms/form-retry/form-draft.d.ts +68 -0
  157. package/dist/components/forms/form-retry/form-draft.d.ts.map +1 -0
  158. package/dist/components/forms/form-retry/form-draft.js +161 -0
  159. package/dist/components/forms/form-retry/index.d.ts +19 -0
  160. package/dist/components/forms/form-retry/index.d.ts.map +1 -0
  161. package/dist/components/forms/form-retry/index.js +16 -0
  162. package/dist/components/forms/form-retry/submission-key.d.ts +244 -0
  163. package/dist/components/forms/form-retry/submission-key.d.ts.map +1 -0
  164. package/dist/components/forms/form-retry/submission-key.js +342 -0
  165. package/dist/components/forms/form-retry/submitted-draft.d.ts +112 -0
  166. package/dist/components/forms/form-retry/submitted-draft.d.ts.map +1 -0
  167. package/dist/components/forms/form-retry/submitted-draft.js +270 -0
  168. package/dist/components/forms/form-retry/types.d.ts +84 -0
  169. package/dist/components/forms/form-retry/types.d.ts.map +1 -0
  170. package/dist/components/forms/form-retry/types.js +13 -0
  171. package/dist/components/forms/index.d.ts +7 -1
  172. package/dist/components/forms/index.d.ts.map +1 -1
  173. package/dist/components/forms/index.js +8 -0
  174. package/dist/components/forms/native-file-field.d.ts +94 -0
  175. package/dist/components/forms/native-file-field.d.ts.map +1 -0
  176. package/dist/components/forms/native-file-field.js +147 -0
  177. package/dist/components/forms/signature-pad-logic.d.ts +50 -0
  178. package/dist/components/forms/signature-pad-logic.d.ts.map +1 -0
  179. package/dist/components/forms/signature-pad-logic.js +48 -0
  180. package/dist/components/forms/types.d.ts +41 -0
  181. package/dist/components/forms/types.d.ts.map +1 -1
  182. package/dist/components/nav/FilterChips.svelte +8 -1
  183. package/dist/components/nav/FilterChips.svelte.d.ts +2 -0
  184. package/dist/components/nav/FilterChips.svelte.d.ts.map +1 -1
  185. package/dist/components/ui/Button.svelte +9 -2
  186. package/dist/components/ui/Button.svelte.d.ts +2 -0
  187. package/dist/components/ui/Button.svelte.d.ts.map +1 -1
  188. package/dist/i18n/strings.ui.d.ts +24 -0
  189. package/dist/i18n/strings.ui.d.ts.map +1 -1
  190. package/dist/i18n/strings.ui.js +26 -0
  191. package/dist/styles/tokens.css +2 -0
  192. package/dist/svelte/__tests__/playground.test.js +2 -1
  193. package/dist/svelte/playground/CapturePreview.svelte +56 -0
  194. package/dist/svelte/playground/CapturePreview.svelte.d.ts +4 -0
  195. package/dist/svelte/playground/CapturePreview.svelte.d.ts.map +1 -0
  196. package/dist/svelte/playground.d.ts.map +1 -1
  197. package/dist/svelte/playground.js +10 -0
  198. package/dist/themes/ThemeProvider.svelte +9 -0
  199. package/dist/themes/ThemeProvider.svelte.d.ts +2 -0
  200. package/dist/themes/ThemeProvider.svelte.d.ts.map +1 -1
  201. package/dist/themes/css-generator.d.ts.map +1 -1
  202. package/dist/themes/css-generator.js +3 -0
  203. package/dist/themes/styles/glass.css +4 -0
  204. package/dist/themes/styles/happyvertical.css +4 -0
  205. package/dist/themes/styles/material.css +4 -0
  206. package/dist/themes/styles/smrt.css +4 -0
  207. package/dist/themes/styles/studio.css +4 -0
  208. package/package.json +8 -2
@@ -0,0 +1,107 @@
1
+ /**
2
+ * `createFormRetry()` with smrt-ui's own `Form` (#3291, deferred on #3265).
3
+ *
4
+ * `Form` forwards Svelte attachments to its native `<form>` (#3265, shipped in
5
+ * #3331), so the pairing is SvelteKit's `enhance` through `fromAction` with
6
+ * the retry submit function, plus `retry.attach` for the hidden key field.
7
+ * `enhance` is the structural fake from the form-retry tests, attached as the
8
+ * real action and driven by real submit events; the action behind it records
9
+ * rows the way `runOnce()` does.
10
+ */
11
+ import { fireEvent, render, screen } from '@testing-library/svelte';
12
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
13
+ import { fakeEnhanceAction, fakeRunOnceServer, } from '../form-retry/__tests__/fake-kit.js';
14
+ import { memoryStorage } from '../form-retry/__tests__/storage.js';
15
+ import { createFormRetry } from '../form-retry/controller.js';
16
+ import { resetFormRetryMemory } from '../form-retry/submission-key.js';
17
+ import FormRetryFormFixture from './form-retry-form.fixture.svelte';
18
+ beforeEach(() => {
19
+ resetFormRetryMemory();
20
+ sessionStorage.clear();
21
+ });
22
+ function setup() {
23
+ const server = fakeRunOnceServer();
24
+ const kit = fakeEnhanceAction(server);
25
+ const retry = createFormRetry({ form: 'report', storage: memoryStorage() });
26
+ const { container } = render(FormRetryFormFixture, {
27
+ props: { enhance: kit.action, retry },
28
+ });
29
+ const form = container.querySelector('form');
30
+ const title = screen.getByRole('textbox', { name: 'Title' });
31
+ const send = async () => {
32
+ await fireEvent.click(screen.getByRole('button', { name: 'Send' }));
33
+ };
34
+ const type = async (value) => {
35
+ await fireEvent.input(title, { target: { value } });
36
+ };
37
+ return { server, kit, retry, form, title, send, type };
38
+ }
39
+ describe('createFormRetry with an enhanced smrt-ui Form', () => {
40
+ it('keeps the key field inside Form and posts the key in the body', async () => {
41
+ const { server, kit, retry, form, send, type } = setup();
42
+ const token = retry.token;
43
+ const fields = form.querySelectorAll('input[name="submissionKey"]');
44
+ expect(fields).toHaveLength(1);
45
+ expect(fields[0].value).toBe(token);
46
+ await type('Hose');
47
+ await send();
48
+ await kit.settle();
49
+ expect(server.requests).toHaveLength(1);
50
+ expect(server.requests[0].get('submissionKey')).toBe(token);
51
+ expect(server.requests[0].get('title')).toBe('Hose');
52
+ });
53
+ it('refuses a double submit while the first is in flight', async () => {
54
+ const { server, kit, retry, send, type } = setup();
55
+ await type('Hose');
56
+ const held = server.hold();
57
+ await send();
58
+ await vi.waitFor(() => expect(retry.state.status).toBe('submitting'));
59
+ await send();
60
+ expect(retry.state.status).toBe('busy');
61
+ held.release();
62
+ await kit.settle();
63
+ expect(kit.enhanced.cancelled).toBe(1);
64
+ expect(server.requests).toHaveLength(1);
65
+ expect(server.rows).toHaveLength(1);
66
+ });
67
+ it('retries a lost response under the same key, recording one row', async () => {
68
+ const { server, kit, retry, title, send, type } = setup();
69
+ await type('Hose');
70
+ const token = retry.token;
71
+ server.next('lost-response');
72
+ await send();
73
+ await kit.settle();
74
+ expect(retry.state.status).toBe('transport-error');
75
+ expect(kit.enhanced.unmounted).toBe(0);
76
+ expect(title.value).toBe('Hose');
77
+ await send();
78
+ await kit.settle();
79
+ expect(server.requests.map((body) => body.get('submissionKey'))).toEqual([
80
+ token,
81
+ token,
82
+ ]);
83
+ expect(server.rows).toHaveLength(1);
84
+ });
85
+ it('resets on success only when the form is unchanged', async () => {
86
+ const { server, kit, retry, title, send, type } = setup();
87
+ await type('First');
88
+ const token = retry.token;
89
+ const held = server.hold();
90
+ await send();
91
+ await vi.waitFor(() => expect(retry.state.status).toBe('submitting'));
92
+ await type('Second');
93
+ held.release();
94
+ await kit.settle();
95
+ expect(retry.state.status).toBe('success');
96
+ expect(title.value).toBe('Second');
97
+ expect(retry.token).toBe(token);
98
+ await send();
99
+ await kit.settle();
100
+ expect(server.rows.map((row) => row.fields.title)).toEqual([
101
+ ['First'],
102
+ ['Second'],
103
+ ]);
104
+ expect(title.value).toBe('');
105
+ expect(retry.token).not.toBe(token);
106
+ });
107
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `createFormRetry()` around #3331's choice controls (#3291). `Listbox`,
3
+ * `Combobox` and `MultiSelect` post their selection through hidden inputs, so
4
+ * the "unchanged since submit" check must compare hidden inputs too — every
5
+ * one except the retry helper's own key field, which rotates by design.
6
+ *
7
+ * A selection changed while a submit is in flight was never sent: the success
8
+ * must keep it (and its key), and the next submit must post it.
9
+ */
10
+ import { render } from '@testing-library/svelte';
11
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
12
+ import { fakeEnhance, fakeRunOnceServer, } from '../form-retry/__tests__/fake-kit.js';
13
+ import { memoryStorage } from '../form-retry/__tests__/storage.js';
14
+ import { createFormRetry } from '../form-retry/controller.js';
15
+ import { resetFormRetryMemory } from '../form-retry/submission-key.js';
16
+ import HiddenControlsForm from './hidden-controls-form.fixture.svelte';
17
+ beforeEach(() => {
18
+ resetFormRetryMemory();
19
+ sessionStorage.clear();
20
+ });
21
+ function setup() {
22
+ const view = render(HiddenControlsForm);
23
+ const form = view.container.querySelector('form');
24
+ const server = fakeRunOnceServer();
25
+ const retry = createFormRetry({ form: 'choices', storage: memoryStorage() });
26
+ retry.attach(form);
27
+ const kit = fakeEnhance(form, retry.enhance(), server);
28
+ const title = form.querySelector('[name=title]');
29
+ title.value = 'Hose';
30
+ return { ...view, form, server, retry, kit, title };
31
+ }
32
+ const posted = (body, name) => body.getAll(name);
33
+ describe('hidden-input-backed controls under createFormRetry', () => {
34
+ it.each([
35
+ ['Listbox', 'listbox', 'b'],
36
+ ['Combobox', 'combobox', 'b'],
37
+ ['MultiSelect', 'multi', ['a', 'b']],
38
+ ])('a %s change during an in-flight submit survives the success', async (_name, prop, next) => {
39
+ const { form, server, retry, kit, rerender } = setup();
40
+ const token = retry.token;
41
+ const field = prop;
42
+ const before = posted(new FormData(form), field);
43
+ const held = server.hold();
44
+ const first = kit.submit();
45
+ await vi.waitFor(() => expect(retry.state.status).toBe('submitting'));
46
+ await rerender({ [prop]: next });
47
+ const changed = posted(new FormData(form), field);
48
+ expect(changed).not.toEqual(before);
49
+ held.release();
50
+ await first;
51
+ expect(retry.state.status).toBe('success');
52
+ expect(kit.updates).toEqual([{ reset: false }]);
53
+ expect(posted(new FormData(form), field)).toEqual(changed);
54
+ expect(retry.token).toBe(token);
55
+ await kit.submit();
56
+ expect(posted(server.requests[0], field)).toEqual(before);
57
+ expect(posted(server.requests[1], field)).toEqual(changed);
58
+ expect(server.rows).toHaveLength(2);
59
+ });
60
+ it('still resets when only the key field changed', async () => {
61
+ const { form, retry, kit, title } = setup();
62
+ const token = retry.token;
63
+ await kit.submit();
64
+ expect(kit.updates).toEqual([{ reset: true }]);
65
+ expect(title.value).toBe('');
66
+ expect(retry.token).not.toBe(token);
67
+ expect(form.querySelector('[name=submissionKey]').value).toBe(retry.token);
68
+ });
69
+ });
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import Combobox from '../Combobox.svelte';
3
+ import Listbox from '../Listbox.svelte';
4
+ import MultiSelect from '../MultiSelect.svelte';
5
+
6
+ let {
7
+ listbox = $bindable('a'),
8
+ combobox = $bindable('a'),
9
+ multi = $bindable<Array<string | number>>(['a']),
10
+ }: {
11
+ listbox?: string | number;
12
+ combobox?: string;
13
+ multi?: Array<string | number>;
14
+ } = $props();
15
+
16
+ const options = [
17
+ { value: 'a', label: 'Alpha' },
18
+ { value: 'b', label: 'Bravo' },
19
+ ];
20
+ </script>
21
+
22
+ <!-- #3331's choice controls post through hidden inputs, not visible fields. -->
23
+ <form method="POST">
24
+ <input name="title" aria-label="Title" />
25
+ <Listbox name="listbox" label="Listbox" bind:value={listbox} {options} interaction={false} />
26
+ <Combobox name="combobox" label="Combobox" bind:value={combobox} {options} interaction={false} />
27
+ <MultiSelect name="multi" label="Multi" bind:values={multi} {options} interaction={false} />
28
+ <button type="submit">Send</button>
29
+ </form>
@@ -0,0 +1,9 @@
1
+ type $$ComponentProps = {
2
+ listbox?: string | number;
3
+ combobox?: string;
4
+ multi?: Array<string | number>;
5
+ };
6
+ declare const HiddenControlsForm: import("svelte").Component<$$ComponentProps, {}, "combobox" | "listbox" | "multi">;
7
+ type HiddenControlsForm = ReturnType<typeof HiddenControlsForm>;
8
+ export default HiddenControlsForm;
9
+ //# sourceMappingURL=hidden-controls-form.fixture.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hidden-controls-form.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/hidden-controls-form.fixture.svelte.ts"],"names":[],"mappings":"AAOC,KAAK,gBAAgB,GAAI;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;CAChC,CAAC;AA6BF,QAAA,MAAM,kBAAkB,oFAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import type { ControlInteractionRegistry } from '../control-interaction.js';
3
+ import Form from '../Form.svelte';
4
+ import Listbox from '../Listbox.svelte';
5
+
6
+ interface Props {
7
+ name?: string;
8
+ unnamed?: boolean;
9
+ value?: string | number;
10
+ disabled?: boolean;
11
+ fieldsetDisabled?: boolean;
12
+ registry?: ControlInteractionRegistry;
13
+ }
14
+ let {
15
+ name = 'vendor',
16
+ unnamed = false,
17
+ value = $bindable(),
18
+ disabled = false,
19
+ fieldsetDisabled = false,
20
+ registry,
21
+ }: Props = $props();
22
+ </script>
23
+ <Form formId="vendor-form" interactionRegistry={registry} aria-label="Vendor form">
24
+ <fieldset disabled={fieldsetDisabled}>
25
+ <Listbox name={unnamed ? undefined : name} label="Vendor" options={[{ value: 42, label: 'Plate supplier' }, { value: 'steel', label: 'Steel supplier' }, { value: 'blocked', label: 'Disabled supplier', disabled: true }]} bind:value {disabled} interaction={{ id: 'vendor' }} />
26
+ </fieldset>
27
+ </Form>
@@ -0,0 +1,13 @@
1
+ import type { ControlInteractionRegistry } from '../control-interaction.js';
2
+ interface Props {
3
+ name?: string;
4
+ unnamed?: boolean;
5
+ value?: string | number;
6
+ disabled?: boolean;
7
+ fieldsetDisabled?: boolean;
8
+ registry?: ControlInteractionRegistry;
9
+ }
10
+ declare const ListboxForm: import("svelte").Component<Props, {}, "value">;
11
+ type ListboxForm = ReturnType<typeof ListboxForm>;
12
+ export default ListboxForm;
13
+ //# sourceMappingURL=listbox-form.fixture.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listbox-form.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/listbox-form.fixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAK5E,UAAU,KAAK;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,EAAE,0BAA0B,CAAC;CACvC;AAuBD,QAAA,MAAM,WAAW,gDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import type { ControlInteractionRegistry } from '../control-interaction.js';
3
+ import Form from '../Form.svelte';
4
+ import MultiSelect from '../MultiSelect.svelte';
5
+
6
+ interface Props {
7
+ name?: string;
8
+ unnamed?: boolean;
9
+ values?: Array<string | number>;
10
+ disabled?: boolean;
11
+ fieldsetDisabled?: boolean;
12
+ registry?: ControlInteractionRegistry;
13
+ }
14
+ let {
15
+ name = 'roles',
16
+ unnamed = false,
17
+ values = $bindable([]),
18
+ disabled = false,
19
+ fieldsetDisabled = false,
20
+ registry,
21
+ }: Props = $props();
22
+ </script>
23
+ <Form formId="roles-form" interactionRegistry={registry} aria-label="Roles form">
24
+ <fieldset disabled={fieldsetDisabled}>
25
+ <MultiSelect name={unnamed ? undefined : name} label="Roles" options={[{ value: 42, label: 'Operator' }, { value: 'admin', label: 'Administrator' }, { value: 'blocked', label: 'Disabled role', disabled: true }]} bind:values {disabled} interaction={{ id: 'roles' }} />
26
+ </fieldset>
27
+ </Form>
@@ -0,0 +1,13 @@
1
+ import type { ControlInteractionRegistry } from '../control-interaction.js';
2
+ interface Props {
3
+ name?: string;
4
+ unnamed?: boolean;
5
+ values?: Array<string | number>;
6
+ disabled?: boolean;
7
+ fieldsetDisabled?: boolean;
8
+ registry?: ControlInteractionRegistry;
9
+ }
10
+ declare const MultiselectForm: import("svelte").Component<Props, {}, "values">;
11
+ type MultiselectForm = ReturnType<typeof MultiselectForm>;
12
+ export default MultiselectForm;
13
+ //# sourceMappingURL=multiselect-form.fixture.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multiselect-form.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/multiselect-form.fixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAK5E,UAAU,KAAK;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IAChC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,EAAE,0BAA0B,CAAC;CACvC;AAuBD,QAAA,MAAM,eAAe,iDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Native file-field posting strategies (smrt#3290).
3
+ */
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { assignFileWithDataTransfer, attachFormDataFallback, attachFormResetListener, clearFileInput, detectNativeFileFieldStrategy, fileNameForType, } from '../native-file-field.js';
6
+ import { submittedEntries, TestDataTransfer, TestFormDataEvent, } from './capture-test-env.js';
7
+ function fileInput() {
8
+ const input = document.createElement('input');
9
+ input.type = 'file';
10
+ return input;
11
+ }
12
+ const env = (overrides) => ({
13
+ File,
14
+ createFileInput: fileInput,
15
+ ...overrides,
16
+ });
17
+ afterEach(() => {
18
+ document.body.innerHTML = '';
19
+ });
20
+ describe('detectNativeFileFieldStrategy', () => {
21
+ it('prefers DataTransfer when an input accepts its files', () => {
22
+ expect(detectNativeFileFieldStrategy(env({
23
+ DataTransfer: TestDataTransfer,
24
+ FormDataEvent: TestFormDataEvent,
25
+ }))).toBe('data-transfer');
26
+ });
27
+ it('falls back to the formdata event without DataTransfer', () => {
28
+ expect(detectNativeFileFieldStrategy(env({ FormDataEvent: TestFormDataEvent }))).toBe('formdata-event');
29
+ });
30
+ it('falls back when the DataTransfer constructor throws (old engines)', () => {
31
+ class IllegalConstructor {
32
+ constructor() {
33
+ throw new TypeError('Illegal constructor');
34
+ }
35
+ }
36
+ expect(detectNativeFileFieldStrategy(env({
37
+ DataTransfer: IllegalConstructor,
38
+ FormDataEvent: TestFormDataEvent,
39
+ }))).toBe('formdata-event');
40
+ });
41
+ it('falls back when the input rejects the files assignment', () => {
42
+ class PlainDataTransfer {
43
+ items = { add: () => null };
44
+ files = [];
45
+ }
46
+ expect(detectNativeFileFieldStrategy(env({
47
+ DataTransfer: PlainDataTransfer,
48
+ FormDataEvent: TestFormDataEvent,
49
+ }))).toBe('formdata-event');
50
+ });
51
+ it('reports none when neither API exists, and during SSR', () => {
52
+ expect(detectNativeFileFieldStrategy(env({}))).toBe('none');
53
+ expect(detectNativeFileFieldStrategy({})).toBe('none');
54
+ });
55
+ });
56
+ describe('assignFileWithDataTransfer / clearFileInput', () => {
57
+ it('puts the file into the input so a native submit posts it, then clears it', () => {
58
+ const form = document.createElement('form');
59
+ const input = fileInput();
60
+ input.name = 'photo';
61
+ form.append(input);
62
+ const file = new File(['jpeg'], 'photo.jpg', { type: 'image/jpeg' });
63
+ expect(assignFileWithDataTransfer(input, file, TestDataTransfer)).toBe(true);
64
+ expect(new FormData(form).get('photo')).toBe(file);
65
+ clearFileInput(input);
66
+ const empty = new FormData(form).get('photo');
67
+ expect(empty.name).toBe('');
68
+ expect(empty.size).toBe(0);
69
+ });
70
+ it('returns false without a DataTransfer constructor', () => {
71
+ expect(assignFileWithDataTransfer(fileInput(), new File([], 'x'), undefined)).toBe(false);
72
+ });
73
+ });
74
+ describe('attachFormDataFallback', () => {
75
+ it('appends the committed file to its own form only, until disposed', () => {
76
+ const form = document.createElement('form');
77
+ const other = document.createElement('form');
78
+ document.body.append(form, other);
79
+ const file = new File(['png'], 'signature.png', { type: 'image/png' });
80
+ const dispose = attachFormDataFallback(document, () => ({
81
+ form,
82
+ name: 'signature',
83
+ file,
84
+ }));
85
+ expect(submittedEntries(form).getAll('signature')).toEqual([file]);
86
+ expect(submittedEntries(other).has('signature')).toBe(false);
87
+ dispose();
88
+ expect(submittedEntries(form).has('signature')).toBe(false);
89
+ });
90
+ it('posts the native empty-file entry when nothing is committed', () => {
91
+ const form = document.createElement('form');
92
+ document.body.append(form);
93
+ attachFormDataFallback(document, () => ({
94
+ form,
95
+ name: 'photo',
96
+ file: null,
97
+ }));
98
+ const entry = submittedEntries(form).get('photo');
99
+ expect(entry.name).toBe('');
100
+ expect(entry.size).toBe(0);
101
+ expect(entry.type).toBe('application/octet-stream');
102
+ });
103
+ it('contributes nothing while its source input is disabled by a fieldset', () => {
104
+ document.body.innerHTML =
105
+ '<form><fieldset><input type="file" hidden /></fieldset></form>';
106
+ const form = document.querySelector('form');
107
+ const fieldset = form.querySelector('fieldset');
108
+ const source = form.querySelector('input');
109
+ const file = new File(['png'], 'signature.png', { type: 'image/png' });
110
+ let committed = file;
111
+ attachFormDataFallback(document, () => ({
112
+ form,
113
+ name: 'signature',
114
+ file: committed,
115
+ source,
116
+ }));
117
+ expect(submittedEntries(form).getAll('signature')).toEqual([file]);
118
+ fieldset.disabled = true;
119
+ // Both a committed file and the empty entry are filtered, as the browser
120
+ // filters a disabled native control.
121
+ expect(submittedEntries(form).has('signature')).toBe(false);
122
+ committed = null;
123
+ expect(submittedEntries(form).has('signature')).toBe(false);
124
+ fieldset.disabled = false;
125
+ expect(submittedEntries(form).get('signature').size).toBe(0);
126
+ });
127
+ it('contributes nothing when the entry is unavailable', () => {
128
+ const form = document.createElement('form');
129
+ document.body.append(form);
130
+ const read = vi.fn(() => null);
131
+ attachFormDataFallback(document, read);
132
+ expect([...submittedEntries(form).keys()]).toEqual([]);
133
+ expect(read).toHaveBeenCalled();
134
+ });
135
+ });
136
+ describe('fileNameForType', () => {
137
+ it('maps known image types and falls back to bin', () => {
138
+ expect(fileNameForType('photo', 'image/jpeg')).toBe('photo.jpg');
139
+ expect(fileNameForType('photo', 'image/png')).toBe('photo.png');
140
+ expect(fileNameForType('photo', 'image/webp')).toBe('photo.webp');
141
+ expect(fileNameForType('photo', 'image/heic')).toBe('photo.bin');
142
+ });
143
+ });
144
+ describe('attachFormResetListener', () => {
145
+ it('fires for the owning form only, read at dispatch time, until disposed', () => {
146
+ document.body.innerHTML = '<form id="a"></form><form id="b"></form>';
147
+ const a = document.getElementById('a');
148
+ const b = document.getElementById('b');
149
+ let owner = a;
150
+ const onReset = vi.fn();
151
+ const dispose = attachFormResetListener(document, () => owner, onReset);
152
+ b.reset();
153
+ expect(onReset).not.toHaveBeenCalled();
154
+ a.reset();
155
+ expect(onReset).toHaveBeenCalledTimes(1);
156
+ owner = b;
157
+ b.reset();
158
+ expect(onReset).toHaveBeenCalledTimes(2);
159
+ owner = null;
160
+ b.reset();
161
+ expect(onReset).toHaveBeenCalledTimes(2);
162
+ owner = a;
163
+ dispose();
164
+ a.reset();
165
+ expect(onReset).toHaveBeenCalledTimes(2);
166
+ });
167
+ it('is not hidden by a page listener that stops propagation', () => {
168
+ document.body.innerHTML = '<form></form>';
169
+ const form = document.querySelector('form');
170
+ form.addEventListener('reset', (event) => event.stopPropagation());
171
+ const onReset = vi.fn();
172
+ const dispose = attachFormResetListener(document, () => form, onReset);
173
+ form.reset();
174
+ expect(onReset).toHaveBeenCalledTimes(1);
175
+ dispose();
176
+ });
177
+ });
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import SegmentedControl from '../SegmentedControl.svelte';
3
+
4
+ let {
5
+ required = false,
6
+ disabled = false,
7
+ outerDisabled = false,
8
+ initial = 'one',
9
+ named = true,
10
+ onvaluechange,
11
+ }: {
12
+ required?: boolean;
13
+ disabled?: boolean;
14
+ outerDisabled?: boolean;
15
+ initial?: string | number;
16
+ named?: boolean;
17
+ onvaluechange?: (value: string | number) => void;
18
+ } = $props();
19
+ let value = $state(initial);
20
+ </script>
21
+ <form>
22
+ <fieldset disabled={outerDisabled}>
23
+ <SegmentedControl name={named ? 'kind' : undefined} label="Kind" bind:value {onvaluechange} {required} {disabled}
24
+ options={[{ value: 'one', label: 'One' }, { value: 'off', label: 'Disabled', disabled: true }, { value: 0, label: 'Zero' }]} density="touch" />
25
+ </fieldset>
26
+ <span data-bound-value data-bound-type={typeof value}>{String(value)}</span>
27
+ <button type="reset">Reset</button>
28
+ </form>
@@ -0,0 +1,12 @@
1
+ type $$ComponentProps = {
2
+ required?: boolean;
3
+ disabled?: boolean;
4
+ outerDisabled?: boolean;
5
+ initial?: string | number;
6
+ named?: boolean;
7
+ onvaluechange?: (value: string | number) => void;
8
+ };
9
+ declare const SegmentedPosting: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type SegmentedPosting = ReturnType<typeof SegmentedPosting>;
11
+ export default SegmentedPosting;
12
+ //# sourceMappingURL=segmented-posting.fixture.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segmented-posting.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/segmented-posting.fixture.svelte.ts"],"names":[],"mappings":"AAKC,KAAK,gBAAgB,GAAI;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;CAClD,CAAC;AAwBF,QAAA,MAAM,gBAAgB,sDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,92 @@
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/svelte';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import Fixture from './segmented-posting.fixture.svelte';
5
+ function form(container) {
6
+ return container.querySelector('form');
7
+ }
8
+ describe('SegmentedControl native forms', () => {
9
+ it('posts selected values and preserves numeric zero in bindings', async () => {
10
+ const { container } = render(Fixture);
11
+ expect(new FormData(form(container)).get('kind')).toBe('one');
12
+ await userEvent.click(screen.getByRole('radio', { name: 'Zero' }));
13
+ expect(new FormData(form(container)).get('kind')).toBe('0');
14
+ expect(container.querySelector('[data-bound-value]')).toHaveTextContent('0');
15
+ });
16
+ it.each([
17
+ { disabled: true },
18
+ { outerDisabled: true },
19
+ { initial: 'off' },
20
+ { initial: 'missing' },
21
+ { named: false },
22
+ ])('omits disabled, unmatched, or unnamed values %j', (props) => {
23
+ const { container } = render(Fixture, { props });
24
+ expect(new FormData(form(container)).has('kind')).toBe(false);
25
+ });
26
+ it('requires an enabled selection including zero', async () => {
27
+ const { container } = render(Fixture, {
28
+ props: { required: true, initial: 'missing' },
29
+ });
30
+ expect(form(container).checkValidity()).toBe(false);
31
+ await userEvent.click(screen.getByRole('radio', { name: 'Zero' }));
32
+ expect(form(container).checkValidity()).toBe(true);
33
+ });
34
+ it('validates required unnamed controls without posting a generated field', async () => {
35
+ const { container } = render(Fixture, {
36
+ props: { required: true, named: false, initial: 'missing' },
37
+ });
38
+ expect(form(container).checkValidity()).toBe(false);
39
+ await userEvent.click(screen.getByRole('radio', { name: 'Zero' }));
40
+ expect(form(container).checkValidity()).toBe(true);
41
+ expect([...new FormData(form(container)).keys()]).toEqual([]);
42
+ });
43
+ it('resets to the initial selection and binding and honors reset cancellation', async () => {
44
+ const { container } = render(Fixture);
45
+ await userEvent.click(screen.getByRole('radio', { name: 'Zero' }));
46
+ await userEvent.click(screen.getByRole('button', { name: 'Reset' }));
47
+ await waitFor(() => expect(new FormData(form(container)).get('kind')).toBe('one'));
48
+ await waitFor(() => expect(container.querySelector('[data-bound-value]')).toHaveTextContent('one'));
49
+ await userEvent.click(screen.getByRole('radio', { name: 'Zero' }));
50
+ form(container).addEventListener('reset', (e) => e.preventDefault());
51
+ await userEvent.click(screen.getByRole('button', { name: 'Reset' }));
52
+ expect(new FormData(form(container)).get('kind')).toBe('0');
53
+ });
54
+ it('resets a numeric initial value and clears unmatched initial values', async () => {
55
+ const { container, unmount } = render(Fixture, { props: { initial: 0 } });
56
+ await userEvent.click(screen.getByRole('radio', { name: 'One' }));
57
+ form(container).reset();
58
+ await waitFor(() => expect(container.querySelector('[data-bound-value]')).toHaveTextContent('0'));
59
+ expect(container.querySelector('[data-bound-value]')).toHaveAttribute('data-bound-type', 'number');
60
+ unmount();
61
+ const second = render(Fixture, {
62
+ props: { initial: 'missing', required: true },
63
+ });
64
+ await userEvent.click(screen.getByRole('radio', { name: 'One' }));
65
+ form(second.container).reset();
66
+ await waitFor(() => expect(second.container.querySelector('[data-bound-value]')).toHaveTextContent('undefined'));
67
+ expect(form(second.container).checkValidity()).toBe(false);
68
+ expect(new FormData(form(second.container)).has('kind')).toBe(false);
69
+ });
70
+ it('emits one native change and one value callback per pointer selection', async () => {
71
+ const onvaluechange = vi.fn();
72
+ const { container } = render(Fixture, { props: { onvaluechange } });
73
+ const onchange = vi.fn();
74
+ form(container).addEventListener('change', onchange);
75
+ await userEvent.click(screen.getByRole('radio', { name: 'Zero' }));
76
+ expect(onvaluechange).toHaveBeenCalledExactlyOnceWith(0);
77
+ expect(onchange).toHaveBeenCalledTimes(1);
78
+ });
79
+ it('supports arrow/home/end navigation while skipping disabled options', async () => {
80
+ render(Fixture);
81
+ const one = screen.getByRole('radio', { name: 'One' });
82
+ one.focus();
83
+ await fireEvent.keyDown(one, { key: 'ArrowRight' });
84
+ expect(screen.getByRole('radio', { name: 'Zero' })).toBeChecked();
85
+ expect(screen.getByRole('radio', { name: 'Zero' })).toHaveFocus();
86
+ await fireEvent.keyDown(screen.getByRole('radio', { name: 'Zero' }), {
87
+ key: 'Home',
88
+ });
89
+ expect(one).toBeChecked();
90
+ expect(screen.getByRole('radio', { name: 'Disabled' })).toBeDisabled();
91
+ });
92
+ });