@spectral_labs/ui 1.7.0 → 1.8.0-next.0

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