@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,225 +0,0 @@
|
|
|
1
|
-
import { tabbable } from 'tabbable';
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { FocusScopeState } from '../focus-scope.svelte.js';
|
|
4
|
-
// Mock the tabbable package
|
|
5
|
-
vi.mock('tabbable', () => ({
|
|
6
|
-
tabbable: vi.fn(() => []),
|
|
7
|
-
}));
|
|
8
|
-
const mockedTabbable = vi.mocked(tabbable);
|
|
9
|
-
function createContainer() {
|
|
10
|
-
const container = document.createElement('div');
|
|
11
|
-
document.body.appendChild(container);
|
|
12
|
-
return container;
|
|
13
|
-
}
|
|
14
|
-
function createTabbableElements(container, count) {
|
|
15
|
-
const elements = [];
|
|
16
|
-
for (let i = 0; i < count; i++) {
|
|
17
|
-
const btn = document.createElement('button');
|
|
18
|
-
btn.textContent = `Button ${i}`;
|
|
19
|
-
container.appendChild(btn);
|
|
20
|
-
elements.push(btn);
|
|
21
|
-
}
|
|
22
|
-
return elements;
|
|
23
|
-
}
|
|
24
|
-
describe('focusScopeState', () => {
|
|
25
|
-
let container;
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
container = createContainer();
|
|
28
|
-
});
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
container.remove();
|
|
31
|
-
vi.restoreAllMocks();
|
|
32
|
-
});
|
|
33
|
-
describe('mount()', () => {
|
|
34
|
-
it('stores previously focused element and focuses first tabbable', () => {
|
|
35
|
-
const outsideButton = document.createElement('button');
|
|
36
|
-
document.body.appendChild(outsideButton);
|
|
37
|
-
outsideButton.focus();
|
|
38
|
-
expect(document.activeElement).toBe(outsideButton);
|
|
39
|
-
const tabbableElements = createTabbableElements(container, 3);
|
|
40
|
-
mockedTabbable.mockReturnValue(tabbableElements);
|
|
41
|
-
const focusSpy = vi.spyOn(tabbableElements[0], 'focus');
|
|
42
|
-
const props = { element: container, enabled: true };
|
|
43
|
-
const scope = new FocusScopeState(props);
|
|
44
|
-
scope.mount();
|
|
45
|
-
expect(focusSpy).toHaveBeenCalledWith({ preventScroll: false });
|
|
46
|
-
// Unmount should restore to the outsideButton
|
|
47
|
-
scope.unmount();
|
|
48
|
-
expect(outsideButton).toBe(document.activeElement);
|
|
49
|
-
outsideButton.remove();
|
|
50
|
-
});
|
|
51
|
-
it('focuses the container itself when no tabbable children exist', () => {
|
|
52
|
-
mockedTabbable.mockReturnValue([]);
|
|
53
|
-
const focusSpy = vi.spyOn(container, 'focus');
|
|
54
|
-
const props = { element: container, enabled: true };
|
|
55
|
-
const scope = new FocusScopeState(props);
|
|
56
|
-
scope.mount();
|
|
57
|
-
expect(focusSpy).toHaveBeenCalledWith({ preventScroll: false });
|
|
58
|
-
});
|
|
59
|
-
it('does nothing when enabled is false', () => {
|
|
60
|
-
const tabbableElements = createTabbableElements(container, 2);
|
|
61
|
-
mockedTabbable.mockReturnValue(tabbableElements);
|
|
62
|
-
const focusSpy = vi.spyOn(tabbableElements[0], 'focus');
|
|
63
|
-
const props = { element: container, enabled: false };
|
|
64
|
-
const scope = new FocusScopeState(props);
|
|
65
|
-
scope.mount();
|
|
66
|
-
expect(focusSpy).not.toHaveBeenCalled();
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
describe('unmount()', () => {
|
|
70
|
-
it('restores previously focused element', () => {
|
|
71
|
-
const outsideButton = document.createElement('button');
|
|
72
|
-
document.body.appendChild(outsideButton);
|
|
73
|
-
outsideButton.focus();
|
|
74
|
-
const tabbableElements = createTabbableElements(container, 2);
|
|
75
|
-
mockedTabbable.mockReturnValue(tabbableElements);
|
|
76
|
-
const props = { element: container, enabled: true };
|
|
77
|
-
const scope = new FocusScopeState(props);
|
|
78
|
-
scope.mount();
|
|
79
|
-
scope.unmount();
|
|
80
|
-
expect(document.activeElement).toBe(outsideButton);
|
|
81
|
-
outsideButton.remove();
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
describe('handleKeydown()', () => {
|
|
85
|
-
it('wraps Tab from last to first tabbable', () => {
|
|
86
|
-
const tabbableElements = createTabbableElements(container, 3);
|
|
87
|
-
mockedTabbable.mockReturnValue(tabbableElements);
|
|
88
|
-
// Focus the last element
|
|
89
|
-
tabbableElements[2].focus();
|
|
90
|
-
const props = { element: container, enabled: true, loop: true };
|
|
91
|
-
const scope = new FocusScopeState(props);
|
|
92
|
-
const focusSpy = vi.spyOn(tabbableElements[0], 'focus');
|
|
93
|
-
const event = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true });
|
|
94
|
-
const preventSpy = vi.spyOn(event, 'preventDefault');
|
|
95
|
-
scope.handleKeydown(event);
|
|
96
|
-
expect(preventSpy).toHaveBeenCalled();
|
|
97
|
-
expect(focusSpy).toHaveBeenCalled();
|
|
98
|
-
});
|
|
99
|
-
it('wraps Shift+Tab from first to last tabbable', () => {
|
|
100
|
-
const tabbableElements = createTabbableElements(container, 3);
|
|
101
|
-
mockedTabbable.mockReturnValue(tabbableElements);
|
|
102
|
-
// Focus the first element
|
|
103
|
-
tabbableElements[0].focus();
|
|
104
|
-
const props = { element: container, enabled: true, loop: true };
|
|
105
|
-
const scope = new FocusScopeState(props);
|
|
106
|
-
const focusSpy = vi.spyOn(tabbableElements[2], 'focus');
|
|
107
|
-
const event = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true, bubbles: true });
|
|
108
|
-
const preventSpy = vi.spyOn(event, 'preventDefault');
|
|
109
|
-
scope.handleKeydown(event);
|
|
110
|
-
expect(preventSpy).toHaveBeenCalled();
|
|
111
|
-
expect(focusSpy).toHaveBeenCalled();
|
|
112
|
-
});
|
|
113
|
-
it('does nothing for non-Tab keys', () => {
|
|
114
|
-
const tabbableElements = createTabbableElements(container, 2);
|
|
115
|
-
mockedTabbable.mockReturnValue(tabbableElements);
|
|
116
|
-
const props = { element: container, enabled: true };
|
|
117
|
-
const scope = new FocusScopeState(props);
|
|
118
|
-
const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true });
|
|
119
|
-
const preventSpy = vi.spyOn(event, 'preventDefault');
|
|
120
|
-
scope.handleKeydown(event);
|
|
121
|
-
expect(preventSpy).not.toHaveBeenCalled();
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
describe('sSR safety', () => {
|
|
125
|
-
it('mount() does nothing when disabled (SSR scenario)', () => {
|
|
126
|
-
const tabbableElements = createTabbableElements(container, 2);
|
|
127
|
-
mockedTabbable.mockReturnValue(tabbableElements);
|
|
128
|
-
const focusSpy = vi.spyOn(tabbableElements[0], 'focus');
|
|
129
|
-
const onOpen = vi.fn();
|
|
130
|
-
const props = {
|
|
131
|
-
element: container,
|
|
132
|
-
enabled: false,
|
|
133
|
-
onOpenAutoFocus: onOpen,
|
|
134
|
-
};
|
|
135
|
-
const scope = new FocusScopeState(props);
|
|
136
|
-
scope.mount();
|
|
137
|
-
expect(focusSpy).not.toHaveBeenCalled();
|
|
138
|
-
expect(onOpen).not.toHaveBeenCalled();
|
|
139
|
-
});
|
|
140
|
-
it('mount() with null element does not throw and does not focus', () => {
|
|
141
|
-
mockedTabbable.mockReturnValue([]);
|
|
142
|
-
const onOpen = vi.fn();
|
|
143
|
-
const props = { element: null, enabled: true, onOpenAutoFocus: onOpen };
|
|
144
|
-
const scope = new FocusScopeState(props);
|
|
145
|
-
// Should not throw even with null element
|
|
146
|
-
scope.mount();
|
|
147
|
-
// onOpenAutoFocus is NOT called because element guard skips early
|
|
148
|
-
expect(onOpen).not.toHaveBeenCalled();
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
describe('onOpenAutoFocus', () => {
|
|
152
|
-
it('callback is called on mount', () => {
|
|
153
|
-
const tabbableElements = createTabbableElements(container, 2);
|
|
154
|
-
mockedTabbable.mockReturnValue(tabbableElements);
|
|
155
|
-
const onOpenAutoFocus = vi.fn();
|
|
156
|
-
const props = {
|
|
157
|
-
element: container,
|
|
158
|
-
enabled: true,
|
|
159
|
-
onOpenAutoFocus,
|
|
160
|
-
};
|
|
161
|
-
const scope = new FocusScopeState(props);
|
|
162
|
-
scope.mount();
|
|
163
|
-
expect(onOpenAutoFocus).toHaveBeenCalledOnce();
|
|
164
|
-
expect(onOpenAutoFocus).toHaveBeenCalledWith(expect.any(Event));
|
|
165
|
-
});
|
|
166
|
-
it('can be prevented to skip auto-focus', () => {
|
|
167
|
-
const tabbableElements = createTabbableElements(container, 2);
|
|
168
|
-
mockedTabbable.mockReturnValue(tabbableElements);
|
|
169
|
-
const focusSpy = vi.spyOn(tabbableElements[0], 'focus');
|
|
170
|
-
const onOpenAutoFocus = vi.fn((event) => {
|
|
171
|
-
event.preventDefault();
|
|
172
|
-
});
|
|
173
|
-
const props = {
|
|
174
|
-
element: container,
|
|
175
|
-
enabled: true,
|
|
176
|
-
onOpenAutoFocus,
|
|
177
|
-
};
|
|
178
|
-
const scope = new FocusScopeState(props);
|
|
179
|
-
scope.mount();
|
|
180
|
-
expect(onOpenAutoFocus).toHaveBeenCalledOnce();
|
|
181
|
-
expect(focusSpy).not.toHaveBeenCalled();
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
describe('onCloseAutoFocus', () => {
|
|
185
|
-
it('callback is called on unmount', () => {
|
|
186
|
-
const tabbableElements = createTabbableElements(container, 2);
|
|
187
|
-
mockedTabbable.mockReturnValue(tabbableElements);
|
|
188
|
-
const onCloseAutoFocus = vi.fn();
|
|
189
|
-
const props = {
|
|
190
|
-
element: container,
|
|
191
|
-
enabled: true,
|
|
192
|
-
onCloseAutoFocus,
|
|
193
|
-
};
|
|
194
|
-
const scope = new FocusScopeState(props);
|
|
195
|
-
scope.mount();
|
|
196
|
-
scope.unmount();
|
|
197
|
-
expect(onCloseAutoFocus).toHaveBeenCalledOnce();
|
|
198
|
-
expect(onCloseAutoFocus).toHaveBeenCalledWith(expect.any(Event));
|
|
199
|
-
});
|
|
200
|
-
it('can be prevented to skip focus restore', () => {
|
|
201
|
-
const outsideButton = document.createElement('button');
|
|
202
|
-
document.body.appendChild(outsideButton);
|
|
203
|
-
outsideButton.focus();
|
|
204
|
-
const tabbableElements = createTabbableElements(container, 2);
|
|
205
|
-
mockedTabbable.mockReturnValue(tabbableElements);
|
|
206
|
-
const restoreSpy = vi.spyOn(outsideButton, 'focus');
|
|
207
|
-
const onCloseAutoFocus = vi.fn((event) => {
|
|
208
|
-
event.preventDefault();
|
|
209
|
-
});
|
|
210
|
-
const props = {
|
|
211
|
-
element: container,
|
|
212
|
-
enabled: true,
|
|
213
|
-
onCloseAutoFocus,
|
|
214
|
-
};
|
|
215
|
-
const scope = new FocusScopeState(props);
|
|
216
|
-
scope.mount();
|
|
217
|
-
restoreSpy.mockClear(); // clear any calls from mount phase
|
|
218
|
-
scope.unmount();
|
|
219
|
-
expect(onCloseAutoFocus).toHaveBeenCalledOnce();
|
|
220
|
-
// Focus should NOT have been restored because we prevented it
|
|
221
|
-
expect(restoreSpy).not.toHaveBeenCalled();
|
|
222
|
-
outsideButton.remove();
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
});
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { GraceAreaState } from '../grace-area.svelte.js';
|
|
3
|
-
describe('graceAreaState', () => {
|
|
4
|
-
function createMockElement(rect) {
|
|
5
|
-
const el = document.createElement('div');
|
|
6
|
-
el.getBoundingClientRect = () => ({
|
|
7
|
-
...rect,
|
|
8
|
-
width: rect.right - rect.left,
|
|
9
|
-
height: rect.bottom - rect.top,
|
|
10
|
-
x: rect.left,
|
|
11
|
-
y: rect.top,
|
|
12
|
-
toJSON: () => { },
|
|
13
|
-
});
|
|
14
|
-
return el;
|
|
15
|
-
}
|
|
16
|
-
it('returns false when not enabled', () => {
|
|
17
|
-
const state = new GraceAreaState({
|
|
18
|
-
triggerElement: document.createElement('div'),
|
|
19
|
-
contentElement: document.createElement('div'),
|
|
20
|
-
enabled: false,
|
|
21
|
-
});
|
|
22
|
-
state.setPointerExit({ x: 0, y: 0 });
|
|
23
|
-
expect(state.isPointInGraceArea({ x: 50, y: 50 })).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
it('returns false when no pointer exit is set', () => {
|
|
26
|
-
const state = new GraceAreaState({
|
|
27
|
-
triggerElement: document.createElement('div'),
|
|
28
|
-
contentElement: document.createElement('div'),
|
|
29
|
-
enabled: true,
|
|
30
|
-
});
|
|
31
|
-
expect(state.isPointInGraceArea({ x: 50, y: 50 })).toBe(false);
|
|
32
|
-
});
|
|
33
|
-
it('detects point inside grace area polygon', () => {
|
|
34
|
-
const content = createMockElement({ left: 100, top: 100, right: 200, bottom: 200 });
|
|
35
|
-
const state = new GraceAreaState({
|
|
36
|
-
triggerElement: document.createElement('div'),
|
|
37
|
-
contentElement: content,
|
|
38
|
-
enabled: true,
|
|
39
|
-
});
|
|
40
|
-
// Pointer exits trigger at (50, 150) — to the left of content
|
|
41
|
-
state.setPointerExit({ x: 50, y: 150 });
|
|
42
|
-
// Point between trigger exit and content should be in grace area
|
|
43
|
-
expect(state.isPointInGraceArea({ x: 75, y: 150 })).toBe(true);
|
|
44
|
-
});
|
|
45
|
-
it('detects point outside grace area', () => {
|
|
46
|
-
const content = createMockElement({ left: 100, top: 100, right: 200, bottom: 200 });
|
|
47
|
-
const state = new GraceAreaState({
|
|
48
|
-
triggerElement: document.createElement('div'),
|
|
49
|
-
contentElement: content,
|
|
50
|
-
enabled: true,
|
|
51
|
-
});
|
|
52
|
-
state.setPointerExit({ x: 50, y: 150 });
|
|
53
|
-
// Point far away from the grace area
|
|
54
|
-
expect(state.isPointInGraceArea({ x: 300, y: 300 })).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
it('reset clears pointer exit', () => {
|
|
57
|
-
const content = createMockElement({ left: 100, top: 100, right: 200, bottom: 200 });
|
|
58
|
-
const state = new GraceAreaState({
|
|
59
|
-
triggerElement: document.createElement('div'),
|
|
60
|
-
contentElement: content,
|
|
61
|
-
enabled: true,
|
|
62
|
-
});
|
|
63
|
-
state.setPointerExit({ x: 50, y: 150 });
|
|
64
|
-
state.reset();
|
|
65
|
-
expect(state.isPointInGraceArea({ x: 75, y: 150 })).toBe(false);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { arraysAreEqual, boolToStr, getDataOpenClosed, getDirectionalKeys, isBrowser, isFunction, isHTMLElement, isNull, isNullish, isNumber, isString, isUndefined, kbd, mergeProps, next, prev, resetIdCounter, useId, wrapArray, } from '../index.js';
|
|
3
|
-
describe('kbd', () => {
|
|
4
|
-
it('has standard key constants', () => {
|
|
5
|
-
expect(kbd.ESCAPE).toBe('Escape');
|
|
6
|
-
expect(kbd.ENTER).toBe('Enter');
|
|
7
|
-
expect(kbd.SPACE).toBe(' ');
|
|
8
|
-
expect(kbd.TAB).toBe('Tab');
|
|
9
|
-
expect(kbd.ARROW_DOWN).toBe('ArrowDown');
|
|
10
|
-
});
|
|
11
|
-
});
|
|
12
|
-
describe('getDirectionalKeys', () => {
|
|
13
|
-
it('returns down/up for vertical orientation', () => {
|
|
14
|
-
const keys = getDirectionalKeys('ltr', 'vertical');
|
|
15
|
-
expect(keys.nextKey).toBe('ArrowDown');
|
|
16
|
-
expect(keys.prevKey).toBe('ArrowUp');
|
|
17
|
-
});
|
|
18
|
-
it('returns right/left for horizontal LTR', () => {
|
|
19
|
-
const keys = getDirectionalKeys('ltr', 'horizontal');
|
|
20
|
-
expect(keys.nextKey).toBe('ArrowRight');
|
|
21
|
-
expect(keys.prevKey).toBe('ArrowLeft');
|
|
22
|
-
});
|
|
23
|
-
it('returns left/right for horizontal RTL', () => {
|
|
24
|
-
const keys = getDirectionalKeys('rtl', 'horizontal');
|
|
25
|
-
expect(keys.nextKey).toBe('ArrowLeft');
|
|
26
|
-
expect(keys.prevKey).toBe('ArrowRight');
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
describe('dom helpers', () => {
|
|
30
|
-
it('isBrowser returns true in jsdom', () => {
|
|
31
|
-
expect(isBrowser).toBe(true);
|
|
32
|
-
});
|
|
33
|
-
it('isHTMLElement checks correctly', () => {
|
|
34
|
-
expect(isHTMLElement(document.createElement('div'))).toBe(true);
|
|
35
|
-
expect(isHTMLElement('string')).toBe(false);
|
|
36
|
-
expect(isHTMLElement(null)).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
it('getDataOpenClosed returns correct value', () => {
|
|
39
|
-
expect(getDataOpenClosed(true)).toBe('open');
|
|
40
|
-
expect(getDataOpenClosed(false)).toBe('closed');
|
|
41
|
-
});
|
|
42
|
-
it('boolToStr returns string boolean', () => {
|
|
43
|
-
expect(boolToStr(true)).toBe('true');
|
|
44
|
-
expect(boolToStr(false)).toBe('false');
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
describe('array helpers', () => {
|
|
48
|
-
it('wrapArray starts at the given index', () => {
|
|
49
|
-
expect(wrapArray([1, 2, 3, 4], 2)).toEqual([3, 4, 1, 2]);
|
|
50
|
-
});
|
|
51
|
-
it('next returns next item', () => {
|
|
52
|
-
expect(next([1, 2, 3], 0)).toBe(2);
|
|
53
|
-
expect(next([1, 2, 3], 2, true)).toBe(1);
|
|
54
|
-
expect(next([1, 2, 3], 2, false)).toBeUndefined();
|
|
55
|
-
});
|
|
56
|
-
it('prev returns previous item', () => {
|
|
57
|
-
expect(prev([1, 2, 3], 1)).toBe(1);
|
|
58
|
-
expect(prev([1, 2, 3], 0, true)).toBe(3);
|
|
59
|
-
expect(prev([1, 2, 3], 0, false)).toBeUndefined();
|
|
60
|
-
});
|
|
61
|
-
it('arraysAreEqual handles NaN', () => {
|
|
62
|
-
expect(arraysAreEqual([1, 2, 3], [1, 2, 3])).toBe(true);
|
|
63
|
-
expect(arraysAreEqual([1, 2], [1, 2, 3])).toBe(false);
|
|
64
|
-
expect(arraysAreEqual([Number.NaN], [Number.NaN])).toBe(true);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
describe('is helpers', () => {
|
|
68
|
-
it('isString', () => {
|
|
69
|
-
expect(isString('hello')).toBe(true);
|
|
70
|
-
expect(isString(123)).toBe(false);
|
|
71
|
-
});
|
|
72
|
-
it('isNumber', () => {
|
|
73
|
-
expect(isNumber(42)).toBe(true);
|
|
74
|
-
expect(isNumber(Number.NaN)).toBe(false);
|
|
75
|
-
});
|
|
76
|
-
it('isFunction', () => {
|
|
77
|
-
expect(isFunction(() => { })).toBe(true);
|
|
78
|
-
expect(isFunction('fn')).toBe(false);
|
|
79
|
-
});
|
|
80
|
-
it('isNull / isUndefined / isNullish', () => {
|
|
81
|
-
expect(isNull(null)).toBe(true);
|
|
82
|
-
expect(isNull(undefined)).toBe(false);
|
|
83
|
-
expect(isUndefined(undefined)).toBe(true);
|
|
84
|
-
expect(isNullish(null)).toBe(true);
|
|
85
|
-
expect(isNullish(undefined)).toBe(true);
|
|
86
|
-
expect(isNullish(0)).toBe(false);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
describe('useId', () => {
|
|
90
|
-
it('generates unique IDs', () => {
|
|
91
|
-
resetIdCounter();
|
|
92
|
-
const id1 = useId();
|
|
93
|
-
const id2 = useId();
|
|
94
|
-
expect(id1).toMatch(/^sp-\d+$/);
|
|
95
|
-
expect(id2).toMatch(/^sp-\d+$/);
|
|
96
|
-
expect(id1).not.toBe(id2);
|
|
97
|
-
});
|
|
98
|
-
it('supports custom prefix', () => {
|
|
99
|
-
resetIdCounter();
|
|
100
|
-
const id = useId('dialog');
|
|
101
|
-
expect(id).toMatch(/^dialog-\d+$/);
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
describe('mergeProps', () => {
|
|
105
|
-
it('merges simple props (last wins)', () => {
|
|
106
|
-
const result = mergeProps({ a: 1 }, { a: 2, b: 3 });
|
|
107
|
-
expect(result).toEqual({ a: 2, b: 3 });
|
|
108
|
-
});
|
|
109
|
-
it('combines event handlers', () => {
|
|
110
|
-
const calls = [];
|
|
111
|
-
const result = mergeProps({ onclick: () => calls.push('first') }, { onclick: () => calls.push('second') });
|
|
112
|
-
result.onclick();
|
|
113
|
-
expect(calls).toEqual(['first', 'second']);
|
|
114
|
-
});
|
|
115
|
-
it('combines class strings', () => {
|
|
116
|
-
const result = mergeProps({ class: 'a' }, { class: 'b' });
|
|
117
|
-
expect(result.class).toBe('a b');
|
|
118
|
-
});
|
|
119
|
-
it('combines style strings', () => {
|
|
120
|
-
const result = mergeProps({ style: 'color: red' }, { style: 'font-size: 12px' });
|
|
121
|
-
expect(result.style).toBe('color: red; font-size: 12px');
|
|
122
|
-
});
|
|
123
|
-
});
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Portal } from '../index.js'
|
|
3
|
-
|
|
4
|
-
const {
|
|
5
|
-
target = undefined,
|
|
6
|
-
disabled = false,
|
|
7
|
-
show = true,
|
|
8
|
-
}: {
|
|
9
|
-
target?: HTMLElement | string
|
|
10
|
-
disabled?: boolean
|
|
11
|
-
show?: boolean
|
|
12
|
-
} = $props()
|
|
13
|
-
</script>
|
|
14
|
-
|
|
15
|
-
<div data-testid="origin">
|
|
16
|
-
{#if show}
|
|
17
|
-
<Portal to={target} {disabled}>
|
|
18
|
-
<div data-testid="portalled">hello</div>
|
|
19
|
-
</Portal>
|
|
20
|
-
{/if}
|
|
21
|
-
</div>
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { PortalState } from '../portal.svelte.js';
|
|
3
|
-
describe('portalState', () => {
|
|
4
|
-
it('resolves to document.body by default', () => {
|
|
5
|
-
const portal = new PortalState({});
|
|
6
|
-
expect(portal.resolveTarget()).toBe(document.body);
|
|
7
|
-
});
|
|
8
|
-
it('resolves a CSS selector', () => {
|
|
9
|
-
const target = document.createElement('div');
|
|
10
|
-
target.id = 'portal-target';
|
|
11
|
-
document.body.appendChild(target);
|
|
12
|
-
const portal = new PortalState({ target: '#portal-target' });
|
|
13
|
-
expect(portal.resolveTarget()).toBe(target);
|
|
14
|
-
target.remove();
|
|
15
|
-
});
|
|
16
|
-
it('falls back to body for missing selector', () => {
|
|
17
|
-
const portal = new PortalState({ target: '#nonexistent' });
|
|
18
|
-
expect(portal.resolveTarget()).toBe(document.body);
|
|
19
|
-
});
|
|
20
|
-
it('resolves a direct HTMLElement reference', () => {
|
|
21
|
-
const target = document.createElement('div');
|
|
22
|
-
const portal = new PortalState({ target });
|
|
23
|
-
expect(portal.resolveTarget()).toBe(target);
|
|
24
|
-
});
|
|
25
|
-
it('returns null when disabled', () => {
|
|
26
|
-
const portal = new PortalState({ disabled: true });
|
|
27
|
-
expect(portal.resolveTarget()).toBeNull();
|
|
28
|
-
});
|
|
29
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { flushSync, mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import PortalTest from './PortalTest.svelte';
|
|
4
|
-
describe('portal component', () => {
|
|
5
|
-
let host;
|
|
6
|
-
let instance;
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
host = document.createElement('div');
|
|
9
|
-
document.body.appendChild(host);
|
|
10
|
-
});
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
if (instance)
|
|
13
|
-
unmount(instance);
|
|
14
|
-
instance = undefined;
|
|
15
|
-
host.remove();
|
|
16
|
-
});
|
|
17
|
-
it('renders children into document.body by default', () => {
|
|
18
|
-
instance = mount(PortalTest, { target: host });
|
|
19
|
-
flushSync();
|
|
20
|
-
const origin = host.querySelector('[data-testid="origin"]');
|
|
21
|
-
expect(origin?.querySelector('[data-testid="portalled"]')).toBeNull();
|
|
22
|
-
expect(document.body.querySelector('[data-testid="portalled"]')).toBeTruthy();
|
|
23
|
-
});
|
|
24
|
-
it('renders children into a specific HTMLElement target', () => {
|
|
25
|
-
const target = document.createElement('div');
|
|
26
|
-
target.id = 'custom-target';
|
|
27
|
-
document.body.appendChild(target);
|
|
28
|
-
instance = mount(PortalTest, { target: host, props: { target } });
|
|
29
|
-
flushSync();
|
|
30
|
-
expect(target.querySelector('[data-testid="portalled"]')).toBeTruthy();
|
|
31
|
-
target.remove();
|
|
32
|
-
});
|
|
33
|
-
it('resolves a CSS selector target', () => {
|
|
34
|
-
const target = document.createElement('div');
|
|
35
|
-
target.id = 'selector-target';
|
|
36
|
-
document.body.appendChild(target);
|
|
37
|
-
instance = mount(PortalTest, { target: host, props: { target: '#selector-target' } });
|
|
38
|
-
flushSync();
|
|
39
|
-
expect(target.querySelector('[data-testid="portalled"]')).toBeTruthy();
|
|
40
|
-
target.remove();
|
|
41
|
-
});
|
|
42
|
-
it('renders inline when disabled', () => {
|
|
43
|
-
instance = mount(PortalTest, { target: host, props: { disabled: true } });
|
|
44
|
-
flushSync();
|
|
45
|
-
const origin = host.querySelector('[data-testid="origin"]');
|
|
46
|
-
expect(origin?.querySelector('[data-testid="portalled"]')).toBeTruthy();
|
|
47
|
-
});
|
|
48
|
-
it('unmounts portalled content when parent removes Portal', () => {
|
|
49
|
-
instance = mount(PortalTest, { target: host, props: { show: true } });
|
|
50
|
-
flushSync();
|
|
51
|
-
expect(document.body.querySelector('[data-testid="portalled"]')).toBeTruthy();
|
|
52
|
-
unmount(instance);
|
|
53
|
-
instance = undefined;
|
|
54
|
-
flushSync();
|
|
55
|
-
expect(document.body.querySelector('[data-testid="portalled"]')).toBeNull();
|
|
56
|
-
});
|
|
57
|
-
});
|