@spectral_labs/ui 1.6.1 → 1.8.0-next.0

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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/THIRD_PARTY_NOTICES.md +201 -0
  3. package/dist/components/command/command.svelte.d.ts +30 -0
  4. package/dist/components/command/command.svelte.d.ts.map +1 -1
  5. package/dist/components/command/command.svelte.js +85 -4
  6. package/dist/components/command/components/command-group.svelte +25 -15
  7. package/dist/components/command/components/command-group.svelte.d.ts.map +1 -1
  8. package/dist/components/command/components/command-input.svelte +54 -5
  9. package/dist/components/command/components/command-input.svelte.d.ts +1 -1
  10. package/dist/components/command/components/command-input.svelte.d.ts.map +1 -1
  11. package/dist/components/command/components/command-list.svelte +22 -3
  12. package/dist/components/command/components/command-list.svelte.d.ts.map +1 -1
  13. package/dist/components/command/components/command-root.svelte +43 -4
  14. package/dist/components/command/components/command-root.svelte.d.ts.map +1 -1
  15. package/dist/components/command/index.d.ts +1 -0
  16. package/dist/components/command/index.d.ts.map +1 -1
  17. package/dist/components/command/types.d.ts +59 -4
  18. package/dist/components/command/types.d.ts.map +1 -1
  19. package/package.json +8 -2
  20. package/dist/components/__tests__/overlay-scrim-source.test.js +0 -134
  21. package/dist/components/accordion/__tests__/AccordionTest.svelte +0 -31
  22. package/dist/components/accordion/__tests__/accordion.test.svelte.js +0 -139
  23. package/dist/components/alert-dialog/__tests__/AlertDialogTest.svelte +0 -28
  24. package/dist/components/alert-dialog/__tests__/alert-dialog.a11y.test.js +0 -42
  25. package/dist/components/alert-dialog/__tests__/alert-dialog.integration.test.svelte.js +0 -51
  26. package/dist/components/alert-dialog/__tests__/alert-dialog.test.svelte.js +0 -113
  27. package/dist/components/app-bar/__tests__/app-bar.test.svelte.js +0 -30
  28. package/dist/components/aspect-ratio/__tests__/aspect-ratio.test.svelte.js +0 -22
  29. package/dist/components/avatar/__tests__/avatar.test.svelte.js +0 -26
  30. package/dist/components/badge/__tests__/badge.test.svelte.js +0 -30
  31. package/dist/components/bottom-sheet/__tests__/BottomSheetTest.svelte +0 -22
  32. package/dist/components/bottom-sheet/__tests__/bottom-sheet.test.svelte.js +0 -83
  33. package/dist/components/button/__tests__/button.test.svelte.js +0 -63
  34. package/dist/components/calendar/__tests__/CalendarTest.svelte +0 -56
  35. package/dist/components/calendar/__tests__/calendar.test.svelte.js +0 -190
  36. package/dist/components/card/__tests__/card.test.svelte.js +0 -30
  37. package/dist/components/checkbox/__tests__/checkbox.test.svelte.js +0 -44
  38. package/dist/components/chips/__tests__/chips.test.svelte.js +0 -26
  39. package/dist/components/collapsible/__tests__/CollapsibleTest.svelte +0 -18
  40. package/dist/components/collapsible/__tests__/collapsible.test.svelte.js +0 -125
  41. package/dist/components/combobox/__tests__/ComboboxTest.svelte +0 -40
  42. package/dist/components/combobox/__tests__/combobox.test.svelte.js +0 -255
  43. package/dist/components/command/__tests__/CommandTest.svelte +0 -77
  44. package/dist/components/command/__tests__/command-item-passthrough.test.svelte.js +0 -284
  45. package/dist/components/command/__tests__/command.a11y.test.js +0 -30
  46. package/dist/components/command/__tests__/command.integration.test.svelte.js +0 -68
  47. package/dist/components/command/__tests__/command.test.svelte.js +0 -230
  48. package/dist/components/context-menu/__tests__/ContextMenuNonInteractiveTest.svelte +0 -32
  49. package/dist/components/context-menu/__tests__/ContextMenuTest.svelte +0 -25
  50. package/dist/components/context-menu/__tests__/context-menu.test.svelte.js +0 -141
  51. package/dist/components/date-field/__tests__/DateFieldTest.svelte +0 -20
  52. package/dist/components/date-field/__tests__/date-field.test.svelte.js +0 -139
  53. package/dist/components/date-picker/__tests__/DatePickerTest.svelte +0 -20
  54. package/dist/components/date-picker/__tests__/date-picker.test.svelte.js +0 -190
  55. package/dist/components/date-range-field/__tests__/DateRangeFieldTest.svelte +0 -20
  56. package/dist/components/date-range-field/__tests__/date-range-field.test.svelte.js +0 -130
  57. package/dist/components/date-range-picker/__tests__/DateRangePickerTest.svelte +0 -18
  58. package/dist/components/date-range-picker/__tests__/date-range-picker.test.svelte.js +0 -156
  59. package/dist/components/dialog/__tests__/DialogTest.svelte +0 -34
  60. package/dist/components/dialog/__tests__/dialog.test.svelte.js +0 -184
  61. package/dist/components/drop-zone/__tests__/drop-zone.test.svelte.js +0 -26
  62. package/dist/components/dropdown-menu/__tests__/DropdownMenuRadioTest.svelte +0 -90
  63. package/dist/components/dropdown-menu/__tests__/DropdownMenuTest.svelte +0 -71
  64. package/dist/components/dropdown-menu/__tests__/dropdown-menu-radio-group.test.svelte.js +0 -306
  65. package/dist/components/dropdown-menu/__tests__/dropdown-menu.test.svelte.js +0 -567
  66. package/dist/components/empty-state/__tests__/empty-state.test.svelte.js +0 -26
  67. package/dist/components/fab/__tests__/fab.test.svelte.js +0 -30
  68. package/dist/components/field/__tests__/FieldTest.svelte +0 -26
  69. package/dist/components/field/__tests__/field.test.svelte.js +0 -74
  70. package/dist/components/grid-list/__tests__/GridListTest.svelte +0 -46
  71. package/dist/components/grid-list/__tests__/grid-list.test.svelte.js +0 -231
  72. package/dist/components/hover-card/__tests__/HoverCardTest.svelte +0 -30
  73. package/dist/components/hover-card/__tests__/hover-card.test.svelte.js +0 -236
  74. package/dist/components/indicator/__tests__/indicator.test.svelte.js +0 -30
  75. package/dist/components/input/__tests__/input.test.svelte.js +0 -67
  76. package/dist/components/label/__tests__/label.test.svelte.js +0 -22
  77. package/dist/components/language-select/__tests__/language-select.test.svelte.js +0 -199
  78. package/dist/components/language-select/__tests__/languages.test.js +0 -87
  79. package/dist/components/link-preview/__tests__/LinkPreviewTest.svelte +0 -37
  80. package/dist/components/link-preview/__tests__/link-preview.a11y.test.js +0 -28
  81. package/dist/components/link-preview/__tests__/link-preview.integration.test.svelte.js +0 -54
  82. package/dist/components/link-preview/__tests__/link-preview.test.svelte.js +0 -215
  83. package/dist/components/list/__tests__/list.test.svelte.js +0 -22
  84. package/dist/components/menubar/__tests__/MenubarTest.svelte +0 -52
  85. package/dist/components/menubar/__tests__/menubar.a11y.test.js +0 -28
  86. package/dist/components/menubar/__tests__/menubar.integration.test.svelte.js +0 -57
  87. package/dist/components/menubar/__tests__/menubar.test.svelte.js +0 -328
  88. package/dist/components/meter/__tests__/meter.test.svelte.js +0 -26
  89. package/dist/components/nav-bar/__tests__/nav-bar.test.svelte.js +0 -23
  90. package/dist/components/nav-drawer/__tests__/nav-drawer.test.svelte.js +0 -28
  91. package/dist/components/nav-rail/__tests__/nav-rail.test.svelte.js +0 -28
  92. package/dist/components/pagination/__tests__/pagination.test.svelte.js +0 -26
  93. package/dist/components/pin-input/__tests__/pin-input.test.svelte.js +0 -71
  94. package/dist/components/popover/__tests__/PopoverTest.svelte +0 -27
  95. package/dist/components/popover/__tests__/popover.test.svelte.js +0 -196
  96. package/dist/components/progress/__tests__/progress.test.svelte.js +0 -32
  97. package/dist/components/radio-group/__tests__/radio-group.test.svelte.js +0 -22
  98. package/dist/components/range-calendar/__tests__/RangeCalendarTest.svelte +0 -23
  99. package/dist/components/range-calendar/__tests__/range-calendar.test.svelte.js +0 -105
  100. package/dist/components/rating-group/__tests__/rating-group.test.svelte.js +0 -26
  101. package/dist/components/scroll-area/__tests__/scroll-area.test.svelte.js +0 -34
  102. package/dist/components/search-field/__tests__/SearchFieldTest.svelte +0 -34
  103. package/dist/components/search-field/__tests__/search-field.test.svelte.js +0 -118
  104. package/dist/components/select/__tests__/SelectLabelPersistenceTest.svelte +0 -31
  105. package/dist/components/select/__tests__/SelectTest.svelte +0 -33
  106. package/dist/components/select/__tests__/select.test.svelte.js +0 -221
  107. package/dist/components/separator/__tests__/separator.test.svelte.js +0 -39
  108. package/dist/components/sheet/__tests__/SheetTest.svelte +0 -34
  109. package/dist/components/sheet/__tests__/sheet.test.svelte.js +0 -193
  110. package/dist/components/side-sheet/__tests__/side-sheet.test.svelte.js +0 -138
  111. package/dist/components/sidebar/__tests__/SidebarFullTest.svelte +0 -45
  112. package/dist/components/sidebar/__tests__/SidebarTest.svelte +0 -9
  113. package/dist/components/sidebar/__tests__/sidebar.test.svelte.js +0 -186
  114. package/dist/components/simple-dialog/__tests__/SimpleDialogTest.svelte +0 -40
  115. package/dist/components/simple-dialog/__tests__/simple-dialog.test.svelte.js +0 -50
  116. package/dist/components/simple-tooltip/__tests__/SimpleTooltipTest.svelte +0 -19
  117. package/dist/components/simple-tooltip/__tests__/simple-tooltip.test.svelte.js +0 -61
  118. package/dist/components/slider/__tests__/slider.test.svelte.js +0 -42
  119. package/dist/components/sortable-list/__tests__/NestedSortableListTest.svelte +0 -49
  120. package/dist/components/sortable-list/__tests__/SortableListTest.svelte +0 -42
  121. package/dist/components/sortable-list/__tests__/sortable-list.nested.test.svelte.js +0 -112
  122. package/dist/components/sortable-list/__tests__/sortable-list.test.svelte.js +0 -175
  123. package/dist/components/splitter/__tests__/splitter.test.svelte.js +0 -26
  124. package/dist/components/switch/__tests__/switch.test.svelte.js +0 -44
  125. package/dist/components/table/__tests__/table.test.svelte.js +0 -22
  126. package/dist/components/tabs/__tests__/TabsTest.svelte +0 -32
  127. package/dist/components/tabs/__tests__/tabs.test.svelte.js +0 -227
  128. package/dist/components/textarea/__tests__/textarea.test.svelte.js +0 -55
  129. package/dist/components/time-field/__tests__/TimeFieldTest.svelte +0 -20
  130. package/dist/components/time-field/__tests__/time-field.test.svelte.js +0 -100
  131. package/dist/components/time-range-field/__tests__/TimeRangeFieldTest.svelte +0 -20
  132. package/dist/components/time-range-field/__tests__/time-range-field.test.svelte.js +0 -124
  133. package/dist/components/toast/__tests__/toast.test.svelte.js +0 -46
  134. package/dist/components/toggle/__tests__/toggle.test.svelte.js +0 -38
  135. package/dist/components/toggle-group/__tests__/toggle-group.test.svelte.js +0 -22
  136. package/dist/components/toolbar/__tests__/ToolbarCompositionTest.svelte +0 -18
  137. package/dist/components/toolbar/__tests__/ToolbarTabstopTest.svelte +0 -10
  138. package/dist/components/toolbar/__tests__/toolbar.test.svelte.js +0 -63
  139. package/dist/components/tooltip/__tests__/TooltipTest.svelte +0 -22
  140. package/dist/components/tooltip/__tests__/tooltip.test.svelte.js +0 -225
  141. package/dist/components/tree/__tests__/TreeLabelContentTest.svelte +0 -21
  142. package/dist/components/tree/__tests__/TreeTest.svelte +0 -24
  143. package/dist/components/tree/__tests__/tree.test.svelte.js +0 -229
  144. package/dist/components/virtualizer/__tests__/waterfall-layout.test.js +0 -82
  145. package/dist/internal/announcer/__tests__/announcer.test.js +0 -42
  146. package/dist/internal/dismissible-layer/__tests__/dismissible-layer.test.js +0 -196
  147. package/dist/internal/drag-drop/__tests__/auto-scroll.test.js +0 -104
  148. package/dist/internal/drag-drop/__tests__/drag-session.test.svelte.js +0 -803
  149. package/dist/internal/drag-drop/__tests__/drag-source.test.svelte.js +0 -130
  150. package/dist/internal/drag-drop/__tests__/drop-delegate.test.js +0 -223
  151. package/dist/internal/drag-drop/__tests__/types.test.js +0 -20
  152. package/dist/internal/escape-layer/__tests__/escape-layer.test.js +0 -113
  153. package/dist/internal/focus-scope/__tests__/focus-scope.test.js +0 -225
  154. package/dist/internal/grace-area/__tests__/grace-area.test.js +0 -67
  155. package/dist/internal/helpers/__tests__/helpers.test.js +0 -123
  156. package/dist/internal/portal/__tests__/PortalTest.svelte +0 -21
  157. package/dist/internal/portal/__tests__/portal.test.js +0 -29
  158. package/dist/internal/portal/__tests__/portal.test.svelte.js +0 -57
  159. package/dist/internal/scroll-lock/__tests__/scroll-lock.test.js +0 -160
  160. package/dist/internal/typeahead/__tests__/typeahead.test.js +0 -61
  161. package/src/themes/__tests__/carbon-dark-contrast.test.ts +0 -170
  162. package/src/themes/__tests__/generated-css-drift.test.ts +0 -53
  163. package/src/themes/__tests__/scrim-opaque.test.ts +0 -52
@@ -1,184 +0,0 @@
1
- import { flushSync, mount, unmount } from 'svelte';
2
- import { afterEach, describe, expect, it } from 'vitest';
3
- import DialogRoot from '../components/dialog-root.svelte';
4
- import DialogTest from './DialogTest.svelte';
5
- describe('dialog', () => {
6
- let target;
7
- let instance;
8
- function setup(props = {}) {
9
- target = document.createElement('div');
10
- document.body.appendChild(target);
11
- // Pass portalTo=target by default so existing tests keep querying `target`
12
- instance = mount(DialogTest, { target, props: { portalTo: target, ...props } });
13
- flushSync();
14
- return target;
15
- }
16
- function setupRoot(props = {}) {
17
- target = document.createElement('div');
18
- document.body.appendChild(target);
19
- instance = mount(DialogRoot, { target, props });
20
- return target;
21
- }
22
- afterEach(() => {
23
- if (instance)
24
- unmount(instance);
25
- target?.remove();
26
- });
27
- // --- Basic rendering ---
28
- it('renders root', () => {
29
- setupRoot();
30
- expect(target.querySelector('.sp-dialog-root')).toBeTruthy();
31
- });
32
- it('data-state is closed by default', () => {
33
- setupRoot();
34
- expect(target.querySelector('.sp-dialog-root')?.getAttribute('data-state')).toBe('closed');
35
- });
36
- // --- Trigger ARIA ---
37
- it('trigger has aria-haspopup="dialog"', () => {
38
- setup();
39
- const trigger = target.querySelector('.sp-dialog-trigger');
40
- expect(trigger?.getAttribute('aria-haspopup')).toBe('dialog');
41
- });
42
- it('trigger has aria-expanded=false when closed', () => {
43
- setup();
44
- const trigger = target.querySelector('.sp-dialog-trigger');
45
- expect(trigger?.getAttribute('aria-expanded')).toBe('false');
46
- });
47
- it('trigger has aria-controls pointing to content id', () => {
48
- setup({ open: true });
49
- const trigger = target.querySelector('.sp-dialog-trigger');
50
- const content = target.querySelector('[role="dialog"]');
51
- expect(trigger?.getAttribute('aria-controls')).toBe(content?.id);
52
- });
53
- it('trigger toggles open state on click', () => {
54
- setup();
55
- const trigger = target.querySelector('.sp-dialog-trigger');
56
- trigger.click();
57
- flushSync();
58
- expect(target.querySelector('.sp-dialog-root')?.getAttribute('data-state')).toBe('open');
59
- });
60
- // --- Content ARIA ---
61
- it('content has role="dialog"', () => {
62
- setup({ open: true });
63
- expect(target.querySelector('[role="dialog"]')).toBeTruthy();
64
- });
65
- it('content has aria-modal="true"', () => {
66
- setup({ open: true });
67
- const content = target.querySelector('[role="dialog"]');
68
- expect(content?.getAttribute('aria-modal')).toBe('true');
69
- });
70
- it('content has aria-labelledby pointing to title', () => {
71
- setup({ open: true });
72
- const content = target.querySelector('[role="dialog"]');
73
- const title = target.querySelector('.sp-dialog-title');
74
- expect(content?.getAttribute('aria-labelledby')).toBe(title?.id);
75
- });
76
- it('content has aria-describedby pointing to description', () => {
77
- setup({ open: true });
78
- const content = target.querySelector('[role="dialog"]');
79
- const desc = target.querySelector('.sp-dialog-description');
80
- expect(content?.getAttribute('aria-describedby')).toBe(desc?.id);
81
- });
82
- it('content has data-state="open" when open', () => {
83
- setup({ open: true });
84
- const content = target.querySelector('[role="dialog"]');
85
- expect(content?.getAttribute('data-state')).toBe('open');
86
- });
87
- // --- Content visibility ---
88
- it('content is not rendered when closed', () => {
89
- setup({ open: false });
90
- expect(target.querySelector('[role="dialog"]')).toBeNull();
91
- });
92
- it('content is rendered when open', () => {
93
- setup({ open: true });
94
- expect(target.querySelector('[role="dialog"]')).toBeTruthy();
95
- });
96
- // --- Overlay ---
97
- it('overlay has aria-hidden="true"', () => {
98
- setup({ open: true });
99
- const overlay = target.querySelector('.sp-dialog-overlay');
100
- expect(overlay?.getAttribute('aria-hidden')).toBe('true');
101
- });
102
- // --- Close button ---
103
- it('close button closes the dialog', () => {
104
- setup({ open: true });
105
- const closeBtn = target.querySelector('.sp-dialog-close');
106
- closeBtn.click();
107
- flushSync();
108
- expect(target.querySelector('[role="dialog"]')).toBeNull();
109
- });
110
- // --- Escape key ---
111
- it('escape key closes the dialog', () => {
112
- setup({ open: true });
113
- // Flush to ensure $effect that mounts EscapeLayerState has run
114
- flushSync();
115
- expect(target.querySelector('[role="dialog"]')).toBeTruthy();
116
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
117
- flushSync();
118
- expect(target.querySelector('[role="dialog"]')).toBeNull();
119
- });
120
- // --- Title ---
121
- it('title renders as h2 by default', () => {
122
- setup({ open: true });
123
- const title = target.querySelector('.sp-dialog-title');
124
- expect(title?.tagName).toBe('H2');
125
- });
126
- // --- Description ---
127
- it('description renders as <p>', () => {
128
- setup({ open: true });
129
- const desc = target.querySelector('.sp-dialog-description');
130
- expect(desc?.tagName).toBe('P');
131
- });
132
- // --- Trigger aria-expanded updates ---
133
- it('trigger aria-expanded becomes true when dialog opens', () => {
134
- setup();
135
- const trigger = target.querySelector('.sp-dialog-trigger');
136
- expect(trigger.getAttribute('aria-expanded')).toBe('false');
137
- trigger.click();
138
- flushSync();
139
- expect(trigger.getAttribute('aria-expanded')).toBe('true');
140
- });
141
- // --- Portal ---
142
- it('portals content to document.body by default when open', () => {
143
- const t = document.createElement('div');
144
- t.id = 'dialog-host';
145
- document.body.appendChild(t);
146
- const inst = mount(DialogTest, { target: t, props: { open: true } });
147
- flushSync();
148
- try {
149
- expect(t.querySelector('[role="dialog"]')).toBeNull();
150
- expect(document.body.querySelector('[role="dialog"]')).toBeTruthy();
151
- }
152
- finally {
153
- unmount(inst);
154
- t.remove();
155
- }
156
- });
157
- it('renders content inline when portalTo is explicitly set to the host', () => {
158
- const t = document.createElement('div');
159
- document.body.appendChild(t);
160
- const inst = mount(DialogTest, {
161
- target: t,
162
- props: { open: true, portalTo: t },
163
- });
164
- flushSync();
165
- try {
166
- expect(t.querySelector('[role="dialog"]')).toBeTruthy();
167
- }
168
- finally {
169
- unmount(inst);
170
- t.remove();
171
- }
172
- });
173
- // --- Presentation prop ---
174
- it('sets data-presentation attribute matching prop', () => {
175
- setup({ open: true, presentation: 'fullscreen-mobile' });
176
- const content = target.querySelector('[role="dialog"]');
177
- expect(content?.getAttribute('data-presentation')).toBe('fullscreen-mobile');
178
- });
179
- it('defaults data-presentation to "dialog"', () => {
180
- setup({ open: true });
181
- const content = target.querySelector('[role="dialog"]');
182
- expect(content?.getAttribute('data-presentation')).toBe('dialog');
183
- });
184
- });
@@ -1,26 +0,0 @@
1
- import { mount, unmount } from 'svelte';
2
- import { afterEach, describe, expect, it } from 'vitest';
3
- import DropZone from '../drop-zone.svelte';
4
- describe('drop-zone', () => {
5
- let target;
6
- let instance;
7
- function setup(props = {}) {
8
- target = document.createElement('div');
9
- document.body.appendChild(target);
10
- instance = mount(DropZone, { target, props });
11
- return target;
12
- }
13
- afterEach(() => {
14
- if (instance)
15
- unmount(instance);
16
- target?.remove();
17
- });
18
- it('renders with role=button', () => {
19
- setup();
20
- expect(target.querySelector('[role="button"]')).toBeTruthy();
21
- });
22
- it('has tabindex', () => {
23
- setup();
24
- expect(target.querySelector('[tabindex]')).toBeTruthy();
25
- });
26
- });
@@ -1,90 +0,0 @@
1
- <script lang="ts">
2
- import DropdownMenuContent from '../components/dropdown-menu-content.svelte'
3
- import DropdownMenuItem from '../components/dropdown-menu-item.svelte'
4
- import DropdownMenuRadioGroup from '../components/dropdown-menu-radio-group.svelte'
5
- import DropdownMenuRadioItem from '../components/dropdown-menu-radio-item.svelte'
6
- import DropdownMenuRoot from '../components/dropdown-menu-root.svelte'
7
- import DropdownMenuTrigger from '../components/dropdown-menu-trigger.svelte'
8
-
9
- let {
10
- open = $bindable(true),
11
- value = $bindable('document'),
12
- portalTo,
13
- onValueChange,
14
- closeOnSelect = undefined,
15
- disabledValue = undefined,
16
- textValues = undefined,
17
- itemAttrs = undefined,
18
- onItemClick = undefined,
19
- withLeadingItem = false,
20
- orphan = false,
21
- groupAttrs = undefined,
22
- onItemKeydown = undefined,
23
- onItemSelect = undefined,
24
- parentTarget = 'bibliographie',
25
- }: {
26
- open?: boolean
27
- value?: string
28
- portalTo?: HTMLElement | string
29
- onValueChange?: (value: string) => void
30
- closeOnSelect?: boolean
31
- /** Value of the single item rendered disabled. */
32
- disabledValue?: string
33
- /** Per-value `textValue` overrides for typeahead. */
34
- textValues?: Record<string, string>
35
- /** Arbitrary HTML attributes spread onto the `placeholders` item. */
36
- itemAttrs?: Record<string, unknown>
37
- /** Consumer-supplied click handler on the `placeholders` item. */
38
- onItemClick?: (event: MouseEvent) => void
39
- /** A plain Item BEFORE the group — proves roving spans both kinds. */
40
- withLeadingItem?: boolean
41
- /** Renders a RadioItem with NO enclosing RadioGroup (guard-rail case). */
42
- orphan?: boolean
43
- /** Arbitrary HTML attributes spread onto the `role="group"` node. */
44
- groupAttrs?: Record<string, unknown>
45
- /** Consumer-supplied keydown handler on the `placeholders` item. */
46
- onItemKeydown?: (event: KeyboardEvent) => void
47
- /** Consumer-supplied `onSelect` on the `placeholders` item. */
48
- onItemSelect?: (event: Event) => void
49
- /** Value the PARENT-driven button assigns (see `parent-set` below). */
50
- parentTarget?: string
51
- } = $props()
52
-
53
- const OPTIONS = [
54
- { value: 'document', label: 'Document' },
55
- { value: 'placeholders', label: 'Placeholders' },
56
- { value: 'bibliographie', label: 'Bibliographie' },
57
- ]
58
- </script>
59
-
60
- <!-- Lives OUTSIDE the menu: clicking it is the PARENT moving `value`, the
61
- one path that must move the checked row without any notification. -->
62
- <button data-testid="parent-set" onclick={() => (value = parentTarget)}>set from parent</button>
63
-
64
- <DropdownMenuRoot bind:open>
65
- <DropdownMenuTrigger>Affichage</DropdownMenuTrigger>
66
- <DropdownMenuContent {portalTo}>
67
- {#if withLeadingItem}
68
- <DropdownMenuItem>Plan</DropdownMenuItem>
69
- {/if}
70
- {#if orphan}
71
- <DropdownMenuRadioItem value="orphan">Orphan</DropdownMenuRadioItem>
72
- {/if}
73
- <DropdownMenuRadioGroup bind:value {onValueChange} {...groupAttrs ?? {}}>
74
- {#each OPTIONS as option (option.value)}
75
- <DropdownMenuRadioItem
76
- value={option.value}
77
- disabled={option.value === disabledValue}
78
- textValue={textValues?.[option.value]}
79
- {closeOnSelect}
80
- onclick={option.value === 'placeholders' ? onItemClick : undefined}
81
- onkeydown={option.value === 'placeholders' ? onItemKeydown : undefined}
82
- onSelect={option.value === 'placeholders' ? onItemSelect : undefined}
83
- {...option.value === 'placeholders' ? (itemAttrs ?? {}) : {}}
84
- >
85
- {option.label}
86
- </DropdownMenuRadioItem>
87
- {/each}
88
- </DropdownMenuRadioGroup>
89
- </DropdownMenuContent>
90
- </DropdownMenuRoot>
@@ -1,71 +0,0 @@
1
- <script lang="ts">
2
- import DropdownMenuCheckboxItem from '../components/dropdown-menu-checkbox-item.svelte'
3
- import DropdownMenuContent from '../components/dropdown-menu-content.svelte'
4
- import DropdownMenuItem from '../components/dropdown-menu-item.svelte'
5
- import DropdownMenuRoot from '../components/dropdown-menu-root.svelte'
6
- import DropdownMenuSubContent from '../components/dropdown-menu-sub-content.svelte'
7
- import DropdownMenuSubTrigger from '../components/dropdown-menu-sub-trigger.svelte'
8
- import DropdownMenuSub from '../components/dropdown-menu-sub.svelte'
9
- import DropdownMenuTrigger from '../components/dropdown-menu-trigger.svelte'
10
-
11
- let {
12
- open = $bindable(false),
13
- portalTo,
14
- onOpenChange,
15
- withSub = false,
16
- subFirst = false,
17
- subDisabled = false,
18
- subTextValue = undefined,
19
- withCheckbox = false,
20
- withSecondSub = false,
21
- }: {
22
- open?: boolean
23
- portalTo?: HTMLElement | string
24
- onOpenChange?: (open: boolean) => void
25
- withSub?: boolean
26
- subFirst?: boolean
27
- subDisabled?: boolean
28
- subTextValue?: string
29
- withCheckbox?: boolean
30
- withSecondSub?: boolean
31
- } = $props()
32
- </script>
33
-
34
- {#snippet subBlock()}
35
- <DropdownMenuSub>
36
- <DropdownMenuSubTrigger disabled={subDisabled} textValue={subTextValue}
37
- >More options</DropdownMenuSubTrigger
38
- >
39
- <DropdownMenuSubContent>
40
- <DropdownMenuItem>Sub Item 1</DropdownMenuItem>
41
- <DropdownMenuItem>Sub Item 2</DropdownMenuItem>
42
- </DropdownMenuSubContent>
43
- </DropdownMenuSub>
44
- {/snippet}
45
-
46
- <DropdownMenuRoot bind:open {onOpenChange}>
47
- <DropdownMenuTrigger>Open Menu</DropdownMenuTrigger>
48
- <DropdownMenuContent {portalTo}>
49
- {#if withSub && subFirst}
50
- {@render subBlock()}
51
- {/if}
52
- <DropdownMenuItem>Item 1</DropdownMenuItem>
53
- <DropdownMenuItem>Item 2</DropdownMenuItem>
54
- <DropdownMenuItem>Item 3</DropdownMenuItem>
55
- {#if withCheckbox}
56
- <DropdownMenuCheckboxItem>Notifications</DropdownMenuCheckboxItem>
57
- {/if}
58
- {#if withSub && !subFirst}
59
- {@render subBlock()}
60
- {/if}
61
- {#if withSecondSub}
62
- <DropdownMenuSub>
63
- <DropdownMenuSubTrigger>Other options</DropdownMenuSubTrigger>
64
- <DropdownMenuSubContent>
65
- <DropdownMenuItem>Sub B Item 1</DropdownMenuItem>
66
- <DropdownMenuItem>Sub B Item 2</DropdownMenuItem>
67
- </DropdownMenuSubContent>
68
- </DropdownMenuSub>
69
- {/if}
70
- </DropdownMenuContent>
71
- </DropdownMenuRoot>
@@ -1,306 +0,0 @@
1
- import { flushSync, mount, unmount } from 'svelte';
2
- import { afterEach, describe, expect, it, vi } from 'vitest';
3
- import DropdownMenuRadioTest from './DropdownMenuRadioTest.svelte';
4
- /**
5
- * `DropdownMenu.RadioGroup` / `RadioItem` — the single-choice row family.
6
- *
7
- * These rows exist because a menu that offers "pick exactly one of N" had no
8
- * honest markup here: `CheckboxItem` announces `menuitemcheckbox`, which tells
9
- * a screen-reader user the options are independent when they are not.
10
- *
11
- * The suite also pins the two properties this family is REQUIRED to have from
12
- * birth, and which the older rows lack (spectral-ui#201): consumer HTML
13
- * attributes reach the DOM node, and a consumer event handler COMPOSES with
14
- * the built-in one instead of being dropped.
15
- */
16
- describe('dropdown-menu radio group', () => {
17
- let target;
18
- let instance;
19
- function setup(props = {}) {
20
- target = document.createElement('div');
21
- document.body.appendChild(target);
22
- instance = mount(DropdownMenuRadioTest, {
23
- target,
24
- props: { portalTo: target, ...props },
25
- });
26
- flushSync();
27
- return target;
28
- }
29
- function radios() {
30
- return Array.from(target.querySelectorAll('[role="menuitemradio"]'));
31
- }
32
- function radio(value) {
33
- const el = target.querySelector(`[role="menuitemradio"][data-value="${value}"]`);
34
- if (!el)
35
- throw new Error(`no radio item for value "${value}"`);
36
- return el;
37
- }
38
- /** Same idiom as `dropdown-menu.test.svelte.ts`: drive the REAL focused
39
- * element, so the Content -> collection -> focus chain is exercised rather
40
- * than merely inspected. */
41
- function pressKey(key) {
42
- const el = document.activeElement;
43
- el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
44
- flushSync();
45
- }
46
- function activeValue() {
47
- return document.activeElement?.getAttribute('data-value') ?? null;
48
- }
49
- afterEach(() => {
50
- if (instance)
51
- unmount(instance);
52
- instance = undefined;
53
- target?.remove();
54
- });
55
- // ── ARIA ──────────────────────────────────────────────────────────────
56
- it('wraps its rows in role="group" — never role="radiogroup", invalid inside a menu', () => {
57
- setup();
58
- expect(target.querySelector('.sp-dropdown-menu-radio-group')?.getAttribute('role')).toBe('group');
59
- // The ratified contract FORBIDS `radiogroup`: a `menu` may only own
60
- // menuitem / menuitemcheckbox / menuitemradio / group / separator.
61
- expect(target.querySelector('[role="radiogroup"]')).toBeNull();
62
- const items = radios();
63
- expect(items).toHaveLength(3);
64
- expect(items.map((el) => el.getAttribute('aria-checked'))).toEqual(['true', 'false', 'false']);
65
- });
66
- it('reflects the selected value in data-state, for styling parity with CheckboxItem', () => {
67
- setup();
68
- expect(radio('document').getAttribute('data-state')).toBe('checked');
69
- expect(radio('placeholders').getAttribute('data-state')).toBe('unchecked');
70
- });
71
- it('moves the checked row when the PARENT changes `value` after mount', () => {
72
- // Mounting straight at the target value would stay green even if the group
73
- // snapshotted the prop at init — the move has to happen AFTER mount, driven
74
- // from outside, with the menu open.
75
- const calls = [];
76
- setup({ onValueChange: (v) => calls.push(v) });
77
- expect(radios().map((el) => el.getAttribute('aria-checked'))).toEqual([
78
- 'true',
79
- 'false',
80
- 'false',
81
- ]);
82
- target.querySelector('[data-testid="parent-set"]').click();
83
- flushSync();
84
- expect(radios().map((el) => el.getAttribute('aria-checked'))).toEqual([
85
- 'false',
86
- 'false',
87
- 'true',
88
- ]);
89
- expect(radio('bibliographie').getAttribute('data-state')).toBe('checked');
90
- // The parent moved it; the group has nothing to report back.
91
- expect(calls).toEqual([]);
92
- });
93
- // ── Selection ─────────────────────────────────────────────────────────
94
- // `closeOnSelect: false` in the selection tests below is not incidental: the
95
- // default closes the menu, which unmounts the rows — a post-click assertion
96
- // on `aria-checked` would then fail for the wrong reason. Closing has its
97
- // own test.
98
- it('selects on click and notifies once with the new value', () => {
99
- const calls = [];
100
- let selects = 0;
101
- setup({
102
- closeOnSelect: false,
103
- onItemSelect: () => selects++,
104
- onValueChange: (v) => calls.push(v),
105
- });
106
- radio('placeholders').click();
107
- flushSync();
108
- expect(calls).toEqual(['placeholders']);
109
- expect(radio('placeholders').getAttribute('aria-checked')).toBe('true');
110
- expect(radio('document').getAttribute('aria-checked')).toBe('false');
111
- // Proves the `onSelect` wiring is live — without this, the `select: 0` of
112
- // the disabled test below would hold no matter what the component did.
113
- expect(selects).toBe(1);
114
- });
115
- it('does NOT re-notify when the already-selected row is chosen again', () => {
116
- const calls = [];
117
- setup({ onValueChange: (v) => calls.push(v) });
118
- radio('document').click();
119
- flushSync();
120
- expect(calls).toEqual([]);
121
- });
122
- it('selects on Enter and on Space', () => {
123
- const calls = [];
124
- setup({ closeOnSelect: false, onValueChange: (v) => calls.push(v) });
125
- radio('placeholders').dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
126
- flushSync();
127
- expect(calls).toEqual(['placeholders']);
128
- radio('bibliographie').dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }));
129
- flushSync();
130
- expect(calls).toEqual(['placeholders', 'bibliographie']);
131
- });
132
- it('closes the menu after a selection, and keeps it open when closeOnSelect is false', () => {
133
- setup();
134
- radio('placeholders').click();
135
- flushSync();
136
- expect(target.querySelector('[role="menu"]')).toBeNull();
137
- unmount(instance);
138
- instance = undefined;
139
- target.remove();
140
- setup({ closeOnSelect: false });
141
- radio('placeholders').click();
142
- flushSync();
143
- expect(target.querySelector('[role="menu"]')).toBeTruthy();
144
- });
145
- // ── Disabled ──────────────────────────────────────────────────────────
146
- it('a disabled row is TOTALLY inert — the consumer handler does not run either', () => {
147
- const counts = { click: 0, keydown: 0, select: 0, change: 0 };
148
- setup({
149
- disabledValue: 'placeholders',
150
- onItemClick: () => counts.click++,
151
- onItemKeydown: () => counts.keydown++,
152
- onItemSelect: () => counts.select++,
153
- onValueChange: () => counts.change++,
154
- });
155
- const disabled = radio('placeholders');
156
- expect(disabled.getAttribute('aria-disabled')).toBe('true');
157
- expect(disabled.hasAttribute('data-disabled')).toBe(true);
158
- disabled.click();
159
- flushSync();
160
- disabled.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
161
- flushSync();
162
- disabled.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }));
163
- flushSync();
164
- // A dimmed, unselectable row that still fires an application effect is not
165
- // disabled — it only LOOKS disabled.
166
- expect(counts).toEqual({ click: 0, keydown: 0, select: 0, change: 0 });
167
- expect(disabled.getAttribute('aria-checked')).toBe('false');
168
- // ... and none of it dismissed the menu.
169
- expect(target.querySelector('[role="menu"]')).toBeTruthy();
170
- // `getOwnLevelMenuItems` filters on `[data-roving-focus-item]:not([data-disabled])`.
171
- const rovable = target.querySelectorAll('[data-roving-focus-item]:not([data-disabled])');
172
- expect(Array.from(rovable)).not.toContain(disabled);
173
- });
174
- // ── Keyboard: the chain, not the wiring ───────────────────────────────
175
- //
176
- // The `data-*` hooks below can all be present while the
177
- // Content -> collection -> focus chain is broken, so these drive the real
178
- // focused element instead of asserting attributes.
179
- it('is reachable by ArrowDown from a plain Item, and Enter selects the FOCUSED row', () => {
180
- const calls = [];
181
- setup({
182
- withLeadingItem: true,
183
- closeOnSelect: false,
184
- onValueChange: (v) => calls.push(v),
185
- });
186
- // Opening autofocuses the first roving row — the plain Item.
187
- expect(document.activeElement?.textContent?.trim()).toBe('Plan');
188
- pressKey('ArrowDown');
189
- expect(document.activeElement?.getAttribute('role')).toBe('menuitemradio');
190
- expect(activeValue()).toBe('document');
191
- pressKey('ArrowDown');
192
- expect(activeValue()).toBe('placeholders');
193
- pressKey('Enter');
194
- expect(calls).toEqual(['placeholders']);
195
- expect(radio('placeholders').getAttribute('aria-checked')).toBe('true');
196
- });
197
- it('roving SKIPS a disabled row on ArrowDown', () => {
198
- setup({ withLeadingItem: true, disabledValue: 'placeholders' });
199
- expect(document.activeElement?.textContent?.trim()).toBe('Plan');
200
- pressKey('ArrowDown');
201
- expect(activeValue()).toBe('document');
202
- pressKey('ArrowDown');
203
- // `placeholders` is disabled — roving lands on the next enabled row.
204
- expect(activeValue()).toBe('bibliographie');
205
- });
206
- it('typeahead focuses the matching row, and `textValue` redirects the match', () => {
207
- setup({ withLeadingItem: true });
208
- pressKey('b');
209
- expect(activeValue()).toBe('bibliographie');
210
- unmount(instance);
211
- instance = undefined;
212
- target.remove();
213
- // With the override, `Bibliographie` no longer answers to `b` — and no
214
- // other row does, so focus stays put.
215
- setup({ withLeadingItem: true, textValues: { bibliographie: 'zzz' } });
216
- const before = document.activeElement;
217
- pressKey('b');
218
- expect(document.activeElement).toBe(before);
219
- // Remount before the second probe: typeahead ACCUMULATES a buffer, so a
220
- // `z` typed after a failed `b` searches for "bz" and would match nothing —
221
- // the test would then pass or fail for a reason unrelated to `textValue`.
222
- unmount(instance);
223
- instance = undefined;
224
- target.remove();
225
- setup({ withLeadingItem: true, textValues: { bibliographie: 'zzz' } });
226
- pressKey('z');
227
- expect(activeValue()).toBe('bibliographie');
228
- });
229
- // ── Roving focus + typeahead wiring ───────────────────────────────────
230
- it('joins the roving collection alongside plain items', () => {
231
- setup({ withLeadingItem: true });
232
- const rovable = Array.from(target.querySelectorAll('[data-roving-focus-item]:not([data-disabled])'));
233
- // Plan + the three radio rows, in document order.
234
- expect(rovable).toHaveLength(4);
235
- expect(rovable[0].getAttribute('role')).toBe('menuitem');
236
- expect(rovable.slice(1).map((el) => el.getAttribute('role'))).toEqual([
237
- 'menuitemradio',
238
- 'menuitemradio',
239
- 'menuitemradio',
240
- ]);
241
- });
242
- it('exposes its label to typeahead, and lets `textValue` override it', () => {
243
- setup({ textValues: { bibliographie: 'zzz' } });
244
- // No override → the content's typeahead falls back to textContent.
245
- expect(radio('document').getAttribute('data-text-value')).toBeNull();
246
- expect(radio('document').textContent?.trim()).toBe('Document');
247
- // Override → the attribute the content reads FIRST.
248
- expect(radio('bibliographie').getAttribute('data-text-value')).toBe('zzz');
249
- });
250
- // ── The two properties #201 costs the older rows ───────────────────────
251
- it('forwards consumer HTML attributes to the GROUP element', () => {
252
- setup({ groupAttrs: { 'aria-label': 'Panneau', 'data-testid': 'view-group' } });
253
- const group = target.querySelector('.sp-dropdown-menu-radio-group');
254
- expect(group.getAttribute('aria-label')).toBe('Panneau');
255
- expect(group.getAttribute('data-testid')).toBe('view-group');
256
- // The role stays ours.
257
- expect(group.getAttribute('role')).toBe('group');
258
- });
259
- it('forwards consumer HTML attributes to the row element', () => {
260
- setup({ itemAttrs: { 'data-testid': 'view-placeholders', 'aria-keyshortcuts': 'p' } });
261
- const el = radio('placeholders');
262
- expect(el.getAttribute('data-testid')).toBe('view-placeholders');
263
- expect(el.getAttribute('aria-keyshortcuts')).toBe('p');
264
- });
265
- it('keeps its own class alongside a consumer class', () => {
266
- setup({ itemAttrs: { class: 'view-row' } });
267
- const el = radio('placeholders');
268
- expect(el.classList.contains('view-row')).toBe(true);
269
- expect(el.classList.contains('sp-dropdown-menu-item')).toBe(true);
270
- });
271
- it('cOMPOSES a consumer handler with the built-in selection', () => {
272
- const seen = [];
273
- setup({
274
- closeOnSelect: false,
275
- onItemClick: () => seen.push('consumer'),
276
- onValueChange: (v) => seen.push(`selected:${v}`),
277
- });
278
- radio('placeholders').click();
279
- flushSync();
280
- // Both ran, consumer first — and selection actually happened.
281
- expect(seen).toEqual(['consumer', 'selected:placeholders']);
282
- });
283
- it('lets a consumer handler cancel the selection with preventDefault', () => {
284
- const calls = [];
285
- setup({
286
- onItemClick: (event) => event.preventDefault(),
287
- onValueChange: (v) => calls.push(v),
288
- });
289
- radio('placeholders').click();
290
- flushSync();
291
- expect(calls).toEqual([]);
292
- expect(radio('placeholders').getAttribute('aria-checked')).toBe('false');
293
- // A cancelled selection must not close the menu either.
294
- expect(target.querySelector('[role="menu"]')).toBeTruthy();
295
- });
296
- // ── Guard rail ────────────────────────────────────────────────────────
297
- it('throws a NAMED error when a RadioItem is used outside a RadioGroup', () => {
298
- const spy = vi.spyOn(console, 'error').mockImplementation(() => { });
299
- try {
300
- expect(() => setup({ orphan: true })).toThrow(/RadioGroup/);
301
- }
302
- finally {
303
- spy.mockRestore();
304
- }
305
- });
306
- });