@spectral_labs/ui 1.7.0 → 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 (150) hide show
  1. package/LICENSE +21 -0
  2. package/THIRD_PARTY_NOTICES.md +201 -0
  3. package/package.json +8 -2
  4. package/dist/components/__tests__/overlay-scrim-source.test.js +0 -134
  5. package/dist/components/accordion/__tests__/AccordionTest.svelte +0 -31
  6. package/dist/components/accordion/__tests__/accordion.test.svelte.js +0 -139
  7. package/dist/components/alert-dialog/__tests__/AlertDialogTest.svelte +0 -28
  8. package/dist/components/alert-dialog/__tests__/alert-dialog.a11y.test.js +0 -42
  9. package/dist/components/alert-dialog/__tests__/alert-dialog.integration.test.svelte.js +0 -51
  10. package/dist/components/alert-dialog/__tests__/alert-dialog.test.svelte.js +0 -113
  11. package/dist/components/app-bar/__tests__/app-bar.test.svelte.js +0 -30
  12. package/dist/components/aspect-ratio/__tests__/aspect-ratio.test.svelte.js +0 -22
  13. package/dist/components/avatar/__tests__/avatar.test.svelte.js +0 -26
  14. package/dist/components/badge/__tests__/badge.test.svelte.js +0 -30
  15. package/dist/components/bottom-sheet/__tests__/BottomSheetTest.svelte +0 -22
  16. package/dist/components/bottom-sheet/__tests__/bottom-sheet.test.svelte.js +0 -83
  17. package/dist/components/button/__tests__/button.test.svelte.js +0 -63
  18. package/dist/components/calendar/__tests__/CalendarTest.svelte +0 -56
  19. package/dist/components/calendar/__tests__/calendar.test.svelte.js +0 -190
  20. package/dist/components/card/__tests__/card.test.svelte.js +0 -30
  21. package/dist/components/checkbox/__tests__/checkbox.test.svelte.js +0 -44
  22. package/dist/components/chips/__tests__/chips.test.svelte.js +0 -26
  23. package/dist/components/collapsible/__tests__/CollapsibleTest.svelte +0 -18
  24. package/dist/components/collapsible/__tests__/collapsible.test.svelte.js +0 -125
  25. package/dist/components/combobox/__tests__/ComboboxTest.svelte +0 -40
  26. package/dist/components/combobox/__tests__/combobox.test.svelte.js +0 -255
  27. package/dist/components/command/__tests__/CommandInlineDirectTest.svelte +0 -29
  28. package/dist/components/command/__tests__/CommandInlineTest.svelte +0 -89
  29. package/dist/components/command/__tests__/CommandTest.svelte +0 -77
  30. package/dist/components/command/__tests__/command-inline.test.svelte.js +0 -392
  31. package/dist/components/command/__tests__/command-item-passthrough.test.svelte.js +0 -284
  32. package/dist/components/command/__tests__/command.a11y.test.js +0 -30
  33. package/dist/components/command/__tests__/command.integration.test.svelte.js +0 -68
  34. package/dist/components/command/__tests__/command.test.svelte.js +0 -230
  35. package/dist/components/context-menu/__tests__/ContextMenuNonInteractiveTest.svelte +0 -32
  36. package/dist/components/context-menu/__tests__/ContextMenuTest.svelte +0 -25
  37. package/dist/components/context-menu/__tests__/context-menu.test.svelte.js +0 -141
  38. package/dist/components/date-field/__tests__/DateFieldTest.svelte +0 -20
  39. package/dist/components/date-field/__tests__/date-field.test.svelte.js +0 -139
  40. package/dist/components/date-picker/__tests__/DatePickerTest.svelte +0 -20
  41. package/dist/components/date-picker/__tests__/date-picker.test.svelte.js +0 -190
  42. package/dist/components/date-range-field/__tests__/DateRangeFieldTest.svelte +0 -20
  43. package/dist/components/date-range-field/__tests__/date-range-field.test.svelte.js +0 -130
  44. package/dist/components/date-range-picker/__tests__/DateRangePickerTest.svelte +0 -18
  45. package/dist/components/date-range-picker/__tests__/date-range-picker.test.svelte.js +0 -156
  46. package/dist/components/dialog/__tests__/DialogTest.svelte +0 -34
  47. package/dist/components/dialog/__tests__/dialog.test.svelte.js +0 -184
  48. package/dist/components/drop-zone/__tests__/drop-zone.test.svelte.js +0 -26
  49. package/dist/components/dropdown-menu/__tests__/DropdownMenuRadioTest.svelte +0 -90
  50. package/dist/components/dropdown-menu/__tests__/DropdownMenuTest.svelte +0 -71
  51. package/dist/components/dropdown-menu/__tests__/dropdown-menu-radio-group.test.svelte.js +0 -306
  52. package/dist/components/dropdown-menu/__tests__/dropdown-menu.test.svelte.js +0 -567
  53. package/dist/components/empty-state/__tests__/empty-state.test.svelte.js +0 -26
  54. package/dist/components/fab/__tests__/fab.test.svelte.js +0 -30
  55. package/dist/components/field/__tests__/FieldTest.svelte +0 -26
  56. package/dist/components/field/__tests__/field.test.svelte.js +0 -74
  57. package/dist/components/grid-list/__tests__/GridListTest.svelte +0 -46
  58. package/dist/components/grid-list/__tests__/grid-list.test.svelte.js +0 -231
  59. package/dist/components/hover-card/__tests__/HoverCardTest.svelte +0 -30
  60. package/dist/components/hover-card/__tests__/hover-card.test.svelte.js +0 -236
  61. package/dist/components/indicator/__tests__/indicator.test.svelte.js +0 -30
  62. package/dist/components/input/__tests__/input.test.svelte.js +0 -67
  63. package/dist/components/label/__tests__/label.test.svelte.js +0 -22
  64. package/dist/components/language-select/__tests__/language-select.test.svelte.js +0 -199
  65. package/dist/components/language-select/__tests__/languages.test.js +0 -87
  66. package/dist/components/link-preview/__tests__/LinkPreviewTest.svelte +0 -37
  67. package/dist/components/link-preview/__tests__/link-preview.a11y.test.js +0 -28
  68. package/dist/components/link-preview/__tests__/link-preview.integration.test.svelte.js +0 -54
  69. package/dist/components/link-preview/__tests__/link-preview.test.svelte.js +0 -215
  70. package/dist/components/list/__tests__/list.test.svelte.js +0 -22
  71. package/dist/components/menubar/__tests__/MenubarTest.svelte +0 -52
  72. package/dist/components/menubar/__tests__/menubar.a11y.test.js +0 -28
  73. package/dist/components/menubar/__tests__/menubar.integration.test.svelte.js +0 -57
  74. package/dist/components/menubar/__tests__/menubar.test.svelte.js +0 -328
  75. package/dist/components/meter/__tests__/meter.test.svelte.js +0 -26
  76. package/dist/components/nav-bar/__tests__/nav-bar.test.svelte.js +0 -23
  77. package/dist/components/nav-drawer/__tests__/nav-drawer.test.svelte.js +0 -28
  78. package/dist/components/nav-rail/__tests__/nav-rail.test.svelte.js +0 -28
  79. package/dist/components/pagination/__tests__/pagination.test.svelte.js +0 -26
  80. package/dist/components/pin-input/__tests__/pin-input.test.svelte.js +0 -71
  81. package/dist/components/popover/__tests__/PopoverTest.svelte +0 -27
  82. package/dist/components/popover/__tests__/popover.test.svelte.js +0 -196
  83. package/dist/components/progress/__tests__/progress.test.svelte.js +0 -32
  84. package/dist/components/radio-group/__tests__/radio-group.test.svelte.js +0 -22
  85. package/dist/components/range-calendar/__tests__/RangeCalendarTest.svelte +0 -23
  86. package/dist/components/range-calendar/__tests__/range-calendar.test.svelte.js +0 -105
  87. package/dist/components/rating-group/__tests__/rating-group.test.svelte.js +0 -26
  88. package/dist/components/scroll-area/__tests__/scroll-area.test.svelte.js +0 -34
  89. package/dist/components/search-field/__tests__/SearchFieldTest.svelte +0 -34
  90. package/dist/components/search-field/__tests__/search-field.test.svelte.js +0 -118
  91. package/dist/components/select/__tests__/SelectLabelPersistenceTest.svelte +0 -31
  92. package/dist/components/select/__tests__/SelectTest.svelte +0 -33
  93. package/dist/components/select/__tests__/select.test.svelte.js +0 -221
  94. package/dist/components/separator/__tests__/separator.test.svelte.js +0 -39
  95. package/dist/components/sheet/__tests__/SheetTest.svelte +0 -34
  96. package/dist/components/sheet/__tests__/sheet.test.svelte.js +0 -193
  97. package/dist/components/side-sheet/__tests__/side-sheet.test.svelte.js +0 -138
  98. package/dist/components/sidebar/__tests__/SidebarFullTest.svelte +0 -45
  99. package/dist/components/sidebar/__tests__/SidebarTest.svelte +0 -9
  100. package/dist/components/sidebar/__tests__/sidebar.test.svelte.js +0 -186
  101. package/dist/components/simple-dialog/__tests__/SimpleDialogTest.svelte +0 -40
  102. package/dist/components/simple-dialog/__tests__/simple-dialog.test.svelte.js +0 -50
  103. package/dist/components/simple-tooltip/__tests__/SimpleTooltipTest.svelte +0 -19
  104. package/dist/components/simple-tooltip/__tests__/simple-tooltip.test.svelte.js +0 -61
  105. package/dist/components/slider/__tests__/slider.test.svelte.js +0 -42
  106. package/dist/components/sortable-list/__tests__/NestedSortableListTest.svelte +0 -49
  107. package/dist/components/sortable-list/__tests__/SortableListTest.svelte +0 -42
  108. package/dist/components/sortable-list/__tests__/sortable-list.nested.test.svelte.js +0 -112
  109. package/dist/components/sortable-list/__tests__/sortable-list.test.svelte.js +0 -175
  110. package/dist/components/splitter/__tests__/splitter.test.svelte.js +0 -26
  111. package/dist/components/switch/__tests__/switch.test.svelte.js +0 -44
  112. package/dist/components/table/__tests__/table.test.svelte.js +0 -22
  113. package/dist/components/tabs/__tests__/TabsTest.svelte +0 -32
  114. package/dist/components/tabs/__tests__/tabs.test.svelte.js +0 -227
  115. package/dist/components/textarea/__tests__/textarea.test.svelte.js +0 -55
  116. package/dist/components/time-field/__tests__/TimeFieldTest.svelte +0 -20
  117. package/dist/components/time-field/__tests__/time-field.test.svelte.js +0 -100
  118. package/dist/components/time-range-field/__tests__/TimeRangeFieldTest.svelte +0 -20
  119. package/dist/components/time-range-field/__tests__/time-range-field.test.svelte.js +0 -124
  120. package/dist/components/toast/__tests__/toast.test.svelte.js +0 -46
  121. package/dist/components/toggle/__tests__/toggle.test.svelte.js +0 -38
  122. package/dist/components/toggle-group/__tests__/toggle-group.test.svelte.js +0 -22
  123. package/dist/components/toolbar/__tests__/ToolbarCompositionTest.svelte +0 -18
  124. package/dist/components/toolbar/__tests__/ToolbarTabstopTest.svelte +0 -10
  125. package/dist/components/toolbar/__tests__/toolbar.test.svelte.js +0 -63
  126. package/dist/components/tooltip/__tests__/TooltipTest.svelte +0 -22
  127. package/dist/components/tooltip/__tests__/tooltip.test.svelte.js +0 -225
  128. package/dist/components/tree/__tests__/TreeLabelContentTest.svelte +0 -21
  129. package/dist/components/tree/__tests__/TreeTest.svelte +0 -24
  130. package/dist/components/tree/__tests__/tree.test.svelte.js +0 -229
  131. package/dist/components/virtualizer/__tests__/waterfall-layout.test.js +0 -82
  132. package/dist/internal/announcer/__tests__/announcer.test.js +0 -42
  133. package/dist/internal/dismissible-layer/__tests__/dismissible-layer.test.js +0 -196
  134. package/dist/internal/drag-drop/__tests__/auto-scroll.test.js +0 -104
  135. package/dist/internal/drag-drop/__tests__/drag-session.test.svelte.js +0 -803
  136. package/dist/internal/drag-drop/__tests__/drag-source.test.svelte.js +0 -130
  137. package/dist/internal/drag-drop/__tests__/drop-delegate.test.js +0 -223
  138. package/dist/internal/drag-drop/__tests__/types.test.js +0 -20
  139. package/dist/internal/escape-layer/__tests__/escape-layer.test.js +0 -113
  140. package/dist/internal/focus-scope/__tests__/focus-scope.test.js +0 -225
  141. package/dist/internal/grace-area/__tests__/grace-area.test.js +0 -67
  142. package/dist/internal/helpers/__tests__/helpers.test.js +0 -123
  143. package/dist/internal/portal/__tests__/PortalTest.svelte +0 -21
  144. package/dist/internal/portal/__tests__/portal.test.js +0 -29
  145. package/dist/internal/portal/__tests__/portal.test.svelte.js +0 -57
  146. package/dist/internal/scroll-lock/__tests__/scroll-lock.test.js +0 -160
  147. package/dist/internal/typeahead/__tests__/typeahead.test.js +0 -61
  148. package/src/themes/__tests__/carbon-dark-contrast.test.ts +0 -170
  149. package/src/themes/__tests__/generated-css-drift.test.ts +0 -53
  150. package/src/themes/__tests__/scrim-opaque.test.ts +0 -52
@@ -1,30 +0,0 @@
1
- import { mount, unmount } from 'svelte';
2
- import { afterEach, describe, expect, it } from 'vitest';
3
- import { configureAxe } from 'vitest-axe';
4
- import * as matchers from 'vitest-axe/matchers';
5
- import CommandTest from './CommandTest.svelte';
6
- expect.extend(matchers);
7
- const axe = configureAxe({
8
- rules: {
9
- region: { enabled: false },
10
- 'page-has-heading-one': { enabled: false },
11
- 'landmark-one-main': { enabled: false },
12
- // Command uses listbox > group > option — valid WAI-ARIA but axe doesn't walk through groups
13
- 'aria-required-children': { enabled: false },
14
- },
15
- });
16
- describe('command a11y', () => {
17
- let cleanup;
18
- afterEach(() => cleanup?.());
19
- it('has no axe violations', async () => {
20
- const container = document.createElement('div');
21
- document.body.appendChild(container);
22
- const instance = mount(CommandTest, { target: container });
23
- const results = await axe(container);
24
- cleanup = () => {
25
- unmount(instance);
26
- container.remove();
27
- };
28
- expect(results).toHaveNoViolations();
29
- });
30
- });
@@ -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} />