@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.
- package/LICENSE +21 -0
- package/THIRD_PARTY_NOTICES.md +201 -0
- 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__/CommandInlineDirectTest.svelte +0 -29
- package/dist/components/command/__tests__/CommandInlineTest.svelte +0 -89
- package/dist/components/command/__tests__/CommandTest.svelte +0 -77
- package/dist/components/command/__tests__/command-inline.test.svelte.js +0 -392
- 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,567 +0,0 @@
|
|
|
1
|
-
import { flushSync, mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import DropdownMenuRoot from '../components/dropdown-menu-root.svelte';
|
|
4
|
-
import DropdownMenuTest from './DropdownMenuTest.svelte';
|
|
5
|
-
describe('dropdown-menu', () => {
|
|
6
|
-
let target;
|
|
7
|
-
let instance;
|
|
8
|
-
function setup(props = {}) {
|
|
9
|
-
target = document.createElement('div');
|
|
10
|
-
document.body.appendChild(target);
|
|
11
|
-
instance = mount(DropdownMenuTest, { 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(DropdownMenuRoot, { target, props });
|
|
19
|
-
flushSync();
|
|
20
|
-
return target;
|
|
21
|
-
}
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
if (instance)
|
|
24
|
-
unmount(instance);
|
|
25
|
-
target?.remove();
|
|
26
|
-
});
|
|
27
|
-
it('renders closed by default', () => {
|
|
28
|
-
setupRoot();
|
|
29
|
-
const root = target.querySelector('.sp-dropdown-menu-root');
|
|
30
|
-
expect(root).toBeTruthy();
|
|
31
|
-
expect(root?.getAttribute('data-state')).toBe('closed');
|
|
32
|
-
});
|
|
33
|
-
it('content not rendered when closed', () => {
|
|
34
|
-
setup();
|
|
35
|
-
expect(target.querySelector('[role="menu"]')).toBeNull();
|
|
36
|
-
});
|
|
37
|
-
it('content renders when open', () => {
|
|
38
|
-
setup({ open: true });
|
|
39
|
-
expect(target.querySelector('[role="menu"]')).toBeTruthy();
|
|
40
|
-
});
|
|
41
|
-
it('calls onOpenChange with new value on each open transition', () => {
|
|
42
|
-
const calls = [];
|
|
43
|
-
const localTarget = document.createElement('div');
|
|
44
|
-
document.body.appendChild(localTarget);
|
|
45
|
-
instance = mount(DropdownMenuTest, {
|
|
46
|
-
target: localTarget,
|
|
47
|
-
props: {
|
|
48
|
-
open: false,
|
|
49
|
-
portalTo: localTarget,
|
|
50
|
-
onOpenChange: (open) => calls.push(open),
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
target = localTarget;
|
|
54
|
-
flushSync();
|
|
55
|
-
const trigger = localTarget.querySelector('.sp-dropdown-menu-trigger');
|
|
56
|
-
trigger.click();
|
|
57
|
-
flushSync();
|
|
58
|
-
expect(calls).toEqual([true]);
|
|
59
|
-
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
|
60
|
-
flushSync();
|
|
61
|
-
expect(calls).toEqual([true, false]);
|
|
62
|
-
});
|
|
63
|
-
describe('subTrigger', () => {
|
|
64
|
-
function pressKey(key) {
|
|
65
|
-
const el = document.activeElement;
|
|
66
|
-
el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
|
|
67
|
-
flushSync();
|
|
68
|
-
}
|
|
69
|
-
it('participates in roving focus (data-roving-focus-item + tabindex -1)', () => {
|
|
70
|
-
setup({ open: true, withSub: true });
|
|
71
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
72
|
-
expect(subTrigger).toBeTruthy();
|
|
73
|
-
expect(subTrigger.hasAttribute('data-roving-focus-item')).toBe(true);
|
|
74
|
-
expect(subTrigger.getAttribute('tabindex')).toBe('-1');
|
|
75
|
-
});
|
|
76
|
-
it('carries both item and sub-trigger classes (visual parity hook)', () => {
|
|
77
|
-
setup({ open: true, withSub: true });
|
|
78
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
79
|
-
expect(subTrigger.classList.contains('sp-dropdown-menu-item')).toBe(true);
|
|
80
|
-
expect(subTrigger.classList.contains('sp-dropdown-sub-trigger')).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
it('is autofocused on open when it is the first row', () => {
|
|
83
|
-
setup({ open: true, withSub: true, subFirst: true });
|
|
84
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
85
|
-
expect(document.activeElement).toBe(subTrigger);
|
|
86
|
-
});
|
|
87
|
-
it('is reached by ArrowDown/ArrowUp roving among plain items', () => {
|
|
88
|
-
setup({ open: true, withSub: true });
|
|
89
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
90
|
-
// Autofocus lands on Item 1; sub-trigger is the 4th row.
|
|
91
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
|
|
92
|
-
pressKey('ArrowDown');
|
|
93
|
-
pressKey('ArrowDown');
|
|
94
|
-
pressKey('ArrowDown');
|
|
95
|
-
expect(document.activeElement).toBe(subTrigger);
|
|
96
|
-
pressKey('ArrowUp');
|
|
97
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Item 3');
|
|
98
|
-
});
|
|
99
|
-
it('is skipped by autofocus and roving when disabled', () => {
|
|
100
|
-
setup({ open: true, withSub: true, subFirst: true, subDisabled: true });
|
|
101
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
102
|
-
expect(subTrigger.hasAttribute('data-disabled')).toBe(true);
|
|
103
|
-
// Autofocus skips the disabled sub-trigger even though it is first.
|
|
104
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
|
|
105
|
-
// Roving loops over the three plain items without ever landing on it.
|
|
106
|
-
pressKey('ArrowDown');
|
|
107
|
-
pressKey('ArrowDown');
|
|
108
|
-
pressKey('ArrowDown');
|
|
109
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
|
|
110
|
-
pressKey('ArrowUp');
|
|
111
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Item 3');
|
|
112
|
-
});
|
|
113
|
-
it('opens the sub content on ArrowRight and reflects aria-expanded', () => {
|
|
114
|
-
setup({ open: true, withSub: true, subFirst: true });
|
|
115
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
116
|
-
expect(document.activeElement).toBe(subTrigger);
|
|
117
|
-
expect(subTrigger.getAttribute('aria-expanded')).toBe('false');
|
|
118
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
119
|
-
pressKey('ArrowRight');
|
|
120
|
-
expect(subTrigger.getAttribute('aria-expanded')).toBe('true');
|
|
121
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
122
|
-
});
|
|
123
|
-
it('opens the sub content on Enter', () => {
|
|
124
|
-
setup({ open: true, withSub: true, subFirst: true });
|
|
125
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
126
|
-
pressKey('Enter');
|
|
127
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
128
|
-
});
|
|
129
|
-
it('renders textValue as data-text-value (typeahead escape hatch)', () => {
|
|
130
|
-
setup({ open: true, withSub: true, subTextValue: 'Share' });
|
|
131
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
132
|
-
expect(subTrigger.getAttribute('data-text-value')).toBe('Share');
|
|
133
|
-
});
|
|
134
|
-
it('is wired to the sub content via aria-controls / aria-labelledby', () => {
|
|
135
|
-
setup({ open: true, withSub: true });
|
|
136
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
137
|
-
// Closed: no aria-controls (same conditional plumbing as the root trigger).
|
|
138
|
-
expect(subTrigger.hasAttribute('aria-controls')).toBe(false);
|
|
139
|
-
subTrigger.click();
|
|
140
|
-
flushSync();
|
|
141
|
-
const subContent = target.querySelector('.sp-dropdown-menu-sub-content');
|
|
142
|
-
expect(subTrigger.id).toBeTruthy();
|
|
143
|
-
expect(subContent.id).toBeTruthy();
|
|
144
|
-
expect(subTrigger.getAttribute('aria-controls')).toBe(subContent.id);
|
|
145
|
-
expect(subContent.getAttribute('aria-labelledby')).toBe(subTrigger.id);
|
|
146
|
-
});
|
|
147
|
-
it('omits data-text-value when textValue is not provided', () => {
|
|
148
|
-
setup({ open: true, withSub: true });
|
|
149
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
150
|
-
expect(subTrigger.hasAttribute('data-text-value')).toBe(false);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
describe('sub keyboard model (focus-into-sub + restore)', () => {
|
|
154
|
-
function pressKey(key) {
|
|
155
|
-
const el = document.activeElement;
|
|
156
|
-
el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }));
|
|
157
|
-
flushSync();
|
|
158
|
-
}
|
|
159
|
-
it('keyboard open (ArrowRight) moves focus to the first sub item', () => {
|
|
160
|
-
setup({ open: true, withSub: true, subFirst: true });
|
|
161
|
-
// Autofocus lands on the sub trigger (first row).
|
|
162
|
-
pressKey('ArrowRight');
|
|
163
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
164
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub Item 1');
|
|
165
|
-
});
|
|
166
|
-
it('keyboard open (Enter) moves focus to the first sub item', () => {
|
|
167
|
-
setup({ open: true, withSub: true, subFirst: true });
|
|
168
|
-
pressKey('Enter');
|
|
169
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub Item 1');
|
|
170
|
-
});
|
|
171
|
-
it('pointer open does NOT steal focus (APG)', () => {
|
|
172
|
-
vi.useFakeTimers();
|
|
173
|
-
try {
|
|
174
|
-
setup({ open: true, withSub: true });
|
|
175
|
-
// Autofocus lands on Item 1 (sub trigger is the last row).
|
|
176
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
|
|
177
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
178
|
-
subTrigger.dispatchEvent(new MouseEvent('pointerenter', { clientX: 10, clientY: 10 }));
|
|
179
|
-
flushSync();
|
|
180
|
-
// Hover-open still waits the 150ms delay…
|
|
181
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
182
|
-
vi.advanceTimersByTime(150);
|
|
183
|
-
flushSync();
|
|
184
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
185
|
-
// …and focus stayed where it was.
|
|
186
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
|
|
187
|
-
}
|
|
188
|
-
finally {
|
|
189
|
-
vi.useRealTimers();
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
it('arrowLeft inside the sub closes it and restores focus to the trigger', () => {
|
|
193
|
-
setup({ open: true, withSub: true, subFirst: true });
|
|
194
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
195
|
-
pressKey('ArrowRight');
|
|
196
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub Item 1');
|
|
197
|
-
pressKey('ArrowLeft');
|
|
198
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
199
|
-
// Focus restored to the trigger row — no dead end on document.body.
|
|
200
|
-
expect(document.activeElement).toBe(subTrigger);
|
|
201
|
-
// The parent menu stayed open.
|
|
202
|
-
expect(target.querySelector('.sp-dropdown-menu-content')).toBeTruthy();
|
|
203
|
-
});
|
|
204
|
-
it('escape inside the sub closes ONLY the sub and restores focus', () => {
|
|
205
|
-
setup({ open: true, withSub: true, subFirst: true });
|
|
206
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
207
|
-
pressKey('ArrowRight');
|
|
208
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub Item 1');
|
|
209
|
-
pressKey('Escape');
|
|
210
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
211
|
-
expect(document.activeElement).toBe(subTrigger);
|
|
212
|
-
expect(target.querySelector('.sp-dropdown-menu-content')).toBeTruthy();
|
|
213
|
-
// A second Escape (sub layer unmounted) closes the whole menu.
|
|
214
|
-
pressKey('Escape');
|
|
215
|
-
expect(target.querySelector('.sp-dropdown-menu-content')).toBeNull();
|
|
216
|
-
});
|
|
217
|
-
it('sub content has its own ArrowUp/Down/Home/End roving over sub items', () => {
|
|
218
|
-
setup({ open: true, withSub: true, subFirst: true });
|
|
219
|
-
pressKey('ArrowRight');
|
|
220
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub Item 1');
|
|
221
|
-
pressKey('ArrowDown');
|
|
222
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub Item 2');
|
|
223
|
-
// Loops within the sub — never escapes into the parent collection.
|
|
224
|
-
pressKey('ArrowDown');
|
|
225
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub Item 1');
|
|
226
|
-
pressKey('ArrowUp');
|
|
227
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub Item 2');
|
|
228
|
-
pressKey('Home');
|
|
229
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub Item 1');
|
|
230
|
-
pressKey('End');
|
|
231
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub Item 2');
|
|
232
|
-
});
|
|
233
|
-
it('parent roving skips sub items while the sub is open', () => {
|
|
234
|
-
setup({ open: true, withSub: true, subFirst: true });
|
|
235
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
236
|
-
// Pointer open (click) keeps focus on the trigger.
|
|
237
|
-
subTrigger.click();
|
|
238
|
-
flushSync();
|
|
239
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
240
|
-
expect(document.activeElement).toBe(subTrigger);
|
|
241
|
-
// ArrowDown from the sub trigger goes to the next PARENT item,
|
|
242
|
-
// not into the open sub.
|
|
243
|
-
pressKey('ArrowDown');
|
|
244
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
|
|
245
|
-
});
|
|
246
|
-
it('parent typeahead ignores sub items while the sub is open', () => {
|
|
247
|
-
setup({ open: true, withSub: true, subFirst: true });
|
|
248
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
249
|
-
subTrigger.click();
|
|
250
|
-
flushSync();
|
|
251
|
-
expect(document.activeElement).toBe(subTrigger);
|
|
252
|
-
// 's' would match 'Sub Item 1' if sub rows leaked into the parent
|
|
253
|
-
// collection; with scoping there is no parent match, focus stays put.
|
|
254
|
-
pressKey('s');
|
|
255
|
-
expect(document.activeElement).toBe(subTrigger);
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
describe('sub pointer model (sibling hover close + grace area)', () => {
|
|
259
|
-
function openSubWithPointer() {
|
|
260
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
261
|
-
subTrigger.click();
|
|
262
|
-
flushSync();
|
|
263
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
264
|
-
return subTrigger;
|
|
265
|
-
}
|
|
266
|
-
function mockRect(el, rect) {
|
|
267
|
-
el.getBoundingClientRect = () => ({
|
|
268
|
-
x: rect.left ?? 0,
|
|
269
|
-
y: rect.top ?? 0,
|
|
270
|
-
top: rect.top ?? 0,
|
|
271
|
-
left: rect.left ?? 0,
|
|
272
|
-
right: rect.right ?? 0,
|
|
273
|
-
bottom: rect.bottom ?? 0,
|
|
274
|
-
width: (rect.right ?? 0) - (rect.left ?? 0),
|
|
275
|
-
height: (rect.bottom ?? 0) - (rect.top ?? 0),
|
|
276
|
-
toJSON: () => ({}),
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
it('pointerenter on a sibling plain item closes the open sub', () => {
|
|
280
|
-
setup({ open: true, withSub: true });
|
|
281
|
-
openSubWithPointer();
|
|
282
|
-
const item = Array.from(target.querySelectorAll('.sp-dropdown-menu-content .sp-dropdown-menu-item')).find((el) => el.textContent?.trim() === 'Item 1');
|
|
283
|
-
item.dispatchEvent(new MouseEvent('pointerenter', { clientX: 0, clientY: 0 }));
|
|
284
|
-
flushSync();
|
|
285
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
286
|
-
// The parent menu itself stayed open.
|
|
287
|
-
expect(target.querySelector('.sp-dropdown-menu-content')).toBeTruthy();
|
|
288
|
-
});
|
|
289
|
-
it('pointerenter on a sibling checkbox item closes the open sub', () => {
|
|
290
|
-
setup({ open: true, withSub: true, withCheckbox: true });
|
|
291
|
-
openSubWithPointer();
|
|
292
|
-
const checkbox = target.querySelector('[role="menuitemcheckbox"]');
|
|
293
|
-
checkbox.dispatchEvent(new MouseEvent('pointerenter', { clientX: 0, clientY: 0 }));
|
|
294
|
-
flushSync();
|
|
295
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
296
|
-
});
|
|
297
|
-
it('pointerenter on a sub item (inside the sub) does NOT close it', () => {
|
|
298
|
-
setup({ open: true, withSub: true });
|
|
299
|
-
openSubWithPointer();
|
|
300
|
-
const subItem = Array.from(target.querySelectorAll('.sp-dropdown-menu-sub-content .sp-dropdown-menu-item')).find((el) => el.textContent?.trim() === 'Sub Item 1');
|
|
301
|
-
subItem.dispatchEvent(new MouseEvent('pointerenter', { clientX: 0, clientY: 0 }));
|
|
302
|
-
flushSync();
|
|
303
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
304
|
-
});
|
|
305
|
-
it('closes when the pointer leaves the trigger away from the content', () => {
|
|
306
|
-
setup({ open: true, withSub: true });
|
|
307
|
-
const subTrigger = openSubWithPointer();
|
|
308
|
-
subTrigger.dispatchEvent(new MouseEvent('pointerleave', { clientX: 50, clientY: 50 }));
|
|
309
|
-
flushSync();
|
|
310
|
-
// Still open while nothing else happened…
|
|
311
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
312
|
-
// …but a pointermove outside trigger, content and polygon closes it.
|
|
313
|
-
document.body.dispatchEvent(new MouseEvent('pointermove', { clientX: 400, clientY: 400, bubbles: true }));
|
|
314
|
-
flushSync();
|
|
315
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
316
|
-
});
|
|
317
|
-
it('stays open while the pointer travels through the grace polygon (mocked rects)', () => {
|
|
318
|
-
setup({ open: true, withSub: true });
|
|
319
|
-
const subTrigger = openSubWithPointer();
|
|
320
|
-
const subContent = target.querySelector('.sp-dropdown-menu-sub-content');
|
|
321
|
-
// Content sits to the right of the trigger.
|
|
322
|
-
mockRect(subContent, { left: 100, right: 280, top: 0, bottom: 100 });
|
|
323
|
-
// Exit the trigger at (90, 50), heading toward the content.
|
|
324
|
-
subTrigger.dispatchEvent(new MouseEvent('pointerleave', { clientX: 90, clientY: 50 }));
|
|
325
|
-
flushSync();
|
|
326
|
-
// Diagonal travel inside the polygon [exit → content top-left → content bottom-left].
|
|
327
|
-
document.body.dispatchEvent(new MouseEvent('pointermove', { clientX: 95, clientY: 50, bubbles: true }));
|
|
328
|
-
flushSync();
|
|
329
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
330
|
-
// A sibling item under the diagonal is entered mid-travel: suppressed by the grace area.
|
|
331
|
-
const item = Array.from(target.querySelectorAll('.sp-dropdown-menu-content .sp-dropdown-menu-item')).find((el) => el.textContent?.trim() === 'Item 1');
|
|
332
|
-
item.dispatchEvent(new MouseEvent('pointerenter', { clientX: 96, clientY: 55 }));
|
|
333
|
-
flushSync();
|
|
334
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
335
|
-
// Reaching the content resolves the travel — the sub stays open.
|
|
336
|
-
subContent.dispatchEvent(new MouseEvent('pointermove', { clientX: 150, clientY: 50, bubbles: true }));
|
|
337
|
-
flushSync();
|
|
338
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
339
|
-
// Grace tracking resolved: entering a sibling now closes the sub.
|
|
340
|
-
item.dispatchEvent(new MouseEvent('pointerenter', { clientX: 50, clientY: 55 }));
|
|
341
|
-
flushSync();
|
|
342
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
343
|
-
});
|
|
344
|
-
it('closes via the safety timeout when the travel never resolves', () => {
|
|
345
|
-
vi.useFakeTimers();
|
|
346
|
-
try {
|
|
347
|
-
setup({ open: true, withSub: true });
|
|
348
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
349
|
-
subTrigger.click();
|
|
350
|
-
flushSync();
|
|
351
|
-
const subContent = target.querySelector('.sp-dropdown-menu-sub-content');
|
|
352
|
-
mockRect(subContent, { left: 100, right: 280, top: 0, bottom: 100 });
|
|
353
|
-
subTrigger.dispatchEvent(new MouseEvent('pointerleave', { clientX: 90, clientY: 50 }));
|
|
354
|
-
flushSync();
|
|
355
|
-
// Pointer stalls inside the polygon…
|
|
356
|
-
document.body.dispatchEvent(new MouseEvent('pointermove', { clientX: 95, clientY: 50, bubbles: true }));
|
|
357
|
-
flushSync();
|
|
358
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
359
|
-
// …and the 500ms safety timeout closes the sub (tooltip/link-preview parity).
|
|
360
|
-
vi.advanceTimersByTime(500);
|
|
361
|
-
flushSync();
|
|
362
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
363
|
-
}
|
|
364
|
-
finally {
|
|
365
|
-
vi.useRealTimers();
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
it('re-entering the trigger cancels the grace close tracking', () => {
|
|
369
|
-
setup({ open: true, withSub: true });
|
|
370
|
-
const subTrigger = openSubWithPointer();
|
|
371
|
-
subTrigger.dispatchEvent(new MouseEvent('pointerleave', { clientX: 50, clientY: 50 }));
|
|
372
|
-
flushSync();
|
|
373
|
-
// Pointer comes back on the trigger before leaving the polygon.
|
|
374
|
-
subTrigger.dispatchEvent(new MouseEvent('pointerenter', { clientX: 51, clientY: 50 }));
|
|
375
|
-
flushSync();
|
|
376
|
-
// A later far-away pointermove no longer closes anything (tracking stopped).
|
|
377
|
-
document.body.dispatchEvent(new MouseEvent('pointermove', { clientX: 400, clientY: 400, bubbles: true }));
|
|
378
|
-
flushSync();
|
|
379
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
380
|
-
});
|
|
381
|
-
});
|
|
382
|
-
describe('sub mixed modality (pointer close while keyboard focus is inside)', () => {
|
|
383
|
-
function pressKey(key) {
|
|
384
|
-
const el = document.activeElement;
|
|
385
|
-
el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }));
|
|
386
|
-
flushSync();
|
|
387
|
-
}
|
|
388
|
-
function findParentItem(label) {
|
|
389
|
-
return Array.from(target.querySelectorAll('.sp-dropdown-menu-content .sp-dropdown-menu-item')).find((el) => el.textContent?.trim() === label);
|
|
390
|
-
}
|
|
391
|
-
it('sibling hover close restores focus to the trigger — keyboard stays alive', () => {
|
|
392
|
-
setup({ open: true, withSub: true, subFirst: true });
|
|
393
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
394
|
-
// Keyboard open: focus lands inside the sub.
|
|
395
|
-
pressKey('ArrowRight');
|
|
396
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub Item 1');
|
|
397
|
-
// The mouse resting over the menu drifts onto a sibling row (grace is
|
|
398
|
-
// unarmed — the trigger never saw a pointerleave).
|
|
399
|
-
findParentItem('Item 1').dispatchEvent(new MouseEvent('pointerenter', { clientX: 0, clientY: 0 }));
|
|
400
|
-
flushSync();
|
|
401
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
402
|
-
// Focus restored to the trigger — never orphaned on document.body.
|
|
403
|
-
expect(document.activeElement).toBe(subTrigger);
|
|
404
|
-
// Parent roving still works from there (no dead end).
|
|
405
|
-
pressKey('ArrowDown');
|
|
406
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
|
|
407
|
-
});
|
|
408
|
-
it('grace-exit close restores focus to the trigger', () => {
|
|
409
|
-
setup({ open: true, withSub: true, subFirst: true });
|
|
410
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
411
|
-
pressKey('ArrowRight');
|
|
412
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub Item 1');
|
|
413
|
-
// The pointer brushes across the trigger, then leaves away from the content.
|
|
414
|
-
subTrigger.dispatchEvent(new MouseEvent('pointerleave', { clientX: 50, clientY: 50 }));
|
|
415
|
-
flushSync();
|
|
416
|
-
document.body.dispatchEvent(new MouseEvent('pointermove', { clientX: 400, clientY: 400, bubbles: true }));
|
|
417
|
-
flushSync();
|
|
418
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
419
|
-
expect(document.activeElement).toBe(subTrigger);
|
|
420
|
-
});
|
|
421
|
-
it('safety-timeout close restores focus to the trigger', () => {
|
|
422
|
-
vi.useFakeTimers();
|
|
423
|
-
try {
|
|
424
|
-
setup({ open: true, withSub: true, subFirst: true });
|
|
425
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
426
|
-
pressKey('ArrowRight');
|
|
427
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub Item 1');
|
|
428
|
-
// Grace tracking armed but never resolved: the 500ms safety timer closes.
|
|
429
|
-
subTrigger.dispatchEvent(new MouseEvent('pointerleave', { clientX: 50, clientY: 50 }));
|
|
430
|
-
flushSync();
|
|
431
|
-
vi.advanceTimersByTime(500);
|
|
432
|
-
flushSync();
|
|
433
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
434
|
-
expect(document.activeElement).toBe(subTrigger);
|
|
435
|
-
}
|
|
436
|
-
finally {
|
|
437
|
-
vi.useRealTimers();
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
it('pointer close does NOT move focus when focus is outside the sub', () => {
|
|
441
|
-
setup({ open: true, withSub: true });
|
|
442
|
-
// Autofocus lands on Item 1; pointer open (click) never steals focus.
|
|
443
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
444
|
-
subTrigger.click();
|
|
445
|
-
flushSync();
|
|
446
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
447
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
|
|
448
|
-
findParentItem('Item 2').dispatchEvent(new MouseEvent('pointerenter', { clientX: 0, clientY: 0 }));
|
|
449
|
-
flushSync();
|
|
450
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
451
|
-
// Pure pointer flow: focus stays put, the trigger does not grab it.
|
|
452
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
|
|
453
|
-
});
|
|
454
|
-
});
|
|
455
|
-
describe('sub escape scoping (APG: Escape closes the menu containing focus)', () => {
|
|
456
|
-
function pressKey(key) {
|
|
457
|
-
const el = document.activeElement;
|
|
458
|
-
el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }));
|
|
459
|
-
flushSync();
|
|
460
|
-
}
|
|
461
|
-
it('escape with focus in the PARENT menu and a hover-opened sub closes the whole menu in ONE keypress, without focus teleport', () => {
|
|
462
|
-
setup({ open: true, withSub: true });
|
|
463
|
-
// Autofocus lands on Item 1 (parent row); pointer open never steals focus.
|
|
464
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
465
|
-
subTrigger.click();
|
|
466
|
-
flushSync();
|
|
467
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
468
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
|
|
469
|
-
// ONE Escape: the sub layer defers (focus is NOT inside the sub) and the
|
|
470
|
-
// parent layer closes the whole menu — sub included, no warp to the sub
|
|
471
|
-
// trigger mid-keypress.
|
|
472
|
-
pressKey('Escape');
|
|
473
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
474
|
-
expect(target.querySelector('.sp-dropdown-menu-content')).toBeNull();
|
|
475
|
-
expect(target.querySelector('.sp-dropdown-menu-root')?.getAttribute('data-state')).toBe('closed');
|
|
476
|
-
});
|
|
477
|
-
it('escape with focus INSIDE the sub still closes only the sub (existing behavior preserved)', () => {
|
|
478
|
-
setup({ open: true, withSub: true, subFirst: true });
|
|
479
|
-
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
480
|
-
pressKey('ArrowRight');
|
|
481
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub Item 1');
|
|
482
|
-
pressKey('Escape');
|
|
483
|
-
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
484
|
-
expect(document.activeElement).toBe(subTrigger);
|
|
485
|
-
expect(target.querySelector('.sp-dropdown-menu-content')).toBeTruthy();
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
|
-
describe('single open sub per level', () => {
|
|
489
|
-
function subContents() {
|
|
490
|
-
return Array.from(target.querySelectorAll('.sp-dropdown-menu-sub-content'));
|
|
491
|
-
}
|
|
492
|
-
function findSubTrigger(label) {
|
|
493
|
-
return Array.from(target.querySelectorAll('.sp-dropdown-sub-trigger')).find((el) => el.textContent?.includes(label));
|
|
494
|
-
}
|
|
495
|
-
function mockRect(el, rect) {
|
|
496
|
-
el.getBoundingClientRect = () => ({
|
|
497
|
-
x: rect.left ?? 0,
|
|
498
|
-
y: rect.top ?? 0,
|
|
499
|
-
top: rect.top ?? 0,
|
|
500
|
-
left: rect.left ?? 0,
|
|
501
|
-
right: rect.right ?? 0,
|
|
502
|
-
bottom: rect.bottom ?? 0,
|
|
503
|
-
width: (rect.right ?? 0) - (rect.left ?? 0),
|
|
504
|
-
height: (rect.bottom ?? 0) - (rect.top ?? 0),
|
|
505
|
-
toJSON: () => ({}),
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
it('keyboard open on a sibling sub trigger closes the hover-opened sub', () => {
|
|
509
|
-
setup({ open: true, withSub: true, withSecondSub: true });
|
|
510
|
-
const trigA = findSubTrigger('More options');
|
|
511
|
-
const trigB = findSubTrigger('Other options');
|
|
512
|
-
// Pointer path opens sub A.
|
|
513
|
-
trigA.click();
|
|
514
|
-
flushSync();
|
|
515
|
-
expect(subContents().length).toBe(1);
|
|
516
|
-
expect(subContents()[0].textContent).toContain('Sub Item 1');
|
|
517
|
-
// Keyboard open of sibling B (never goes through onItemEnter): A must
|
|
518
|
-
// close — one open sub per level.
|
|
519
|
-
trigB.focus();
|
|
520
|
-
trigB.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true, cancelable: true }));
|
|
521
|
-
flushSync();
|
|
522
|
-
const open = subContents();
|
|
523
|
-
expect(open.length).toBe(1);
|
|
524
|
-
expect(open[0].textContent).toContain('Sub B Item 1');
|
|
525
|
-
// Keyboard open still moves focus into B (APG).
|
|
526
|
-
expect(document.activeElement?.textContent?.trim()).toBe('Sub B Item 1');
|
|
527
|
-
});
|
|
528
|
-
it('hover-open fired from inside a sibling grace polygon closes the traveled sub (and its safety timer)', () => {
|
|
529
|
-
vi.useFakeTimers();
|
|
530
|
-
try {
|
|
531
|
-
setup({ open: true, withSub: true, withSecondSub: true });
|
|
532
|
-
const trigA = findSubTrigger('More options');
|
|
533
|
-
const trigB = findSubTrigger('Other options');
|
|
534
|
-
trigA.click();
|
|
535
|
-
flushSync();
|
|
536
|
-
const contentA = subContents()[0];
|
|
537
|
-
// A's content sits to the right of its trigger.
|
|
538
|
-
mockRect(contentA, { left: 100, right: 280, top: 0, bottom: 100 });
|
|
539
|
-
// Grace travel armed from A's trigger toward its content.
|
|
540
|
-
trigA.dispatchEvent(new MouseEvent('pointerleave', { clientX: 90, clientY: 50 }));
|
|
541
|
-
flushSync();
|
|
542
|
-
// B's row sits under the diagonal: its pointerenter is spared by the
|
|
543
|
-
// grace check (A stays open), but B's hover timer still arms.
|
|
544
|
-
trigB.dispatchEvent(new MouseEvent('pointerenter', { clientX: 95, clientY: 55 }));
|
|
545
|
-
flushSync();
|
|
546
|
-
expect(subContents().length).toBe(1);
|
|
547
|
-
expect(subContents()[0].textContent).toContain('Sub Item 1');
|
|
548
|
-
// The pointer stalls on B: the 150ms hover-open fires and must
|
|
549
|
-
// force-close A — never two open subs at the same level.
|
|
550
|
-
vi.advanceTimersByTime(150);
|
|
551
|
-
flushSync();
|
|
552
|
-
const open = subContents();
|
|
553
|
-
expect(open.length).toBe(1);
|
|
554
|
-
expect(open[0].textContent).toContain('Sub B Item 1');
|
|
555
|
-
// A's grace safety timer was cancelled by its close: 500ms later
|
|
556
|
-
// nothing closes B.
|
|
557
|
-
vi.advanceTimersByTime(500);
|
|
558
|
-
flushSync();
|
|
559
|
-
expect(subContents().length).toBe(1);
|
|
560
|
-
expect(subContents()[0].textContent).toContain('Sub B Item 1');
|
|
561
|
-
}
|
|
562
|
-
finally {
|
|
563
|
-
vi.useRealTimers();
|
|
564
|
-
}
|
|
565
|
-
});
|
|
566
|
-
});
|
|
567
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import EmptyState from '../empty-state.svelte';
|
|
4
|
-
describe('empty-state', () => {
|
|
5
|
-
let target;
|
|
6
|
-
let instance;
|
|
7
|
-
function setup(props = {}) {
|
|
8
|
-
target = document.createElement('div');
|
|
9
|
-
document.body.appendChild(target);
|
|
10
|
-
instance = mount(EmptyState, { target, props });
|
|
11
|
-
return target;
|
|
12
|
-
}
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
if (instance)
|
|
15
|
-
unmount(instance);
|
|
16
|
-
target?.remove();
|
|
17
|
-
});
|
|
18
|
-
it('renders with role=status', () => {
|
|
19
|
-
setup();
|
|
20
|
-
expect(target.querySelector('[role="status"]')).toBeTruthy();
|
|
21
|
-
});
|
|
22
|
-
it('shows title', () => {
|
|
23
|
-
setup({ title: 'No data' });
|
|
24
|
-
expect(target.querySelector('.sp-empty-state-title')?.textContent).toBe('No data');
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import Fab from '../fab.svelte';
|
|
4
|
-
describe('fab', () => {
|
|
5
|
-
let target;
|
|
6
|
-
let instance;
|
|
7
|
-
function setup(props = {}) {
|
|
8
|
-
target = document.createElement('div');
|
|
9
|
-
document.body.appendChild(target);
|
|
10
|
-
instance = mount(Fab, { target, props });
|
|
11
|
-
return target;
|
|
12
|
-
}
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
if (instance)
|
|
15
|
-
unmount(instance);
|
|
16
|
-
target?.remove();
|
|
17
|
-
});
|
|
18
|
-
it('renders button', () => {
|
|
19
|
-
setup();
|
|
20
|
-
expect(target.querySelector('button')).toBeTruthy();
|
|
21
|
-
});
|
|
22
|
-
it('applies variant', () => {
|
|
23
|
-
setup({ variant: 'tonal' });
|
|
24
|
-
expect(target.querySelector('.sp-fab')?.className.includes('tonal')).toBeTruthy();
|
|
25
|
-
});
|
|
26
|
-
it('applies size', () => {
|
|
27
|
-
setup({ size: 'lg' });
|
|
28
|
-
expect(target.querySelector('.sp-fab')?.className.includes('lg')).toBeTruthy();
|
|
29
|
-
});
|
|
30
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import * as Field from '../index.js'
|
|
3
|
-
|
|
4
|
-
const {
|
|
5
|
-
error = undefined as string | undefined,
|
|
6
|
-
description = 'Help text',
|
|
7
|
-
id = 'my-field',
|
|
8
|
-
required = false,
|
|
9
|
-
}: {
|
|
10
|
-
error?: string
|
|
11
|
-
description?: string
|
|
12
|
-
id?: string
|
|
13
|
-
required?: boolean
|
|
14
|
-
} = $props()
|
|
15
|
-
</script>
|
|
16
|
-
|
|
17
|
-
<Field.Root>
|
|
18
|
-
<Field.Label for={id} {required}>Label text</Field.Label>
|
|
19
|
-
<input {id} type="text" data-testid="input" />
|
|
20
|
-
{#if description}
|
|
21
|
-
<Field.Description>{description}</Field.Description>
|
|
22
|
-
{/if}
|
|
23
|
-
{#if error}
|
|
24
|
-
<Field.Error>{error}</Field.Error>
|
|
25
|
-
{/if}
|
|
26
|
-
</Field.Root>
|