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