@spectral_labs/ui 1.7.0 → 1.8.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/THIRD_PARTY_NOTICES.md +201 -0
  3. package/package.json +8 -2
  4. package/dist/components/__tests__/overlay-scrim-source.test.js +0 -134
  5. package/dist/components/accordion/__tests__/AccordionTest.svelte +0 -31
  6. package/dist/components/accordion/__tests__/accordion.test.svelte.js +0 -139
  7. package/dist/components/alert-dialog/__tests__/AlertDialogTest.svelte +0 -28
  8. package/dist/components/alert-dialog/__tests__/alert-dialog.a11y.test.js +0 -42
  9. package/dist/components/alert-dialog/__tests__/alert-dialog.integration.test.svelte.js +0 -51
  10. package/dist/components/alert-dialog/__tests__/alert-dialog.test.svelte.js +0 -113
  11. package/dist/components/app-bar/__tests__/app-bar.test.svelte.js +0 -30
  12. package/dist/components/aspect-ratio/__tests__/aspect-ratio.test.svelte.js +0 -22
  13. package/dist/components/avatar/__tests__/avatar.test.svelte.js +0 -26
  14. package/dist/components/badge/__tests__/badge.test.svelte.js +0 -30
  15. package/dist/components/bottom-sheet/__tests__/BottomSheetTest.svelte +0 -22
  16. package/dist/components/bottom-sheet/__tests__/bottom-sheet.test.svelte.js +0 -83
  17. package/dist/components/button/__tests__/button.test.svelte.js +0 -63
  18. package/dist/components/calendar/__tests__/CalendarTest.svelte +0 -56
  19. package/dist/components/calendar/__tests__/calendar.test.svelte.js +0 -190
  20. package/dist/components/card/__tests__/card.test.svelte.js +0 -30
  21. package/dist/components/checkbox/__tests__/checkbox.test.svelte.js +0 -44
  22. package/dist/components/chips/__tests__/chips.test.svelte.js +0 -26
  23. package/dist/components/collapsible/__tests__/CollapsibleTest.svelte +0 -18
  24. package/dist/components/collapsible/__tests__/collapsible.test.svelte.js +0 -125
  25. package/dist/components/combobox/__tests__/ComboboxTest.svelte +0 -40
  26. package/dist/components/combobox/__tests__/combobox.test.svelte.js +0 -255
  27. package/dist/components/command/__tests__/CommandInlineDirectTest.svelte +0 -29
  28. package/dist/components/command/__tests__/CommandInlineTest.svelte +0 -89
  29. package/dist/components/command/__tests__/CommandTest.svelte +0 -77
  30. package/dist/components/command/__tests__/command-inline.test.svelte.js +0 -392
  31. package/dist/components/command/__tests__/command-item-passthrough.test.svelte.js +0 -284
  32. package/dist/components/command/__tests__/command.a11y.test.js +0 -30
  33. package/dist/components/command/__tests__/command.integration.test.svelte.js +0 -68
  34. package/dist/components/command/__tests__/command.test.svelte.js +0 -230
  35. package/dist/components/context-menu/__tests__/ContextMenuNonInteractiveTest.svelte +0 -32
  36. package/dist/components/context-menu/__tests__/ContextMenuTest.svelte +0 -25
  37. package/dist/components/context-menu/__tests__/context-menu.test.svelte.js +0 -141
  38. package/dist/components/date-field/__tests__/DateFieldTest.svelte +0 -20
  39. package/dist/components/date-field/__tests__/date-field.test.svelte.js +0 -139
  40. package/dist/components/date-picker/__tests__/DatePickerTest.svelte +0 -20
  41. package/dist/components/date-picker/__tests__/date-picker.test.svelte.js +0 -190
  42. package/dist/components/date-range-field/__tests__/DateRangeFieldTest.svelte +0 -20
  43. package/dist/components/date-range-field/__tests__/date-range-field.test.svelte.js +0 -130
  44. package/dist/components/date-range-picker/__tests__/DateRangePickerTest.svelte +0 -18
  45. package/dist/components/date-range-picker/__tests__/date-range-picker.test.svelte.js +0 -156
  46. package/dist/components/dialog/__tests__/DialogTest.svelte +0 -34
  47. package/dist/components/dialog/__tests__/dialog.test.svelte.js +0 -184
  48. package/dist/components/drop-zone/__tests__/drop-zone.test.svelte.js +0 -26
  49. package/dist/components/dropdown-menu/__tests__/DropdownMenuRadioTest.svelte +0 -90
  50. package/dist/components/dropdown-menu/__tests__/DropdownMenuTest.svelte +0 -71
  51. package/dist/components/dropdown-menu/__tests__/dropdown-menu-radio-group.test.svelte.js +0 -306
  52. package/dist/components/dropdown-menu/__tests__/dropdown-menu.test.svelte.js +0 -567
  53. package/dist/components/empty-state/__tests__/empty-state.test.svelte.js +0 -26
  54. package/dist/components/fab/__tests__/fab.test.svelte.js +0 -30
  55. package/dist/components/field/__tests__/FieldTest.svelte +0 -26
  56. package/dist/components/field/__tests__/field.test.svelte.js +0 -74
  57. package/dist/components/grid-list/__tests__/GridListTest.svelte +0 -46
  58. package/dist/components/grid-list/__tests__/grid-list.test.svelte.js +0 -231
  59. package/dist/components/hover-card/__tests__/HoverCardTest.svelte +0 -30
  60. package/dist/components/hover-card/__tests__/hover-card.test.svelte.js +0 -236
  61. package/dist/components/indicator/__tests__/indicator.test.svelte.js +0 -30
  62. package/dist/components/input/__tests__/input.test.svelte.js +0 -67
  63. package/dist/components/label/__tests__/label.test.svelte.js +0 -22
  64. package/dist/components/language-select/__tests__/language-select.test.svelte.js +0 -199
  65. package/dist/components/language-select/__tests__/languages.test.js +0 -87
  66. package/dist/components/link-preview/__tests__/LinkPreviewTest.svelte +0 -37
  67. package/dist/components/link-preview/__tests__/link-preview.a11y.test.js +0 -28
  68. package/dist/components/link-preview/__tests__/link-preview.integration.test.svelte.js +0 -54
  69. package/dist/components/link-preview/__tests__/link-preview.test.svelte.js +0 -215
  70. package/dist/components/list/__tests__/list.test.svelte.js +0 -22
  71. package/dist/components/menubar/__tests__/MenubarTest.svelte +0 -52
  72. package/dist/components/menubar/__tests__/menubar.a11y.test.js +0 -28
  73. package/dist/components/menubar/__tests__/menubar.integration.test.svelte.js +0 -57
  74. package/dist/components/menubar/__tests__/menubar.test.svelte.js +0 -328
  75. package/dist/components/meter/__tests__/meter.test.svelte.js +0 -26
  76. package/dist/components/nav-bar/__tests__/nav-bar.test.svelte.js +0 -23
  77. package/dist/components/nav-drawer/__tests__/nav-drawer.test.svelte.js +0 -28
  78. package/dist/components/nav-rail/__tests__/nav-rail.test.svelte.js +0 -28
  79. package/dist/components/pagination/__tests__/pagination.test.svelte.js +0 -26
  80. package/dist/components/pin-input/__tests__/pin-input.test.svelte.js +0 -71
  81. package/dist/components/popover/__tests__/PopoverTest.svelte +0 -27
  82. package/dist/components/popover/__tests__/popover.test.svelte.js +0 -196
  83. package/dist/components/progress/__tests__/progress.test.svelte.js +0 -32
  84. package/dist/components/radio-group/__tests__/radio-group.test.svelte.js +0 -22
  85. package/dist/components/range-calendar/__tests__/RangeCalendarTest.svelte +0 -23
  86. package/dist/components/range-calendar/__tests__/range-calendar.test.svelte.js +0 -105
  87. package/dist/components/rating-group/__tests__/rating-group.test.svelte.js +0 -26
  88. package/dist/components/scroll-area/__tests__/scroll-area.test.svelte.js +0 -34
  89. package/dist/components/search-field/__tests__/SearchFieldTest.svelte +0 -34
  90. package/dist/components/search-field/__tests__/search-field.test.svelte.js +0 -118
  91. package/dist/components/select/__tests__/SelectLabelPersistenceTest.svelte +0 -31
  92. package/dist/components/select/__tests__/SelectTest.svelte +0 -33
  93. package/dist/components/select/__tests__/select.test.svelte.js +0 -221
  94. package/dist/components/separator/__tests__/separator.test.svelte.js +0 -39
  95. package/dist/components/sheet/__tests__/SheetTest.svelte +0 -34
  96. package/dist/components/sheet/__tests__/sheet.test.svelte.js +0 -193
  97. package/dist/components/side-sheet/__tests__/side-sheet.test.svelte.js +0 -138
  98. package/dist/components/sidebar/__tests__/SidebarFullTest.svelte +0 -45
  99. package/dist/components/sidebar/__tests__/SidebarTest.svelte +0 -9
  100. package/dist/components/sidebar/__tests__/sidebar.test.svelte.js +0 -186
  101. package/dist/components/simple-dialog/__tests__/SimpleDialogTest.svelte +0 -40
  102. package/dist/components/simple-dialog/__tests__/simple-dialog.test.svelte.js +0 -50
  103. package/dist/components/simple-tooltip/__tests__/SimpleTooltipTest.svelte +0 -19
  104. package/dist/components/simple-tooltip/__tests__/simple-tooltip.test.svelte.js +0 -61
  105. package/dist/components/slider/__tests__/slider.test.svelte.js +0 -42
  106. package/dist/components/sortable-list/__tests__/NestedSortableListTest.svelte +0 -49
  107. package/dist/components/sortable-list/__tests__/SortableListTest.svelte +0 -42
  108. package/dist/components/sortable-list/__tests__/sortable-list.nested.test.svelte.js +0 -112
  109. package/dist/components/sortable-list/__tests__/sortable-list.test.svelte.js +0 -175
  110. package/dist/components/splitter/__tests__/splitter.test.svelte.js +0 -26
  111. package/dist/components/switch/__tests__/switch.test.svelte.js +0 -44
  112. package/dist/components/table/__tests__/table.test.svelte.js +0 -22
  113. package/dist/components/tabs/__tests__/TabsTest.svelte +0 -32
  114. package/dist/components/tabs/__tests__/tabs.test.svelte.js +0 -227
  115. package/dist/components/textarea/__tests__/textarea.test.svelte.js +0 -55
  116. package/dist/components/time-field/__tests__/TimeFieldTest.svelte +0 -20
  117. package/dist/components/time-field/__tests__/time-field.test.svelte.js +0 -100
  118. package/dist/components/time-range-field/__tests__/TimeRangeFieldTest.svelte +0 -20
  119. package/dist/components/time-range-field/__tests__/time-range-field.test.svelte.js +0 -124
  120. package/dist/components/toast/__tests__/toast.test.svelte.js +0 -46
  121. package/dist/components/toggle/__tests__/toggle.test.svelte.js +0 -38
  122. package/dist/components/toggle-group/__tests__/toggle-group.test.svelte.js +0 -22
  123. package/dist/components/toolbar/__tests__/ToolbarCompositionTest.svelte +0 -18
  124. package/dist/components/toolbar/__tests__/ToolbarTabstopTest.svelte +0 -10
  125. package/dist/components/toolbar/__tests__/toolbar.test.svelte.js +0 -63
  126. package/dist/components/tooltip/__tests__/TooltipTest.svelte +0 -22
  127. package/dist/components/tooltip/__tests__/tooltip.test.svelte.js +0 -225
  128. package/dist/components/tree/__tests__/TreeLabelContentTest.svelte +0 -21
  129. package/dist/components/tree/__tests__/TreeTest.svelte +0 -24
  130. package/dist/components/tree/__tests__/tree.test.svelte.js +0 -229
  131. package/dist/components/virtualizer/__tests__/waterfall-layout.test.js +0 -82
  132. package/dist/internal/announcer/__tests__/announcer.test.js +0 -42
  133. package/dist/internal/dismissible-layer/__tests__/dismissible-layer.test.js +0 -196
  134. package/dist/internal/drag-drop/__tests__/auto-scroll.test.js +0 -104
  135. package/dist/internal/drag-drop/__tests__/drag-session.test.svelte.js +0 -803
  136. package/dist/internal/drag-drop/__tests__/drag-source.test.svelte.js +0 -130
  137. package/dist/internal/drag-drop/__tests__/drop-delegate.test.js +0 -223
  138. package/dist/internal/drag-drop/__tests__/types.test.js +0 -20
  139. package/dist/internal/escape-layer/__tests__/escape-layer.test.js +0 -113
  140. package/dist/internal/focus-scope/__tests__/focus-scope.test.js +0 -225
  141. package/dist/internal/grace-area/__tests__/grace-area.test.js +0 -67
  142. package/dist/internal/helpers/__tests__/helpers.test.js +0 -123
  143. package/dist/internal/portal/__tests__/PortalTest.svelte +0 -21
  144. package/dist/internal/portal/__tests__/portal.test.js +0 -29
  145. package/dist/internal/portal/__tests__/portal.test.svelte.js +0 -57
  146. package/dist/internal/scroll-lock/__tests__/scroll-lock.test.js +0 -160
  147. package/dist/internal/typeahead/__tests__/typeahead.test.js +0 -61
  148. package/src/themes/__tests__/carbon-dark-contrast.test.ts +0 -170
  149. package/src/themes/__tests__/generated-css-drift.test.ts +0 -53
  150. package/src/themes/__tests__/scrim-opaque.test.ts +0 -52
@@ -1,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
- });