@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,68 +0,0 @@
1
- import { flushSync, mount, unmount } from 'svelte';
2
- import { afterEach, describe, expect, it } from 'vitest';
3
- import CommandTest from './CommandTest.svelte';
4
- describe('command integration', () => {
5
- let target;
6
- let instance;
7
- function setup(props = {}) {
8
- target = document.createElement('div');
9
- document.body.appendChild(target);
10
- // Pass portalTo: target so portal-rendered content stays within target for queries
11
- instance = mount(CommandTest, { target, props: { portalTo: target, ...props } });
12
- flushSync();
13
- return target;
14
- }
15
- afterEach(() => {
16
- if (instance)
17
- unmount(instance);
18
- target?.remove();
19
- });
20
- it('search filters items in real-time', () => {
21
- setup();
22
- flushSync();
23
- const input = target.querySelector('[role="combobox"]');
24
- const initialCount = target.querySelectorAll('[role="option"]').length;
25
- expect(initialCount).toBeGreaterThan(0);
26
- input.value = 'cop';
27
- input.dispatchEvent(new Event('input', { bubbles: true }));
28
- flushSync();
29
- const visible = target.querySelectorAll('[role="option"]');
30
- expect(visible.length).toBe(1);
31
- expect(visible[0]?.getAttribute('data-value')).toBe('copy');
32
- });
33
- it('keyword matching finds aliases', () => {
34
- setup();
35
- flushSync();
36
- const input = target.querySelector('[role="combobox"]');
37
- input.value = 'duplicate';
38
- input.dispatchEvent(new Event('input', { bubbles: true }));
39
- flushSync();
40
- const visible = target.querySelectorAll('[role="option"]');
41
- expect(visible.length).toBe(1);
42
- expect(visible[0]?.getAttribute('data-value')).toBe('copy');
43
- });
44
- it('escape clears search text when closeOnEscape is false', () => {
45
- // When closeOnEscape is false, the EscapeLayer does not close the palette,
46
- // allowing the input's own Escape handler to clear the search text.
47
- setup({ closeOnEscape: false });
48
- flushSync();
49
- const input = target.querySelector('[role="combobox"]');
50
- input.value = 'cop';
51
- input.dispatchEvent(new Event('input', { bubbles: true }));
52
- flushSync();
53
- expect(target.querySelectorAll('[role="option"]').length).toBe(1);
54
- input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
55
- flushSync();
56
- expect(target.querySelectorAll('[role="option"]').length).toBeGreaterThan(1);
57
- });
58
- it('empty state shows when no matches', () => {
59
- setup();
60
- flushSync();
61
- const input = target.querySelector('[role="combobox"]');
62
- input.value = 'zzzzz';
63
- input.dispatchEvent(new Event('input', { bubbles: true }));
64
- flushSync();
65
- expect(target.querySelectorAll('[role="option"]').length).toBe(0);
66
- expect(target.querySelector('.sp-command-empty')).toBeTruthy();
67
- });
68
- });
@@ -1,230 +0,0 @@
1
- import { flushSync, mount, unmount } from 'svelte';
2
- import { afterEach, describe, expect, it } from 'vitest';
3
- import { defaultFilter } from '../command.svelte.js';
4
- import CommandTest from './CommandTest.svelte';
5
- // jsdom does not define PointerEvent — polyfill it for tests
6
- if (typeof globalThis.PointerEvent === 'undefined') {
7
- // @ts-expect-error — minimal polyfill for testing
8
- globalThis.PointerEvent = class PointerEvent extends MouseEvent {
9
- constructor(type, init) {
10
- super(type, init);
11
- }
12
- };
13
- }
14
- describe('command', () => {
15
- let target;
16
- let instance;
17
- function setup(props = {}) {
18
- target = document.createElement('div');
19
- document.body.appendChild(target);
20
- // Pass portalTo: target so portal-rendered content stays within target for queries
21
- instance = mount(CommandTest, { target, props: { portalTo: target, ...props } });
22
- flushSync();
23
- return target;
24
- }
25
- afterEach(() => {
26
- if (instance)
27
- unmount(instance);
28
- target?.remove();
29
- });
30
- // --- Basic rendering ---
31
- it('renders command root', () => {
32
- setup();
33
- expect(target.querySelector('.sp-command')).toBeTruthy();
34
- });
35
- it('renders input with role="combobox"', () => {
36
- setup();
37
- const input = target.querySelector('[role="combobox"]');
38
- expect(input).toBeTruthy();
39
- });
40
- it('renders list with role="listbox"', () => {
41
- setup();
42
- const list = target.querySelector('[role="listbox"]');
43
- expect(list).toBeTruthy();
44
- });
45
- it('renders items with role="option"', () => {
46
- setup();
47
- const items = target.querySelectorAll('[role="option"]');
48
- expect(items.length).toBe(5); // copy, paste, cut, home, settings
49
- });
50
- // --- Input ARIA ---
51
- it('input has aria-expanded="true"', () => {
52
- setup();
53
- const input = target.querySelector('[role="combobox"]');
54
- expect(input?.getAttribute('aria-expanded')).toBe('true');
55
- });
56
- it('input has aria-autocomplete="list"', () => {
57
- setup();
58
- const input = target.querySelector('[role="combobox"]');
59
- expect(input?.getAttribute('aria-autocomplete')).toBe('list');
60
- });
61
- it('input has aria-controls pointing to list', () => {
62
- setup();
63
- const input = target.querySelector('[role="combobox"]');
64
- const list = target.querySelector('[role="listbox"]');
65
- expect(input?.getAttribute('aria-controls')).toBe(list?.id);
66
- });
67
- // --- Groups ---
68
- it('groups have role="group"', () => {
69
- setup();
70
- const groups = target.querySelectorAll('[role="group"]');
71
- expect(groups.length).toBe(2);
72
- });
73
- it('groups have aria-labelledby pointing to heading', () => {
74
- setup();
75
- const group = target.querySelector('[role="group"]');
76
- const headingId = group?.getAttribute('aria-labelledby');
77
- expect(headingId).toBeTruthy();
78
- const heading = target.querySelector(`#${headingId}`);
79
- expect(heading?.textContent).toBe('Actions');
80
- });
81
- // --- Separator ---
82
- it('separator has role="separator"', () => {
83
- setup();
84
- const sep = target.querySelector('[role="separator"]');
85
- expect(sep).toBeTruthy();
86
- });
87
- // --- Disabled item ---
88
- it('disabled item has aria-disabled', () => {
89
- setup();
90
- const settings = target.querySelector('[data-value="settings"]');
91
- expect(settings?.getAttribute('aria-disabled')).toBe('true');
92
- });
93
- // --- Selection ---
94
- it('first enabled item is selected by default', () => {
95
- setup();
96
- flushSync(); // Wait for $effect item registration
97
- const selected = target.querySelector('[data-selected="true"]');
98
- expect(selected?.getAttribute('data-value')).toBe('copy');
99
- });
100
- // --- Keyboard navigation ---
101
- it('arrowDown moves selection to next item', () => {
102
- setup();
103
- flushSync();
104
- const input = target.querySelector('[role="combobox"]');
105
- input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
106
- flushSync();
107
- const selected = target.querySelector('[data-selected="true"]');
108
- expect(selected?.getAttribute('data-value')).toBe('paste');
109
- });
110
- it('arrowUp moves selection to previous item', () => {
111
- setup();
112
- flushSync();
113
- const input = target.querySelector('[role="combobox"]');
114
- // Move down first, then back up
115
- input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
116
- flushSync();
117
- input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
118
- flushSync();
119
- const selected = target.querySelector('[data-selected="true"]');
120
- expect(selected?.getAttribute('data-value')).toBe('copy');
121
- });
122
- // --- Empty state ---
123
- it('shows empty state when no items match', () => {
124
- setup();
125
- const input = target.querySelector('[role="combobox"]');
126
- // Simulate typing something that won't match
127
- input.value = 'zzzzzzz';
128
- input.dispatchEvent(new Event('input', { bubbles: true }));
129
- flushSync();
130
- const empty = target.querySelector('.sp-command-empty');
131
- expect(empty).toBeTruthy();
132
- expect(empty?.textContent).toContain('No results found');
133
- });
134
- it('hides empty state when items match', () => {
135
- setup();
136
- expect(target.querySelector('.sp-command-empty')).toBeNull();
137
- });
138
- // --- Overlay layers ---
139
- it('closes on Escape when closeOnEscape is true (default)', () => {
140
- const target = document.createElement('div');
141
- document.body.appendChild(target);
142
- const inst = mount(CommandTest, { target, props: { open: true, portalTo: target } });
143
- flushSync();
144
- try {
145
- expect(target.querySelector('[data-command-root]')).toBeTruthy();
146
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
147
- flushSync();
148
- expect(target.querySelector('[data-command-root]')).toBeNull();
149
- }
150
- finally {
151
- unmount(inst);
152
- target.remove();
153
- }
154
- });
155
- it('calls both onOpenChange and onClose on close (backward compat)', () => {
156
- const openCalls = [];
157
- const closeCalls = [];
158
- const localTarget = document.createElement('div');
159
- document.body.appendChild(localTarget);
160
- const localInstance = mount(CommandTest, {
161
- target: localTarget,
162
- props: {
163
- open: true,
164
- portalTo: localTarget,
165
- onOpenChange: (open) => openCalls.push(open),
166
- onClose: () => closeCalls.push(1),
167
- },
168
- });
169
- flushSync();
170
- try {
171
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
172
- flushSync();
173
- expect(openCalls).toEqual([false]);
174
- expect(closeCalls).toEqual([1]);
175
- }
176
- finally {
177
- unmount(localInstance);
178
- localTarget.remove();
179
- }
180
- });
181
- it('closes on outside pointerdown', () => {
182
- const target = document.createElement('div');
183
- document.body.appendChild(target);
184
- const outside = document.createElement('button');
185
- outside.setAttribute('data-testid', 'outside');
186
- document.body.appendChild(outside);
187
- const inst = mount(CommandTest, { target, props: { open: true, portalTo: target } });
188
- flushSync();
189
- try {
190
- // jsdom returns {0,0,0,0} for all getBoundingClientRect — use coordinates > 0
191
- // so the dismissible-layer considers this click as "outside" the element
192
- outside.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: 500, clientY: 500 }));
193
- flushSync();
194
- expect(target.querySelector('[data-command-root]')).toBeNull();
195
- }
196
- finally {
197
- unmount(inst);
198
- target.remove();
199
- outside.remove();
200
- }
201
- });
202
- });
203
- describe('command defaultFilter', () => {
204
- it('returns 1 for empty search', () => {
205
- expect(defaultFilter('anything', '')).toBe(1);
206
- });
207
- it('returns 1 for exact match', () => {
208
- expect(defaultFilter('copy', 'copy')).toBe(1);
209
- });
210
- it('returns high score for prefix match', () => {
211
- const score = defaultFilter('copy', 'cop');
212
- expect(score).toBeGreaterThanOrEqual(0.9);
213
- });
214
- it('returns 0 for no match', () => {
215
- expect(defaultFilter('copy', 'xyz')).toBe(0);
216
- });
217
- it('matches against keywords', () => {
218
- const score = defaultFilter('copy', 'duplicate', ['duplicate']);
219
- expect(score).toBe(1);
220
- });
221
- it('is case insensitive', () => {
222
- const score = defaultFilter('Copy', 'copy');
223
- expect(score).toBe(1);
224
- });
225
- it('contains match scores lower than prefix', () => {
226
- const prefixScore = defaultFilter('copy', 'cop');
227
- const containsScore = defaultFilter('deep copy', 'copy');
228
- expect(prefixScore).toBeGreaterThan(containsScore);
229
- });
230
- });
@@ -1,32 +0,0 @@
1
- <script lang="ts">
2
- import DropdownMenuItem from '../../dropdown-menu/components/dropdown-menu-item.svelte'
3
- import ContextMenuContent from '../components/context-menu-content.svelte'
4
- import ContextMenuRoot from '../components/context-menu-root.svelte'
5
- import ContextMenuTrigger from '../components/context-menu-trigger.svelte'
6
-
7
- let {
8
- open = $bindable(false),
9
- onOpenChange,
10
- portalTo,
11
- }: {
12
- open?: boolean
13
- onOpenChange?: (open: boolean) => void
14
- portalTo?: HTMLElement | string
15
- } = $props()
16
- </script>
17
-
18
- <!--
19
- Mirrors the Refero V2 CollectionTree pattern: a `Tree.Item`-like focusable
20
- child (role="treeitem" + tabindex=0) is wrapped by ContextMenu.Trigger with
21
- `nonInteractive`. The treeitem stays the only interactive node — the trigger
22
- wrapper attaches event listeners but no role/tabindex/aria-disabled.
23
- -->
24
- <ContextMenuRoot bind:open {onOpenChange}>
25
- <ContextMenuTrigger nonInteractive>
26
- <div role="treeitem" tabindex="0" aria-level="1" data-testid="treeitem">Tree item</div>
27
- </ContextMenuTrigger>
28
- <ContextMenuContent {portalTo}>
29
- <DropdownMenuItem>Item 1</DropdownMenuItem>
30
- <DropdownMenuItem>Item 2</DropdownMenuItem>
31
- </ContextMenuContent>
32
- </ContextMenuRoot>
@@ -1,25 +0,0 @@
1
- <script lang="ts">
2
- import DropdownMenuItem from '../../dropdown-menu/components/dropdown-menu-item.svelte'
3
- import ContextMenuContent from '../components/context-menu-content.svelte'
4
- import ContextMenuRoot from '../components/context-menu-root.svelte'
5
- import ContextMenuTrigger from '../components/context-menu-trigger.svelte'
6
-
7
- let {
8
- open = $bindable(false),
9
- onOpenChange,
10
- portalTo,
11
- }: {
12
- open?: boolean
13
- onOpenChange?: (open: boolean) => void
14
- portalTo?: HTMLElement | string
15
- } = $props()
16
- </script>
17
-
18
- <ContextMenuRoot bind:open {onOpenChange}>
19
- <ContextMenuTrigger>Right-click me</ContextMenuTrigger>
20
- <ContextMenuContent {portalTo}>
21
- <DropdownMenuItem>Item 1</DropdownMenuItem>
22
- <DropdownMenuItem>Item 2</DropdownMenuItem>
23
- <DropdownMenuItem>Item 3</DropdownMenuItem>
24
- </ContextMenuContent>
25
- </ContextMenuRoot>
@@ -1,141 +0,0 @@
1
- import { flushSync, mount, unmount } from 'svelte';
2
- import { afterEach, describe, expect, it, vi } from 'vitest';
3
- import ContextMenuRoot from '../components/context-menu-root.svelte';
4
- import ContextMenuNonInteractiveTest from './ContextMenuNonInteractiveTest.svelte';
5
- import ContextMenuTest from './ContextMenuTest.svelte';
6
- describe('context-menu', () => {
7
- let target;
8
- let instance;
9
- function setup(props = {}) {
10
- target = document.createElement('div');
11
- document.body.appendChild(target);
12
- instance = mount(ContextMenuTest, { 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(ContextMenuRoot, { target, props });
20
- flushSync();
21
- return target;
22
- }
23
- afterEach(() => {
24
- if (instance)
25
- unmount(instance);
26
- target?.remove();
27
- });
28
- it('renders root container', () => {
29
- setupRoot();
30
- expect(target.querySelector('.sp-context-menu-root')).toBeTruthy();
31
- });
32
- it('content not rendered when closed', () => {
33
- setup();
34
- expect(target.querySelector('[role="menu"]')).toBeNull();
35
- });
36
- it('content renders when open', () => {
37
- setup({ open: true });
38
- expect(target.querySelector('[role="menu"]')).toBeTruthy();
39
- });
40
- it('calls onOpenChange with new value on each open transition', () => {
41
- const calls = [];
42
- const localTarget = document.createElement('div');
43
- document.body.appendChild(localTarget);
44
- const localInstance = mount(ContextMenuTest, {
45
- target: localTarget,
46
- props: {
47
- open: false,
48
- onOpenChange: (open) => calls.push(open),
49
- },
50
- });
51
- flushSync();
52
- try {
53
- const trigger = localTarget.querySelector('.sp-context-menu-trigger');
54
- trigger.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: 100, clientY: 100 }));
55
- flushSync();
56
- expect(calls).toEqual([true]);
57
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
58
- flushSync();
59
- expect(calls).toEqual([true, false]);
60
- }
61
- finally {
62
- unmount(localInstance);
63
- localTarget.remove();
64
- }
65
- });
66
- // -------------------------------------------------------------------------
67
- // nonInteractive prop
68
- //
69
- // Use when the child is already focusable/interactive (e.g. a Tree.Item with
70
- // role="treeitem" + tabindex=0). The trigger wrapper then attaches event
71
- // listeners only, without role/tabindex/aria-disabled, so it does not nest a
72
- // second interactive control inside its child — fixing axe `nested-interactive`
73
- // + `aria-required-parent` when wrapping a treeitem.
74
- // -------------------------------------------------------------------------
75
- describe('nonInteractive prop', () => {
76
- function setupNI(props = {}) {
77
- target = document.createElement('div');
78
- document.body.appendChild(target);
79
- instance = mount(ContextMenuNonInteractiveTest, {
80
- target,
81
- props: { portalTo: target, ...props },
82
- });
83
- flushSync();
84
- return target;
85
- }
86
- it('wrapper has no role, no tabindex, no aria-disabled when nonInteractive', () => {
87
- setupNI();
88
- const trigger = target.querySelector('.sp-context-menu-trigger');
89
- expect(trigger).toBeTruthy();
90
- expect(trigger.getAttribute('role')).toBeNull();
91
- expect(trigger.getAttribute('tabindex')).toBeNull();
92
- expect(trigger.getAttribute('aria-disabled')).toBeNull();
93
- });
94
- it('treeitem child has no [role="button"] ancestor when nonInteractive', () => {
95
- setupNI();
96
- const treeitem = target.querySelector('[role="treeitem"]');
97
- expect(treeitem).toBeTruthy();
98
- // Structural ARIA assertion: the treeitem must be a direct interactive
99
- // descendant of the tree (no role="button" wrapper between them) so axe's
100
- // `nested-interactive` + `aria-required-parent` rules stop firing.
101
- expect(treeitem.closest('[role="button"]')).toBeNull();
102
- });
103
- it('right-click on wrapper still opens menu when nonInteractive', () => {
104
- const calls = [];
105
- setupNI({ onOpenChange: (open) => calls.push(open) });
106
- const trigger = target.querySelector('.sp-context-menu-trigger');
107
- trigger.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: 100, clientY: 100 }));
108
- flushSync();
109
- expect(calls).toEqual([true]);
110
- });
111
- it('forwards Shift+F10 from focusable child to the wrapper handler when nonInteractive', () => {
112
- const calls = [];
113
- setupNI({ onOpenChange: (open) => calls.push(open) });
114
- const child = target.querySelector('[data-testid="treeitem"]');
115
- child.focus();
116
- child.dispatchEvent(new KeyboardEvent('keydown', { key: 'F10', shiftKey: true, bubbles: true }));
117
- flushSync();
118
- expect(calls).toEqual([true]);
119
- });
120
- it('long-press (touchstart + 700ms hold) still opens menu when nonInteractive', () => {
121
- vi.useFakeTimers();
122
- try {
123
- const calls = [];
124
- setupNI({ onOpenChange: (open) => calls.push(open) });
125
- const trigger = target.querySelector('.sp-context-menu-trigger');
126
- // jsdom lacks TouchEvent constructor — synthesise a bubbling Event with a touches array.
127
- const evt = new Event('touchstart', { bubbles: true });
128
- Object.defineProperty(evt, 'touches', {
129
- value: [{ clientX: 100, clientY: 100 }],
130
- });
131
- trigger.dispatchEvent(evt);
132
- vi.advanceTimersByTime(700);
133
- flushSync();
134
- expect(calls).toEqual([true]);
135
- }
136
- finally {
137
- vi.useRealTimers();
138
- }
139
- });
140
- });
141
- });
@@ -1,20 +0,0 @@
1
- <script lang="ts">
2
- import type { DateValue } from '../../../internal/date-time/types.js'
3
- import * as DateField from '../index.js'
4
-
5
- let {
6
- value = $bindable<DateValue | undefined>(undefined),
7
- disabled = false,
8
- readonly: isReadonly = false,
9
- locale = 'en-US',
10
- ariaLabel,
11
- }: {
12
- value?: DateValue
13
- disabled?: boolean
14
- readonly?: boolean
15
- locale?: string
16
- ariaLabel?: string
17
- } = $props()
18
- </script>
19
-
20
- <DateField.Root bind:value {disabled} readonly={isReadonly} {locale} aria-label={ariaLabel} />
@@ -1,139 +0,0 @@
1
- import { CalendarDate } from '@internationalized/date';
2
- import { flushSync, mount, unmount } from 'svelte';
3
- import { afterEach, describe, expect, it } from 'vitest';
4
- import DateFieldTest from './DateFieldTest.svelte';
5
- describe('date-field', () => {
6
- let target;
7
- let instance;
8
- function setup(props = {}) {
9
- target = document.createElement('div');
10
- document.body.appendChild(target);
11
- instance = mount(DateFieldTest, { target, props });
12
- flushSync();
13
- return target;
14
- }
15
- afterEach(() => {
16
- if (instance)
17
- unmount(instance);
18
- target?.remove();
19
- });
20
- // 1. Renders group role container
21
- it('renders group role container', () => {
22
- setup();
23
- const group = target.querySelector('[role="group"]');
24
- expect(group).toBeTruthy();
25
- expect(group?.classList.contains('sp-date-field')).toBe(true);
26
- });
27
- // 2. Renders day/month/year segments (role="spinbutton")
28
- it('renders day, month, and year segments with role="spinbutton"', () => {
29
- setup();
30
- const spinbuttons = target.querySelectorAll('[role="spinbutton"]');
31
- expect(spinbuttons.length).toBe(3);
32
- });
33
- // 3. Segments have aria-label identifying part (day/month/year or jour/mois/année)
34
- it('segments have aria-label identifying day, month, year', () => {
35
- setup();
36
- const spinbuttons = target.querySelectorAll('[role="spinbutton"]');
37
- const labels = Array.from(spinbuttons).map((s) => s.getAttribute('aria-label')?.toLowerCase() ?? '');
38
- // Each label should match day/month/year (en) or jour/mois/année (fr) etc.
39
- const hasDayLabel = labels.some((l) => /day|jour|giorno|día|tag/i.test(l));
40
- const hasMonthLabel = labels.some((l) => /month|mois|mese|mes|monat/i.test(l));
41
- const hasYearLabel = labels.some((l) => /year|année|anno|año|jahr/i.test(l));
42
- expect(hasDayLabel).toBe(true);
43
- expect(hasMonthLabel).toBe(true);
44
- expect(hasYearLabel).toBe(true);
45
- });
46
- // 4. Renders value when provided
47
- it('renders provided value in segments', () => {
48
- const date = new CalendarDate(2024, 6, 15);
49
- setup({ value: date });
50
- const spinbuttons = target.querySelectorAll('[role="spinbutton"]');
51
- const texts = Array.from(spinbuttons).map((s) => s.textContent?.trim() ?? '');
52
- // 2024, 06, 15 should appear (order may vary by locale)
53
- expect(texts.includes('2024')).toBe(true);
54
- // Month may render as "06" or "6"
55
- expect(texts.includes('06') || texts.includes('6')).toBe(true);
56
- expect(texts.includes('15')).toBe(true);
57
- });
58
- // 5. Typing a digit updates segment
59
- it('typing a digit on the day segment updates its value', () => {
60
- setup();
61
- // Find segment with aria-label "day"
62
- const daySegment = target.querySelector('[role="spinbutton"][aria-label="day"]');
63
- expect(daySegment).toBeTruthy();
64
- daySegment.focus();
65
- flushSync();
66
- daySegment.dispatchEvent(new KeyboardEvent('keydown', { key: '5', bubbles: true }));
67
- flushSync();
68
- // The segment content should reflect digit 5 (may be padded as "05")
69
- const text = daySegment.textContent?.trim() ?? '';
70
- expect(Number.parseInt(text, 10)).toBe(5);
71
- });
72
- // 6. ArrowUp increments segment
73
- it('arrowUp increments the month segment', () => {
74
- const date = new CalendarDate(2024, 6, 15);
75
- setup({ value: date });
76
- const monthSegment = target.querySelector('[role="spinbutton"][aria-label="month"]');
77
- expect(monthSegment).toBeTruthy();
78
- monthSegment.focus();
79
- flushSync();
80
- monthSegment.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
81
- flushSync();
82
- expect(monthSegment.textContent?.trim()).toBe('07');
83
- });
84
- // 7. ArrowDown decrements segment
85
- it('arrowDown decrements the month segment', () => {
86
- const date = new CalendarDate(2024, 6, 15);
87
- setup({ value: date });
88
- const monthSegment = target.querySelector('[role="spinbutton"][aria-label="month"]');
89
- expect(monthSegment).toBeTruthy();
90
- monthSegment.focus();
91
- flushSync();
92
- monthSegment.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
93
- flushSync();
94
- expect(monthSegment.textContent?.trim()).toBe('05');
95
- });
96
- // 8. Tab moves between segments (no error thrown)
97
- it('tab key dispatched on segment does not throw', () => {
98
- setup();
99
- const spinbuttons = target.querySelectorAll('[role="spinbutton"]');
100
- expect(spinbuttons.length).toBe(3);
101
- expect(() => {
102
- spinbuttons[0].focus();
103
- flushSync();
104
- spinbuttons[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
105
- flushSync();
106
- }).not.toThrow();
107
- });
108
- // 9. Disabled → group has data-disabled, segments have tabindex=-1
109
- it('disabled field has data-disabled on group and tabindex=-1 on segments', () => {
110
- setup({ disabled: true });
111
- const group = target.querySelector('[role="group"]');
112
- expect(group?.hasAttribute('data-disabled')).toBe(true);
113
- const spinbuttons = target.querySelectorAll('[role="spinbutton"]');
114
- spinbuttons.forEach((s) => {
115
- expect(s.getAttribute('tabindex')).toBe('-1');
116
- });
117
- });
118
- // 10. Readonly → segments have aria-readonly="true"
119
- it('readonly field has aria-readonly on segments', () => {
120
- setup({ readonly: true });
121
- const spinbuttons = target.querySelectorAll('[role="spinbutton"]');
122
- expect(spinbuttons.length).toBeGreaterThan(0);
123
- spinbuttons.forEach((s) => {
124
- expect(s.getAttribute('aria-readonly')).toBe('true');
125
- });
126
- });
127
- // 11. locale="fr-FR" renders segments (smoke — no crash, still 3 spinbuttons)
128
- it('locale="fr-FR" renders three spinbutton segments without error', () => {
129
- setup({ locale: 'fr-FR' });
130
- const spinbuttons = target.querySelectorAll('[role="spinbutton"]');
131
- expect(spinbuttons.length).toBe(3);
132
- });
133
- // 12. Group has aria-label
134
- it('group has an aria-label', () => {
135
- setup({ ariaLabel: 'Date de naissance' });
136
- const group = target.querySelector('[role="group"]');
137
- expect(group?.getAttribute('aria-label')).toBe('Date de naissance');
138
- });
139
- });