@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.
- package/LICENSE +21 -0
- package/THIRD_PARTY_NOTICES.md +201 -0
- package/dist/components/command/command.svelte.d.ts +30 -0
- package/dist/components/command/command.svelte.d.ts.map +1 -1
- package/dist/components/command/command.svelte.js +85 -4
- package/dist/components/command/components/command-group.svelte +25 -15
- package/dist/components/command/components/command-group.svelte.d.ts.map +1 -1
- package/dist/components/command/components/command-input.svelte +54 -5
- package/dist/components/command/components/command-input.svelte.d.ts +1 -1
- package/dist/components/command/components/command-input.svelte.d.ts.map +1 -1
- package/dist/components/command/components/command-list.svelte +22 -3
- package/dist/components/command/components/command-list.svelte.d.ts.map +1 -1
- package/dist/components/command/components/command-root.svelte +43 -4
- package/dist/components/command/components/command-root.svelte.d.ts.map +1 -1
- package/dist/components/command/index.d.ts +1 -0
- package/dist/components/command/index.d.ts.map +1 -1
- package/dist/components/command/types.d.ts +59 -4
- package/dist/components/command/types.d.ts.map +1 -1
- package/package.json +8 -2
- package/dist/components/__tests__/overlay-scrim-source.test.js +0 -134
- package/dist/components/accordion/__tests__/AccordionTest.svelte +0 -31
- package/dist/components/accordion/__tests__/accordion.test.svelte.js +0 -139
- package/dist/components/alert-dialog/__tests__/AlertDialogTest.svelte +0 -28
- package/dist/components/alert-dialog/__tests__/alert-dialog.a11y.test.js +0 -42
- package/dist/components/alert-dialog/__tests__/alert-dialog.integration.test.svelte.js +0 -51
- package/dist/components/alert-dialog/__tests__/alert-dialog.test.svelte.js +0 -113
- package/dist/components/app-bar/__tests__/app-bar.test.svelte.js +0 -30
- package/dist/components/aspect-ratio/__tests__/aspect-ratio.test.svelte.js +0 -22
- package/dist/components/avatar/__tests__/avatar.test.svelte.js +0 -26
- package/dist/components/badge/__tests__/badge.test.svelte.js +0 -30
- package/dist/components/bottom-sheet/__tests__/BottomSheetTest.svelte +0 -22
- package/dist/components/bottom-sheet/__tests__/bottom-sheet.test.svelte.js +0 -83
- package/dist/components/button/__tests__/button.test.svelte.js +0 -63
- package/dist/components/calendar/__tests__/CalendarTest.svelte +0 -56
- package/dist/components/calendar/__tests__/calendar.test.svelte.js +0 -190
- package/dist/components/card/__tests__/card.test.svelte.js +0 -30
- package/dist/components/checkbox/__tests__/checkbox.test.svelte.js +0 -44
- package/dist/components/chips/__tests__/chips.test.svelte.js +0 -26
- package/dist/components/collapsible/__tests__/CollapsibleTest.svelte +0 -18
- package/dist/components/collapsible/__tests__/collapsible.test.svelte.js +0 -125
- package/dist/components/combobox/__tests__/ComboboxTest.svelte +0 -40
- package/dist/components/combobox/__tests__/combobox.test.svelte.js +0 -255
- package/dist/components/command/__tests__/CommandTest.svelte +0 -77
- package/dist/components/command/__tests__/command-item-passthrough.test.svelte.js +0 -284
- package/dist/components/command/__tests__/command.a11y.test.js +0 -30
- package/dist/components/command/__tests__/command.integration.test.svelte.js +0 -68
- package/dist/components/command/__tests__/command.test.svelte.js +0 -230
- package/dist/components/context-menu/__tests__/ContextMenuNonInteractiveTest.svelte +0 -32
- package/dist/components/context-menu/__tests__/ContextMenuTest.svelte +0 -25
- package/dist/components/context-menu/__tests__/context-menu.test.svelte.js +0 -141
- package/dist/components/date-field/__tests__/DateFieldTest.svelte +0 -20
- package/dist/components/date-field/__tests__/date-field.test.svelte.js +0 -139
- package/dist/components/date-picker/__tests__/DatePickerTest.svelte +0 -20
- package/dist/components/date-picker/__tests__/date-picker.test.svelte.js +0 -190
- package/dist/components/date-range-field/__tests__/DateRangeFieldTest.svelte +0 -20
- package/dist/components/date-range-field/__tests__/date-range-field.test.svelte.js +0 -130
- package/dist/components/date-range-picker/__tests__/DateRangePickerTest.svelte +0 -18
- package/dist/components/date-range-picker/__tests__/date-range-picker.test.svelte.js +0 -156
- package/dist/components/dialog/__tests__/DialogTest.svelte +0 -34
- package/dist/components/dialog/__tests__/dialog.test.svelte.js +0 -184
- package/dist/components/drop-zone/__tests__/drop-zone.test.svelte.js +0 -26
- package/dist/components/dropdown-menu/__tests__/DropdownMenuRadioTest.svelte +0 -90
- package/dist/components/dropdown-menu/__tests__/DropdownMenuTest.svelte +0 -71
- package/dist/components/dropdown-menu/__tests__/dropdown-menu-radio-group.test.svelte.js +0 -306
- package/dist/components/dropdown-menu/__tests__/dropdown-menu.test.svelte.js +0 -567
- package/dist/components/empty-state/__tests__/empty-state.test.svelte.js +0 -26
- package/dist/components/fab/__tests__/fab.test.svelte.js +0 -30
- package/dist/components/field/__tests__/FieldTest.svelte +0 -26
- package/dist/components/field/__tests__/field.test.svelte.js +0 -74
- package/dist/components/grid-list/__tests__/GridListTest.svelte +0 -46
- package/dist/components/grid-list/__tests__/grid-list.test.svelte.js +0 -231
- package/dist/components/hover-card/__tests__/HoverCardTest.svelte +0 -30
- package/dist/components/hover-card/__tests__/hover-card.test.svelte.js +0 -236
- package/dist/components/indicator/__tests__/indicator.test.svelte.js +0 -30
- package/dist/components/input/__tests__/input.test.svelte.js +0 -67
- package/dist/components/label/__tests__/label.test.svelte.js +0 -22
- package/dist/components/language-select/__tests__/language-select.test.svelte.js +0 -199
- package/dist/components/language-select/__tests__/languages.test.js +0 -87
- package/dist/components/link-preview/__tests__/LinkPreviewTest.svelte +0 -37
- package/dist/components/link-preview/__tests__/link-preview.a11y.test.js +0 -28
- package/dist/components/link-preview/__tests__/link-preview.integration.test.svelte.js +0 -54
- package/dist/components/link-preview/__tests__/link-preview.test.svelte.js +0 -215
- package/dist/components/list/__tests__/list.test.svelte.js +0 -22
- package/dist/components/menubar/__tests__/MenubarTest.svelte +0 -52
- package/dist/components/menubar/__tests__/menubar.a11y.test.js +0 -28
- package/dist/components/menubar/__tests__/menubar.integration.test.svelte.js +0 -57
- package/dist/components/menubar/__tests__/menubar.test.svelte.js +0 -328
- package/dist/components/meter/__tests__/meter.test.svelte.js +0 -26
- package/dist/components/nav-bar/__tests__/nav-bar.test.svelte.js +0 -23
- package/dist/components/nav-drawer/__tests__/nav-drawer.test.svelte.js +0 -28
- package/dist/components/nav-rail/__tests__/nav-rail.test.svelte.js +0 -28
- package/dist/components/pagination/__tests__/pagination.test.svelte.js +0 -26
- package/dist/components/pin-input/__tests__/pin-input.test.svelte.js +0 -71
- package/dist/components/popover/__tests__/PopoverTest.svelte +0 -27
- package/dist/components/popover/__tests__/popover.test.svelte.js +0 -196
- package/dist/components/progress/__tests__/progress.test.svelte.js +0 -32
- package/dist/components/radio-group/__tests__/radio-group.test.svelte.js +0 -22
- package/dist/components/range-calendar/__tests__/RangeCalendarTest.svelte +0 -23
- package/dist/components/range-calendar/__tests__/range-calendar.test.svelte.js +0 -105
- package/dist/components/rating-group/__tests__/rating-group.test.svelte.js +0 -26
- package/dist/components/scroll-area/__tests__/scroll-area.test.svelte.js +0 -34
- package/dist/components/search-field/__tests__/SearchFieldTest.svelte +0 -34
- package/dist/components/search-field/__tests__/search-field.test.svelte.js +0 -118
- package/dist/components/select/__tests__/SelectLabelPersistenceTest.svelte +0 -31
- package/dist/components/select/__tests__/SelectTest.svelte +0 -33
- package/dist/components/select/__tests__/select.test.svelte.js +0 -221
- package/dist/components/separator/__tests__/separator.test.svelte.js +0 -39
- package/dist/components/sheet/__tests__/SheetTest.svelte +0 -34
- package/dist/components/sheet/__tests__/sheet.test.svelte.js +0 -193
- package/dist/components/side-sheet/__tests__/side-sheet.test.svelte.js +0 -138
- package/dist/components/sidebar/__tests__/SidebarFullTest.svelte +0 -45
- package/dist/components/sidebar/__tests__/SidebarTest.svelte +0 -9
- package/dist/components/sidebar/__tests__/sidebar.test.svelte.js +0 -186
- package/dist/components/simple-dialog/__tests__/SimpleDialogTest.svelte +0 -40
- package/dist/components/simple-dialog/__tests__/simple-dialog.test.svelte.js +0 -50
- package/dist/components/simple-tooltip/__tests__/SimpleTooltipTest.svelte +0 -19
- package/dist/components/simple-tooltip/__tests__/simple-tooltip.test.svelte.js +0 -61
- package/dist/components/slider/__tests__/slider.test.svelte.js +0 -42
- package/dist/components/sortable-list/__tests__/NestedSortableListTest.svelte +0 -49
- package/dist/components/sortable-list/__tests__/SortableListTest.svelte +0 -42
- package/dist/components/sortable-list/__tests__/sortable-list.nested.test.svelte.js +0 -112
- package/dist/components/sortable-list/__tests__/sortable-list.test.svelte.js +0 -175
- package/dist/components/splitter/__tests__/splitter.test.svelte.js +0 -26
- package/dist/components/switch/__tests__/switch.test.svelte.js +0 -44
- package/dist/components/table/__tests__/table.test.svelte.js +0 -22
- package/dist/components/tabs/__tests__/TabsTest.svelte +0 -32
- package/dist/components/tabs/__tests__/tabs.test.svelte.js +0 -227
- package/dist/components/textarea/__tests__/textarea.test.svelte.js +0 -55
- package/dist/components/time-field/__tests__/TimeFieldTest.svelte +0 -20
- package/dist/components/time-field/__tests__/time-field.test.svelte.js +0 -100
- package/dist/components/time-range-field/__tests__/TimeRangeFieldTest.svelte +0 -20
- package/dist/components/time-range-field/__tests__/time-range-field.test.svelte.js +0 -124
- package/dist/components/toast/__tests__/toast.test.svelte.js +0 -46
- package/dist/components/toggle/__tests__/toggle.test.svelte.js +0 -38
- package/dist/components/toggle-group/__tests__/toggle-group.test.svelte.js +0 -22
- package/dist/components/toolbar/__tests__/ToolbarCompositionTest.svelte +0 -18
- package/dist/components/toolbar/__tests__/ToolbarTabstopTest.svelte +0 -10
- package/dist/components/toolbar/__tests__/toolbar.test.svelte.js +0 -63
- package/dist/components/tooltip/__tests__/TooltipTest.svelte +0 -22
- package/dist/components/tooltip/__tests__/tooltip.test.svelte.js +0 -225
- package/dist/components/tree/__tests__/TreeLabelContentTest.svelte +0 -21
- package/dist/components/tree/__tests__/TreeTest.svelte +0 -24
- package/dist/components/tree/__tests__/tree.test.svelte.js +0 -229
- package/dist/components/virtualizer/__tests__/waterfall-layout.test.js +0 -82
- package/dist/internal/announcer/__tests__/announcer.test.js +0 -42
- package/dist/internal/dismissible-layer/__tests__/dismissible-layer.test.js +0 -196
- package/dist/internal/drag-drop/__tests__/auto-scroll.test.js +0 -104
- package/dist/internal/drag-drop/__tests__/drag-session.test.svelte.js +0 -803
- package/dist/internal/drag-drop/__tests__/drag-source.test.svelte.js +0 -130
- package/dist/internal/drag-drop/__tests__/drop-delegate.test.js +0 -223
- package/dist/internal/drag-drop/__tests__/types.test.js +0 -20
- package/dist/internal/escape-layer/__tests__/escape-layer.test.js +0 -113
- package/dist/internal/focus-scope/__tests__/focus-scope.test.js +0 -225
- package/dist/internal/grace-area/__tests__/grace-area.test.js +0 -67
- package/dist/internal/helpers/__tests__/helpers.test.js +0 -123
- package/dist/internal/portal/__tests__/PortalTest.svelte +0 -21
- package/dist/internal/portal/__tests__/portal.test.js +0 -29
- package/dist/internal/portal/__tests__/portal.test.svelte.js +0 -57
- package/dist/internal/scroll-lock/__tests__/scroll-lock.test.js +0 -160
- package/dist/internal/typeahead/__tests__/typeahead.test.js +0 -61
- package/src/themes/__tests__/carbon-dark-contrast.test.ts +0 -170
- package/src/themes/__tests__/generated-css-drift.test.ts +0 -53
- package/src/themes/__tests__/scrim-opaque.test.ts +0 -52
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { flushSync, mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import ComboboxRoot from '../components/combobox-root.svelte';
|
|
4
|
-
import ComboboxTest from './ComboboxTest.svelte';
|
|
5
|
-
describe('combobox', () => {
|
|
6
|
-
let target;
|
|
7
|
-
let instance;
|
|
8
|
-
function setup(props = {}) {
|
|
9
|
-
target = document.createElement('div');
|
|
10
|
-
document.body.appendChild(target);
|
|
11
|
-
instance = mount(ComboboxTest, { target, props: { portalTo: target, ...props } });
|
|
12
|
-
flushSync();
|
|
13
|
-
return target;
|
|
14
|
-
}
|
|
15
|
-
function setupRoot(props = {}) {
|
|
16
|
-
target = document.createElement('div');
|
|
17
|
-
document.body.appendChild(target);
|
|
18
|
-
instance = mount(ComboboxRoot, { target, props });
|
|
19
|
-
return target;
|
|
20
|
-
}
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
if (instance)
|
|
23
|
-
unmount(instance);
|
|
24
|
-
target?.remove();
|
|
25
|
-
});
|
|
26
|
-
// --- Existing tests ---
|
|
27
|
-
it('renders closed by default', () => {
|
|
28
|
-
setupRoot();
|
|
29
|
-
const root = target.querySelector('.sp-combobox-root');
|
|
30
|
-
expect(root?.getAttribute('data-state')).toBe('closed');
|
|
31
|
-
});
|
|
32
|
-
it('renders hidden input when name is provided', () => {
|
|
33
|
-
setupRoot({ name: 'fruit', value: 'Apple' });
|
|
34
|
-
const input = target.querySelector('input[type="hidden"][name="fruit"]');
|
|
35
|
-
expect(input).toBeTruthy();
|
|
36
|
-
expect(input?.value).toBe('Apple');
|
|
37
|
-
});
|
|
38
|
-
// --- Input ARIA ---
|
|
39
|
-
it('input has role="combobox"', () => {
|
|
40
|
-
setup();
|
|
41
|
-
const input = target.querySelector('[role="combobox"]');
|
|
42
|
-
expect(input).toBeTruthy();
|
|
43
|
-
});
|
|
44
|
-
it('input has aria-expanded="false" when closed', () => {
|
|
45
|
-
setup();
|
|
46
|
-
const input = target.querySelector('[role="combobox"]');
|
|
47
|
-
expect(input?.getAttribute('aria-expanded')).toBe('false');
|
|
48
|
-
});
|
|
49
|
-
it('input has aria-autocomplete="list"', () => {
|
|
50
|
-
setup();
|
|
51
|
-
const input = target.querySelector('[role="combobox"]');
|
|
52
|
-
expect(input?.getAttribute('aria-autocomplete')).toBe('list');
|
|
53
|
-
});
|
|
54
|
-
it('input has autocomplete="off"', () => {
|
|
55
|
-
setup();
|
|
56
|
-
const input = target.querySelector('[role="combobox"]');
|
|
57
|
-
expect(input?.autocomplete).toBe('off');
|
|
58
|
-
});
|
|
59
|
-
// --- Open on focus ---
|
|
60
|
-
it('opens on input focus', () => {
|
|
61
|
-
setup();
|
|
62
|
-
const input = target.querySelector('[role="combobox"]');
|
|
63
|
-
input.dispatchEvent(new FocusEvent('focus', { bubbles: true }));
|
|
64
|
-
flushSync();
|
|
65
|
-
expect(target.querySelector('.sp-combobox-root')?.getAttribute('data-state')).toBe('open');
|
|
66
|
-
});
|
|
67
|
-
// --- Keyboard navigation ---
|
|
68
|
-
it('arrowDown opens listbox and navigates to next item', () => {
|
|
69
|
-
setup();
|
|
70
|
-
const input = target.querySelector('[role="combobox"]');
|
|
71
|
-
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
|
72
|
-
flushSync();
|
|
73
|
-
// Should be open
|
|
74
|
-
expect(target.querySelector('.sp-combobox-root')?.getAttribute('data-state')).toBe('open');
|
|
75
|
-
});
|
|
76
|
-
it('escape closes the listbox', () => {
|
|
77
|
-
setup();
|
|
78
|
-
const input = target.querySelector('[role="combobox"]');
|
|
79
|
-
// Open first
|
|
80
|
-
input.dispatchEvent(new FocusEvent('focus', { bubbles: true }));
|
|
81
|
-
flushSync();
|
|
82
|
-
expect(target.querySelector('.sp-combobox-root')?.getAttribute('data-state')).toBe('open');
|
|
83
|
-
// Escape
|
|
84
|
-
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
85
|
-
flushSync();
|
|
86
|
-
expect(target.querySelector('.sp-combobox-root')?.getAttribute('data-state')).toBe('closed');
|
|
87
|
-
});
|
|
88
|
-
// --- IME composition guard ---
|
|
89
|
-
it('ignores keydown during IME composition', () => {
|
|
90
|
-
setup();
|
|
91
|
-
const input = target.querySelector('[role="combobox"]');
|
|
92
|
-
// Simulate IME composition event (isComposing: true)
|
|
93
|
-
const event = new KeyboardEvent('keydown', {
|
|
94
|
-
key: 'ArrowDown',
|
|
95
|
-
bubbles: true,
|
|
96
|
-
composed: true,
|
|
97
|
-
});
|
|
98
|
-
// Override isComposing since KeyboardEvent constructor doesn't support it directly
|
|
99
|
-
Object.defineProperty(event, 'isComposing', { value: true });
|
|
100
|
-
input.dispatchEvent(event);
|
|
101
|
-
flushSync();
|
|
102
|
-
// Should NOT have opened because isComposing was true
|
|
103
|
-
expect(target.querySelector('.sp-combobox-root')?.getAttribute('data-state')).toBe('closed');
|
|
104
|
-
});
|
|
105
|
-
it('calls onOpenChange with new value on each open transition', () => {
|
|
106
|
-
const calls = [];
|
|
107
|
-
const localTarget = document.createElement('div');
|
|
108
|
-
document.body.appendChild(localTarget);
|
|
109
|
-
const localInstance = mount(ComboboxTest, {
|
|
110
|
-
target: localTarget,
|
|
111
|
-
props: {
|
|
112
|
-
open: false,
|
|
113
|
-
portalTo: localTarget,
|
|
114
|
-
onOpenChange: (open) => calls.push(open),
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
flushSync();
|
|
118
|
-
try {
|
|
119
|
-
const input = localTarget.querySelector('.sp-combobox-input');
|
|
120
|
-
input.dispatchEvent(new FocusEvent('focus', { bubbles: true }));
|
|
121
|
-
flushSync();
|
|
122
|
-
expect(calls).toEqual([true]);
|
|
123
|
-
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
124
|
-
flushSync();
|
|
125
|
-
expect(calls).toEqual([true, false]);
|
|
126
|
-
}
|
|
127
|
-
finally {
|
|
128
|
-
unmount(localInstance);
|
|
129
|
-
localTarget.remove();
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
it('processes keydown when not composing', () => {
|
|
133
|
-
setup();
|
|
134
|
-
const input = target.querySelector('[role="combobox"]');
|
|
135
|
-
const event = new KeyboardEvent('keydown', {
|
|
136
|
-
key: 'ArrowDown',
|
|
137
|
-
bubbles: true,
|
|
138
|
-
});
|
|
139
|
-
Object.defineProperty(event, 'isComposing', { value: false });
|
|
140
|
-
input.dispatchEvent(event);
|
|
141
|
-
flushSync();
|
|
142
|
-
// Should have opened because isComposing was false
|
|
143
|
-
expect(target.querySelector('.sp-combobox-root')?.getAttribute('data-state')).toBe('open');
|
|
144
|
-
});
|
|
145
|
-
describe('content positioning (FloatingLayerState)', () => {
|
|
146
|
-
// Floating UI's computePosition is async; give it a microtask to run.
|
|
147
|
-
async function openAndWaitForFloating(props = {}) {
|
|
148
|
-
setup(props);
|
|
149
|
-
const input = target.querySelector('[role="combobox"]');
|
|
150
|
-
input.dispatchEvent(new FocusEvent('focus', { bubbles: true }));
|
|
151
|
-
flushSync();
|
|
152
|
-
}
|
|
153
|
-
it('applies inline position:absolute on content after opening', async () => {
|
|
154
|
-
await openAndWaitForFloating({ disablePortal: true });
|
|
155
|
-
const content = target.querySelector('.sp-combobox-content');
|
|
156
|
-
await vi.waitFor(() => {
|
|
157
|
-
expect(content.style.position).toBe('absolute');
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
it('applies inline top and left written by Floating UI (not CSS 100%/0)', async () => {
|
|
161
|
-
await openAndWaitForFloating({ disablePortal: true });
|
|
162
|
-
const content = target.querySelector('.sp-combobox-content');
|
|
163
|
-
await vi.waitFor(() => {
|
|
164
|
-
// Floating UI writes pixel values — in jsdom they resolve to '0px'
|
|
165
|
-
// since getBoundingClientRect returns zero, but the values must be
|
|
166
|
-
// *inline styles* (not CSS rules) so the '100%' top and '0' right
|
|
167
|
-
// that caused the bug are gone.
|
|
168
|
-
expect(content.style.top).toMatch(/px$/);
|
|
169
|
-
expect(content.style.left).toMatch(/px$/);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
it('accepts the side prop (default: bottom)', async () => {
|
|
173
|
-
await openAndWaitForFloating({ disablePortal: true, side: 'top' });
|
|
174
|
-
const content = target.querySelector('.sp-combobox-content');
|
|
175
|
-
expect(content).toBeTruthy();
|
|
176
|
-
});
|
|
177
|
-
it('accepts the sideOffset prop', async () => {
|
|
178
|
-
await openAndWaitForFloating({ disablePortal: true, sideOffset: 12 });
|
|
179
|
-
const content = target.querySelector('.sp-combobox-content');
|
|
180
|
-
expect(content).toBeTruthy();
|
|
181
|
-
});
|
|
182
|
-
it('accepts the align prop', async () => {
|
|
183
|
-
await openAndWaitForFloating({ disablePortal: true, align: 'center' });
|
|
184
|
-
const content = target.querySelector('.sp-combobox-content');
|
|
185
|
-
expect(content).toBeTruthy();
|
|
186
|
-
});
|
|
187
|
-
it('does not stretch to full body width anymore when portaled', async () => {
|
|
188
|
-
// Regression: the bug rendered the content with right:0 against body,
|
|
189
|
-
// producing a full-viewport-width menu. After the fix, the content is
|
|
190
|
-
// positioned (via FloatingLayerState) and does NOT have `right: 0`
|
|
191
|
-
// inherited from the stylesheet.
|
|
192
|
-
await openAndWaitForFloating({});
|
|
193
|
-
const content = document.body.querySelector('.sp-combobox-content');
|
|
194
|
-
await vi.waitFor(() => {
|
|
195
|
-
const cs = window.getComputedStyle(content);
|
|
196
|
-
// Floating UI writes `left` inline → `right` in CSS should not be 0
|
|
197
|
-
// (the old buggy rule `right: 0` is removed). A reasonable proxy:
|
|
198
|
-
// assert that `width` isn't pinned to viewport / body width.
|
|
199
|
-
expect(cs.right).not.toBe('0px');
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
describe('portal opt-out (disablePortal)', () => {
|
|
204
|
-
it('portals to document.body by default', () => {
|
|
205
|
-
const localTarget = document.createElement('div');
|
|
206
|
-
document.body.appendChild(localTarget);
|
|
207
|
-
const localInstance = mount(ComboboxTest, {
|
|
208
|
-
target: localTarget,
|
|
209
|
-
props: { open: true },
|
|
210
|
-
});
|
|
211
|
-
flushSync();
|
|
212
|
-
try {
|
|
213
|
-
expect(localTarget.querySelector('.sp-combobox-content')).toBeNull();
|
|
214
|
-
expect(document.body.querySelector('.sp-combobox-content')).toBeTruthy();
|
|
215
|
-
}
|
|
216
|
-
finally {
|
|
217
|
-
unmount(localInstance);
|
|
218
|
-
localTarget.remove();
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
it('renders inline when disablePortal={true}', () => {
|
|
222
|
-
const localTarget = document.createElement('div');
|
|
223
|
-
document.body.appendChild(localTarget);
|
|
224
|
-
const localInstance = mount(ComboboxTest, {
|
|
225
|
-
target: localTarget,
|
|
226
|
-
props: { open: true, disablePortal: true },
|
|
227
|
-
});
|
|
228
|
-
flushSync();
|
|
229
|
-
try {
|
|
230
|
-
expect(localTarget.querySelector('.sp-combobox-content')).toBeTruthy();
|
|
231
|
-
}
|
|
232
|
-
finally {
|
|
233
|
-
unmount(localInstance);
|
|
234
|
-
localTarget.remove();
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
it('preserves consumer class on the content element when inline', () => {
|
|
238
|
-
const localTarget = document.createElement('div');
|
|
239
|
-
document.body.appendChild(localTarget);
|
|
240
|
-
const localInstance = mount(ComboboxTest, {
|
|
241
|
-
target: localTarget,
|
|
242
|
-
props: { open: true, disablePortal: true, contentClass: 'my-consumer-class' },
|
|
243
|
-
});
|
|
244
|
-
flushSync();
|
|
245
|
-
try {
|
|
246
|
-
const el = localTarget.querySelector('.sp-combobox-content.my-consumer-class');
|
|
247
|
-
expect(el).toBeTruthy();
|
|
248
|
-
}
|
|
249
|
-
finally {
|
|
250
|
-
unmount(localInstance);
|
|
251
|
-
localTarget.remove();
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
});
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import CommandEmpty from '../components/command-empty.svelte'
|
|
3
|
-
import CommandGroup from '../components/command-group.svelte'
|
|
4
|
-
import CommandInput from '../components/command-input.svelte'
|
|
5
|
-
import CommandItem from '../components/command-item.svelte'
|
|
6
|
-
import CommandList from '../components/command-list.svelte'
|
|
7
|
-
import CommandRoot from '../components/command-root.svelte'
|
|
8
|
-
import CommandSeparator from '../components/command-separator.svelte'
|
|
9
|
-
|
|
10
|
-
let {
|
|
11
|
-
value = $bindable(''),
|
|
12
|
-
shouldFilter = true,
|
|
13
|
-
open = $bindable(true),
|
|
14
|
-
portalTo = undefined,
|
|
15
|
-
closeOnEscape = true,
|
|
16
|
-
closeOnOutsideClick = true,
|
|
17
|
-
onOpenChange = undefined,
|
|
18
|
-
onClose = undefined,
|
|
19
|
-
itemAttrs = undefined,
|
|
20
|
-
disabledItemAttrs = undefined,
|
|
21
|
-
onItemSelect = undefined,
|
|
22
|
-
onDisabledItemSelect = undefined,
|
|
23
|
-
}: {
|
|
24
|
-
value?: string
|
|
25
|
-
shouldFilter?: boolean
|
|
26
|
-
open?: boolean
|
|
27
|
-
portalTo?: HTMLElement | string
|
|
28
|
-
closeOnEscape?: boolean
|
|
29
|
-
closeOnOutsideClick?: boolean
|
|
30
|
-
onOpenChange?: (open: boolean) => void
|
|
31
|
-
onClose?: () => void
|
|
32
|
-
/** Arbitrary HTML attributes / native handlers spread onto the `copy` item. */
|
|
33
|
-
itemAttrs?: Record<string, unknown>
|
|
34
|
-
/** Same, spread onto the `settings` item (the disabled one). */
|
|
35
|
-
disabledItemAttrs?: Record<string, unknown>
|
|
36
|
-
/** Spectral `onSelect` of the `copy` item. */
|
|
37
|
-
onItemSelect?: (value: string) => void
|
|
38
|
-
/** Spectral `onSelect` of the `settings` item. */
|
|
39
|
-
onDisabledItemSelect?: (value: string) => void
|
|
40
|
-
} = $props()
|
|
41
|
-
</script>
|
|
42
|
-
|
|
43
|
-
<CommandRoot
|
|
44
|
-
bind:value
|
|
45
|
-
bind:open
|
|
46
|
-
{shouldFilter}
|
|
47
|
-
{portalTo}
|
|
48
|
-
{closeOnEscape}
|
|
49
|
-
{closeOnOutsideClick}
|
|
50
|
-
{onOpenChange}
|
|
51
|
-
{onClose}
|
|
52
|
-
>
|
|
53
|
-
<CommandInput placeholder="Type a command..." />
|
|
54
|
-
<CommandList>
|
|
55
|
-
<CommandEmpty>No results found.</CommandEmpty>
|
|
56
|
-
<CommandGroup heading="Actions">
|
|
57
|
-
<CommandItem
|
|
58
|
-
value="copy"
|
|
59
|
-
keywords={['duplicate']}
|
|
60
|
-
onSelect={onItemSelect}
|
|
61
|
-
{...itemAttrs ?? {}}>Copy</CommandItem
|
|
62
|
-
>
|
|
63
|
-
<CommandItem value="paste">Paste</CommandItem>
|
|
64
|
-
<CommandItem value="cut">Cut</CommandItem>
|
|
65
|
-
</CommandGroup>
|
|
66
|
-
<CommandSeparator />
|
|
67
|
-
<CommandGroup heading="Navigation">
|
|
68
|
-
<CommandItem value="home">Home</CommandItem>
|
|
69
|
-
<CommandItem
|
|
70
|
-
value="settings"
|
|
71
|
-
disabled
|
|
72
|
-
onSelect={onDisabledItemSelect}
|
|
73
|
-
{...disabledItemAttrs ?? {}}>Settings</CommandItem
|
|
74
|
-
>
|
|
75
|
-
</CommandGroup>
|
|
76
|
-
</CommandList>
|
|
77
|
-
</CommandRoot>
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
import { flushSync, mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import CommandTest from './CommandTest.svelte';
|
|
4
|
-
// jsdom does not define PointerEvent — polyfill it for tests
|
|
5
|
-
if (typeof globalThis.PointerEvent === 'undefined') {
|
|
6
|
-
// @ts-expect-error — minimal polyfill for testing
|
|
7
|
-
globalThis.PointerEvent = class PointerEvent extends MouseEvent {
|
|
8
|
-
constructor(type, init) {
|
|
9
|
-
super(type, init);
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* spectral-ui#205 — `Command.Item` forwards native HTML attributes and handlers
|
|
15
|
-
* to its real `role="option"` node.
|
|
16
|
-
*
|
|
17
|
-
* Contract pinned here:
|
|
18
|
-
* - consumer attributes (`title`, `aria-*`, `data-testid`, `style`, …) reach
|
|
19
|
-
* the option, and a value reassigned by the parent AFTER mount is reflected;
|
|
20
|
-
* - the option's IDENTITY stays under the component's control: `role`, `id`,
|
|
21
|
-
* `tabindex`, `aria-selected`, `aria-disabled` and the internal `data-*`
|
|
22
|
-
* (`data-command-item`, `data-value`, `data-selected`, `data-disabled`)
|
|
23
|
-
* cannot be overridden — the spread comes FIRST, the identity comes after;
|
|
24
|
-
* - native handlers COMPOSE with the built-in behaviour: the consumer's runs
|
|
25
|
-
* first, `preventDefault()` cancels the Spectral selection, `onSelect` only
|
|
26
|
-
* fires after a real selection;
|
|
27
|
-
* - activation of a disabled row is inert: consumer `onclick` / `onkeydown` do
|
|
28
|
-
* not run either, while `onpointermove` stays observable (a dimmed row can
|
|
29
|
-
* still be hovered) without ever selecting the row.
|
|
30
|
-
*
|
|
31
|
-
* Tests titled `guard —` were already green on the pre-#205 tree (nothing was
|
|
32
|
-
* forwarded, so nothing could be overridden, leaked or mis-composed); their
|
|
33
|
-
* discriminating power comes from the mutant campaign below, each mutant
|
|
34
|
-
* applied alone on the final tree and killed by the named test(s):
|
|
35
|
-
* - spread placed LAST → identity guard, Enter-from-input guard
|
|
36
|
-
* - rest not spread → forwarding, post-mount reactivity, identity
|
|
37
|
-
* - `keywords` left in rest → no-leak guard
|
|
38
|
-
* - consumer handler run BEFORE the disabled guard → disabled-inert guard
|
|
39
|
-
* - onclick / onkeydown / onpointermove ignore `defaultPrevented` → cancel pins
|
|
40
|
-
* - onkeydown not composed → onkeydown compose + cancel
|
|
41
|
-
* - rest captured statically at init → post-mount reactivity
|
|
42
|
-
* - `data-command-item` rendered boolean → identity
|
|
43
|
-
* - onpointermove guarded on disabled → disabled-observable pin
|
|
44
|
-
* - hover routed through `select()` → hover-does-not-fire-onSelect guard
|
|
45
|
-
* - Space branch dropped → Space activation guard
|
|
46
|
-
* - disabled keydown returns before `preventDefault()` → disabled-Space pin
|
|
47
|
-
* - `'id'` dropped from the type's `Omit` → `src/__typecheck__` pin
|
|
48
|
-
*
|
|
49
|
-
* Keyboard note: Enter pressed in the INPUT activates the selected option
|
|
50
|
-
* through a synthetic `click()` (command-input.svelte), so a consumer observes
|
|
51
|
-
* that path through `onclick`, not `onkeydown`.
|
|
52
|
-
*/
|
|
53
|
-
describe('command item — native attribute passthrough (#205)', () => {
|
|
54
|
-
let target;
|
|
55
|
-
let instance;
|
|
56
|
-
function setup(props = {}) {
|
|
57
|
-
target = document.createElement('div');
|
|
58
|
-
document.body.appendChild(target);
|
|
59
|
-
instance = mount(CommandTest, { target, props: { portalTo: target, ...props } });
|
|
60
|
-
flushSync();
|
|
61
|
-
flushSync(); // $effect item registration
|
|
62
|
-
return target;
|
|
63
|
-
}
|
|
64
|
-
const option = (value) => target.querySelector(`[role="option"][data-value="${value}"]`);
|
|
65
|
-
const input = () => target.querySelector('input[role="combobox"]');
|
|
66
|
-
afterEach(() => {
|
|
67
|
-
if (instance)
|
|
68
|
-
unmount(instance);
|
|
69
|
-
target?.remove();
|
|
70
|
-
});
|
|
71
|
-
// ── Forwarding ────────────────────────────────────────────────────────
|
|
72
|
-
it('forwards consumer HTML attributes to the option element', () => {
|
|
73
|
-
setup({
|
|
74
|
-
itemAttrs: {
|
|
75
|
-
title: 'Copy the selection',
|
|
76
|
-
'aria-label': 'Copy',
|
|
77
|
-
'aria-describedby': 'copy-hint',
|
|
78
|
-
'data-testid': 'opt-copy',
|
|
79
|
-
style: 'color: red',
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
const el = option('copy');
|
|
83
|
-
expect(el.getAttribute('title')).toBe('Copy the selection');
|
|
84
|
-
expect(el.getAttribute('aria-label')).toBe('Copy');
|
|
85
|
-
expect(el.getAttribute('aria-describedby')).toBe('copy-hint');
|
|
86
|
-
expect(el.getAttribute('data-testid')).toBe('opt-copy');
|
|
87
|
-
expect(el.style.color).toBe('red');
|
|
88
|
-
});
|
|
89
|
-
it('reflects a consumer attribute reassigned by the parent after mount', () => {
|
|
90
|
-
target = document.createElement('div');
|
|
91
|
-
document.body.appendChild(target);
|
|
92
|
-
const props = $state({
|
|
93
|
-
portalTo: target,
|
|
94
|
-
itemAttrs: { title: 'Before', 'aria-describedby': 'hint-before' },
|
|
95
|
-
});
|
|
96
|
-
instance = mount(CommandTest, { target, props });
|
|
97
|
-
flushSync();
|
|
98
|
-
expect(option('copy').getAttribute('title')).toBe('Before');
|
|
99
|
-
props.itemAttrs = { title: 'After', 'aria-describedby': 'hint-after' };
|
|
100
|
-
flushSync();
|
|
101
|
-
expect(option('copy').getAttribute('title')).toBe('After');
|
|
102
|
-
expect(option('copy').getAttribute('aria-describedby')).toBe('hint-after');
|
|
103
|
-
});
|
|
104
|
-
it('guard — keeps the DS class alongside a consumer class', () => {
|
|
105
|
-
setup({ itemAttrs: { class: 'mine' } });
|
|
106
|
-
const el = option('copy');
|
|
107
|
-
expect(el.classList.contains('sp-command-item')).toBe(true);
|
|
108
|
-
expect(el.classList.contains('mine')).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
// ── Identity stays ours ───────────────────────────────────────────────
|
|
111
|
-
it('keeps its identity attributes when a consumer tries to override them', () => {
|
|
112
|
-
setup({
|
|
113
|
-
itemAttrs: {
|
|
114
|
-
role: 'menuitem',
|
|
115
|
-
id: 'consumer-id',
|
|
116
|
-
tabindex: 0,
|
|
117
|
-
'aria-selected': 'false',
|
|
118
|
-
'aria-disabled': 'true',
|
|
119
|
-
'data-selected': 'false',
|
|
120
|
-
'data-value': 'leak',
|
|
121
|
-
'data-disabled': '',
|
|
122
|
-
'data-command-item': 'nope',
|
|
123
|
-
'data-testid': 'still-forwarded',
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
// `copy` is the first enabled item → auto-selected.
|
|
127
|
-
const el = target.querySelector('[data-testid="still-forwarded"]');
|
|
128
|
-
expect(el).toBeTruthy();
|
|
129
|
-
expect(el.getAttribute('role')).toBe('option');
|
|
130
|
-
expect(el.id).toMatch(/^sp-command-item-\d+$/);
|
|
131
|
-
expect(el.getAttribute('tabindex')).toBe('-1');
|
|
132
|
-
expect(el.getAttribute('aria-selected')).toBe('true');
|
|
133
|
-
expect(el.hasAttribute('aria-disabled')).toBe(false);
|
|
134
|
-
expect(el.getAttribute('data-selected')).toBe('true');
|
|
135
|
-
expect(el.getAttribute('data-value')).toBe('copy');
|
|
136
|
-
expect(el.hasAttribute('data-disabled')).toBe(false);
|
|
137
|
-
// `data-command-item` keeps its shipped form: present, empty-valued.
|
|
138
|
-
expect(el.hasAttribute('data-command-item')).toBe(true);
|
|
139
|
-
expect(el.getAttribute('data-command-item')).toBe('');
|
|
140
|
-
});
|
|
141
|
-
it('guard — Enter in the input activates the item Spectral selected, whatever the consumer claims', () => {
|
|
142
|
-
const selected = [];
|
|
143
|
-
setup({
|
|
144
|
-
itemAttrs: { 'data-selected': 'false' },
|
|
145
|
-
onItemSelect: (v) => selected.push(v),
|
|
146
|
-
});
|
|
147
|
-
input().dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
148
|
-
flushSync();
|
|
149
|
-
expect(selected).toEqual(['copy']);
|
|
150
|
-
});
|
|
151
|
-
it('guard — does not leak component props to the DOM', () => {
|
|
152
|
-
setup({ onItemSelect: () => { } });
|
|
153
|
-
const copy = option('copy');
|
|
154
|
-
// (`on*` keys never surface as attributes in Svelte, so only these can fail.)
|
|
155
|
-
for (const attr of ['value', 'keywords']) {
|
|
156
|
-
expect(copy.hasAttribute(attr)).toBe(false);
|
|
157
|
-
}
|
|
158
|
-
expect(option('settings').hasAttribute('disabled')).toBe(false);
|
|
159
|
-
});
|
|
160
|
-
// ── Handler composition ───────────────────────────────────────────────
|
|
161
|
-
it('composes a consumer onclick with the built-in selection (consumer first)', () => {
|
|
162
|
-
const seen = [];
|
|
163
|
-
setup({
|
|
164
|
-
itemAttrs: { onclick: () => seen.push('consumer') },
|
|
165
|
-
onItemSelect: (v) => seen.push(`selected:${v}`),
|
|
166
|
-
});
|
|
167
|
-
option('copy').click();
|
|
168
|
-
flushSync();
|
|
169
|
-
expect(seen).toEqual(['consumer', 'selected:copy']);
|
|
170
|
-
});
|
|
171
|
-
it('lets a consumer onclick cancel the selection with preventDefault', () => {
|
|
172
|
-
const selected = [];
|
|
173
|
-
setup({
|
|
174
|
-
itemAttrs: { onclick: (e) => e.preventDefault() },
|
|
175
|
-
onItemSelect: (v) => selected.push(v),
|
|
176
|
-
});
|
|
177
|
-
// Move the selection away first, so a (wrongly) successful click is visible.
|
|
178
|
-
input().dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
|
179
|
-
flushSync();
|
|
180
|
-
expect(option('paste').getAttribute('data-selected')).toBe('true');
|
|
181
|
-
option('copy').click();
|
|
182
|
-
flushSync();
|
|
183
|
-
expect(selected).toEqual([]);
|
|
184
|
-
expect(option('copy').getAttribute('data-selected')).toBe('false');
|
|
185
|
-
expect(option('paste').getAttribute('data-selected')).toBe('true');
|
|
186
|
-
});
|
|
187
|
-
it('composes a consumer onkeydown with Enter on the option (consumer first)', () => {
|
|
188
|
-
const seen = [];
|
|
189
|
-
setup({
|
|
190
|
-
itemAttrs: { onkeydown: () => seen.push('consumer') },
|
|
191
|
-
onItemSelect: (v) => seen.push(`selected:${v}`),
|
|
192
|
-
});
|
|
193
|
-
option('copy').dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }));
|
|
194
|
-
flushSync();
|
|
195
|
-
expect(seen).toEqual(['consumer', 'selected:copy']);
|
|
196
|
-
});
|
|
197
|
-
it('guard — composes a consumer onkeydown with Space on the option (consumer first)', () => {
|
|
198
|
-
const seen = [];
|
|
199
|
-
setup({
|
|
200
|
-
itemAttrs: { onkeydown: () => seen.push('consumer') },
|
|
201
|
-
onItemSelect: (v) => seen.push(`selected:${v}`),
|
|
202
|
-
});
|
|
203
|
-
option('copy').dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true }));
|
|
204
|
-
flushSync();
|
|
205
|
-
expect(seen).toEqual(['consumer', 'selected:copy']);
|
|
206
|
-
});
|
|
207
|
-
it('lets a consumer onkeydown cancel the Enter/Space activation with preventDefault', () => {
|
|
208
|
-
const selected = [];
|
|
209
|
-
setup({
|
|
210
|
-
itemAttrs: { onkeydown: (e) => e.preventDefault() },
|
|
211
|
-
onItemSelect: (v) => selected.push(v),
|
|
212
|
-
});
|
|
213
|
-
option('copy').dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true }));
|
|
214
|
-
flushSync();
|
|
215
|
-
expect(selected).toEqual([]);
|
|
216
|
-
});
|
|
217
|
-
it('composes a consumer onpointermove with the hover-selection — which never fires onSelect', () => {
|
|
218
|
-
const seen = [];
|
|
219
|
-
setup({
|
|
220
|
-
itemAttrs: { onpointermove: () => seen.push('consumer') },
|
|
221
|
-
onItemSelect: (v) => seen.push(`selected:${v}`),
|
|
222
|
-
});
|
|
223
|
-
// Start from a different selection so the hover-select is observable.
|
|
224
|
-
input().dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
|
225
|
-
flushSync();
|
|
226
|
-
expect(option('paste').getAttribute('data-selected')).toBe('true');
|
|
227
|
-
option('copy').dispatchEvent(new PointerEvent('pointermove', { bubbles: true, cancelable: true }));
|
|
228
|
-
flushSync();
|
|
229
|
-
expect(seen).toEqual(['consumer']);
|
|
230
|
-
expect(option('copy').getAttribute('data-selected')).toBe('true');
|
|
231
|
-
});
|
|
232
|
-
it('lets a consumer onpointermove cancel the hover-selection with preventDefault', () => {
|
|
233
|
-
setup({ itemAttrs: { onpointermove: (e) => e.preventDefault() } });
|
|
234
|
-
input().dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
|
235
|
-
flushSync();
|
|
236
|
-
option('copy').dispatchEvent(new PointerEvent('pointermove', { bubbles: true, cancelable: true }));
|
|
237
|
-
flushSync();
|
|
238
|
-
expect(option('copy').getAttribute('data-selected')).toBe('false');
|
|
239
|
-
expect(option('paste').getAttribute('data-selected')).toBe('true');
|
|
240
|
-
});
|
|
241
|
-
// ── Disabled: activation inert, hover observable ──────────────────────
|
|
242
|
-
it('guard — activation of a disabled item is inert: consumer onclick/onkeydown do not run', () => {
|
|
243
|
-
const seen = [];
|
|
244
|
-
setup({
|
|
245
|
-
disabledItemAttrs: {
|
|
246
|
-
onclick: () => seen.push('click'),
|
|
247
|
-
onkeydown: () => seen.push('keydown'),
|
|
248
|
-
},
|
|
249
|
-
onDisabledItemSelect: (v) => seen.push(`selected:${v}`),
|
|
250
|
-
});
|
|
251
|
-
const settings = option('settings');
|
|
252
|
-
settings.click();
|
|
253
|
-
settings.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
254
|
-
flushSync();
|
|
255
|
-
expect(seen).toEqual([]);
|
|
256
|
-
expect(option('copy').getAttribute('data-selected')).toBe('true');
|
|
257
|
-
expect(settings.getAttribute('data-selected')).toBe('false');
|
|
258
|
-
});
|
|
259
|
-
it('still prevents the Space default on a focused disabled item, without running the consumer onkeydown', () => {
|
|
260
|
-
const seen = [];
|
|
261
|
-
setup({ disabledItemAttrs: { onkeydown: () => seen.push('keydown') } });
|
|
262
|
-
const event = new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true });
|
|
263
|
-
option('settings').dispatchEvent(event);
|
|
264
|
-
flushSync();
|
|
265
|
-
// Pre-#205 the item prevented Enter/Space before bailing on `disabled`,
|
|
266
|
-
// so Space on a dimmed, focused row never scrolled the list.
|
|
267
|
-
expect(event.defaultPrevented).toBe(true);
|
|
268
|
-
expect(seen).toEqual([]);
|
|
269
|
-
expect(option('copy').getAttribute('data-selected')).toBe('true');
|
|
270
|
-
});
|
|
271
|
-
it('keeps a consumer onpointermove observable on a disabled item without selecting it', () => {
|
|
272
|
-
const seen = [];
|
|
273
|
-
setup({
|
|
274
|
-
disabledItemAttrs: { onpointermove: () => seen.push('move') },
|
|
275
|
-
onDisabledItemSelect: (v) => seen.push(`selected:${v}`),
|
|
276
|
-
});
|
|
277
|
-
const settings = option('settings');
|
|
278
|
-
settings.dispatchEvent(new PointerEvent('pointermove', { bubbles: true, cancelable: true }));
|
|
279
|
-
flushSync();
|
|
280
|
-
expect(seen).toEqual(['move']);
|
|
281
|
-
expect(settings.getAttribute('data-selected')).toBe('false');
|
|
282
|
-
expect(option('copy').getAttribute('data-selected')).toBe('true');
|
|
283
|
-
});
|
|
284
|
-
});
|
|
@@ -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
|
-
});
|