@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,225 +0,0 @@
1
- import { flushSync, mount, unmount } from 'svelte';
2
- import { afterEach, describe, expect, it, vi } from 'vitest';
3
- import { resetDismissibleLayerStack } from '../../../internal/dismissible-layer/index.js';
4
- import TooltipRoot from '../components/tooltip-root.svelte';
5
- import { createTooltipProviderState } from '../tooltip-provider.svelte.js';
6
- import TooltipTest from './TooltipTest.svelte';
7
- // jsdom does not define PointerEvent — polyfill it for tests
8
- if (typeof globalThis.PointerEvent === 'undefined') {
9
- // @ts-expect-error polyfill
10
- globalThis.PointerEvent = class PointerEvent extends MouseEvent {
11
- constructor(type, init) {
12
- super(type, init);
13
- }
14
- };
15
- }
16
- function pointerDownOutside(x = 500, y = 500) {
17
- document.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: x, clientY: y }));
18
- }
19
- describe('tooltip', () => {
20
- let target;
21
- let instance;
22
- function setup(props = {}) {
23
- target = document.createElement('div');
24
- document.body.appendChild(target);
25
- instance = mount(TooltipTest, { target, props: { portalTo: target, ...props } });
26
- flushSync();
27
- return target;
28
- }
29
- function setupRoot(props = {}) {
30
- target = document.createElement('div');
31
- document.body.appendChild(target);
32
- instance = mount(TooltipRoot, { target, props });
33
- return target;
34
- }
35
- afterEach(() => {
36
- if (instance)
37
- unmount(instance);
38
- target?.remove();
39
- });
40
- // --- Basic rendering ---
41
- it('renders root', () => {
42
- setupRoot();
43
- expect(target.querySelector('.sp-tooltip-root')).toBeTruthy();
44
- });
45
- it('renders trigger with role="presentation"', () => {
46
- setup();
47
- const trigger = target.querySelector('.sp-tooltip-trigger');
48
- expect(trigger?.getAttribute('role')).toBe('presentation');
49
- });
50
- it('root has data-state="closed" by default', () => {
51
- setup();
52
- const root = target.querySelector('.sp-tooltip-root');
53
- expect(root?.getAttribute('data-state')).toBe('closed');
54
- });
55
- // --- ARIA attributes ---
56
- it('trigger has no aria-describedby when tooltip is closed', () => {
57
- setup();
58
- const trigger = target.querySelector('.sp-tooltip-trigger');
59
- expect(trigger?.getAttribute('aria-describedby')).toBeNull();
60
- });
61
- it('content has role="tooltip" when visible', () => {
62
- vi.useFakeTimers();
63
- setup({ openDelay: 0, closeDelay: 0 });
64
- const trigger = target.querySelector('.sp-tooltip-trigger');
65
- trigger.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
66
- vi.advanceTimersByTime(10);
67
- flushSync();
68
- const content = target.querySelector('[role="tooltip"]');
69
- expect(content).toBeTruthy();
70
- vi.useRealTimers();
71
- });
72
- it('content has data-side attribute', () => {
73
- vi.useFakeTimers();
74
- setup({ openDelay: 0 });
75
- const trigger = target.querySelector('.sp-tooltip-trigger');
76
- trigger.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
77
- vi.advanceTimersByTime(710);
78
- flushSync();
79
- const content = target.querySelector('[role="tooltip"]');
80
- expect(content?.getAttribute('data-side')).toBe('top');
81
- vi.useRealTimers();
82
- });
83
- // --- Open/close behavior ---
84
- it('opens on mouseenter and closes on mouseleave', () => {
85
- vi.useFakeTimers();
86
- setup({ openDelay: 0, closeDelay: 0 });
87
- const trigger = target.querySelector('.sp-tooltip-trigger');
88
- trigger.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
89
- vi.advanceTimersByTime(10);
90
- flushSync();
91
- expect(target.querySelector('[role="tooltip"]')).toBeTruthy();
92
- // Mouseleave triggers grace area timeout (500ms) then close delay (0ms)
93
- trigger.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true, clientX: -100, clientY: -100 }));
94
- vi.advanceTimersByTime(510);
95
- flushSync();
96
- expect(target.querySelector('[role="tooltip"]')).toBeNull();
97
- vi.useRealTimers();
98
- });
99
- it('opens on focus and closes on blur', () => {
100
- vi.useFakeTimers();
101
- setup({ openDelay: 0, closeDelay: 0 });
102
- const trigger = target.querySelector('.sp-tooltip-trigger');
103
- trigger.dispatchEvent(new FocusEvent('focus', { bubbles: true }));
104
- vi.advanceTimersByTime(10);
105
- flushSync();
106
- expect(target.querySelector('[role="tooltip"]')).toBeTruthy();
107
- trigger.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
108
- vi.advanceTimersByTime(10);
109
- flushSync();
110
- expect(target.querySelector('[role="tooltip"]')).toBeNull();
111
- vi.useRealTimers();
112
- });
113
- it('closes on Escape key when open', () => {
114
- vi.useFakeTimers();
115
- // Use the shared target/instance so afterEach handles cleanup correctly
116
- setup();
117
- const trigger = target.querySelector('.sp-tooltip-trigger');
118
- // Open the tooltip via mouseenter
119
- trigger.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
120
- vi.advanceTimersByTime(10);
121
- flushSync();
122
- expect(target.querySelector('[role="tooltip"]')).toBeTruthy();
123
- // Dispatch Escape key — should close the tooltip
124
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
125
- vi.advanceTimersByTime(10);
126
- flushSync();
127
- expect(target.querySelector('[role="tooltip"]')).toBeNull();
128
- vi.useRealTimers();
129
- });
130
- it('closes on outside pointerdown when closeOnOutsideClick is true', () => {
131
- vi.useFakeTimers();
132
- const t = document.createElement('div');
133
- document.body.appendChild(t);
134
- const inst = mount(TooltipTest, {
135
- target: t,
136
- props: { openDelay: 0, closeDelay: 0, closeOnOutsideClick: true, portalTo: t },
137
- });
138
- flushSync();
139
- const trigger = t.querySelector('.sp-tooltip-trigger');
140
- trigger.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
141
- vi.advanceTimersByTime(10);
142
- flushSync();
143
- try {
144
- expect(t.querySelector('[role="tooltip"]')).toBeTruthy();
145
- pointerDownOutside();
146
- vi.advanceTimersByTime(10); // close timer (closeDelay: 0) fires
147
- flushSync();
148
- expect(t.querySelector('[role="tooltip"]')).toBeNull();
149
- }
150
- finally {
151
- unmount(inst);
152
- resetDismissibleLayerStack();
153
- t.remove();
154
- vi.useRealTimers();
155
- }
156
- });
157
- it('does NOT close on outside pointerdown when closeOnOutsideClick is false (default)', () => {
158
- vi.useFakeTimers();
159
- const t = document.createElement('div');
160
- document.body.appendChild(t);
161
- const inst = mount(TooltipTest, {
162
- target: t,
163
- props: { openDelay: 0, closeDelay: 0, portalTo: t },
164
- });
165
- flushSync();
166
- const trigger = t.querySelector('.sp-tooltip-trigger');
167
- trigger.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
168
- vi.advanceTimersByTime(10);
169
- flushSync();
170
- try {
171
- expect(t.querySelector('[role="tooltip"]')).toBeTruthy();
172
- pointerDownOutside();
173
- vi.advanceTimersByTime(10);
174
- flushSync();
175
- expect(t.querySelector('[role="tooltip"]')).toBeTruthy();
176
- }
177
- finally {
178
- unmount(inst);
179
- resetDismissibleLayerStack();
180
- t.remove();
181
- vi.useRealTimers();
182
- }
183
- });
184
- });
185
- describe('tooltip provider state', () => {
186
- it('isOpenDelayed is true initially', () => {
187
- const state = createTooltipProviderState();
188
- expect(state.isOpenDelayed).toBe(true);
189
- });
190
- it('isOpenDelayed becomes false after onOpen', () => {
191
- const state = createTooltipProviderState();
192
- state.onOpen();
193
- expect(state.isOpenDelayed).toBe(false);
194
- });
195
- it('isOpenDelayed resets to true after skipDelayDuration', () => {
196
- vi.useFakeTimers();
197
- const state = createTooltipProviderState({ skipDelayDuration: 100 });
198
- state.onOpen();
199
- expect(state.isOpenDelayed).toBe(false);
200
- state.onClose();
201
- expect(state.isOpenDelayed).toBe(false); // still false during skip window
202
- vi.advanceTimersByTime(100);
203
- expect(state.isOpenDelayed).toBe(true); // reset after skip duration
204
- vi.useRealTimers();
205
- });
206
- it('opening within skipDelayDuration keeps isOpenDelayed false', () => {
207
- vi.useFakeTimers();
208
- const state = createTooltipProviderState({ skipDelayDuration: 300 });
209
- state.onOpen();
210
- state.onClose();
211
- // Within skip window, open again
212
- vi.advanceTimersByTime(100);
213
- expect(state.isOpenDelayed).toBe(false);
214
- state.onOpen(); // opening cancels the reset timer
215
- vi.advanceTimersByTime(300);
216
- expect(state.isOpenDelayed).toBe(false); // still false because onOpen cleared the timer
217
- vi.useRealTimers();
218
- });
219
- it('skipDelayDuration: 0 resets immediately', () => {
220
- const state = createTooltipProviderState({ skipDelayDuration: 0 });
221
- state.onOpen();
222
- state.onClose();
223
- expect(state.isOpenDelayed).toBe(true); // immediate reset
224
- });
225
- });
@@ -1,21 +0,0 @@
1
- <script lang="ts">
2
- import * as Tree from '../index.js'
3
- </script>
4
-
5
- <!--
6
- Test fixture for the `labelContent` snippet on Tree.Item. Renders a
7
- custom HTML structure (icon stand-in + text span) in place of the
8
- built-in plain-text label, so the test can assert that the snippet
9
- output reaches the DOM exactly where the `label` string used to.
10
- -->
11
- <Tree.Root selectionMode="single" aria-label="Label-content tree">
12
- <Tree.Item id="lib" hasChildren textValue="My Library">
13
- {#snippet labelContent()}
14
- <svg data-testid="lib-icon" aria-hidden="true" width="16" height="16" viewBox="0 0 24 24">
15
- <path d="M3 21h18M5 21V7l7-4 7 4v14" />
16
- </svg>
17
- <span data-testid="lib-label">My Library</span>
18
- {/snippet}
19
- <Tree.Item id="col-1" label="Design" />
20
- </Tree.Item>
21
- </Tree.Root>
@@ -1,24 +0,0 @@
1
- <script lang="ts">
2
- import * as Tree from '../index.js'
3
-
4
- let {
5
- selectionMode = 'multiple' as 'none' | 'single' | 'multiple',
6
- selectedKeys = $bindable<Set<string>>(new Set<string>()),
7
- expandedKeys = $bindable<Set<string>>(new Set<string>()),
8
- disabled = false,
9
- }: {
10
- selectionMode?: 'none' | 'single' | 'multiple'
11
- selectedKeys?: Set<string>
12
- expandedKeys?: Set<string>
13
- disabled?: boolean
14
- } = $props()
15
- </script>
16
-
17
- <Tree.Root bind:selectedKeys bind:expandedKeys {selectionMode} {disabled} aria-label="Test tree">
18
- <Tree.Item id="root-a" label="Folder A" hasChildren={true}>
19
- <Tree.Item id="root-a-1" label="Item A1" />
20
- <Tree.Item id="root-a-2" label="Item A2" />
21
- </Tree.Item>
22
- <Tree.Item id="root-b" label="Item B" />
23
- <Tree.Item id="root-c" label="Item C (disabled)" disabled={true} />
24
- </Tree.Root>
@@ -1,229 +0,0 @@
1
- import { flushSync, mount, unmount } from 'svelte';
2
- import { afterEach, describe, expect, it } from 'vitest';
3
- import TreeLabelContentTest from './TreeLabelContentTest.svelte';
4
- import TreeTest from './TreeTest.svelte';
5
- describe('tree', () => {
6
- let target;
7
- let instance;
8
- function setup(props = {}) {
9
- target = document.createElement('div');
10
- document.body.appendChild(target);
11
- instance = mount(TreeTest, { target, props });
12
- flushSync();
13
- }
14
- afterEach(() => {
15
- if (instance)
16
- unmount(instance);
17
- instance = undefined;
18
- target?.remove();
19
- });
20
- it('renders tree with role="tree"', () => {
21
- setup();
22
- expect(target.querySelector('[role="tree"]')).toBeTruthy();
23
- });
24
- it('renders root items as treeitems', () => {
25
- setup();
26
- // At minimum: root-a, root-b, root-c (children of root-a are hidden by default)
27
- expect(target.querySelectorAll('[role="treeitem"]').length).toBeGreaterThanOrEqual(3);
28
- });
29
- it('parent item with hasChildren has aria-expanded attribute', () => {
30
- setup();
31
- const folder = target.querySelector('[data-tree-item="root-a"]');
32
- expect(folder).toBeTruthy();
33
- expect(folder?.hasAttribute('aria-expanded')).toBe(true);
34
- });
35
- it('parent item starts collapsed (aria-expanded="false")', () => {
36
- setup();
37
- const folder = target.querySelector('[data-tree-item="root-a"]');
38
- expect(folder?.getAttribute('aria-expanded')).toBe('false');
39
- });
40
- it('expanding folder via expandedKeys reveals nested items', () => {
41
- setup({ expandedKeys: new Set(['root-a']) });
42
- // root-a, root-a-1, root-a-2, root-b, root-c = 5
43
- expect(target.querySelectorAll('[role="treeitem"]').length).toBeGreaterThanOrEqual(5);
44
- });
45
- it('expanded folder shows aria-expanded="true"', () => {
46
- setup({ expandedKeys: new Set(['root-a']) });
47
- const folder = target.querySelector('[data-tree-item="root-a"]');
48
- expect(folder?.getAttribute('aria-expanded')).toBe('true');
49
- });
50
- it('collapsing folder hides nested items', () => {
51
- // First mount with expansion, then without
52
- setup({ expandedKeys: new Set(['root-a']) });
53
- const countExpanded = target.querySelectorAll('[role="treeitem"]').length;
54
- unmount(instance);
55
- instance = mount(TreeTest, { target, props: { expandedKeys: new Set() } });
56
- flushSync();
57
- const countCollapsed = target.querySelectorAll('[role="treeitem"]').length;
58
- expect(countCollapsed).toBeLessThan(countExpanded);
59
- });
60
- it('clicking leaf item adds it to selected state (multiple mode)', () => {
61
- setup({ selectionMode: 'multiple' });
62
- const itemB = target.querySelector('[data-tree-item="root-b"]');
63
- itemB?.click();
64
- flushSync();
65
- expect(target.querySelector('[data-selected]')).toBeTruthy();
66
- });
67
- it('selectionMode "none" disables click selection', () => {
68
- setup({ selectionMode: 'none' });
69
- const itemB = target.querySelector('[data-tree-item="root-b"]');
70
- itemB?.click();
71
- flushSync();
72
- expect(target.querySelector('[data-selected]')).toBeNull();
73
- });
74
- it('selectionMode "single" replaces selection on each click', () => {
75
- setup({ selectionMode: 'single' });
76
- const itemB = target.querySelector('[data-tree-item="root-b"]');
77
- const itemC = target.querySelector('[data-tree-item="root-c"]');
78
- itemB?.click();
79
- flushSync();
80
- itemC?.click();
81
- flushSync();
82
- // At most one item selected in single mode
83
- const selected = target.querySelectorAll('[data-selected]');
84
- expect(selected.length).toBeLessThanOrEqual(1);
85
- });
86
- it('disabled tree ignores clicks on items', () => {
87
- setup({ disabled: true });
88
- const itemB = target.querySelector('[data-tree-item="root-b"]');
89
- itemB?.click();
90
- flushSync();
91
- expect(target.querySelector('[data-selected]')).toBeNull();
92
- });
93
- it('disabled item has data-disabled attribute', () => {
94
- setup({ selectionMode: 'multiple' });
95
- const itemC = target.querySelector('[data-tree-item="root-c"]');
96
- expect(itemC?.getAttribute('data-disabled')).toBeTruthy();
97
- });
98
- it('respects bindable selectedKeys initial value', () => {
99
- setup({ selectedKeys: new Set(['root-b']) });
100
- const itemB = target.querySelector('[data-tree-item="root-b"]');
101
- expect(itemB?.hasAttribute('data-selected')).toBe(true);
102
- });
103
- it('tree has aria-label', () => {
104
- setup();
105
- const tree = target.querySelector('[role="tree"]');
106
- expect(tree?.getAttribute('aria-label')).toBe('Test tree');
107
- });
108
- it('multiple-mode tree has aria-multiselectable', () => {
109
- setup({ selectionMode: 'multiple' });
110
- const tree = target.querySelector('[role="tree"]');
111
- expect(tree?.getAttribute('aria-multiselectable')).toBe('true');
112
- });
113
- it('enter key selects focused item', () => {
114
- setup({ selectionMode: 'multiple' });
115
- const itemB = target.querySelector('[data-tree-item="root-b"]');
116
- itemB?.focus();
117
- itemB?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
118
- flushSync();
119
- expect(target.querySelector('[data-selected]')).toBeTruthy();
120
- });
121
- it('space key selects focused item', () => {
122
- setup({ selectionMode: 'multiple' });
123
- const itemB = target.querySelector('[data-tree-item="root-b"]');
124
- itemB?.focus();
125
- itemB?.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }));
126
- flushSync();
127
- expect(target.querySelector('[data-selected]')).toBeTruthy();
128
- });
129
- it('arrowRight key on collapsed folder triggers expand handler', () => {
130
- setup();
131
- const folderA = target.querySelector('[data-tree-item="root-a"]');
132
- folderA?.focus();
133
- folderA?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
134
- flushSync();
135
- expect(folderA).toBeTruthy();
136
- });
137
- it('arrowLeft key on expanded folder triggers collapse handler', () => {
138
- setup({ expandedKeys: new Set(['root-a']) });
139
- const folderA = target.querySelector('[data-tree-item="root-a"]');
140
- folderA?.focus();
141
- folderA?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
142
- flushSync();
143
- expect(folderA).toBeTruthy();
144
- });
145
- it('arrowDown key does not throw (moves focus to next visible item)', () => {
146
- setup();
147
- const items = target.querySelectorAll('[role="treeitem"]');
148
- const first = items[0];
149
- first?.focus();
150
- first?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
151
- flushSync();
152
- expect(items.length).toBeGreaterThan(0);
153
- });
154
- it('arrowUp key does not throw (moves focus to previous item)', () => {
155
- setup();
156
- const items = target.querySelectorAll('[role="treeitem"]');
157
- const second = items[1];
158
- second?.focus();
159
- second?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
160
- flushSync();
161
- expect(items.length).toBeGreaterThan(1);
162
- });
163
- it('home key moves focus to first item', () => {
164
- setup();
165
- const items = target.querySelectorAll('[role="treeitem"]');
166
- const last = items[items.length - 1];
167
- last?.focus();
168
- last?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true }));
169
- flushSync();
170
- expect(items.length).toBeGreaterThan(0);
171
- });
172
- it('end key moves focus to last item', () => {
173
- setup();
174
- const items = target.querySelectorAll('[role="treeitem"]');
175
- const first = items[0];
176
- first?.focus();
177
- first?.dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true }));
178
- flushSync();
179
- expect(items.length).toBeGreaterThan(0);
180
- });
181
- it('keyboard events are ignored when tree is disabled', () => {
182
- setup({ disabled: true, selectionMode: 'multiple' });
183
- const itemB = target.querySelector('[data-tree-item="root-b"]');
184
- itemB?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
185
- flushSync();
186
- expect(target.querySelector('[data-selected]')).toBeNull();
187
- });
188
- it('ctrl+click in multiple mode toggles individual item off', () => {
189
- setup({ selectionMode: 'multiple' });
190
- const itemB = target.querySelector('[data-tree-item="root-b"]');
191
- // First click to select
192
- itemB?.click();
193
- flushSync();
194
- expect(target.querySelector('[data-selected]')).toBeTruthy();
195
- // Ctrl+click to deselect
196
- itemB?.dispatchEvent(new MouseEvent('click', { ctrlKey: true, bubbles: true }));
197
- flushSync();
198
- expect(target.querySelector('[data-tree-item="root-b"][data-selected]')).toBeNull();
199
- });
200
- describe('labelContent snippet', () => {
201
- let snippetTarget;
202
- let snippetInstance;
203
- function setupSnippet() {
204
- snippetTarget = document.createElement('div');
205
- document.body.appendChild(snippetTarget);
206
- snippetInstance = mount(TreeLabelContentTest, { target: snippetTarget });
207
- flushSync();
208
- }
209
- afterEach(() => {
210
- if (snippetInstance)
211
- unmount(snippetInstance);
212
- snippetInstance = undefined;
213
- snippetTarget?.remove();
214
- });
215
- it('renders the labelContent snippet inside .sp-tree-item-content', () => {
216
- setupSnippet();
217
- const item = snippetTarget.querySelector('[data-tree-item="lib"]');
218
- const content = item?.querySelector('.sp-tree-item-content');
219
- expect(content).toBeTruthy();
220
- // The custom snippet's children land inside the content slot —
221
- // both the icon and the named label span are reachable.
222
- expect(content?.querySelector('[data-testid="lib-icon"]')).toBeTruthy();
223
- expect(content?.querySelector('[data-testid="lib-label"]')?.textContent).toBe('My Library');
224
- // The plain-text label fallback path is bypassed when the snippet
225
- // is provided — the content slot has no stray text-only child.
226
- expect(content?.textContent?.trim()).toBe('My Library');
227
- });
228
- });
229
- });
@@ -1,82 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { WaterfallLayout } from '../layouts/waterfall-layout.js';
3
- function makeItems(count, width = 800, height = 600) {
4
- return Array.from({ length: count }, (_, i) => ({
5
- id: `item-${i}`,
6
- width,
7
- height,
8
- }));
9
- }
10
- describe('waterfallLayout', () => {
11
- it('creates without error', () => {
12
- const layout = new WaterfallLayout();
13
- expect(layout).toBeDefined();
14
- });
15
- it('handles empty items', () => {
16
- const layout = new WaterfallLayout();
17
- layout.update([], 500);
18
- expect(layout.getContentSize()).toEqual({ width: 500, height: 0 });
19
- expect(layout.getVisibleLayoutInfos({ x: 0, y: 0, width: 500, height: 500 })).toEqual([]);
20
- });
21
- it('computes layout for uniform items', () => {
22
- const layout = new WaterfallLayout({ minItemSize: 200, gap: 10 });
23
- const items = makeItems(4, 400, 300); // 4:3 aspect ratio
24
- layout.update(items, 420); // fits 2 columns: 2 * 200 + 1 * 10 = 410
25
- const size = layout.getContentSize();
26
- expect(size.width).toBe(420);
27
- expect(size.height).toBeGreaterThan(0);
28
- // All 4 items should have layout info
29
- for (let i = 0; i < 4; i++) {
30
- const info = layout.getLayoutInfo(`item-${i}`);
31
- expect(info).not.toBeNull();
32
- expect(info.key).toBe(`item-${i}`);
33
- expect(info.rect.width).toBeGreaterThan(0);
34
- expect(info.rect.height).toBeGreaterThan(0);
35
- }
36
- });
37
- it('returns only visible items', () => {
38
- const layout = new WaterfallLayout({ minItemSize: 100, gap: 10 });
39
- const items = makeItems(100, 400, 300);
40
- layout.update(items, 500);
41
- // Only get items in the first 300px
42
- const visible = layout.getVisibleLayoutInfos({ x: 0, y: 0, width: 500, height: 300 });
43
- expect(visible.length).toBeLessThan(100);
44
- expect(visible.length).toBeGreaterThan(0);
45
- // All visible items should be within the rect
46
- for (const info of visible) {
47
- const bottom = info.rect.y + info.rect.height;
48
- expect(bottom).toBeGreaterThanOrEqual(0);
49
- expect(info.rect.y).toBeLessThanOrEqual(300);
50
- }
51
- });
52
- it('handles single column', () => {
53
- const layout = new WaterfallLayout({ minItemSize: 400 });
54
- const items = makeItems(3, 400, 300);
55
- layout.update(items, 300); // narrower than minItemSize → 1 column
56
- const visible = layout.getVisibleLayoutInfos({ x: 0, y: 0, width: 300, height: 9999 });
57
- expect(visible.length).toBe(3);
58
- // All items should be in column 0 (same x)
59
- const xs = new Set(visible.map((v) => v.rect.x));
60
- expect(xs.size).toBe(1);
61
- });
62
- it('distributes items to shortest column', () => {
63
- const layout = new WaterfallLayout({ minItemSize: 100, gap: 0 });
64
- // Items with different heights
65
- const items = [
66
- { id: 'tall', width: 100, height: 200 }, // tall
67
- { id: 'short', width: 100, height: 50 }, // short
68
- { id: 'medium', width: 100, height: 100 }, // medium — should go in shortest column
69
- ];
70
- layout.update(items, 200); // 2 columns
71
- const _tall = layout.getLayoutInfo('tall');
72
- const short = layout.getLayoutInfo('short');
73
- const medium = layout.getLayoutInfo('medium');
74
- // medium should be placed in the column with 'short' (shorter column)
75
- expect(medium.rect.x).toBe(short.rect.x);
76
- });
77
- it('getLayoutInfo returns null for unknown key', () => {
78
- const layout = new WaterfallLayout();
79
- layout.update(makeItems(3), 500);
80
- expect(layout.getLayoutInfo('nonexistent')).toBeNull();
81
- });
82
- });
@@ -1,42 +0,0 @@
1
- import { afterEach, describe, expect, it } from 'vitest';
2
- import { AnnouncerState } from '../announcer.svelte.js';
3
- describe('announcerState', () => {
4
- let announcer;
5
- afterEach(() => {
6
- announcer?.unmount();
7
- });
8
- it('creates a live region on mount', () => {
9
- announcer = new AnnouncerState();
10
- announcer.mount();
11
- const region = document.querySelector('[aria-live="polite"]');
12
- expect(region).not.toBeNull();
13
- expect(region?.getAttribute('role')).toBe('status');
14
- });
15
- it('supports assertive politeness', () => {
16
- announcer = new AnnouncerState({ politeness: 'assertive' });
17
- announcer.mount();
18
- const region = document.querySelector('[aria-live="assertive"]');
19
- expect(region).not.toBeNull();
20
- });
21
- it('clears announcement', () => {
22
- announcer = new AnnouncerState();
23
- announcer.mount();
24
- announcer.clear();
25
- const region = document.querySelector('[aria-live="polite"]');
26
- expect(region?.textContent).toBe('');
27
- });
28
- it('removes the live region on unmount', () => {
29
- announcer = new AnnouncerState();
30
- announcer.mount();
31
- announcer.unmount();
32
- const region = document.querySelector('[aria-live="polite"]');
33
- expect(region).toBeNull();
34
- });
35
- it('does not create duplicate regions', () => {
36
- announcer = new AnnouncerState();
37
- announcer.mount();
38
- announcer.mount();
39
- const regions = document.querySelectorAll('[aria-live="polite"]');
40
- expect(regions.length).toBe(1);
41
- });
42
- });