@spectral_labs/ui 1.6.1 → 1.8.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/THIRD_PARTY_NOTICES.md +201 -0
- package/dist/components/command/command.svelte.d.ts +30 -0
- package/dist/components/command/command.svelte.d.ts.map +1 -1
- package/dist/components/command/command.svelte.js +85 -4
- package/dist/components/command/components/command-group.svelte +25 -15
- package/dist/components/command/components/command-group.svelte.d.ts.map +1 -1
- package/dist/components/command/components/command-input.svelte +54 -5
- package/dist/components/command/components/command-input.svelte.d.ts +1 -1
- package/dist/components/command/components/command-input.svelte.d.ts.map +1 -1
- package/dist/components/command/components/command-list.svelte +22 -3
- package/dist/components/command/components/command-list.svelte.d.ts.map +1 -1
- package/dist/components/command/components/command-root.svelte +43 -4
- package/dist/components/command/components/command-root.svelte.d.ts.map +1 -1
- package/dist/components/command/index.d.ts +1 -0
- package/dist/components/command/index.d.ts.map +1 -1
- package/dist/components/command/types.d.ts +59 -4
- package/dist/components/command/types.d.ts.map +1 -1
- package/package.json +8 -2
- package/dist/components/__tests__/overlay-scrim-source.test.js +0 -134
- package/dist/components/accordion/__tests__/AccordionTest.svelte +0 -31
- package/dist/components/accordion/__tests__/accordion.test.svelte.js +0 -139
- package/dist/components/alert-dialog/__tests__/AlertDialogTest.svelte +0 -28
- package/dist/components/alert-dialog/__tests__/alert-dialog.a11y.test.js +0 -42
- package/dist/components/alert-dialog/__tests__/alert-dialog.integration.test.svelte.js +0 -51
- package/dist/components/alert-dialog/__tests__/alert-dialog.test.svelte.js +0 -113
- package/dist/components/app-bar/__tests__/app-bar.test.svelte.js +0 -30
- package/dist/components/aspect-ratio/__tests__/aspect-ratio.test.svelte.js +0 -22
- package/dist/components/avatar/__tests__/avatar.test.svelte.js +0 -26
- package/dist/components/badge/__tests__/badge.test.svelte.js +0 -30
- package/dist/components/bottom-sheet/__tests__/BottomSheetTest.svelte +0 -22
- package/dist/components/bottom-sheet/__tests__/bottom-sheet.test.svelte.js +0 -83
- package/dist/components/button/__tests__/button.test.svelte.js +0 -63
- package/dist/components/calendar/__tests__/CalendarTest.svelte +0 -56
- package/dist/components/calendar/__tests__/calendar.test.svelte.js +0 -190
- package/dist/components/card/__tests__/card.test.svelte.js +0 -30
- package/dist/components/checkbox/__tests__/checkbox.test.svelte.js +0 -44
- package/dist/components/chips/__tests__/chips.test.svelte.js +0 -26
- package/dist/components/collapsible/__tests__/CollapsibleTest.svelte +0 -18
- package/dist/components/collapsible/__tests__/collapsible.test.svelte.js +0 -125
- package/dist/components/combobox/__tests__/ComboboxTest.svelte +0 -40
- package/dist/components/combobox/__tests__/combobox.test.svelte.js +0 -255
- package/dist/components/command/__tests__/CommandTest.svelte +0 -77
- package/dist/components/command/__tests__/command-item-passthrough.test.svelte.js +0 -284
- package/dist/components/command/__tests__/command.a11y.test.js +0 -30
- package/dist/components/command/__tests__/command.integration.test.svelte.js +0 -68
- package/dist/components/command/__tests__/command.test.svelte.js +0 -230
- package/dist/components/context-menu/__tests__/ContextMenuNonInteractiveTest.svelte +0 -32
- package/dist/components/context-menu/__tests__/ContextMenuTest.svelte +0 -25
- package/dist/components/context-menu/__tests__/context-menu.test.svelte.js +0 -141
- package/dist/components/date-field/__tests__/DateFieldTest.svelte +0 -20
- package/dist/components/date-field/__tests__/date-field.test.svelte.js +0 -139
- package/dist/components/date-picker/__tests__/DatePickerTest.svelte +0 -20
- package/dist/components/date-picker/__tests__/date-picker.test.svelte.js +0 -190
- package/dist/components/date-range-field/__tests__/DateRangeFieldTest.svelte +0 -20
- package/dist/components/date-range-field/__tests__/date-range-field.test.svelte.js +0 -130
- package/dist/components/date-range-picker/__tests__/DateRangePickerTest.svelte +0 -18
- package/dist/components/date-range-picker/__tests__/date-range-picker.test.svelte.js +0 -156
- package/dist/components/dialog/__tests__/DialogTest.svelte +0 -34
- package/dist/components/dialog/__tests__/dialog.test.svelte.js +0 -184
- package/dist/components/drop-zone/__tests__/drop-zone.test.svelte.js +0 -26
- package/dist/components/dropdown-menu/__tests__/DropdownMenuRadioTest.svelte +0 -90
- package/dist/components/dropdown-menu/__tests__/DropdownMenuTest.svelte +0 -71
- package/dist/components/dropdown-menu/__tests__/dropdown-menu-radio-group.test.svelte.js +0 -306
- package/dist/components/dropdown-menu/__tests__/dropdown-menu.test.svelte.js +0 -567
- package/dist/components/empty-state/__tests__/empty-state.test.svelte.js +0 -26
- package/dist/components/fab/__tests__/fab.test.svelte.js +0 -30
- package/dist/components/field/__tests__/FieldTest.svelte +0 -26
- package/dist/components/field/__tests__/field.test.svelte.js +0 -74
- package/dist/components/grid-list/__tests__/GridListTest.svelte +0 -46
- package/dist/components/grid-list/__tests__/grid-list.test.svelte.js +0 -231
- package/dist/components/hover-card/__tests__/HoverCardTest.svelte +0 -30
- package/dist/components/hover-card/__tests__/hover-card.test.svelte.js +0 -236
- package/dist/components/indicator/__tests__/indicator.test.svelte.js +0 -30
- package/dist/components/input/__tests__/input.test.svelte.js +0 -67
- package/dist/components/label/__tests__/label.test.svelte.js +0 -22
- package/dist/components/language-select/__tests__/language-select.test.svelte.js +0 -199
- package/dist/components/language-select/__tests__/languages.test.js +0 -87
- package/dist/components/link-preview/__tests__/LinkPreviewTest.svelte +0 -37
- package/dist/components/link-preview/__tests__/link-preview.a11y.test.js +0 -28
- package/dist/components/link-preview/__tests__/link-preview.integration.test.svelte.js +0 -54
- package/dist/components/link-preview/__tests__/link-preview.test.svelte.js +0 -215
- package/dist/components/list/__tests__/list.test.svelte.js +0 -22
- package/dist/components/menubar/__tests__/MenubarTest.svelte +0 -52
- package/dist/components/menubar/__tests__/menubar.a11y.test.js +0 -28
- package/dist/components/menubar/__tests__/menubar.integration.test.svelte.js +0 -57
- package/dist/components/menubar/__tests__/menubar.test.svelte.js +0 -328
- package/dist/components/meter/__tests__/meter.test.svelte.js +0 -26
- package/dist/components/nav-bar/__tests__/nav-bar.test.svelte.js +0 -23
- package/dist/components/nav-drawer/__tests__/nav-drawer.test.svelte.js +0 -28
- package/dist/components/nav-rail/__tests__/nav-rail.test.svelte.js +0 -28
- package/dist/components/pagination/__tests__/pagination.test.svelte.js +0 -26
- package/dist/components/pin-input/__tests__/pin-input.test.svelte.js +0 -71
- package/dist/components/popover/__tests__/PopoverTest.svelte +0 -27
- package/dist/components/popover/__tests__/popover.test.svelte.js +0 -196
- package/dist/components/progress/__tests__/progress.test.svelte.js +0 -32
- package/dist/components/radio-group/__tests__/radio-group.test.svelte.js +0 -22
- package/dist/components/range-calendar/__tests__/RangeCalendarTest.svelte +0 -23
- package/dist/components/range-calendar/__tests__/range-calendar.test.svelte.js +0 -105
- package/dist/components/rating-group/__tests__/rating-group.test.svelte.js +0 -26
- package/dist/components/scroll-area/__tests__/scroll-area.test.svelte.js +0 -34
- package/dist/components/search-field/__tests__/SearchFieldTest.svelte +0 -34
- package/dist/components/search-field/__tests__/search-field.test.svelte.js +0 -118
- package/dist/components/select/__tests__/SelectLabelPersistenceTest.svelte +0 -31
- package/dist/components/select/__tests__/SelectTest.svelte +0 -33
- package/dist/components/select/__tests__/select.test.svelte.js +0 -221
- package/dist/components/separator/__tests__/separator.test.svelte.js +0 -39
- package/dist/components/sheet/__tests__/SheetTest.svelte +0 -34
- package/dist/components/sheet/__tests__/sheet.test.svelte.js +0 -193
- package/dist/components/side-sheet/__tests__/side-sheet.test.svelte.js +0 -138
- package/dist/components/sidebar/__tests__/SidebarFullTest.svelte +0 -45
- package/dist/components/sidebar/__tests__/SidebarTest.svelte +0 -9
- package/dist/components/sidebar/__tests__/sidebar.test.svelte.js +0 -186
- package/dist/components/simple-dialog/__tests__/SimpleDialogTest.svelte +0 -40
- package/dist/components/simple-dialog/__tests__/simple-dialog.test.svelte.js +0 -50
- package/dist/components/simple-tooltip/__tests__/SimpleTooltipTest.svelte +0 -19
- package/dist/components/simple-tooltip/__tests__/simple-tooltip.test.svelte.js +0 -61
- package/dist/components/slider/__tests__/slider.test.svelte.js +0 -42
- package/dist/components/sortable-list/__tests__/NestedSortableListTest.svelte +0 -49
- package/dist/components/sortable-list/__tests__/SortableListTest.svelte +0 -42
- package/dist/components/sortable-list/__tests__/sortable-list.nested.test.svelte.js +0 -112
- package/dist/components/sortable-list/__tests__/sortable-list.test.svelte.js +0 -175
- package/dist/components/splitter/__tests__/splitter.test.svelte.js +0 -26
- package/dist/components/switch/__tests__/switch.test.svelte.js +0 -44
- package/dist/components/table/__tests__/table.test.svelte.js +0 -22
- package/dist/components/tabs/__tests__/TabsTest.svelte +0 -32
- package/dist/components/tabs/__tests__/tabs.test.svelte.js +0 -227
- package/dist/components/textarea/__tests__/textarea.test.svelte.js +0 -55
- package/dist/components/time-field/__tests__/TimeFieldTest.svelte +0 -20
- package/dist/components/time-field/__tests__/time-field.test.svelte.js +0 -100
- package/dist/components/time-range-field/__tests__/TimeRangeFieldTest.svelte +0 -20
- package/dist/components/time-range-field/__tests__/time-range-field.test.svelte.js +0 -124
- package/dist/components/toast/__tests__/toast.test.svelte.js +0 -46
- package/dist/components/toggle/__tests__/toggle.test.svelte.js +0 -38
- package/dist/components/toggle-group/__tests__/toggle-group.test.svelte.js +0 -22
- package/dist/components/toolbar/__tests__/ToolbarCompositionTest.svelte +0 -18
- package/dist/components/toolbar/__tests__/ToolbarTabstopTest.svelte +0 -10
- package/dist/components/toolbar/__tests__/toolbar.test.svelte.js +0 -63
- package/dist/components/tooltip/__tests__/TooltipTest.svelte +0 -22
- package/dist/components/tooltip/__tests__/tooltip.test.svelte.js +0 -225
- package/dist/components/tree/__tests__/TreeLabelContentTest.svelte +0 -21
- package/dist/components/tree/__tests__/TreeTest.svelte +0 -24
- package/dist/components/tree/__tests__/tree.test.svelte.js +0 -229
- package/dist/components/virtualizer/__tests__/waterfall-layout.test.js +0 -82
- package/dist/internal/announcer/__tests__/announcer.test.js +0 -42
- package/dist/internal/dismissible-layer/__tests__/dismissible-layer.test.js +0 -196
- package/dist/internal/drag-drop/__tests__/auto-scroll.test.js +0 -104
- package/dist/internal/drag-drop/__tests__/drag-session.test.svelte.js +0 -803
- package/dist/internal/drag-drop/__tests__/drag-source.test.svelte.js +0 -130
- package/dist/internal/drag-drop/__tests__/drop-delegate.test.js +0 -223
- package/dist/internal/drag-drop/__tests__/types.test.js +0 -20
- package/dist/internal/escape-layer/__tests__/escape-layer.test.js +0 -113
- package/dist/internal/focus-scope/__tests__/focus-scope.test.js +0 -225
- package/dist/internal/grace-area/__tests__/grace-area.test.js +0 -67
- package/dist/internal/helpers/__tests__/helpers.test.js +0 -123
- package/dist/internal/portal/__tests__/PortalTest.svelte +0 -21
- package/dist/internal/portal/__tests__/portal.test.js +0 -29
- package/dist/internal/portal/__tests__/portal.test.svelte.js +0 -57
- package/dist/internal/scroll-lock/__tests__/scroll-lock.test.js +0 -160
- package/dist/internal/typeahead/__tests__/typeahead.test.js +0 -61
- package/src/themes/__tests__/carbon-dark-contrast.test.ts +0 -170
- package/src/themes/__tests__/generated-css-drift.test.ts +0 -53
- package/src/themes/__tests__/scrim-opaque.test.ts +0 -52
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { resetScrollLock, ScrollLockState } from '../scroll-lock.svelte.js';
|
|
3
|
-
describe('scrollLockState', () => {
|
|
4
|
-
beforeEach(() => {
|
|
5
|
-
resetScrollLock();
|
|
6
|
-
document.body.style.overflow = '';
|
|
7
|
-
document.body.style.paddingRight = '';
|
|
8
|
-
});
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
resetScrollLock();
|
|
11
|
-
});
|
|
12
|
-
describe('lock()', () => {
|
|
13
|
-
it('sets body overflow to hidden', () => {
|
|
14
|
-
const lock = new ScrollLockState({ enabled: true });
|
|
15
|
-
lock.lock();
|
|
16
|
-
expect(document.body.style.overflow).toBe('hidden');
|
|
17
|
-
});
|
|
18
|
-
it('compensates scrollbar width in padding-right', () => {
|
|
19
|
-
// In jsdom, window.innerWidth and documentElement.clientWidth are typically
|
|
20
|
-
// both 0, so scrollbarWidth = 0. We mock to simulate a real scrollbar.
|
|
21
|
-
Object.defineProperty(window, 'innerWidth', { value: 1024, configurable: true });
|
|
22
|
-
Object.defineProperty(document.documentElement, 'clientWidth', {
|
|
23
|
-
value: 1007,
|
|
24
|
-
configurable: true,
|
|
25
|
-
});
|
|
26
|
-
const lock = new ScrollLockState({ enabled: true });
|
|
27
|
-
lock.lock();
|
|
28
|
-
expect(document.body.style.paddingRight).toBe('17px');
|
|
29
|
-
// Restore
|
|
30
|
-
Object.defineProperty(window, 'innerWidth', { value: 0, configurable: true });
|
|
31
|
-
Object.defineProperty(document.documentElement, 'clientWidth', {
|
|
32
|
-
value: 0,
|
|
33
|
-
configurable: true,
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
describe('unlock()', () => {
|
|
38
|
-
it('restores body styles after delay', async () => {
|
|
39
|
-
vi.useFakeTimers();
|
|
40
|
-
document.body.style.overflow = 'auto';
|
|
41
|
-
document.body.style.paddingRight = '10px';
|
|
42
|
-
const lock = new ScrollLockState({ enabled: true, restoreDelay: 50 });
|
|
43
|
-
lock.lock();
|
|
44
|
-
expect(document.body.style.overflow).toBe('hidden');
|
|
45
|
-
lock.unlock();
|
|
46
|
-
// Styles should NOT be restored yet (delay pending)
|
|
47
|
-
expect(document.body.style.overflow).toBe('hidden');
|
|
48
|
-
vi.advanceTimersByTime(50);
|
|
49
|
-
// Now styles should be restored
|
|
50
|
-
expect(document.body.style.overflow).toBe('auto');
|
|
51
|
-
expect(document.body.style.paddingRight).toBe('10px');
|
|
52
|
-
vi.useRealTimers();
|
|
53
|
-
});
|
|
54
|
-
it('restores body styles immediately with zero delay', () => {
|
|
55
|
-
document.body.style.overflow = 'visible';
|
|
56
|
-
document.body.style.paddingRight = '5px';
|
|
57
|
-
const lock = new ScrollLockState({ enabled: true, restoreDelay: 0 });
|
|
58
|
-
lock.lock();
|
|
59
|
-
expect(document.body.style.overflow).toBe('hidden');
|
|
60
|
-
lock.unlock();
|
|
61
|
-
expect(document.body.style.overflow).toBe('visible');
|
|
62
|
-
expect(document.body.style.paddingRight).toBe('5px');
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
describe('reference counting', () => {
|
|
66
|
-
it('body only unlocked when all locks released', () => {
|
|
67
|
-
const lock1 = new ScrollLockState({ enabled: true, restoreDelay: 0 });
|
|
68
|
-
const lock2 = new ScrollLockState({ enabled: true, restoreDelay: 0 });
|
|
69
|
-
lock1.lock();
|
|
70
|
-
lock2.lock();
|
|
71
|
-
expect(document.body.style.overflow).toBe('hidden');
|
|
72
|
-
// Release one lock — body should still be locked
|
|
73
|
-
lock1.unlock();
|
|
74
|
-
expect(document.body.style.overflow).toBe('hidden');
|
|
75
|
-
// Release second lock — body should be unlocked
|
|
76
|
-
lock2.unlock();
|
|
77
|
-
expect(document.body.style.overflow).toBe('');
|
|
78
|
-
});
|
|
79
|
-
it('double-locking the same instance does nothing extra', () => {
|
|
80
|
-
const lock = new ScrollLockState({ enabled: true, restoreDelay: 0 });
|
|
81
|
-
lock.lock();
|
|
82
|
-
lock.lock(); // should be no-op
|
|
83
|
-
lock.unlock();
|
|
84
|
-
// If double-locked, this would leave lockCount at 1 and body still hidden
|
|
85
|
-
expect(document.body.style.overflow).toBe('');
|
|
86
|
-
});
|
|
87
|
-
it('double-unlocking the same instance does nothing extra', () => {
|
|
88
|
-
const lock1 = new ScrollLockState({ enabled: true, restoreDelay: 0 });
|
|
89
|
-
const lock2 = new ScrollLockState({ enabled: true, restoreDelay: 0 });
|
|
90
|
-
lock1.lock();
|
|
91
|
-
lock2.lock();
|
|
92
|
-
lock1.unlock();
|
|
93
|
-
lock1.unlock(); // should be no-op
|
|
94
|
-
// lock2 is still active
|
|
95
|
-
expect(document.body.style.overflow).toBe('hidden');
|
|
96
|
-
lock2.unlock();
|
|
97
|
-
expect(document.body.style.overflow).toBe('');
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
describe('destroy()', () => {
|
|
101
|
-
it('calls unlock', () => {
|
|
102
|
-
const lock = new ScrollLockState({ enabled: true, restoreDelay: 0 });
|
|
103
|
-
lock.lock();
|
|
104
|
-
expect(document.body.style.overflow).toBe('hidden');
|
|
105
|
-
lock.destroy();
|
|
106
|
-
expect(document.body.style.overflow).toBe('');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
describe('disabled lock', () => {
|
|
110
|
-
it('does nothing when enabled is false', () => {
|
|
111
|
-
document.body.style.overflow = 'auto';
|
|
112
|
-
const lock = new ScrollLockState({ enabled: false });
|
|
113
|
-
lock.lock();
|
|
114
|
-
expect(document.body.style.overflow).toBe('auto');
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
describe('resetScrollLock()', () => {
|
|
118
|
-
it('resets all internal state and body styles', () => {
|
|
119
|
-
const lock = new ScrollLockState({ enabled: true, restoreDelay: 0 });
|
|
120
|
-
lock.lock();
|
|
121
|
-
expect(document.body.style.overflow).toBe('hidden');
|
|
122
|
-
resetScrollLock();
|
|
123
|
-
expect(document.body.style.overflow).toBe('');
|
|
124
|
-
expect(document.body.style.paddingRight).toBe('');
|
|
125
|
-
// A new lock should work cleanly
|
|
126
|
-
const lock2 = new ScrollLockState({ enabled: true, restoreDelay: 0 });
|
|
127
|
-
lock2.lock();
|
|
128
|
-
expect(document.body.style.overflow).toBe('hidden');
|
|
129
|
-
lock2.unlock();
|
|
130
|
-
expect(document.body.style.overflow).toBe('');
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
describe('timeout teardown safety', () => {
|
|
134
|
-
it('clears the pending restore timer when a new lock is acquired during the delay', () => {
|
|
135
|
-
vi.useFakeTimers();
|
|
136
|
-
const lock1 = new ScrollLockState({ enabled: true, restoreDelay: 50 });
|
|
137
|
-
lock1.lock();
|
|
138
|
-
lock1.unlock();
|
|
139
|
-
// A restore timer is pending
|
|
140
|
-
expect(vi.getTimerCount()).toBe(1);
|
|
141
|
-
// Acquiring a new lock during the delay must clear the pending timer
|
|
142
|
-
const lock2 = new ScrollLockState({ enabled: true, restoreDelay: 50 });
|
|
143
|
-
lock2.lock();
|
|
144
|
-
expect(vi.getTimerCount()).toBe(0);
|
|
145
|
-
lock2.unlock();
|
|
146
|
-
vi.runAllTimers();
|
|
147
|
-
vi.useRealTimers();
|
|
148
|
-
});
|
|
149
|
-
it('clears the pending restore timer when resetScrollLock() is called', () => {
|
|
150
|
-
vi.useFakeTimers();
|
|
151
|
-
const lock = new ScrollLockState({ enabled: true, restoreDelay: 50 });
|
|
152
|
-
lock.lock();
|
|
153
|
-
lock.unlock();
|
|
154
|
-
expect(vi.getTimerCount()).toBe(1);
|
|
155
|
-
resetScrollLock();
|
|
156
|
-
expect(vi.getTimerCount()).toBe(0);
|
|
157
|
-
vi.useRealTimers();
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
});
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { TypeaheadSearch } from '../typeahead.svelte.js';
|
|
3
|
-
describe('typeaheadSearch', () => {
|
|
4
|
-
afterEach(() => {
|
|
5
|
-
vi.restoreAllMocks();
|
|
6
|
-
});
|
|
7
|
-
it('matches prefix on first character', () => {
|
|
8
|
-
const search = new TypeaheadSearch();
|
|
9
|
-
const items = ['Apple', 'Banana', 'Cherry'];
|
|
10
|
-
expect(search.handleTypeahead('b', items)).toBe(1);
|
|
11
|
-
});
|
|
12
|
-
it('accumulates characters for prefix match', () => {
|
|
13
|
-
vi.useFakeTimers();
|
|
14
|
-
const search = new TypeaheadSearch();
|
|
15
|
-
const items = ['Apple', 'Banana', 'Blueberry', 'Cherry'];
|
|
16
|
-
expect(search.handleTypeahead('b', items)).toBe(1);
|
|
17
|
-
expect(search.handleTypeahead('l', items)).toBe(2);
|
|
18
|
-
vi.useRealTimers();
|
|
19
|
-
});
|
|
20
|
-
it('resets after timeout', () => {
|
|
21
|
-
vi.useFakeTimers();
|
|
22
|
-
const search = new TypeaheadSearch({ timeout: 500 });
|
|
23
|
-
const items = ['Apple', 'Banana'];
|
|
24
|
-
search.handleTypeahead('b', items);
|
|
25
|
-
vi.advanceTimersByTime(600);
|
|
26
|
-
// After reset, 'a' should match Apple again
|
|
27
|
-
expect(search.handleTypeahead('a', items)).toBe(0);
|
|
28
|
-
vi.useRealTimers();
|
|
29
|
-
});
|
|
30
|
-
it('cycles through repeated characters', () => {
|
|
31
|
-
vi.useFakeTimers();
|
|
32
|
-
const search = new TypeaheadSearch();
|
|
33
|
-
const items = ['Alpha', 'Apple', 'Avocado', 'Banana'];
|
|
34
|
-
expect(search.handleTypeahead('a', items)).toBe(0); // Alpha
|
|
35
|
-
expect(search.handleTypeahead('a', items)).toBe(1); // Apple (cycle)
|
|
36
|
-
expect(search.handleTypeahead('a', items)).toBe(2); // Avocado (cycle)
|
|
37
|
-
vi.useRealTimers();
|
|
38
|
-
});
|
|
39
|
-
it('returns -1 for no match', () => {
|
|
40
|
-
const search = new TypeaheadSearch();
|
|
41
|
-
expect(search.handleTypeahead('z', ['Apple', 'Banana'])).toBe(-1);
|
|
42
|
-
});
|
|
43
|
-
it('ignores non-printable characters', () => {
|
|
44
|
-
const search = new TypeaheadSearch();
|
|
45
|
-
expect(search.handleTypeahead('Shift', ['Apple'])).toBe(-1);
|
|
46
|
-
expect(search.handleTypeahead('ArrowDown', ['Apple'])).toBe(-1);
|
|
47
|
-
});
|
|
48
|
-
it('is case-insensitive', () => {
|
|
49
|
-
const search = new TypeaheadSearch();
|
|
50
|
-
expect(search.handleTypeahead('A', ['apple', 'banana'])).toBe(0);
|
|
51
|
-
});
|
|
52
|
-
it('reset clears search', () => {
|
|
53
|
-
vi.useFakeTimers();
|
|
54
|
-
const search = new TypeaheadSearch();
|
|
55
|
-
search.handleTypeahead('a', ['Apple']);
|
|
56
|
-
expect(search.currentSearch).toBe('a');
|
|
57
|
-
search.reset();
|
|
58
|
-
expect(search.currentSearch).toBe('');
|
|
59
|
-
vi.useRealTimers();
|
|
60
|
-
});
|
|
61
|
-
});
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Carbon dark — WCAG AA color-contrast regression test.
|
|
3
|
-
*
|
|
4
|
-
* The hand-tuned dark block of `themes/carbon.css` previously had three
|
|
5
|
-
* surfaces that failed WCAG 2.0 AA (≥ 4.5:1 for normal text):
|
|
6
|
-
*
|
|
7
|
-
* - Tree.Item selected: `--sp-sys-color-on-primary` (#fff) on
|
|
8
|
-
* `--sp-sys-color-primary` (#5c7cfa) ≈ 3.67:1
|
|
9
|
-
* - Tabs.Trigger active: `--sp-sys-color-primary` (#5c7cfa) on
|
|
10
|
-
* `--sp-sys-color-surface-container` (#3c3c3c) ≈ 3.00:1
|
|
11
|
-
* - Badge outlined primary: `--sp-sys-color-primary` (#5c7cfa) on
|
|
12
|
-
* surface (~3c3c3c) ≈ 3.00:1
|
|
13
|
-
*
|
|
14
|
-
* Fix surfaces these via three carbon-dark-specific comp tokens, expected
|
|
15
|
-
* to be ≥ 4.5:1 against their relevant background. This test asserts the
|
|
16
|
-
* tokens exist and the ratios pass — running purely on hex values read
|
|
17
|
-
* back from a live `[data-theme="carbon"].dark` element through
|
|
18
|
-
* `getComputedStyle().getPropertyValue(...)`, which jsdom resolves
|
|
19
|
-
* correctly for hex-valued custom properties (unlike rendered `color:`,
|
|
20
|
-
* which axe-core needs and jsdom cannot provide).
|
|
21
|
-
*
|
|
22
|
-
* Badge tonal secondary (#166) is intentionally not covered here: a
|
|
23
|
-
* theoretical OKLCH ratio puts it at ~16:1; we keep the existing ref-palette
|
|
24
|
-
* mapping until a runtime repro lands.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { readFileSync } from 'node:fs'
|
|
28
|
-
import { dirname, resolve } from 'node:path'
|
|
29
|
-
import { fileURLToPath } from 'node:url'
|
|
30
|
-
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
|
31
|
-
|
|
32
|
-
// jsdom + Vitest does not actually evaluate `import "../carbon.css"` against
|
|
33
|
-
// the DOM (CSS modules become inert objects), so we inject the theme as a
|
|
34
|
-
// <style> tag instead — that path *is* understood by jsdom's CSS engine and
|
|
35
|
-
// makes the custom properties below resolve via getComputedStyle().
|
|
36
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
37
|
-
const __dirname = dirname(__filename)
|
|
38
|
-
const CARBON_CSS = readFileSync(resolve(__dirname, '../carbon.css'), 'utf-8')
|
|
39
|
-
|
|
40
|
-
function hexToRgb(hex: string): [number, number, number] {
|
|
41
|
-
const clean = hex.replace(/^#/, '')
|
|
42
|
-
if (clean.length !== 6) throw new Error(`Expected 6-digit hex, got "${hex}"`)
|
|
43
|
-
return [
|
|
44
|
-
Number.parseInt(clean.slice(0, 2), 16),
|
|
45
|
-
Number.parseInt(clean.slice(2, 4), 16),
|
|
46
|
-
Number.parseInt(clean.slice(4, 6), 16),
|
|
47
|
-
]
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function relativeLuminance([r, g, b]: [number, number, number]): number {
|
|
51
|
-
const transform = (v: number) => {
|
|
52
|
-
const u = v / 255
|
|
53
|
-
return u <= 0.03928 ? u / 12.92 : ((u + 0.055) / 1.055) ** 2.4
|
|
54
|
-
}
|
|
55
|
-
return 0.2126 * transform(r) + 0.7152 * transform(g) + 0.0722 * transform(b)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function wcagRatio(hexA: string, hexB: string): number {
|
|
59
|
-
const lA = relativeLuminance(hexToRgb(hexA))
|
|
60
|
-
const lB = relativeLuminance(hexToRgb(hexB))
|
|
61
|
-
const max = Math.max(lA, lB)
|
|
62
|
-
const min = Math.min(lA, lB)
|
|
63
|
-
return (max + 0.05) / (min + 0.05)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
describe('carbon dark — WCAG AA color-contrast', () => {
|
|
67
|
-
let host: HTMLElement
|
|
68
|
-
let cs: CSSStyleDeclaration
|
|
69
|
-
|
|
70
|
-
beforeAll(() => {
|
|
71
|
-
const style = document.createElement('style')
|
|
72
|
-
style.textContent = CARBON_CSS
|
|
73
|
-
document.head.appendChild(style)
|
|
74
|
-
host = document.createElement('div')
|
|
75
|
-
host.setAttribute('data-theme', 'carbon')
|
|
76
|
-
host.classList.add('dark')
|
|
77
|
-
document.body.appendChild(host)
|
|
78
|
-
cs = getComputedStyle(host)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
afterEach(() => {
|
|
82
|
-
// Each test reads from the same host; nothing to clean per-test.
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
function readToken(name: string): string {
|
|
86
|
-
const v = cs.getPropertyValue(name).trim()
|
|
87
|
-
if (!v)
|
|
88
|
-
throw new Error(
|
|
89
|
-
`Custom property "${name}" is not set on the carbon dark host. Either the token is missing from themes/carbon.css or the CSS import did not load.`,
|
|
90
|
-
)
|
|
91
|
-
return v
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
it('tree-item selected pair (--sp-comp-tree-item-selected-bg vs -fg) passes AA (≥ 4.5)', () => {
|
|
95
|
-
const bg = readToken('--sp-comp-tree-item-selected-bg')
|
|
96
|
-
const fg = readToken('--sp-comp-tree-item-selected-fg')
|
|
97
|
-
expect(bg).toMatch(/^#[0-9a-f]{6}$/i)
|
|
98
|
-
expect(fg).toMatch(/^#[0-9a-f]{6}$/i)
|
|
99
|
-
const ratio = wcagRatio(bg, fg)
|
|
100
|
-
expect(ratio).toBeGreaterThanOrEqual(4.5)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('tabs-trigger active label (--sp-comp-tabs-active-label-color on --sp-sys-color-surface-container) passes AA (≥ 4.5)', () => {
|
|
104
|
-
const fg = readToken('--sp-comp-tabs-active-label-color')
|
|
105
|
-
const bg = readToken('--sp-sys-color-surface-container')
|
|
106
|
-
expect(fg).toMatch(/^#[0-9a-f]{6}$/i)
|
|
107
|
-
expect(bg).toMatch(/^#[0-9a-f]{6}$/i)
|
|
108
|
-
const ratio = wcagRatio(bg, fg)
|
|
109
|
-
expect(ratio).toBeGreaterThanOrEqual(4.5)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('badge outlined primary (--sp-comp-badge-outlined-color on --sp-sys-color-surface-container) passes AA (≥ 4.5)', () => {
|
|
113
|
-
// `--sp-comp-badge-outlined-color` is an outer-cascade override token
|
|
114
|
-
// applied by themes (here carbon dark) and read with a fallback inside
|
|
115
|
-
// `.sp-badge.outlined { color: var(--sp-comp-badge-outlined-color,
|
|
116
|
-
// var(--sp-comp-badge-outline-fg)) }`. It lets the theme replace the
|
|
117
|
-
// outlined-foreground without fighting the local `--sp-comp-badge-*`
|
|
118
|
-
// tokens declared on `.sp-badge` itself (custom-property scope rules).
|
|
119
|
-
const fg = readToken('--sp-comp-badge-outlined-color')
|
|
120
|
-
const bg = readToken('--sp-sys-color-surface-container')
|
|
121
|
-
expect(fg).toMatch(/^#[0-9a-f]{6}$/i)
|
|
122
|
-
expect(bg).toMatch(/^#[0-9a-f]{6}$/i)
|
|
123
|
-
const ratio = wcagRatio(bg, fg)
|
|
124
|
-
expect(ratio).toBeGreaterThanOrEqual(4.5)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('none of the carbon-dark comp tokens resolves to the legacy primary #5c7cfa', () => {
|
|
128
|
-
// Sanity check: none of the "fixed" surfaces should resolve to the
|
|
129
|
-
// legacy #5c7cfa fallback. If they do, the fix didn't actually replace
|
|
130
|
-
// the cascade — most likely the comp token landed in carbon.css but the
|
|
131
|
-
// component file still reads the system token directly.
|
|
132
|
-
expect(readToken('--sp-comp-tree-item-selected-bg').toLowerCase()).not.toBe('#5c7cfa')
|
|
133
|
-
expect(readToken('--sp-comp-tabs-active-label-color').toLowerCase()).not.toBe('#5c7cfa')
|
|
134
|
-
expect(readToken('--sp-comp-badge-outlined-color').toLowerCase()).not.toBe('#5c7cfa')
|
|
135
|
-
expect(readToken('--sp-comp-button-filled-container-color').toLowerCase()).not.toBe('#5c7cfa')
|
|
136
|
-
expect(readToken('--sp-comp-button-outlined-label-color').toLowerCase()).not.toBe('#5c7cfa')
|
|
137
|
-
expect(readToken('--sp-comp-button-text-label-color').toLowerCase()).not.toBe('#5c7cfa')
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('button filled primary (--sp-comp-button-filled-container-color vs label-color) passes AA (≥ 4.5)', () => {
|
|
141
|
-
const bg = readToken('--sp-comp-button-filled-container-color')
|
|
142
|
-
const fg = readToken('--sp-comp-button-filled-label-color')
|
|
143
|
-
expect(bg).toMatch(/^#[0-9a-f]{6}$/i)
|
|
144
|
-
expect(fg).toMatch(/^#[0-9a-f]{6}$/i)
|
|
145
|
-
const ratio = wcagRatio(bg, fg)
|
|
146
|
-
expect(ratio).toBeGreaterThanOrEqual(4.5)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('button outlined primary (--sp-comp-button-outlined-label-color on --sp-sys-color-surface-container) passes AA (≥ 4.5)', () => {
|
|
150
|
-
// .sp-button.outlined renders with `background-color: transparent` by
|
|
151
|
-
// default, so the relevant background for the contrast check is the
|
|
152
|
-
// ancestor surface a typical button sits on — that's surface-container
|
|
153
|
-
// (#3c3c3c) in the carbon dark hand-tuned block.
|
|
154
|
-
const fg = readToken('--sp-comp-button-outlined-label-color')
|
|
155
|
-
const bg = readToken('--sp-sys-color-surface-container')
|
|
156
|
-
expect(fg).toMatch(/^#[0-9a-f]{6}$/i)
|
|
157
|
-
expect(bg).toMatch(/^#[0-9a-f]{6}$/i)
|
|
158
|
-
const ratio = wcagRatio(bg, fg)
|
|
159
|
-
expect(ratio).toBeGreaterThanOrEqual(4.5)
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('button text primary (--sp-comp-button-text-label-color on --sp-sys-color-surface-container) passes AA (≥ 4.5)', () => {
|
|
163
|
-
const fg = readToken('--sp-comp-button-text-label-color')
|
|
164
|
-
const bg = readToken('--sp-sys-color-surface-container')
|
|
165
|
-
expect(fg).toMatch(/^#[0-9a-f]{6}$/i)
|
|
166
|
-
expect(bg).toMatch(/^#[0-9a-f]{6}$/i)
|
|
167
|
-
const ratio = wcagRatio(bg, fg)
|
|
168
|
-
expect(ratio).toBeGreaterThanOrEqual(4.5)
|
|
169
|
-
})
|
|
170
|
-
})
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
-
import { createRequire } from 'node:module'
|
|
3
|
-
import { dirname, resolve } from 'node:path'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import { describe, expect, it } from 'vitest'
|
|
6
|
-
import { generateThemeCSS, generateTokensCSS, themes } from '../../../../../scripts/generate-themes'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Durable control between `scripts/generate-themes.ts` and the six committed
|
|
10
|
-
* theme files (+ `tokens.css`): the committed file must be exactly what the
|
|
11
|
-
* generator emits once formatted by the repo's Prettier config. A hand edit
|
|
12
|
-
* of a generated file, or a generator change without regeneration, turns this
|
|
13
|
-
* RED — the two can no longer drift apart silently (spectral-ui#206).
|
|
14
|
-
*
|
|
15
|
-
* Importing the generator must not write anything: `main()` only runs when
|
|
16
|
-
* the script is executed directly.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
20
|
-
const themesDir = resolve(here, '..')
|
|
21
|
-
|
|
22
|
-
// Vitest resolves `prettier` with the `browser` condition (standalone build,
|
|
23
|
-
// no `resolveConfig`); the Node build is what the repo's `prettier --write`
|
|
24
|
-
// runs, so load exactly that one.
|
|
25
|
-
const require = createRequire(import.meta.url)
|
|
26
|
-
const prettier = require('prettier') as typeof import('prettier')
|
|
27
|
-
|
|
28
|
-
async function formatted(css: string, filepath: string): Promise<string> {
|
|
29
|
-
const config = (await prettier.resolveConfig(filepath)) ?? {}
|
|
30
|
-
return prettier.format(css, { ...config, filepath })
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe('generated theme CSS matches the generator (#206)', () => {
|
|
34
|
-
it('exposes the six themes', () => {
|
|
35
|
-
expect(themes.map((t) => t.name).sort()).toEqual(
|
|
36
|
-
['carbon', 'day', 'docs', 'iris-ocr', 'midnight', 'spectral'].sort(),
|
|
37
|
-
)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
for (const theme of themes) {
|
|
41
|
-
it(`${theme.name}.css is prettier(generateThemeCSS)`, async () => {
|
|
42
|
-
const filepath = resolve(themesDir, `${theme.name}.css`)
|
|
43
|
-
const committed = readFileSync(filepath, 'utf8')
|
|
44
|
-
expect(committed).toBe(await formatted(generateThemeCSS(theme), filepath))
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
it('tokens.css is prettier(generateTokensCSS)', async () => {
|
|
49
|
-
const filepath = resolve(themesDir, '..', 'tokens.css')
|
|
50
|
-
const committed = readFileSync(filepath, 'utf8')
|
|
51
|
-
expect(committed).toBe(await formatted(generateTokensCSS(), filepath))
|
|
52
|
-
})
|
|
53
|
-
})
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
-
import { dirname, resolve } from 'node:path'
|
|
3
|
-
import { fileURLToPath } from 'node:url'
|
|
4
|
-
import { describe, expect, it } from 'vitest'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* spectral-ui#206 — `--sp-sys-color-scrim` is an OPAQUE colour in every theme
|
|
8
|
-
* and every mode. The veil's intensity belongs to the component's opacity
|
|
9
|
-
* token (`--sp-comp-*-overlay-opacity`, default 0.32) — never to the token,
|
|
10
|
-
* and never to both at once.
|
|
11
|
-
*
|
|
12
|
-
* This is a SOURCE pin: every `--sp-sys-color-scrim` declaration in the six
|
|
13
|
-
* generated theme files must be the opaque reference tone, and the generator
|
|
14
|
-
* must carry no hand-tuned scrim override that could re-introduce an alpha.
|
|
15
|
-
* The cascade itself (which declaration wins in `.dark`) is proven in a real
|
|
16
|
-
* Chromium by `tests/e2e/scrim-alpha.spec.ts`; jsdom cannot be that oracle.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
20
|
-
const THEMES = ['spectral', 'day', 'midnight', 'carbon', 'iris-ocr', 'docs'] as const
|
|
21
|
-
const OPAQUE = 'var(--sp-ref-palette-neutral-0)'
|
|
22
|
-
|
|
23
|
-
describe('--sp-sys-color-scrim is opaque in every theme and mode (#206)', () => {
|
|
24
|
-
for (const theme of THEMES) {
|
|
25
|
-
it(`${theme}.css declares the scrim as the opaque reference tone only`, () => {
|
|
26
|
-
const css = readFileSync(resolve(here, `../${theme}.css`), 'utf8')
|
|
27
|
-
const declarations = [...css.matchAll(/--sp-sys-color-scrim:([^;]+);/g)].map((m) =>
|
|
28
|
-
m[1].trim(),
|
|
29
|
-
)
|
|
30
|
-
// One in the generated light block, one in the generated dark block —
|
|
31
|
-
// nothing else, so no hand-tuned block can shadow them with an alpha.
|
|
32
|
-
// This single equality subsumes "never translucent": ANY other value, in
|
|
33
|
-
// any spelling (`rgba()`, `color-mix()`, `#RRGGBBAA`, `oklch(… / a)`),
|
|
34
|
-
// and any extra or missing declaration, fails here.
|
|
35
|
-
expect(declarations).toEqual([OPAQUE, OPAQUE])
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
it('the generator carries no hand-tuned color-scrim override', () => {
|
|
40
|
-
const generator = readFileSync(
|
|
41
|
-
resolve(here, '../../../../../scripts/generate-themes.ts'),
|
|
42
|
-
'utf8',
|
|
43
|
-
)
|
|
44
|
-
// A hand-tuned override is a `'color-scrim': '<value>'` entry (the
|
|
45
|
-
// `SystemColorTokens` interface field `'color-scrim': string` is not one).
|
|
46
|
-
const overrides = generator.match(/['"]color-scrim['"]\s*:\s*['"]/g) ?? []
|
|
47
|
-
expect(overrides).toEqual([])
|
|
48
|
-
// The two generated declarations (light + dark) still exist and are opaque.
|
|
49
|
-
const emitted = [...generator.matchAll(/--sp-sys-color-scrim:\s*\$\{ref\('neutral', 0\)\};/g)]
|
|
50
|
-
expect(emitted).toHaveLength(2)
|
|
51
|
-
})
|
|
52
|
-
})
|