@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,67 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, resolve } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { mount, unmount } from 'svelte';
|
|
5
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
-
import Input from '../input.svelte';
|
|
7
|
-
const componentSource = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../input.svelte'), 'utf8');
|
|
8
|
-
describe('input', () => {
|
|
9
|
-
let target;
|
|
10
|
-
let instance;
|
|
11
|
-
function setup(props = {}) {
|
|
12
|
-
target = document.createElement('div');
|
|
13
|
-
document.body.appendChild(target);
|
|
14
|
-
instance = mount(Input, { target, props });
|
|
15
|
-
return target;
|
|
16
|
-
}
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
if (instance)
|
|
19
|
-
unmount(instance);
|
|
20
|
-
target?.remove();
|
|
21
|
-
});
|
|
22
|
-
it('renders an input element', () => {
|
|
23
|
-
setup();
|
|
24
|
-
expect(target.querySelector('input')).toBeTruthy();
|
|
25
|
-
});
|
|
26
|
-
it('applies size class', () => {
|
|
27
|
-
setup({ size: 'lg' });
|
|
28
|
-
expect(target.querySelector('input')?.className).toContain('lg');
|
|
29
|
-
});
|
|
30
|
-
it('sets aria-invalid when invalid', () => {
|
|
31
|
-
setup({ invalid: true });
|
|
32
|
-
expect(target.querySelector('input')?.getAttribute('aria-invalid')).toBe('true');
|
|
33
|
-
});
|
|
34
|
-
it('sets disabled attribute', () => {
|
|
35
|
-
setup({ disabled: true });
|
|
36
|
-
expect(target.querySelector('input')?.hasAttribute('disabled')).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
it('passes placeholder', () => {
|
|
39
|
-
setup({ placeholder: 'Enter email' });
|
|
40
|
-
expect(target.querySelector('input')?.getAttribute('placeholder')).toBe('Enter email');
|
|
41
|
-
});
|
|
42
|
-
// ── iOS Safari focus-zoom contract ────────────────────────────────
|
|
43
|
-
// Real WebKit focus-zoom cannot be reproduced in jsdom. These tests
|
|
44
|
-
// lock the CSS contract that prevents the regression on `size="sm"` :
|
|
45
|
-
// the `sm` typography stays token-driven for desktop, and a clamp to
|
|
46
|
-
// >= 1rem is applied on coarse pointers (touch / iOS). Other sizes
|
|
47
|
-
// inherit the base 1rem and are already iOS-safe.
|
|
48
|
-
describe('iOS Safari focus-zoom contract (size="sm")', () => {
|
|
49
|
-
it('keeps fine-pointer typography on the body-medium token', () => {
|
|
50
|
-
expect(componentSource).toMatch(/\.sp-input\.sm\s*\{[^}]*font-size:\s*var\(--sp-sys-typescale-body-medium-size[^}]*\}/);
|
|
51
|
-
});
|
|
52
|
-
it('clamps the sm input to at least 1rem on coarse pointers', () => {
|
|
53
|
-
expect(componentSource).toMatch(/@media\s*\(pointer:\s*coarse\)\s*\{[\s\S]*?\.sp-input\.sm\s*\{[^}]*font-size:\s*max\(1rem,[^)]*\)[\s\S]*?\}/);
|
|
54
|
-
});
|
|
55
|
-
it('only the sm variant is targeted by the coarse-pointer clamp', () => {
|
|
56
|
-
// The clamp must target `.sp-input.sm` and only `.sp-input.sm`. Other
|
|
57
|
-
// sizes inherit the base 1rem and must not get an override.
|
|
58
|
-
const coarseRules = [
|
|
59
|
-
...componentSource.matchAll(/@media\s*\(pointer:\s*coarse\)\s*\{[^}]*\{/g),
|
|
60
|
-
];
|
|
61
|
-
expect(coarseRules.length).toBe(1);
|
|
62
|
-
expect(coarseRules[0][0]).toContain('.sp-input.sm');
|
|
63
|
-
expect(coarseRules[0][0]).not.toContain('.sp-input.md');
|
|
64
|
-
expect(coarseRules[0][0]).not.toContain('.sp-input.lg');
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import Label from '../label.svelte';
|
|
4
|
-
describe('label', () => {
|
|
5
|
-
let target;
|
|
6
|
-
let instance;
|
|
7
|
-
function setup(props = {}) {
|
|
8
|
-
target = document.createElement('div');
|
|
9
|
-
document.body.appendChild(target);
|
|
10
|
-
instance = mount(Label, { target, props });
|
|
11
|
-
return target;
|
|
12
|
-
}
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
if (instance)
|
|
15
|
-
unmount(instance);
|
|
16
|
-
target?.remove();
|
|
17
|
-
});
|
|
18
|
-
it('renders label element', () => {
|
|
19
|
-
setup();
|
|
20
|
-
expect(target.querySelector('label')).toBeTruthy();
|
|
21
|
-
});
|
|
22
|
-
});
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { flushSync, mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import { getLanguages } from '../../../internal/i18n/languages.js';
|
|
4
|
-
import LanguageSelect from '../language-select.svelte';
|
|
5
|
-
describe('language-select', () => {
|
|
6
|
-
let target;
|
|
7
|
-
let instance;
|
|
8
|
-
function setup(props = {}) {
|
|
9
|
-
target = document.createElement('div');
|
|
10
|
-
document.body.appendChild(target);
|
|
11
|
-
instance = mount(LanguageSelect, { target, props });
|
|
12
|
-
flushSync();
|
|
13
|
-
return target;
|
|
14
|
-
}
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
if (instance)
|
|
17
|
-
unmount(instance);
|
|
18
|
-
target?.remove();
|
|
19
|
-
});
|
|
20
|
-
// ── Shared behavior (variant-agnostic) ────────────────────────────────────
|
|
21
|
-
describe('shared behavior', () => {
|
|
22
|
-
it('renders the wrapper with the sp-language-select class (default variant)', () => {
|
|
23
|
-
setup();
|
|
24
|
-
expect(target.querySelector('.sp-language-select')).toBeTruthy();
|
|
25
|
-
});
|
|
26
|
-
it('renders the label when provided (default variant)', () => {
|
|
27
|
-
setup({ label: 'UI language' });
|
|
28
|
-
expect(target.querySelector('.sp-language-select-label')?.textContent).toBe('UI language');
|
|
29
|
-
});
|
|
30
|
-
it('applies the consumer-supplied class to the wrapper (default variant)', () => {
|
|
31
|
-
setup({ class: 'my-wrapper' });
|
|
32
|
-
expect(target.querySelector('.sp-language-select.my-wrapper')).toBeTruthy();
|
|
33
|
-
});
|
|
34
|
-
it('renders the wrapper correctly in combobox variant', () => {
|
|
35
|
-
setup({ variant: 'combobox' });
|
|
36
|
-
expect(target.querySelector('.sp-language-select')).toBeTruthy();
|
|
37
|
-
});
|
|
38
|
-
it('renders the label in combobox variant', () => {
|
|
39
|
-
setup({ variant: 'combobox', label: 'UI language' });
|
|
40
|
-
expect(target.querySelector('.sp-language-select-label')?.textContent).toBe('UI language');
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
// ── Default variant: 'select' ─────────────────────────────────────────────
|
|
44
|
-
describe('variant="select" (default)', () => {
|
|
45
|
-
it('renders a Select trigger (button), not a Combobox input', () => {
|
|
46
|
-
setup();
|
|
47
|
-
// Both Select.Trigger and Combobox.Input use role="combobox" (WAI-ARIA
|
|
48
|
-
// 1.2 pattern). Distinguish by tag: the Select variant uses a <button>.
|
|
49
|
-
expect(target.querySelector('button.sp-select-trigger')).toBeTruthy();
|
|
50
|
-
expect(target.querySelector('input[role="combobox"]')).toBeNull();
|
|
51
|
-
});
|
|
52
|
-
it('trigger has aria-haspopup="listbox"', () => {
|
|
53
|
-
setup();
|
|
54
|
-
const trigger = target.querySelector('.sp-select-trigger');
|
|
55
|
-
expect(trigger?.getAttribute('aria-haspopup')).toBe('listbox');
|
|
56
|
-
});
|
|
57
|
-
it('passes the label as aria-label on the trigger', () => {
|
|
58
|
-
setup({ label: 'UI language' });
|
|
59
|
-
const trigger = target.querySelector('.sp-select-trigger');
|
|
60
|
-
expect(trigger?.getAttribute('aria-label')).toBe('UI language');
|
|
61
|
-
});
|
|
62
|
-
it('shows the native name of the selected language in the trigger', () => {
|
|
63
|
-
setup({ value: 'fr' });
|
|
64
|
-
const trigger = target.querySelector('.sp-select-trigger');
|
|
65
|
-
expect(trigger?.textContent).toContain('Français');
|
|
66
|
-
});
|
|
67
|
-
it('shows the native name for a non-latin selection (ja)', () => {
|
|
68
|
-
setup({ value: 'ja' });
|
|
69
|
-
const trigger = target.querySelector('.sp-select-trigger');
|
|
70
|
-
expect(trigger?.textContent).toContain('日本語');
|
|
71
|
-
});
|
|
72
|
-
it('displays an RTL language (العربية) correctly', () => {
|
|
73
|
-
setup({ value: 'ar' });
|
|
74
|
-
const trigger = target.querySelector('.sp-select-trigger');
|
|
75
|
-
expect(trigger?.textContent).toContain('العربية');
|
|
76
|
-
});
|
|
77
|
-
it('renders the hidden form input via Select.Root when name and value are provided', () => {
|
|
78
|
-
setup({ name: 'locale', value: 'fr' });
|
|
79
|
-
const hidden = target.querySelector('input[type="hidden"][name="locale"]');
|
|
80
|
-
expect(hidden).toBeTruthy();
|
|
81
|
-
expect(hidden?.value).toBe('fr');
|
|
82
|
-
});
|
|
83
|
-
it('disables the trigger when disabled=true', () => {
|
|
84
|
-
setup({ disabled: true });
|
|
85
|
-
const trigger = target.querySelector('.sp-select-trigger');
|
|
86
|
-
expect(trigger?.disabled).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
it('accepts a custom items subset', () => {
|
|
89
|
-
const subset = getLanguages(['fr', 'en', 'de']);
|
|
90
|
-
setup({ items: subset, value: 'fr' });
|
|
91
|
-
const trigger = target.querySelector('.sp-select-trigger');
|
|
92
|
-
expect(trigger?.textContent).toContain('Français');
|
|
93
|
-
});
|
|
94
|
-
it('updates the trigger display and fires onValueChange when an item is clicked', () => {
|
|
95
|
-
const calls = [];
|
|
96
|
-
setup({ value: 'en', onValueChange: (code) => calls.push(code) });
|
|
97
|
-
// Open the Select by clicking the trigger.
|
|
98
|
-
const trigger = target.querySelector('button.sp-select-trigger');
|
|
99
|
-
trigger.click();
|
|
100
|
-
flushSync();
|
|
101
|
-
// Content is portaled to document.body by default.
|
|
102
|
-
const frItem = document.body.querySelector(`.sp-select-item[data-value="fr"]`);
|
|
103
|
-
expect(frItem).toBeTruthy();
|
|
104
|
-
frItem.click();
|
|
105
|
-
flushSync();
|
|
106
|
-
// Trigger display should now reflect the new selection.
|
|
107
|
-
expect(trigger.textContent).toContain('Français');
|
|
108
|
-
// onValueChange should have fired exactly once with the new code.
|
|
109
|
-
expect(calls).toEqual(['fr']);
|
|
110
|
-
// Cleanup: remove portaled content from document.body before next test.
|
|
111
|
-
document.body.querySelector('.sp-select-content')?.remove();
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
// ── Opt-in variant: 'combobox' ────────────────────────────────────────────
|
|
115
|
-
describe('variant="combobox"', () => {
|
|
116
|
-
it('renders a Combobox root in closed state by default', () => {
|
|
117
|
-
setup({ variant: 'combobox' });
|
|
118
|
-
const root = target.querySelector('.sp-combobox-root');
|
|
119
|
-
expect(root).toBeTruthy();
|
|
120
|
-
expect(root?.getAttribute('data-state')).toBe('closed');
|
|
121
|
-
});
|
|
122
|
-
it('renders a Combobox input (not a Select trigger)', () => {
|
|
123
|
-
setup({ variant: 'combobox' });
|
|
124
|
-
// Distinguish by tag: the Combobox variant uses an <input>.
|
|
125
|
-
expect(target.querySelector('input[role="combobox"]')).toBeTruthy();
|
|
126
|
-
expect(target.querySelector('button.sp-select-trigger')).toBeNull();
|
|
127
|
-
});
|
|
128
|
-
it('passes the label as aria-label on the input', () => {
|
|
129
|
-
setup({ variant: 'combobox', label: 'UI language' });
|
|
130
|
-
const input = target.querySelector('[role="combobox"]');
|
|
131
|
-
expect(input?.getAttribute('aria-label')).toBe('UI language');
|
|
132
|
-
});
|
|
133
|
-
it('uses the custom placeholder when no language is selected', () => {
|
|
134
|
-
setup({ variant: 'combobox', placeholder: 'Choose...' });
|
|
135
|
-
const input = target.querySelector('[role="combobox"]');
|
|
136
|
-
expect(input?.placeholder).toBe('Choose...');
|
|
137
|
-
});
|
|
138
|
-
it('falls back to a default placeholder when none is provided', () => {
|
|
139
|
-
setup({ variant: 'combobox' });
|
|
140
|
-
const input = target.querySelector('[role="combobox"]');
|
|
141
|
-
expect(input?.placeholder).toBe('Select a language');
|
|
142
|
-
});
|
|
143
|
-
it('shows the native name of the selected language in the input', () => {
|
|
144
|
-
setup({ variant: 'combobox', value: 'fr' });
|
|
145
|
-
const input = target.querySelector('[role="combobox"]');
|
|
146
|
-
expect(input?.value).toBe('Français');
|
|
147
|
-
});
|
|
148
|
-
it('shows the native name for a non-latin selection (ja)', () => {
|
|
149
|
-
setup({ variant: 'combobox', value: 'ja' });
|
|
150
|
-
const input = target.querySelector('[role="combobox"]');
|
|
151
|
-
expect(input?.value).toBe('日本語');
|
|
152
|
-
});
|
|
153
|
-
it('displays an RTL language (العربية) correctly', () => {
|
|
154
|
-
setup({ variant: 'combobox', value: 'ar' });
|
|
155
|
-
const input = target.querySelector('[role="combobox"]');
|
|
156
|
-
expect(input?.value).toBe('العربية');
|
|
157
|
-
});
|
|
158
|
-
it('renders a hidden form input with the BCP 47 code when name and value are provided', () => {
|
|
159
|
-
setup({ variant: 'combobox', name: 'locale', value: 'fr' });
|
|
160
|
-
const hidden = target.querySelector('input[type="hidden"][name="locale"]');
|
|
161
|
-
expect(hidden).toBeTruthy();
|
|
162
|
-
expect(hidden?.value).toBe('fr');
|
|
163
|
-
});
|
|
164
|
-
it('does not render a hidden input when no value is set', () => {
|
|
165
|
-
setup({ variant: 'combobox', name: 'locale' });
|
|
166
|
-
const hidden = target.querySelector('input[type="hidden"][name="locale"]');
|
|
167
|
-
expect(hidden).toBeNull();
|
|
168
|
-
});
|
|
169
|
-
it('passes the disabled flag through to the combobox input', () => {
|
|
170
|
-
setup({ variant: 'combobox', disabled: true });
|
|
171
|
-
const input = target.querySelector('[role="combobox"]');
|
|
172
|
-
expect(input?.disabled).toBe(true);
|
|
173
|
-
});
|
|
174
|
-
it('accepts a custom items subset and silently ignores values outside it', () => {
|
|
175
|
-
const subset = getLanguages(['fr', 'en', 'de']);
|
|
176
|
-
setup({ variant: 'combobox', items: subset, value: 'it' });
|
|
177
|
-
const input = target.querySelector('[role="combobox"]');
|
|
178
|
-
expect(input?.placeholder).toBe('Select a language');
|
|
179
|
-
});
|
|
180
|
-
it('updates the input display and fires onValueChange when an item is clicked', () => {
|
|
181
|
-
const calls = [];
|
|
182
|
-
setup({ variant: 'combobox', value: 'en', onValueChange: (code) => calls.push(code) });
|
|
183
|
-
// Open the listbox by focusing the input.
|
|
184
|
-
const input = target.querySelector('input[role="combobox"]');
|
|
185
|
-
input.dispatchEvent(new FocusEvent('focus', { bubbles: true }));
|
|
186
|
-
flushSync();
|
|
187
|
-
// Combobox.Content portals to document.body by default.
|
|
188
|
-
const frItem = document.body.querySelector(`.sp-combobox-item[data-value^="fr"]`);
|
|
189
|
-
expect(frItem).toBeTruthy();
|
|
190
|
-
frItem.click();
|
|
191
|
-
flushSync();
|
|
192
|
-
// After selection the input should reflect the new native name.
|
|
193
|
-
expect(input.value).toBe('Français');
|
|
194
|
-
// onValueChange should have fired exactly once with the new BCP 47 code.
|
|
195
|
-
expect(calls).toEqual(['fr']);
|
|
196
|
-
document.body.querySelector('.sp-combobox-content')?.remove();
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
});
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { getLanguage, getLanguages, LANGUAGES } from '../../../internal/i18n/languages.js';
|
|
3
|
-
describe('i18n/languages', () => {
|
|
4
|
-
describe('languages list', () => {
|
|
5
|
-
it('contains the 16 curated languages', () => {
|
|
6
|
-
expect(LANGUAGES).toHaveLength(16);
|
|
7
|
-
});
|
|
8
|
-
it('exposes valid BCP 47 codes', () => {
|
|
9
|
-
const codes = LANGUAGES.map((l) => l.code);
|
|
10
|
-
expect(codes).toEqual([
|
|
11
|
-
'en',
|
|
12
|
-
'fr',
|
|
13
|
-
'de',
|
|
14
|
-
'it',
|
|
15
|
-
'id',
|
|
16
|
-
'ja',
|
|
17
|
-
'ko',
|
|
18
|
-
'pt',
|
|
19
|
-
'es',
|
|
20
|
-
'zh-Hans',
|
|
21
|
-
'hi',
|
|
22
|
-
'ar',
|
|
23
|
-
'bn',
|
|
24
|
-
'ru',
|
|
25
|
-
'ur',
|
|
26
|
-
'tr',
|
|
27
|
-
]);
|
|
28
|
-
});
|
|
29
|
-
it('marks ar and ur as rtl and the rest as ltr', () => {
|
|
30
|
-
const rtl = LANGUAGES.filter((l) => l.dir === 'rtl').map((l) => l.code);
|
|
31
|
-
expect(rtl).toEqual(['ar', 'ur']);
|
|
32
|
-
const ltr = LANGUAGES.filter((l) => l.dir === 'ltr').map((l) => l.code);
|
|
33
|
-
expect(ltr).toHaveLength(14);
|
|
34
|
-
});
|
|
35
|
-
it('has a non-empty nativeName and englishName for each entry', () => {
|
|
36
|
-
for (const lang of LANGUAGES) {
|
|
37
|
-
expect(lang.nativeName.length).toBeGreaterThan(0);
|
|
38
|
-
expect(lang.englishName.length).toBeGreaterThan(0);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
it('is frozen (cannot be mutated)', () => {
|
|
42
|
-
expect(Object.isFrozen(LANGUAGES)).toBe(true);
|
|
43
|
-
});
|
|
44
|
-
it('has no duplicate codes', () => {
|
|
45
|
-
const codes = LANGUAGES.map((l) => l.code);
|
|
46
|
-
expect(new Set(codes).size).toBe(codes.length);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
describe('getLanguages', () => {
|
|
50
|
-
it('returns the requested subset in the requested order', () => {
|
|
51
|
-
const result = getLanguages(['fr', 'en', 'de']);
|
|
52
|
-
expect(result.map((l) => l.code)).toEqual(['fr', 'en', 'de']);
|
|
53
|
-
});
|
|
54
|
-
it('preserves user-specified order even when reversed vs LANGUAGES', () => {
|
|
55
|
-
const result = getLanguages(['de', 'en', 'fr']);
|
|
56
|
-
expect(result.map((l) => l.code)).toEqual(['de', 'en', 'fr']);
|
|
57
|
-
});
|
|
58
|
-
it('silently drops unknown codes', () => {
|
|
59
|
-
const result = getLanguages(['fr', 'xx', 'en']);
|
|
60
|
-
expect(result.map((l) => l.code)).toEqual(['fr', 'en']);
|
|
61
|
-
});
|
|
62
|
-
it('returns [] for an empty input', () => {
|
|
63
|
-
expect(getLanguages([])).toEqual([]);
|
|
64
|
-
});
|
|
65
|
-
it('returns full LanguageOption objects (not just codes)', () => {
|
|
66
|
-
const [fr] = getLanguages(['fr']);
|
|
67
|
-
expect(fr).toMatchObject({
|
|
68
|
-
code: 'fr',
|
|
69
|
-
nativeName: 'Français',
|
|
70
|
-
englishName: 'French',
|
|
71
|
-
dir: 'ltr',
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
describe('getLanguage', () => {
|
|
76
|
-
it('returns the language matching the given code', () => {
|
|
77
|
-
expect(getLanguage('ja')?.nativeName).toBe('日本語');
|
|
78
|
-
});
|
|
79
|
-
it('returns undefined for unknown codes', () => {
|
|
80
|
-
expect(getLanguage('xx')).toBeUndefined();
|
|
81
|
-
});
|
|
82
|
-
it('finds RTL languages correctly', () => {
|
|
83
|
-
expect(getLanguage('ar')?.dir).toBe('rtl');
|
|
84
|
-
expect(getLanguage('ur')?.dir).toBe('rtl');
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import LinkPreviewContent from '../components/link-preview-content.svelte'
|
|
3
|
-
import LinkPreviewRoot from '../components/link-preview-root.svelte'
|
|
4
|
-
import LinkPreviewTrigger from '../components/link-preview-trigger.svelte'
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
open = false,
|
|
8
|
-
openDelay = 0,
|
|
9
|
-
closeDelay = 0,
|
|
10
|
-
disablePortal = undefined,
|
|
11
|
-
contentClass = undefined,
|
|
12
|
-
}: {
|
|
13
|
-
open?: boolean
|
|
14
|
-
openDelay?: number
|
|
15
|
-
closeDelay?: number
|
|
16
|
-
disablePortal?: boolean
|
|
17
|
-
contentClass?: string
|
|
18
|
-
} = $props()
|
|
19
|
-
|
|
20
|
-
let rootEl: HTMLElement | undefined = $state()
|
|
21
|
-
|
|
22
|
-
$effect(() => {
|
|
23
|
-
if (open && rootEl) {
|
|
24
|
-
const anchor = rootEl.querySelector<HTMLElement>('.sp-link-preview-trigger')
|
|
25
|
-
anchor?.dispatchEvent(new MouseEvent('mouseenter', { bubbles: false }))
|
|
26
|
-
}
|
|
27
|
-
})
|
|
28
|
-
</script>
|
|
29
|
-
|
|
30
|
-
<div bind:this={rootEl}>
|
|
31
|
-
<LinkPreviewRoot {openDelay} {closeDelay}>
|
|
32
|
-
<LinkPreviewTrigger href="https://example.com">Example Link</LinkPreviewTrigger>
|
|
33
|
-
<LinkPreviewContent {disablePortal} class={contentClass}>
|
|
34
|
-
<p>Preview of example.com</p>
|
|
35
|
-
</LinkPreviewContent>
|
|
36
|
-
</LinkPreviewRoot>
|
|
37
|
-
</div>
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import { configureAxe } from 'vitest-axe';
|
|
4
|
-
import * as matchers from 'vitest-axe/matchers';
|
|
5
|
-
import LinkPreviewTest from './LinkPreviewTest.svelte';
|
|
6
|
-
expect.extend(matchers);
|
|
7
|
-
const axe = configureAxe({
|
|
8
|
-
rules: {
|
|
9
|
-
region: { enabled: false },
|
|
10
|
-
'page-has-heading-one': { enabled: false },
|
|
11
|
-
'landmark-one-main': { enabled: false },
|
|
12
|
-
},
|
|
13
|
-
});
|
|
14
|
-
describe('link-preview a11y', () => {
|
|
15
|
-
let cleanup;
|
|
16
|
-
afterEach(() => cleanup?.());
|
|
17
|
-
it('has no axe violations when closed', async () => {
|
|
18
|
-
const container = document.createElement('div');
|
|
19
|
-
document.body.appendChild(container);
|
|
20
|
-
const instance = mount(LinkPreviewTest, { target: container });
|
|
21
|
-
const results = await axe(container);
|
|
22
|
-
cleanup = () => {
|
|
23
|
-
unmount(instance);
|
|
24
|
-
container.remove();
|
|
25
|
-
};
|
|
26
|
-
expect(results).toHaveNoViolations();
|
|
27
|
-
});
|
|
28
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { flushSync, mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import LinkPreviewTest from './LinkPreviewTest.svelte';
|
|
4
|
-
describe('link-preview integration', () => {
|
|
5
|
-
let target;
|
|
6
|
-
let instance;
|
|
7
|
-
function setup(props = {}) {
|
|
8
|
-
target = document.createElement('div');
|
|
9
|
-
document.body.appendChild(target);
|
|
10
|
-
instance = mount(LinkPreviewTest, { target, props });
|
|
11
|
-
return target;
|
|
12
|
-
}
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
vi.useRealTimers();
|
|
15
|
-
if (instance)
|
|
16
|
-
unmount(instance);
|
|
17
|
-
target?.remove();
|
|
18
|
-
});
|
|
19
|
-
it('full flow: hover → preview → leave → closes', () => {
|
|
20
|
-
vi.useFakeTimers();
|
|
21
|
-
setup({ openDelay: 0, closeDelay: 0 });
|
|
22
|
-
const trigger = target.querySelector('.sp-link-preview-trigger');
|
|
23
|
-
trigger.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
24
|
-
vi.advanceTimersByTime(10);
|
|
25
|
-
flushSync();
|
|
26
|
-
// Content is portalled to document.body, not inside target
|
|
27
|
-
expect(document.querySelector('[role="dialog"]')).toBeTruthy();
|
|
28
|
-
trigger.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true, clientX: -100, clientY: -100 }));
|
|
29
|
-
vi.advanceTimersByTime(510);
|
|
30
|
-
flushSync();
|
|
31
|
-
expect(document.querySelector('[role="dialog"]')).toBeNull();
|
|
32
|
-
});
|
|
33
|
-
it('escape closes preview', () => {
|
|
34
|
-
vi.useFakeTimers();
|
|
35
|
-
setup({ openDelay: 0, closeDelay: 0 });
|
|
36
|
-
const trigger = target.querySelector('.sp-link-preview-trigger');
|
|
37
|
-
trigger.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
38
|
-
vi.advanceTimersByTime(10);
|
|
39
|
-
flushSync();
|
|
40
|
-
flushSync();
|
|
41
|
-
// Content is portalled to document.body, not inside target
|
|
42
|
-
expect(document.querySelector('[role="dialog"]')).toBeTruthy();
|
|
43
|
-
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
|
|
44
|
-
vi.advanceTimersByTime(10);
|
|
45
|
-
flushSync();
|
|
46
|
-
expect(document.querySelector('[role="dialog"]')).toBeNull();
|
|
47
|
-
});
|
|
48
|
-
it('trigger is a real link with href', () => {
|
|
49
|
-
setup();
|
|
50
|
-
const trigger = target.querySelector('.sp-link-preview-trigger');
|
|
51
|
-
expect(trigger.tagName).toBe('A');
|
|
52
|
-
expect(trigger.href).toContain('example.com');
|
|
53
|
-
});
|
|
54
|
-
});
|