@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,221 +0,0 @@
1
- import { flushSync, mount, unmount } from 'svelte';
2
- import { afterEach, describe, expect, it } from 'vitest';
3
- import SelectRoot from '../components/select-root.svelte';
4
- import SelectLabelPersistenceTest from './SelectLabelPersistenceTest.svelte';
5
- import SelectTest from './SelectTest.svelte';
6
- describe('select', () => {
7
- let target;
8
- let instance;
9
- function setup(props = {}) {
10
- target = document.createElement('div');
11
- document.body.appendChild(target);
12
- instance = mount(SelectTest, { target, props: { portalTo: target, ...props } });
13
- flushSync();
14
- return target;
15
- }
16
- function setupRoot(props = {}) {
17
- target = document.createElement('div');
18
- document.body.appendChild(target);
19
- instance = mount(SelectRoot, { target, props });
20
- return target;
21
- }
22
- afterEach(() => {
23
- if (instance)
24
- unmount(instance);
25
- target?.remove();
26
- });
27
- // --- Existing tests ---
28
- it('renders hidden input when name is provided', () => {
29
- setupRoot({ name: 'country', value: 'fr' });
30
- const input = target.querySelector('input[type="hidden"][name="country"]');
31
- expect(input).toBeTruthy();
32
- expect(input?.value).toBe('fr');
33
- });
34
- it('renders one hidden input per selected value in multiple mode', () => {
35
- setupRoot({ type: 'multiple', name: 'tags', value: ['a', 'b'] });
36
- const inputs = target.querySelectorAll('input[type="hidden"][name="tags"]');
37
- expect(inputs).toHaveLength(2);
38
- });
39
- it('keeps selected item label casing when content unmounts', async () => {
40
- target = document.createElement('div');
41
- document.body.appendChild(target);
42
- instance = mount(SelectLabelPersistenceTest, { target, props: { portalTo: target } });
43
- const trigger = target.querySelector('[data-testid="trigger"]');
44
- trigger?.click();
45
- await Promise.resolve();
46
- const blueItem = target.querySelector('[data-value="blue"]');
47
- expect(blueItem).toBeTruthy();
48
- blueItem?.click();
49
- await Promise.resolve();
50
- const displayValue = target.querySelector('.sp-select-value');
51
- expect(displayValue?.textContent?.trim()).toBe('Blue');
52
- const toggleContent = target.querySelector('[data-testid="toggle-content"]');
53
- toggleContent?.click();
54
- await Promise.resolve();
55
- const displayValueAfterUnmount = target.querySelector('.sp-select-value');
56
- expect(displayValueAfterUnmount?.textContent?.trim()).toBe('Blue');
57
- });
58
- // --- Trigger ARIA ---
59
- it('trigger has aria-haspopup="listbox"', () => {
60
- setup();
61
- const trigger = target.querySelector('.sp-select-trigger');
62
- expect(trigger?.getAttribute('aria-haspopup')).toBe('listbox');
63
- });
64
- it('trigger has aria-expanded="false" when closed', () => {
65
- setup();
66
- const trigger = target.querySelector('.sp-select-trigger');
67
- expect(trigger?.getAttribute('aria-expanded')).toBe('false');
68
- });
69
- it('trigger has aria-expanded="true" when open', () => {
70
- setup({ open: true });
71
- const trigger = target.querySelector('.sp-select-trigger');
72
- expect(trigger?.getAttribute('aria-expanded')).toBe('true');
73
- });
74
- // --- Content ARIA ---
75
- it('content has role="listbox" when open', () => {
76
- setup({ open: true });
77
- const content = target.querySelector('[role="listbox"]');
78
- expect(content).toBeTruthy();
79
- });
80
- it('content not rendered when closed', () => {
81
- setup({ open: false });
82
- expect(target.querySelector('[role="listbox"]')).toBeNull();
83
- });
84
- // --- Select items ---
85
- it('items have role="option"', () => {
86
- setup({ open: true });
87
- flushSync();
88
- const items = target.querySelectorAll('[role="option"]');
89
- expect(items.length).toBe(20);
90
- });
91
- // --- Keyboard: Page Up/Down ---
92
- it('pageDown jumps 10 items forward', () => {
93
- setup({ open: true });
94
- flushSync();
95
- const content = target.querySelector('[role="listbox"]');
96
- const items = content.querySelectorAll('[data-roving-focus-item]');
97
- // Focus first item
98
- items[0].focus();
99
- expect(document.activeElement).toBe(items[0]);
100
- // Press PageDown
101
- content.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageDown', bubbles: true }));
102
- flushSync();
103
- // Should jump to item at index 10
104
- expect(document.activeElement).toBe(items[10]);
105
- });
106
- it('pageUp jumps 10 items backward', () => {
107
- setup({ open: true });
108
- flushSync();
109
- const content = target.querySelector('[role="listbox"]');
110
- const items = content.querySelectorAll('[data-roving-focus-item]');
111
- // Focus item at index 15
112
- items[15].focus();
113
- expect(document.activeElement).toBe(items[15]);
114
- // Press PageUp
115
- content.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageUp', bubbles: true }));
116
- flushSync();
117
- // Should jump to item at index 5
118
- expect(document.activeElement).toBe(items[5]);
119
- });
120
- it('pageDown clamps to last item', () => {
121
- setup({ open: true });
122
- flushSync();
123
- const content = target.querySelector('[role="listbox"]');
124
- const items = content.querySelectorAll('[data-roving-focus-item]');
125
- // Focus item near the end (index 15)
126
- items[15].focus();
127
- // Press PageDown — should clamp to last item (index 19)
128
- content.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageDown', bubbles: true }));
129
- flushSync();
130
- expect(document.activeElement).toBe(items[19]);
131
- });
132
- it('pageUp clamps to first item', () => {
133
- setup({ open: true });
134
- flushSync();
135
- const content = target.querySelector('[role="listbox"]');
136
- const items = content.querySelectorAll('[data-roving-focus-item]');
137
- // Focus item near the start (index 3)
138
- items[3].focus();
139
- // Press PageUp — should clamp to first item (index 0)
140
- content.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageUp', bubbles: true }));
141
- flushSync();
142
- expect(document.activeElement).toBe(items[0]);
143
- });
144
- // --- Escape key ---
145
- it('escape closes select', () => {
146
- setup({ open: true });
147
- flushSync();
148
- expect(target.querySelector('[role="listbox"]')).toBeTruthy();
149
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
150
- flushSync();
151
- expect(target.querySelector('[role="listbox"]')).toBeNull();
152
- });
153
- // --- onOpenChange callback ---
154
- it('calls onOpenChange with new value on each open transition', () => {
155
- const calls = [];
156
- // Use setup so afterEach handles cleanup and instance is properly tracked.
157
- setup({
158
- open: false,
159
- onOpenChange: (v) => calls.push(v),
160
- });
161
- const trigger = target.querySelector('.sp-select-trigger');
162
- trigger.click();
163
- flushSync();
164
- expect(calls).toEqual([true]);
165
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
166
- flushSync();
167
- expect(calls).toEqual([true, false]);
168
- });
169
- describe('portal opt-out (disablePortal)', () => {
170
- it('portals to document.body by default', () => {
171
- const localTarget = document.createElement('div');
172
- document.body.appendChild(localTarget);
173
- const localInstance = mount(SelectTest, {
174
- target: localTarget,
175
- props: { open: true },
176
- });
177
- flushSync();
178
- try {
179
- expect(localTarget.querySelector('.sp-select-content')).toBeNull();
180
- expect(document.body.querySelector('.sp-select-content')).toBeTruthy();
181
- }
182
- finally {
183
- unmount(localInstance);
184
- localTarget.remove();
185
- }
186
- });
187
- it('renders inline when disablePortal={true}', () => {
188
- const localTarget = document.createElement('div');
189
- document.body.appendChild(localTarget);
190
- const localInstance = mount(SelectTest, {
191
- target: localTarget,
192
- props: { open: true, disablePortal: true },
193
- });
194
- flushSync();
195
- try {
196
- expect(localTarget.querySelector('.sp-select-content')).toBeTruthy();
197
- }
198
- finally {
199
- unmount(localInstance);
200
- localTarget.remove();
201
- }
202
- });
203
- it('preserves consumer class on the content element when inline', () => {
204
- const localTarget = document.createElement('div');
205
- document.body.appendChild(localTarget);
206
- const localInstance = mount(SelectTest, {
207
- target: localTarget,
208
- props: { open: true, disablePortal: true, contentClass: 'my-consumer-class' },
209
- });
210
- flushSync();
211
- try {
212
- const el = localTarget.querySelector('.sp-select-content.my-consumer-class');
213
- expect(el).toBeTruthy();
214
- }
215
- finally {
216
- unmount(localInstance);
217
- localTarget.remove();
218
- }
219
- });
220
- });
221
- });
@@ -1,39 +0,0 @@
1
- import { mount, unmount } from 'svelte';
2
- import { afterEach, describe, expect, it } from 'vitest';
3
- import Separator from '../separator.svelte';
4
- describe('separator', () => {
5
- let target;
6
- let instance;
7
- function setup(props = {}) {
8
- target = document.createElement('div');
9
- document.body.appendChild(target);
10
- instance = mount(Separator, { target, props });
11
- return target;
12
- }
13
- afterEach(() => {
14
- if (instance)
15
- unmount(instance);
16
- target?.remove();
17
- });
18
- it('renders with role="separator" by default', () => {
19
- setup();
20
- const sep = target.querySelector('[role="separator"]');
21
- expect(sep).toBeTruthy();
22
- });
23
- it('has aria-orientation="horizontal" by default', () => {
24
- setup();
25
- const sep = target.querySelector('[role="separator"]');
26
- expect(sep?.getAttribute('aria-orientation')).toBe('horizontal');
27
- });
28
- it('supports vertical orientation', () => {
29
- setup({ orientation: 'vertical' });
30
- const sep = target.querySelector('[role="separator"]');
31
- expect(sep?.getAttribute('aria-orientation')).toBe('vertical');
32
- });
33
- it('renders as decorative (role="none") when decorative=true', () => {
34
- setup({ decorative: true });
35
- const sep = target.querySelector('[role="none"]');
36
- expect(sep).toBeTruthy();
37
- expect(sep?.getAttribute('aria-hidden')).toBe('true');
38
- });
39
- });
@@ -1,34 +0,0 @@
1
- <script lang="ts">
2
- import SheetClose from '../components/sheet-close.svelte'
3
- import SheetContent from '../components/sheet-content.svelte'
4
- import SheetDescription from '../components/sheet-description.svelte'
5
- import SheetOverlay from '../components/sheet-overlay.svelte'
6
- import SheetRoot from '../components/sheet-root.svelte'
7
- import SheetTitle from '../components/sheet-title.svelte'
8
- import SheetTrigger from '../components/sheet-trigger.svelte'
9
-
10
- let {
11
- open = $bindable(false),
12
- trapFocus = true,
13
- preventScroll = true,
14
- portalTo = undefined,
15
- side = 'right' as const,
16
- }: {
17
- open?: boolean
18
- trapFocus?: boolean
19
- preventScroll?: boolean
20
- portalTo?: HTMLElement | string
21
- side?: 'top' | 'right' | 'bottom' | 'left'
22
- } = $props()
23
- </script>
24
-
25
- <SheetRoot bind:open>
26
- <SheetTrigger>Open Sheet</SheetTrigger>
27
- <SheetOverlay />
28
- <SheetContent {trapFocus} {preventScroll} {portalTo} {side}>
29
- <SheetTitle>Test Title</SheetTitle>
30
- <SheetDescription>Test Description</SheetDescription>
31
- <button data-testid="inside-button">Inside</button>
32
- <SheetClose>Close</SheetClose>
33
- </SheetContent>
34
- </SheetRoot>
@@ -1,193 +0,0 @@
1
- import { flushSync, mount, unmount } from 'svelte';
2
- import { afterEach, describe, expect, it } from 'vitest';
3
- import SheetRoot from '../components/sheet-root.svelte';
4
- import SheetTest from './SheetTest.svelte';
5
- describe('sheet', () => {
6
- let target;
7
- let instance;
8
- function setup(props = {}) {
9
- target = document.createElement('div');
10
- document.body.appendChild(target);
11
- // Pass portalTo=target by default so existing tests keep querying `target`
12
- instance = mount(SheetTest, { target, props: { portalTo: target, ...props } });
13
- flushSync();
14
- return target;
15
- }
16
- function setupRoot(props = {}) {
17
- target = document.createElement('div');
18
- document.body.appendChild(target);
19
- instance = mount(SheetRoot, { target, props });
20
- return target;
21
- }
22
- afterEach(() => {
23
- if (instance)
24
- unmount(instance);
25
- target?.remove();
26
- });
27
- // --- Basic rendering ---
28
- it('renders root', () => {
29
- setupRoot();
30
- expect(target.querySelector('.sp-sheet-root')).toBeTruthy();
31
- });
32
- it('data-state is closed by default', () => {
33
- setupRoot();
34
- expect(target.querySelector('.sp-sheet-root')?.getAttribute('data-state')).toBe('closed');
35
- });
36
- // --- Trigger ARIA ---
37
- it('trigger has aria-haspopup="dialog"', () => {
38
- setup();
39
- const trigger = target.querySelector('.sp-sheet-trigger');
40
- expect(trigger?.getAttribute('aria-haspopup')).toBe('dialog');
41
- });
42
- it('trigger has aria-expanded=false when closed', () => {
43
- setup();
44
- const trigger = target.querySelector('.sp-sheet-trigger');
45
- expect(trigger?.getAttribute('aria-expanded')).toBe('false');
46
- });
47
- it('trigger has aria-controls pointing to content id', () => {
48
- setup({ open: true });
49
- const trigger = target.querySelector('.sp-sheet-trigger');
50
- const content = target.querySelector('[role="dialog"]');
51
- expect(trigger?.getAttribute('aria-controls')).toBe(content?.id);
52
- });
53
- it('trigger toggles open state on click', () => {
54
- setup();
55
- const trigger = target.querySelector('.sp-sheet-trigger');
56
- trigger.click();
57
- flushSync();
58
- expect(target.querySelector('.sp-sheet-root')?.getAttribute('data-state')).toBe('open');
59
- });
60
- // --- Content ARIA ---
61
- it('content has role="dialog"', () => {
62
- setup({ open: true });
63
- expect(target.querySelector('[role="dialog"]')).toBeTruthy();
64
- });
65
- it('content has aria-modal="true"', () => {
66
- setup({ open: true });
67
- const content = target.querySelector('[role="dialog"]');
68
- expect(content?.getAttribute('aria-modal')).toBe('true');
69
- });
70
- it('content has aria-labelledby pointing to title', () => {
71
- setup({ open: true });
72
- const content = target.querySelector('[role="dialog"]');
73
- const title = target.querySelector('.sp-sheet-title');
74
- expect(content?.getAttribute('aria-labelledby')).toBe(title?.id);
75
- });
76
- it('content has aria-describedby pointing to description', () => {
77
- setup({ open: true });
78
- const content = target.querySelector('[role="dialog"]');
79
- const desc = target.querySelector('.sp-sheet-description');
80
- expect(content?.getAttribute('aria-describedby')).toBe(desc?.id);
81
- });
82
- it('content has data-state="open" when open', () => {
83
- setup({ open: true });
84
- const content = target.querySelector('[role="dialog"]');
85
- expect(content?.getAttribute('data-state')).toBe('open');
86
- });
87
- // --- Content visibility ---
88
- it('content is not rendered when closed', () => {
89
- setup({ open: false });
90
- expect(target.querySelector('[role="dialog"]')).toBeNull();
91
- });
92
- it('content is rendered when open', () => {
93
- setup({ open: true });
94
- expect(target.querySelector('[role="dialog"]')).toBeTruthy();
95
- });
96
- // --- Overlay ---
97
- it('overlay has aria-hidden="true"', () => {
98
- setup({ open: true });
99
- const overlay = target.querySelector('.sp-sheet-overlay');
100
- expect(overlay?.getAttribute('aria-hidden')).toBe('true');
101
- });
102
- // --- Close button ---
103
- it('close button closes the sheet', () => {
104
- setup({ open: true });
105
- const closeBtn = target.querySelector('.sp-sheet-close');
106
- closeBtn.click();
107
- flushSync();
108
- expect(target.querySelector('[role="dialog"]')).toBeNull();
109
- });
110
- // --- Escape key ---
111
- it('escape key closes the sheet', () => {
112
- setup({ open: true });
113
- // Flush to ensure $effect that mounts EscapeLayerState has run
114
- flushSync();
115
- expect(target.querySelector('[role="dialog"]')).toBeTruthy();
116
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
117
- flushSync();
118
- expect(target.querySelector('[role="dialog"]')).toBeNull();
119
- });
120
- // --- Title ---
121
- it('title renders as h2 by default', () => {
122
- setup({ open: true });
123
- const title = target.querySelector('.sp-sheet-title');
124
- expect(title?.tagName).toBe('H2');
125
- });
126
- // --- Description ---
127
- it('description renders as <p>', () => {
128
- setup({ open: true });
129
- const desc = target.querySelector('.sp-sheet-description');
130
- expect(desc?.tagName).toBe('P');
131
- });
132
- // --- Trigger aria-expanded updates ---
133
- it('trigger aria-expanded becomes true when sheet opens', () => {
134
- setup();
135
- const trigger = target.querySelector('.sp-sheet-trigger');
136
- expect(trigger.getAttribute('aria-expanded')).toBe('false');
137
- trigger.click();
138
- flushSync();
139
- expect(trigger.getAttribute('aria-expanded')).toBe('true');
140
- });
141
- // --- Portal ---
142
- it('portals content to document.body by default when open', () => {
143
- const t = document.createElement('div');
144
- t.id = 'sheet-host';
145
- document.body.appendChild(t);
146
- const inst = mount(SheetTest, { target: t, props: { open: true } });
147
- flushSync();
148
- try {
149
- expect(t.querySelector('[role="dialog"]')).toBeNull();
150
- expect(document.body.querySelector('[role="dialog"]')).toBeTruthy();
151
- }
152
- finally {
153
- unmount(inst);
154
- t.remove();
155
- }
156
- });
157
- it('renders content inline when portalTo is explicitly set to the host', () => {
158
- const t = document.createElement('div');
159
- document.body.appendChild(t);
160
- const inst = mount(SheetTest, {
161
- target: t,
162
- props: { open: true, portalTo: t },
163
- });
164
- flushSync();
165
- try {
166
- expect(t.querySelector('[role="dialog"]')).toBeTruthy();
167
- }
168
- finally {
169
- unmount(inst);
170
- t.remove();
171
- }
172
- });
173
- // --- Side prop ---
174
- it('sets data-side attribute matching prop', () => {
175
- const t = document.createElement('div');
176
- document.body.appendChild(t);
177
- const inst = mount(SheetTest, { target: t, props: { open: true, portalTo: t, side: 'left' } });
178
- flushSync();
179
- try {
180
- const content = t.querySelector('[role="dialog"]');
181
- expect(content?.getAttribute('data-side')).toBe('left');
182
- }
183
- finally {
184
- unmount(inst);
185
- t.remove();
186
- }
187
- });
188
- it('defaults data-side to "right"', () => {
189
- setup({ open: true });
190
- const content = target.querySelector('[role="dialog"]');
191
- expect(content?.getAttribute('data-side')).toBe('right');
192
- });
193
- });
@@ -1,138 +0,0 @@
1
- import { flushSync, mount, unmount } from 'svelte';
2
- import { afterEach, beforeAll, describe, expect, it } from 'vitest';
3
- import SideSheet from '../side-sheet.svelte';
4
- // jsdom does not implement PointerEvent — polyfill it so tests can dispatch
5
- // pointerdown events (the dismissible layer listens to these).
6
- beforeAll(() => {
7
- if (typeof globalThis.PointerEvent === 'undefined') {
8
- ;
9
- globalThis.PointerEvent = class PointerEvent extends MouseEvent {
10
- pointerId;
11
- pointerType;
12
- constructor(type, init = {}) {
13
- super(type, init);
14
- this.pointerId = init.pointerId ?? 0;
15
- this.pointerType =
16
- init.pointerType ?? 'mouse';
17
- }
18
- };
19
- }
20
- });
21
- describe('side-sheet', () => {
22
- let target;
23
- let instance = null;
24
- function setup(props = {}) {
25
- target = document.createElement('div');
26
- document.body.appendChild(target);
27
- instance = mount(SideSheet, { target, props: { open: true, ...props } });
28
- flushSync();
29
- return target;
30
- }
31
- afterEach(() => {
32
- if (instance) {
33
- unmount(instance);
34
- instance = null;
35
- }
36
- target?.remove();
37
- // Clear body styles set by ScrollLockState between tests
38
- document.body.style.overflow = '';
39
- document.body.style.paddingRight = '';
40
- });
41
- // --- Rendering (baseline) ---
42
- it('does not render when open is false', () => {
43
- setup({ open: false });
44
- expect(target.querySelector('.sp-side-sheet')).toBeNull();
45
- });
46
- it('renders when open is true', () => {
47
- setup({ open: true });
48
- expect(target.querySelector('.sp-side-sheet')).toBeTruthy();
49
- });
50
- it('has role="dialog"', () => {
51
- setup({ open: true });
52
- expect(target.querySelector('[role="dialog"]')).toBeTruthy();
53
- });
54
- it('applies placement="end" by default', () => {
55
- setup({ open: true });
56
- expect(target.querySelector('.sp-side-sheet.end')).toBeTruthy();
57
- });
58
- it('applies placement="start" when requested', () => {
59
- setup({ open: true, placement: 'start' });
60
- expect(target.querySelector('.sp-side-sheet.start')).toBeTruthy();
61
- });
62
- // --- Mode standard (no modal behavior) ---
63
- it('standard mode does not render overlay', () => {
64
- setup({ open: true, mode: 'standard' });
65
- expect(target.querySelector('.sp-side-sheet-overlay')).toBeNull();
66
- });
67
- it('standard mode does not set aria-modal', () => {
68
- setup({ open: true, mode: 'standard' });
69
- const sheet = target.querySelector('[role="dialog"]');
70
- expect(sheet?.hasAttribute('aria-modal')).toBe(false);
71
- });
72
- it('standard mode does not lock body scroll', () => {
73
- setup({ open: true, mode: 'standard' });
74
- expect(document.body.style.overflow).not.toBe('hidden');
75
- });
76
- // --- Mode modal (baseline behavior) ---
77
- it('modal mode renders overlay', () => {
78
- setup({ open: true, mode: 'modal' });
79
- expect(target.querySelector('.sp-side-sheet-overlay')).toBeTruthy();
80
- });
81
- it('modal mode sets aria-modal="true"', () => {
82
- setup({ open: true, mode: 'modal' });
83
- const sheet = target.querySelector('[role="dialog"]');
84
- expect(sheet?.getAttribute('aria-modal')).toBe('true');
85
- });
86
- it('modal mode locks body scroll', () => {
87
- setup({ open: true, mode: 'modal' });
88
- expect(document.body.style.overflow).toBe('hidden');
89
- });
90
- it('overlay click closes in modal mode', () => {
91
- setup({ open: true, mode: 'modal' });
92
- const overlay = target.querySelector('.sp-side-sheet-overlay');
93
- overlay.click();
94
- flushSync();
95
- expect(target.querySelector('.sp-side-sheet')).toBeNull();
96
- });
97
- it('escape key closes in modal mode', () => {
98
- setup({ open: true, mode: 'modal' });
99
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
100
- flushSync();
101
- expect(target.querySelector('.sp-side-sheet')).toBeNull();
102
- });
103
- // --- A11y modal (target behavior — will RED before fix, GREEN after) ---
104
- it('modal mode: content has tabindex=-1 for focus entry', () => {
105
- setup({ open: true, mode: 'modal' });
106
- const sheet = target.querySelector('[role="dialog"]');
107
- // FocusScopeState will add tabindex=-1 when mounted as focus scope root
108
- // so the container can receive focus if no tabbable child exists.
109
- expect(sheet.getAttribute('tabindex')).toBe('-1');
110
- });
111
- it('modal mode: pointerdown outside content closes the sheet (dismissible layer)', () => {
112
- setup({ open: true, mode: 'modal' });
113
- // Click on body outside the sheet (not the overlay — the overlay is inside target)
114
- const outsideEl = document.createElement('button');
115
- document.body.appendChild(outsideEl);
116
- outsideEl.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
117
- flushSync();
118
- expect(target.querySelector('.sp-side-sheet')).toBeNull();
119
- outsideEl.remove();
120
- });
121
- it('calls onOpenChange(false) when Escape is pressed in modal mode', () => {
122
- const calls = [];
123
- target = document.createElement('div');
124
- document.body.appendChild(target);
125
- instance = mount(SideSheet, {
126
- target,
127
- props: {
128
- open: true,
129
- mode: 'modal',
130
- onOpenChange: (open) => calls.push(open),
131
- },
132
- });
133
- flushSync();
134
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
135
- flushSync();
136
- expect(calls).toEqual([false]);
137
- });
138
- });
@@ -1,45 +0,0 @@
1
- <script lang="ts">
2
- import * as Sidebar from '../index.js'
3
-
4
- let {
5
- open = $bindable(true),
6
- collapsible = 'icon' as 'offcanvas' | 'icon' | 'none',
7
- }: {
8
- open?: boolean
9
- collapsible?: 'offcanvas' | 'icon' | 'none'
10
- } = $props()
11
- </script>
12
-
13
- <!-- Note: Sidebar.Root does not forward extra attributes (no rest props in SidebarRootProps),
14
- so data-testid is NOT forwarded to the <aside>. We identify the root via .sp-sidebar class.
15
- All other components that accept children/slots can use data-testid on their inner elements. -->
16
- <Sidebar.Provider bind:open>
17
- <Sidebar.Root {collapsible}>
18
- <Sidebar.Header>
19
- <h2 data-testid="sidebar-header">App</h2>
20
- </Sidebar.Header>
21
- <Sidebar.Content>
22
- <Sidebar.Group>
23
- <Sidebar.GroupLabel>Main</Sidebar.GroupLabel>
24
- <Sidebar.GroupContent>
25
- <Sidebar.Menu>
26
- <Sidebar.MenuItem>
27
- <Sidebar.MenuButton data-testid="menu-home">Home</Sidebar.MenuButton>
28
- </Sidebar.MenuItem>
29
- <Sidebar.MenuItem>
30
- <Sidebar.MenuButton data-testid="menu-settings">Settings</Sidebar.MenuButton>
31
- </Sidebar.MenuItem>
32
- </Sidebar.Menu>
33
- </Sidebar.GroupContent>
34
- </Sidebar.Group>
35
- <Sidebar.Separator />
36
- </Sidebar.Content>
37
- <Sidebar.Footer>
38
- <span data-testid="sidebar-footer">Footer</span>
39
- </Sidebar.Footer>
40
- </Sidebar.Root>
41
- <Sidebar.Trigger data-testid="sidebar-trigger">Toggle</Sidebar.Trigger>
42
- <Sidebar.Inset>
43
- <main data-testid="inset-content">Page content</main>
44
- </Sidebar.Inset>
45
- </Sidebar.Provider>
@@ -1,9 +0,0 @@
1
- <script lang="ts">
2
- import SidebarProvider from '../components/sidebar-provider.svelte'
3
-
4
- let { open = $bindable(true) } = $props()
5
- </script>
6
-
7
- <SidebarProvider bind:open>
8
- <div data-testid="child">content</div>
9
- </SidebarProvider>