@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,74 +0,0 @@
|
|
|
1
|
-
import { flushSync, mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import FieldTest from './FieldTest.svelte';
|
|
4
|
-
describe('field', () => {
|
|
5
|
-
let target;
|
|
6
|
-
let instance;
|
|
7
|
-
function setup(props = {}) {
|
|
8
|
-
target = document.createElement('div');
|
|
9
|
-
document.body.appendChild(target);
|
|
10
|
-
instance = mount(FieldTest, { target, props });
|
|
11
|
-
flushSync();
|
|
12
|
-
}
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
if (instance) {
|
|
15
|
-
unmount(instance);
|
|
16
|
-
}
|
|
17
|
-
instance = undefined;
|
|
18
|
-
target?.remove();
|
|
19
|
-
});
|
|
20
|
-
it('renders Field.Root container', () => {
|
|
21
|
-
setup();
|
|
22
|
-
const root = target.querySelector('.sp-field');
|
|
23
|
-
expect(root).toBeTruthy();
|
|
24
|
-
});
|
|
25
|
-
it('renders Field.Label with for attribute linking to input', () => {
|
|
26
|
-
setup({ id: 'username' });
|
|
27
|
-
const label = target.querySelector('label.sp-field-label');
|
|
28
|
-
expect(label).toBeTruthy();
|
|
29
|
-
expect(label?.getAttribute('for')).toBe('username');
|
|
30
|
-
});
|
|
31
|
-
it('renders input with matching id', () => {
|
|
32
|
-
setup({ id: 'email-input' });
|
|
33
|
-
const input = target.querySelector('input[data-testid="input"]');
|
|
34
|
-
expect(input).toBeTruthy();
|
|
35
|
-
expect(input?.id).toBe('email-input');
|
|
36
|
-
});
|
|
37
|
-
it('renders Field.Description when provided', () => {
|
|
38
|
-
setup({ description: 'Please enter a valid email' });
|
|
39
|
-
const desc = target.querySelector('.sp-field-description');
|
|
40
|
-
expect(desc).toBeTruthy();
|
|
41
|
-
expect(desc?.textContent).toContain('Please enter a valid email');
|
|
42
|
-
});
|
|
43
|
-
it('does not render Field.Description when description is empty string', () => {
|
|
44
|
-
setup({ description: '' });
|
|
45
|
-
const desc = target.querySelector('.sp-field-description');
|
|
46
|
-
expect(desc).toBeFalsy();
|
|
47
|
-
});
|
|
48
|
-
it('renders Field.Error with role="alert"', () => {
|
|
49
|
-
setup({ error: 'This field is required' });
|
|
50
|
-
const error = target.querySelector('.sp-field-error');
|
|
51
|
-
expect(error).toBeTruthy();
|
|
52
|
-
expect(error?.getAttribute('role')).toBe('alert');
|
|
53
|
-
expect(error?.textContent).toContain('This field is required');
|
|
54
|
-
});
|
|
55
|
-
it('does not render Field.Error when undefined', () => {
|
|
56
|
-
setup({ error: undefined });
|
|
57
|
-
const error = target.querySelector('.sp-field-error');
|
|
58
|
-
expect(error).toBeFalsy();
|
|
59
|
-
});
|
|
60
|
-
it('field.root renders with data-invalid attribute when invalid is true', () => {
|
|
61
|
-
setup();
|
|
62
|
-
// Modify props to test invalid state (note: FieldTest doesn't expose invalid, but we verify root structure)
|
|
63
|
-
const root = target.querySelector('.sp-field');
|
|
64
|
-
expect(root).toBeTruthy();
|
|
65
|
-
expect(root?.tagName).toBe('DIV');
|
|
66
|
-
});
|
|
67
|
-
it('field.label renders required indicator when required is true', () => {
|
|
68
|
-
setup({ required: true });
|
|
69
|
-
const requiredSpan = target.querySelector('.sp-field-required');
|
|
70
|
-
expect(requiredSpan).toBeTruthy();
|
|
71
|
-
expect(requiredSpan?.getAttribute('aria-hidden')).toBe('true');
|
|
72
|
-
expect(requiredSpan?.textContent).toContain('*');
|
|
73
|
-
});
|
|
74
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { GridListItemData } from '../types.js'
|
|
3
|
-
import Item from '../components/grid-list-item.svelte'
|
|
4
|
-
import * as GridList from '../index.js'
|
|
5
|
-
|
|
6
|
-
interface TestItem extends GridListItemData {
|
|
7
|
-
id: string
|
|
8
|
-
label: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
let {
|
|
12
|
-
items = [
|
|
13
|
-
{ id: 'a', label: 'Item A' },
|
|
14
|
-
{ id: 'b', label: 'Item B' },
|
|
15
|
-
{ id: 'c', label: 'Item C' },
|
|
16
|
-
] as TestItem[],
|
|
17
|
-
selectedKeys = $bindable<Set<string>>(new Set<string>()),
|
|
18
|
-
selectionMode = 'multiple' as 'none' | 'single' | 'multiple',
|
|
19
|
-
disabled = false,
|
|
20
|
-
draggable = false,
|
|
21
|
-
onSelectionChange,
|
|
22
|
-
}: {
|
|
23
|
-
items?: TestItem[]
|
|
24
|
-
selectedKeys?: Set<string>
|
|
25
|
-
selectionMode?: 'none' | 'single' | 'multiple'
|
|
26
|
-
disabled?: boolean
|
|
27
|
-
draggable?: boolean
|
|
28
|
-
onSelectionChange?: (keys: Set<string>) => void
|
|
29
|
-
} = $props()
|
|
30
|
-
</script>
|
|
31
|
-
|
|
32
|
-
<GridList.Root
|
|
33
|
-
bind:selectedKeys
|
|
34
|
-
{items}
|
|
35
|
-
{selectionMode}
|
|
36
|
-
{disabled}
|
|
37
|
-
{draggable}
|
|
38
|
-
{onSelectionChange}
|
|
39
|
-
aria-label="Test grid"
|
|
40
|
-
>
|
|
41
|
-
{#snippet item(itemData)}
|
|
42
|
-
<Item id={itemData.id} textValue={itemData.label}>
|
|
43
|
-
{itemData.label}
|
|
44
|
-
</Item>
|
|
45
|
-
{/snippet}
|
|
46
|
-
</GridList.Root>
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import { flushSync, mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import GridListTest from './GridListTest.svelte';
|
|
4
|
-
// jsdom does not define PointerEvent — polyfill it for tests
|
|
5
|
-
if (typeof globalThis.PointerEvent === 'undefined') {
|
|
6
|
-
// @ts-expect-error polyfill
|
|
7
|
-
globalThis.PointerEvent = class PointerEvent extends MouseEvent {
|
|
8
|
-
constructor(type, init) {
|
|
9
|
-
super(type, init);
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
describe('gridList', () => {
|
|
14
|
-
let target;
|
|
15
|
-
let instance;
|
|
16
|
-
function setup(props = {}) {
|
|
17
|
-
target = document.createElement('div');
|
|
18
|
-
document.body.appendChild(target);
|
|
19
|
-
instance = mount(GridListTest, { target, props });
|
|
20
|
-
flushSync();
|
|
21
|
-
}
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
if (instance) {
|
|
24
|
-
unmount(instance);
|
|
25
|
-
}
|
|
26
|
-
instance = undefined;
|
|
27
|
-
target?.remove();
|
|
28
|
-
});
|
|
29
|
-
// ── Structure ────────────────────────────────────────────────────────────
|
|
30
|
-
it('renders a listbox container', () => {
|
|
31
|
-
setup();
|
|
32
|
-
expect(target.querySelector('[role="listbox"]')).toBeTruthy();
|
|
33
|
-
});
|
|
34
|
-
it('renders all items with role="option"', () => {
|
|
35
|
-
setup();
|
|
36
|
-
expect(target.querySelectorAll('[role="option"]').length).toBe(3);
|
|
37
|
-
});
|
|
38
|
-
it('exposes data-grid-list-item attribute matching each id', () => {
|
|
39
|
-
setup();
|
|
40
|
-
expect(target.querySelector('[data-grid-list-item="a"]')).toBeTruthy();
|
|
41
|
-
expect(target.querySelector('[data-grid-list-item="b"]')).toBeTruthy();
|
|
42
|
-
expect(target.querySelector('[data-grid-list-item="c"]')).toBeTruthy();
|
|
43
|
-
});
|
|
44
|
-
it('propagates aria-label to the listbox', () => {
|
|
45
|
-
setup();
|
|
46
|
-
const listbox = target.querySelector('[role="listbox"]');
|
|
47
|
-
expect(listbox?.getAttribute('aria-label')).toBe('Test grid');
|
|
48
|
-
});
|
|
49
|
-
it('sets aria-multiselectable on multiple mode', () => {
|
|
50
|
-
setup({ selectionMode: 'multiple' });
|
|
51
|
-
const listbox = target.querySelector('[role="listbox"]');
|
|
52
|
-
expect(listbox?.getAttribute('aria-multiselectable')).toBe('true');
|
|
53
|
-
});
|
|
54
|
-
// ── Selection — multiple ─────────────────────────────────────────────────
|
|
55
|
-
it('clicking an item selects it in multiple mode', () => {
|
|
56
|
-
setup({ selectionMode: 'multiple' });
|
|
57
|
-
target.querySelector('[role="option"]')?.click();
|
|
58
|
-
flushSync();
|
|
59
|
-
const selected = target.querySelectorAll('[aria-selected="true"]');
|
|
60
|
-
expect(selected.length).toBeGreaterThanOrEqual(1);
|
|
61
|
-
});
|
|
62
|
-
it('multiple mode with Ctrl accumulates selection across clicks', () => {
|
|
63
|
-
setup({ selectionMode: 'multiple' });
|
|
64
|
-
const options = target.querySelectorAll('[role="option"]');
|
|
65
|
-
// First plain click selects item A
|
|
66
|
-
options[0]?.click();
|
|
67
|
-
flushSync();
|
|
68
|
-
// Ctrl+click adds item B to selection
|
|
69
|
-
options[1]?.dispatchEvent(new MouseEvent('click', { bubbles: true, ctrlKey: true }));
|
|
70
|
-
flushSync();
|
|
71
|
-
expect(target.querySelectorAll('[aria-selected="true"]').length).toBeGreaterThanOrEqual(2);
|
|
72
|
-
});
|
|
73
|
-
it('ctrl+click the same item deselects it in multiple mode', () => {
|
|
74
|
-
setup({ selectionMode: 'multiple' });
|
|
75
|
-
const first = target.querySelector('[role="option"]');
|
|
76
|
-
// Select with plain click
|
|
77
|
-
first?.click();
|
|
78
|
-
flushSync();
|
|
79
|
-
// Ctrl+click same item to deselect
|
|
80
|
-
first?.dispatchEvent(new MouseEvent('click', { bubbles: true, ctrlKey: true }));
|
|
81
|
-
flushSync();
|
|
82
|
-
expect(target.querySelectorAll('[aria-selected="true"]').length).toBe(0);
|
|
83
|
-
});
|
|
84
|
-
// ── Selection — single ───────────────────────────────────────────────────
|
|
85
|
-
it('single mode replaces selection when a second item is clicked', () => {
|
|
86
|
-
setup({ selectionMode: 'single' });
|
|
87
|
-
const options = target.querySelectorAll('[role="option"]');
|
|
88
|
-
options[0]?.click();
|
|
89
|
-
flushSync();
|
|
90
|
-
options[1]?.click();
|
|
91
|
-
flushSync();
|
|
92
|
-
expect(target.querySelectorAll('[aria-selected="true"]').length).toBeLessThanOrEqual(1);
|
|
93
|
-
expect(target.querySelector('[data-grid-list-item="b"]')?.getAttribute('aria-selected')).toBe('true');
|
|
94
|
-
});
|
|
95
|
-
// ── Selection — none ─────────────────────────────────────────────────────
|
|
96
|
-
it('selectionMode="none" prevents selection on click', () => {
|
|
97
|
-
setup({ selectionMode: 'none' });
|
|
98
|
-
target.querySelector('[role="option"]')?.click();
|
|
99
|
-
flushSync();
|
|
100
|
-
expect(target.querySelector('[aria-selected="true"]')).toBeNull();
|
|
101
|
-
});
|
|
102
|
-
// ── Disabled ─────────────────────────────────────────────────────────────
|
|
103
|
-
it('disabled grid ignores item clicks', () => {
|
|
104
|
-
setup({ disabled: true, selectionMode: 'multiple' });
|
|
105
|
-
target.querySelector('[role="option"]')?.click();
|
|
106
|
-
flushSync();
|
|
107
|
-
expect(target.querySelector('[aria-selected="true"]')).toBeNull();
|
|
108
|
-
});
|
|
109
|
-
// ── Bindable selectedKeys ────────────────────────────────────────────────
|
|
110
|
-
it('respects pre-populated bindable selectedKeys', () => {
|
|
111
|
-
setup({ selectedKeys: new Set(['b']), selectionMode: 'multiple' });
|
|
112
|
-
const itemB = target.querySelector('[data-grid-list-item="b"]');
|
|
113
|
-
expect(itemB?.getAttribute('aria-selected')).toBe('true');
|
|
114
|
-
});
|
|
115
|
-
it('fires onSelectionChange callback when selection changes', () => {
|
|
116
|
-
const spy = vi.fn();
|
|
117
|
-
setup({ selectionMode: 'multiple', onSelectionChange: spy });
|
|
118
|
-
target.querySelector('[role="option"]')?.click();
|
|
119
|
-
flushSync();
|
|
120
|
-
expect(spy).toHaveBeenCalledOnce();
|
|
121
|
-
expect(spy.mock.calls[0][0]).toBeInstanceOf(Set);
|
|
122
|
-
});
|
|
123
|
-
// ── Keyboard ─────────────────────────────────────────────────────────────
|
|
124
|
-
it('enter key on a focused item selects it without throwing', () => {
|
|
125
|
-
setup({ selectionMode: 'multiple' });
|
|
126
|
-
const item = target.querySelector('[role="option"]');
|
|
127
|
-
item?.focus();
|
|
128
|
-
item?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
129
|
-
flushSync();
|
|
130
|
-
expect(target.querySelector('[role="listbox"]')).toBeTruthy();
|
|
131
|
-
expect(target.querySelector('[aria-selected="true"]')).toBeTruthy();
|
|
132
|
-
});
|
|
133
|
-
it('space key on a focused item selects it without throwing', () => {
|
|
134
|
-
setup({ selectionMode: 'multiple' });
|
|
135
|
-
const item = target.querySelector('[role="option"]');
|
|
136
|
-
item?.focus();
|
|
137
|
-
item?.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }));
|
|
138
|
-
flushSync();
|
|
139
|
-
expect(target.querySelector('[aria-selected="true"]')).toBeTruthy();
|
|
140
|
-
});
|
|
141
|
-
it('ctrl+A selects all items in multiple mode', () => {
|
|
142
|
-
setup({ selectionMode: 'multiple' });
|
|
143
|
-
const container = target.querySelector('[role="listbox"]');
|
|
144
|
-
container.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', ctrlKey: true, bubbles: true }));
|
|
145
|
-
flushSync();
|
|
146
|
-
expect(target.querySelectorAll('[aria-selected="true"]').length).toBe(3);
|
|
147
|
-
});
|
|
148
|
-
// ── Shift+click range selection ──────────────────────────────────────────
|
|
149
|
-
it('shift+click selects a range in multiple mode', () => {
|
|
150
|
-
setup({ selectionMode: 'multiple' });
|
|
151
|
-
const options = target.querySelectorAll('[role="option"]');
|
|
152
|
-
// Select first
|
|
153
|
-
options[0]?.click();
|
|
154
|
-
flushSync();
|
|
155
|
-
// Shift+click last to extend range
|
|
156
|
-
options[2]?.dispatchEvent(new MouseEvent('click', { bubbles: true, shiftKey: true }));
|
|
157
|
-
flushSync();
|
|
158
|
-
expect(target.querySelectorAll('[aria-selected="true"]').length).toBe(3);
|
|
159
|
-
});
|
|
160
|
-
// ── Typeahead ────────────────────────────────────────────────────────────
|
|
161
|
-
it('printable key dispatched on container triggers typeahead without throwing', () => {
|
|
162
|
-
setup({ selectionMode: 'multiple' });
|
|
163
|
-
const container = target.querySelector('[role="listbox"]');
|
|
164
|
-
container.dispatchEvent(new KeyboardEvent('keydown', { key: 'i', bubbles: true }));
|
|
165
|
-
flushSync();
|
|
166
|
-
// No error — typeahead ran silently (no match is fine)
|
|
167
|
-
expect(target.querySelector('[role="listbox"]')).toBeTruthy();
|
|
168
|
-
});
|
|
169
|
-
// ── Empty state ──────────────────────────────────────────────────────────
|
|
170
|
-
it('renders empty state snippet when items array is empty', () => {
|
|
171
|
-
// Pass empty items list — emptySnippet is optional so no crash expected
|
|
172
|
-
setup({ items: [] });
|
|
173
|
-
expect(target.querySelectorAll('[role="option"]').length).toBe(0);
|
|
174
|
-
});
|
|
175
|
-
// ── Draggable smoke ──────────────────────────────────────────────────────
|
|
176
|
-
it('renders all items without errors when draggable=true', () => {
|
|
177
|
-
setup({ draggable: true, selectionMode: 'multiple' });
|
|
178
|
-
expect(target.querySelectorAll('[role="option"]').length).toBe(3);
|
|
179
|
-
});
|
|
180
|
-
it('items expose data-draggable attribute when draggable=true', () => {
|
|
181
|
-
setup({ draggable: true });
|
|
182
|
-
const items = target.querySelectorAll('[data-draggable]');
|
|
183
|
-
expect(items.length).toBe(3);
|
|
184
|
-
});
|
|
185
|
-
it('draggable items get data-dnd-source attribute attached', () => {
|
|
186
|
-
setup({ draggable: true, selectionMode: 'multiple' });
|
|
187
|
-
flushSync();
|
|
188
|
-
// DragSourceState.attach sets data-dnd-source; verifies registerDragItem ran
|
|
189
|
-
const withSource = target.querySelectorAll('[data-dnd-source]');
|
|
190
|
-
expect(withSource.length).toBeGreaterThanOrEqual(3);
|
|
191
|
-
});
|
|
192
|
-
it('unmounting draggable list cleans up drag sources without throwing', () => {
|
|
193
|
-
setup({ draggable: true, selectionMode: 'multiple' });
|
|
194
|
-
flushSync();
|
|
195
|
-
// Verify items mounted with drag sources attached
|
|
196
|
-
expect(target.querySelectorAll('[data-dnd-source]').length).toBeGreaterThanOrEqual(3);
|
|
197
|
-
// Explicit unmount to exercise detachContainer for-loop (lines 315-319)
|
|
198
|
-
if (instance) {
|
|
199
|
-
unmount(instance);
|
|
200
|
-
instance = undefined;
|
|
201
|
-
flushSync();
|
|
202
|
-
}
|
|
203
|
-
// No error = detachContainer iterated itemSources and detached each
|
|
204
|
-
expect(true).toBe(true);
|
|
205
|
-
});
|
|
206
|
-
it('single mode: clicking selected item again deselects it', () => {
|
|
207
|
-
setup({ selectionMode: 'single' });
|
|
208
|
-
const item = target.querySelector('[role="option"]');
|
|
209
|
-
// First click selects
|
|
210
|
-
item.click();
|
|
211
|
-
flushSync();
|
|
212
|
-
expect(item.getAttribute('aria-selected')).toBe('true');
|
|
213
|
-
// Second click on same item deselects (single mode clears if already selected)
|
|
214
|
-
item.click();
|
|
215
|
-
flushSync();
|
|
216
|
-
expect(target.querySelectorAll('[aria-selected="true"]').length).toBe(0);
|
|
217
|
-
});
|
|
218
|
-
// ── Arrow key navigation ─────────────────────────────────────────────────
|
|
219
|
-
it('arrowDown dispatched on container does not throw', () => {
|
|
220
|
-
setup({ selectionMode: 'multiple' });
|
|
221
|
-
const container = target.querySelector('[role="listbox"]');
|
|
222
|
-
container.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
|
223
|
-
flushSync();
|
|
224
|
-
expect(target.querySelector('[role="listbox"]')).toBeTruthy();
|
|
225
|
-
});
|
|
226
|
-
it('selectionMode data attribute reflects current mode', () => {
|
|
227
|
-
setup({ selectionMode: 'single' });
|
|
228
|
-
const listbox = target.querySelector('[role="listbox"]');
|
|
229
|
-
expect(listbox.getAttribute('data-selection-mode')).toBe('single');
|
|
230
|
-
});
|
|
231
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import * as HoverCard from '../index.js'
|
|
3
|
-
|
|
4
|
-
let {
|
|
5
|
-
open = $bindable(false),
|
|
6
|
-
onOpenChange = undefined,
|
|
7
|
-
openDelay = 0,
|
|
8
|
-
closeDelay = 0,
|
|
9
|
-
portalTo = undefined,
|
|
10
|
-
disablePortal = undefined,
|
|
11
|
-
contentClass = undefined,
|
|
12
|
-
}: {
|
|
13
|
-
open?: boolean
|
|
14
|
-
onOpenChange?: (open: boolean) => void
|
|
15
|
-
openDelay?: number
|
|
16
|
-
closeDelay?: number
|
|
17
|
-
portalTo?: HTMLElement | string
|
|
18
|
-
disablePortal?: boolean
|
|
19
|
-
contentClass?: string
|
|
20
|
-
} = $props()
|
|
21
|
-
</script>
|
|
22
|
-
|
|
23
|
-
<HoverCard.Root bind:open {onOpenChange} {openDelay} {closeDelay}>
|
|
24
|
-
<HoverCard.Trigger>
|
|
25
|
-
<button type="button" data-testid="trigger">hover me</button>
|
|
26
|
-
</HoverCard.Trigger>
|
|
27
|
-
<HoverCard.Content {portalTo} {disablePortal} class={contentClass}>
|
|
28
|
-
<p data-testid="content">card body</p>
|
|
29
|
-
</HoverCard.Content>
|
|
30
|
-
</HoverCard.Root>
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import { flushSync, mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import HoverCardTest from './HoverCardTest.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
|
-
describe('hoverCard', () => {
|
|
14
|
-
let target;
|
|
15
|
-
let instance;
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
vi.useFakeTimers();
|
|
18
|
-
});
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
vi.useRealTimers();
|
|
21
|
-
if (instance)
|
|
22
|
-
unmount(instance);
|
|
23
|
-
instance = undefined;
|
|
24
|
-
target?.remove();
|
|
25
|
-
});
|
|
26
|
-
it('renders trigger and stays closed by default', () => {
|
|
27
|
-
target = document.createElement('div');
|
|
28
|
-
document.body.appendChild(target);
|
|
29
|
-
instance = mount(HoverCardTest, { target });
|
|
30
|
-
flushSync();
|
|
31
|
-
expect(target.querySelector('[data-testid="trigger"]')).toBeTruthy();
|
|
32
|
-
expect(document.body.querySelector('[data-testid="content"]')).toBeNull();
|
|
33
|
-
});
|
|
34
|
-
it('renders content when open=true', () => {
|
|
35
|
-
target = document.createElement('div');
|
|
36
|
-
document.body.appendChild(target);
|
|
37
|
-
instance = mount(HoverCardTest, { target, props: { open: true, portalTo: target } });
|
|
38
|
-
flushSync();
|
|
39
|
-
expect(target.querySelector('[data-testid="content"]')).toBeTruthy();
|
|
40
|
-
});
|
|
41
|
-
it('portals content to document.body by default when open', () => {
|
|
42
|
-
target = document.createElement('div');
|
|
43
|
-
document.body.appendChild(target);
|
|
44
|
-
instance = mount(HoverCardTest, { target, props: { open: true } });
|
|
45
|
-
flushSync();
|
|
46
|
-
expect(document.body.querySelector('[data-testid="content"]')).toBeTruthy();
|
|
47
|
-
expect(target.querySelector('[data-testid="content"]')).toBeNull();
|
|
48
|
-
});
|
|
49
|
-
it('opens after pointerenter when openDelay=0', async () => {
|
|
50
|
-
target = document.createElement('div');
|
|
51
|
-
document.body.appendChild(target);
|
|
52
|
-
instance = mount(HoverCardTest, { target, props: { openDelay: 0 } });
|
|
53
|
-
flushSync();
|
|
54
|
-
const triggerWrapper = target.querySelector('.sp-hover-card-trigger');
|
|
55
|
-
expect(triggerWrapper).toBeTruthy();
|
|
56
|
-
triggerWrapper?.dispatchEvent(new PointerEvent('pointerenter', { bubbles: true }));
|
|
57
|
-
await vi.advanceTimersByTimeAsync(10);
|
|
58
|
-
flushSync();
|
|
59
|
-
expect(document.body.querySelector('[data-testid="content"]')).toBeTruthy();
|
|
60
|
-
});
|
|
61
|
-
it('closes after pointerleave when closeDelay=0', async () => {
|
|
62
|
-
target = document.createElement('div');
|
|
63
|
-
document.body.appendChild(target);
|
|
64
|
-
instance = mount(HoverCardTest, { target, props: { open: true, closeDelay: 0 } });
|
|
65
|
-
flushSync();
|
|
66
|
-
const triggerWrapper = target.querySelector('.sp-hover-card-trigger');
|
|
67
|
-
triggerWrapper?.dispatchEvent(new PointerEvent('pointerleave', { bubbles: true }));
|
|
68
|
-
await vi.advanceTimersByTimeAsync(10);
|
|
69
|
-
flushSync();
|
|
70
|
-
expect(document.body.querySelector('[data-testid="content"]')).toBeNull();
|
|
71
|
-
});
|
|
72
|
-
it('respects openDelay timer before opening', async () => {
|
|
73
|
-
target = document.createElement('div');
|
|
74
|
-
document.body.appendChild(target);
|
|
75
|
-
instance = mount(HoverCardTest, { target, props: { openDelay: 50 } });
|
|
76
|
-
flushSync();
|
|
77
|
-
const triggerWrapper = target.querySelector('.sp-hover-card-trigger');
|
|
78
|
-
triggerWrapper?.dispatchEvent(new PointerEvent('pointerenter', { bubbles: true }));
|
|
79
|
-
await vi.advanceTimersByTimeAsync(10);
|
|
80
|
-
flushSync();
|
|
81
|
-
expect(document.body.querySelector('[data-testid="content"]')).toBeNull();
|
|
82
|
-
await vi.advanceTimersByTimeAsync(60);
|
|
83
|
-
flushSync();
|
|
84
|
-
expect(document.body.querySelector('[data-testid="content"]')).toBeTruthy();
|
|
85
|
-
});
|
|
86
|
-
it('cancels pending open if pointerleave before openDelay elapses', async () => {
|
|
87
|
-
target = document.createElement('div');
|
|
88
|
-
document.body.appendChild(target);
|
|
89
|
-
instance = mount(HoverCardTest, { target, props: { openDelay: 100 } });
|
|
90
|
-
flushSync();
|
|
91
|
-
const triggerWrapper = target.querySelector('.sp-hover-card-trigger');
|
|
92
|
-
triggerWrapper?.dispatchEvent(new PointerEvent('pointerenter', { bubbles: true }));
|
|
93
|
-
triggerWrapper?.dispatchEvent(new PointerEvent('pointerleave', { bubbles: true }));
|
|
94
|
-
await vi.advanceTimersByTimeAsync(150);
|
|
95
|
-
flushSync();
|
|
96
|
-
expect(document.body.querySelector('[data-testid="content"]')).toBeNull();
|
|
97
|
-
});
|
|
98
|
-
it('opens on focus (keyboard accessibility)', async () => {
|
|
99
|
-
target = document.createElement('div');
|
|
100
|
-
document.body.appendChild(target);
|
|
101
|
-
instance = mount(HoverCardTest, { target, props: { openDelay: 0 } });
|
|
102
|
-
flushSync();
|
|
103
|
-
const triggerWrapper = target.querySelector('.sp-hover-card-trigger');
|
|
104
|
-
triggerWrapper?.dispatchEvent(new FocusEvent('focus', { bubbles: true }));
|
|
105
|
-
await vi.advanceTimersByTimeAsync(10);
|
|
106
|
-
flushSync();
|
|
107
|
-
expect(document.body.querySelector('[data-testid="content"]')).toBeTruthy();
|
|
108
|
-
});
|
|
109
|
-
it('closes on blur', async () => {
|
|
110
|
-
target = document.createElement('div');
|
|
111
|
-
document.body.appendChild(target);
|
|
112
|
-
instance = mount(HoverCardTest, { target, props: { open: true, closeDelay: 0 } });
|
|
113
|
-
flushSync();
|
|
114
|
-
const triggerWrapper = target.querySelector('.sp-hover-card-trigger');
|
|
115
|
-
triggerWrapper?.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
|
|
116
|
-
await vi.advanceTimersByTimeAsync(10);
|
|
117
|
-
flushSync();
|
|
118
|
-
expect(document.body.querySelector('[data-testid="content"]')).toBeNull();
|
|
119
|
-
});
|
|
120
|
-
it('cancels close timer when pointer enters content (grace area)', async () => {
|
|
121
|
-
target = document.createElement('div');
|
|
122
|
-
document.body.appendChild(target);
|
|
123
|
-
instance = mount(HoverCardTest, { target, props: { open: true, closeDelay: 50 } });
|
|
124
|
-
flushSync();
|
|
125
|
-
const triggerWrapper = target.querySelector('.sp-hover-card-trigger');
|
|
126
|
-
triggerWrapper?.dispatchEvent(new PointerEvent('pointerleave', { bubbles: true }));
|
|
127
|
-
const contentEl = document.body.querySelector('.sp-hover-card-content');
|
|
128
|
-
contentEl?.dispatchEvent(new PointerEvent('pointerenter', { bubbles: true }));
|
|
129
|
-
await vi.advanceTimersByTimeAsync(80);
|
|
130
|
-
flushSync();
|
|
131
|
-
// Card should still be open
|
|
132
|
-
expect(document.body.querySelector('[data-testid="content"]')).toBeTruthy();
|
|
133
|
-
});
|
|
134
|
-
it('closes on Escape keydown', async () => {
|
|
135
|
-
target = document.createElement('div');
|
|
136
|
-
document.body.appendChild(target);
|
|
137
|
-
instance = mount(HoverCardTest, { target, props: { open: true, closeDelay: 0 } });
|
|
138
|
-
flushSync();
|
|
139
|
-
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
140
|
-
await vi.advanceTimersByTimeAsync(10);
|
|
141
|
-
flushSync();
|
|
142
|
-
expect(document.body.querySelector('[data-testid="content"]')).toBeNull();
|
|
143
|
-
});
|
|
144
|
-
it('portalTo override renders content in host instead of body', () => {
|
|
145
|
-
target = document.createElement('div');
|
|
146
|
-
document.body.appendChild(target);
|
|
147
|
-
instance = mount(HoverCardTest, {
|
|
148
|
-
target,
|
|
149
|
-
props: { open: true, portalTo: target },
|
|
150
|
-
});
|
|
151
|
-
flushSync();
|
|
152
|
-
expect(target.querySelector('[data-testid="content"]')).toBeTruthy();
|
|
153
|
-
});
|
|
154
|
-
describe('portal opt-out (disablePortal)', () => {
|
|
155
|
-
it('portals to document.body by default', () => {
|
|
156
|
-
const localTarget = document.createElement('div');
|
|
157
|
-
document.body.appendChild(localTarget);
|
|
158
|
-
const localInstance = mount(HoverCardTest, {
|
|
159
|
-
target: localTarget,
|
|
160
|
-
props: { open: true },
|
|
161
|
-
});
|
|
162
|
-
flushSync();
|
|
163
|
-
try {
|
|
164
|
-
expect(localTarget.querySelector('.sp-hover-card-content')).toBeNull();
|
|
165
|
-
expect(document.body.querySelector('.sp-hover-card-content')).toBeTruthy();
|
|
166
|
-
}
|
|
167
|
-
finally {
|
|
168
|
-
unmount(localInstance);
|
|
169
|
-
localTarget.remove();
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
it('renders inline when disablePortal={true}', () => {
|
|
173
|
-
const localTarget = document.createElement('div');
|
|
174
|
-
document.body.appendChild(localTarget);
|
|
175
|
-
const localInstance = mount(HoverCardTest, {
|
|
176
|
-
target: localTarget,
|
|
177
|
-
props: { open: true, disablePortal: true },
|
|
178
|
-
});
|
|
179
|
-
flushSync();
|
|
180
|
-
try {
|
|
181
|
-
expect(localTarget.querySelector('.sp-hover-card-content')).toBeTruthy();
|
|
182
|
-
}
|
|
183
|
-
finally {
|
|
184
|
-
unmount(localInstance);
|
|
185
|
-
localTarget.remove();
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
it('preserves consumer class on the content element when inline', () => {
|
|
189
|
-
const localTarget = document.createElement('div');
|
|
190
|
-
document.body.appendChild(localTarget);
|
|
191
|
-
const localInstance = mount(HoverCardTest, {
|
|
192
|
-
target: localTarget,
|
|
193
|
-
props: { open: true, disablePortal: true, contentClass: 'my-consumer-class' },
|
|
194
|
-
});
|
|
195
|
-
flushSync();
|
|
196
|
-
try {
|
|
197
|
-
const el = localTarget.querySelector('.sp-hover-card-content.my-consumer-class');
|
|
198
|
-
expect(el).toBeTruthy();
|
|
199
|
-
}
|
|
200
|
-
finally {
|
|
201
|
-
unmount(localInstance);
|
|
202
|
-
localTarget.remove();
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
it('calls onOpenChange with new value on each open transition', () => {
|
|
207
|
-
const calls = [];
|
|
208
|
-
const localTarget = document.createElement('div');
|
|
209
|
-
document.body.appendChild(localTarget);
|
|
210
|
-
vi.useFakeTimers();
|
|
211
|
-
const localInstance = mount(HoverCardTest, {
|
|
212
|
-
target: localTarget,
|
|
213
|
-
props: {
|
|
214
|
-
open: false,
|
|
215
|
-
onOpenChange: (open) => calls.push(open),
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
flushSync();
|
|
219
|
-
try {
|
|
220
|
-
const trigger = localTarget.querySelector('.sp-hover-card-trigger');
|
|
221
|
-
trigger.dispatchEvent(new PointerEvent('pointerenter', { bubbles: true }));
|
|
222
|
-
vi.advanceTimersByTime(800);
|
|
223
|
-
flushSync();
|
|
224
|
-
expect(calls).toEqual([true]);
|
|
225
|
-
trigger.dispatchEvent(new PointerEvent('pointerleave', { bubbles: true }));
|
|
226
|
-
vi.advanceTimersByTime(400);
|
|
227
|
-
flushSync();
|
|
228
|
-
expect(calls).toEqual([true, false]);
|
|
229
|
-
}
|
|
230
|
-
finally {
|
|
231
|
-
unmount(localInstance);
|
|
232
|
-
localTarget.remove();
|
|
233
|
-
vi.useRealTimers();
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import Indicator from '../indicator.svelte';
|
|
4
|
-
describe('indicator', () => {
|
|
5
|
-
let target;
|
|
6
|
-
let instance;
|
|
7
|
-
function setup(props = {}) {
|
|
8
|
-
target = document.createElement('div');
|
|
9
|
-
document.body.appendChild(target);
|
|
10
|
-
instance = mount(Indicator, { 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('has aria-live', () => {
|
|
23
|
-
setup();
|
|
24
|
-
expect(target.querySelector('[aria-live="polite"]')).toBeTruthy();
|
|
25
|
-
});
|
|
26
|
-
it('applies status class', () => {
|
|
27
|
-
setup({ status: 'success' });
|
|
28
|
-
expect(target.querySelector('.sp-indicator')?.className.includes('success')).toBeTruthy();
|
|
29
|
-
});
|
|
30
|
-
});
|