@spectral_labs/ui 1.4.0 → 1.5.1
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/dist/components/combobox/components/combobox-item.svelte +1 -1
- package/dist/components/dropdown-menu/__tests__/DropdownMenuTest.svelte +25 -0
- package/dist/components/dropdown-menu/__tests__/dropdown-menu.test.svelte.js +67 -0
- package/dist/components/dropdown-menu/components/dropdown-menu-checkbox-item.svelte +2 -2
- package/dist/components/dropdown-menu/components/dropdown-menu-item.svelte +2 -2
- package/dist/components/dropdown-menu/components/dropdown-menu-sub-trigger.svelte +37 -1
- package/dist/components/dropdown-menu/components/dropdown-menu-sub-trigger.svelte.d.ts.map +1 -1
- package/dist/components/menubar/__tests__/MenubarTest.svelte +17 -1
- package/dist/components/menubar/__tests__/menubar.test.svelte.js +35 -0
- package/dist/components/select/components/select-item.svelte +1 -1
- package/package.json +1 -1
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
align-items: center;
|
|
56
56
|
gap: 0.5rem;
|
|
57
57
|
padding: 0.5rem 0.75rem;
|
|
58
|
-
border-radius: var(--sp-sys-shape-corner-
|
|
58
|
+
border-radius: var(--sp-sys-shape-corner-md, 12px);
|
|
59
59
|
font-size: var(--sp-sys-typescale-body-medium-size, 0.875rem);
|
|
60
60
|
color: var(--sp-sys-color-on-surface);
|
|
61
61
|
cursor: pointer;
|
|
@@ -2,24 +2,49 @@
|
|
|
2
2
|
import DropdownMenuContent from '../components/dropdown-menu-content.svelte'
|
|
3
3
|
import DropdownMenuItem from '../components/dropdown-menu-item.svelte'
|
|
4
4
|
import DropdownMenuRoot from '../components/dropdown-menu-root.svelte'
|
|
5
|
+
import DropdownMenuSubContent from '../components/dropdown-menu-sub-content.svelte'
|
|
6
|
+
import DropdownMenuSubTrigger from '../components/dropdown-menu-sub-trigger.svelte'
|
|
7
|
+
import DropdownMenuSub from '../components/dropdown-menu-sub.svelte'
|
|
5
8
|
import DropdownMenuTrigger from '../components/dropdown-menu-trigger.svelte'
|
|
6
9
|
|
|
7
10
|
let {
|
|
8
11
|
open = $bindable(false),
|
|
9
12
|
portalTo,
|
|
10
13
|
onOpenChange,
|
|
14
|
+
withSub = false,
|
|
15
|
+
subFirst = false,
|
|
16
|
+
subDisabled = false,
|
|
11
17
|
}: {
|
|
12
18
|
open?: boolean
|
|
13
19
|
portalTo?: HTMLElement | string
|
|
14
20
|
onOpenChange?: (open: boolean) => void
|
|
21
|
+
withSub?: boolean
|
|
22
|
+
subFirst?: boolean
|
|
23
|
+
subDisabled?: boolean
|
|
15
24
|
} = $props()
|
|
16
25
|
</script>
|
|
17
26
|
|
|
27
|
+
{#snippet subBlock()}
|
|
28
|
+
<DropdownMenuSub>
|
|
29
|
+
<DropdownMenuSubTrigger disabled={subDisabled}>More options</DropdownMenuSubTrigger>
|
|
30
|
+
<DropdownMenuSubContent>
|
|
31
|
+
<DropdownMenuItem>Sub Item 1</DropdownMenuItem>
|
|
32
|
+
<DropdownMenuItem>Sub Item 2</DropdownMenuItem>
|
|
33
|
+
</DropdownMenuSubContent>
|
|
34
|
+
</DropdownMenuSub>
|
|
35
|
+
{/snippet}
|
|
36
|
+
|
|
18
37
|
<DropdownMenuRoot bind:open {onOpenChange}>
|
|
19
38
|
<DropdownMenuTrigger>Open Menu</DropdownMenuTrigger>
|
|
20
39
|
<DropdownMenuContent {portalTo}>
|
|
40
|
+
{#if withSub && subFirst}
|
|
41
|
+
{@render subBlock()}
|
|
42
|
+
{/if}
|
|
21
43
|
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
|
22
44
|
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
|
23
45
|
<DropdownMenuItem>Item 3</DropdownMenuItem>
|
|
46
|
+
{#if withSub && !subFirst}
|
|
47
|
+
{@render subBlock()}
|
|
48
|
+
{/if}
|
|
24
49
|
</DropdownMenuContent>
|
|
25
50
|
</DropdownMenuRoot>
|
|
@@ -60,4 +60,71 @@ describe('dropdown-menu', () => {
|
|
|
60
60
|
flushSync();
|
|
61
61
|
expect(calls).toEqual([true, false]);
|
|
62
62
|
});
|
|
63
|
+
describe('subTrigger', () => {
|
|
64
|
+
function pressKey(key) {
|
|
65
|
+
const el = document.activeElement;
|
|
66
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
|
|
67
|
+
flushSync();
|
|
68
|
+
}
|
|
69
|
+
it('participates in roving focus (data-roving-focus-item + tabindex -1)', () => {
|
|
70
|
+
setup({ open: true, withSub: true });
|
|
71
|
+
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
72
|
+
expect(subTrigger).toBeTruthy();
|
|
73
|
+
expect(subTrigger.hasAttribute('data-roving-focus-item')).toBe(true);
|
|
74
|
+
expect(subTrigger.getAttribute('tabindex')).toBe('-1');
|
|
75
|
+
});
|
|
76
|
+
it('carries both item and sub-trigger classes (visual parity hook)', () => {
|
|
77
|
+
setup({ open: true, withSub: true });
|
|
78
|
+
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
79
|
+
expect(subTrigger.classList.contains('sp-dropdown-menu-item')).toBe(true);
|
|
80
|
+
expect(subTrigger.classList.contains('sp-dropdown-sub-trigger')).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it('is autofocused on open when it is the first row', () => {
|
|
83
|
+
setup({ open: true, withSub: true, subFirst: true });
|
|
84
|
+
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
85
|
+
expect(document.activeElement).toBe(subTrigger);
|
|
86
|
+
});
|
|
87
|
+
it('is reached by ArrowDown/ArrowUp roving among plain items', () => {
|
|
88
|
+
setup({ open: true, withSub: true });
|
|
89
|
+
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
90
|
+
// Autofocus lands on Item 1; sub-trigger is the 4th row.
|
|
91
|
+
expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
|
|
92
|
+
pressKey('ArrowDown');
|
|
93
|
+
pressKey('ArrowDown');
|
|
94
|
+
pressKey('ArrowDown');
|
|
95
|
+
expect(document.activeElement).toBe(subTrigger);
|
|
96
|
+
pressKey('ArrowUp');
|
|
97
|
+
expect(document.activeElement?.textContent?.trim()).toBe('Item 3');
|
|
98
|
+
});
|
|
99
|
+
it('is skipped by autofocus and roving when disabled', () => {
|
|
100
|
+
setup({ open: true, withSub: true, subFirst: true, subDisabled: true });
|
|
101
|
+
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
102
|
+
expect(subTrigger.hasAttribute('data-disabled')).toBe(true);
|
|
103
|
+
// Autofocus skips the disabled sub-trigger even though it is first.
|
|
104
|
+
expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
|
|
105
|
+
// Roving loops over the three plain items without ever landing on it.
|
|
106
|
+
pressKey('ArrowDown');
|
|
107
|
+
pressKey('ArrowDown');
|
|
108
|
+
pressKey('ArrowDown');
|
|
109
|
+
expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
|
|
110
|
+
pressKey('ArrowUp');
|
|
111
|
+
expect(document.activeElement?.textContent?.trim()).toBe('Item 3');
|
|
112
|
+
});
|
|
113
|
+
it('opens the sub content on ArrowRight and reflects aria-expanded', () => {
|
|
114
|
+
setup({ open: true, withSub: true, subFirst: true });
|
|
115
|
+
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
116
|
+
expect(document.activeElement).toBe(subTrigger);
|
|
117
|
+
expect(subTrigger.getAttribute('aria-expanded')).toBe('false');
|
|
118
|
+
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
119
|
+
pressKey('ArrowRight');
|
|
120
|
+
expect(subTrigger.getAttribute('aria-expanded')).toBe('true');
|
|
121
|
+
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
122
|
+
});
|
|
123
|
+
it('opens the sub content on Enter', () => {
|
|
124
|
+
setup({ open: true, withSub: true, subFirst: true });
|
|
125
|
+
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
|
|
126
|
+
pressKey('Enter');
|
|
127
|
+
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
63
130
|
});
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
align-items: center;
|
|
58
58
|
gap: 0.5rem;
|
|
59
59
|
padding: 0.5rem 0.75rem;
|
|
60
|
-
border-radius: var(--sp-sys-shape-corner-
|
|
60
|
+
border-radius: var(--sp-sys-shape-corner-md, 12px);
|
|
61
61
|
font-family: var(--sp-sys-typescale-body-medium-font, inherit);
|
|
62
62
|
font-size: var(--sp-sys-typescale-body-medium-size, 0.875rem);
|
|
63
63
|
color: var(--sp-sys-color-on-surface);
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
outline: none;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
.sp-dropdown-menu-item.checkbox:focus,
|
|
69
|
+
.sp-dropdown-menu-item.checkbox:focus-visible,
|
|
70
70
|
.sp-dropdown-menu-item.checkbox:hover:not([data-disabled]) {
|
|
71
71
|
background-color: color-mix(
|
|
72
72
|
in oklch,
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
align-items: center;
|
|
46
46
|
gap: 0.5rem;
|
|
47
47
|
padding: 0.5rem 0.75rem;
|
|
48
|
-
border-radius: var(--sp-sys-shape-corner-
|
|
48
|
+
border-radius: var(--sp-sys-shape-corner-md, 12px);
|
|
49
49
|
font-family: var(--sp-sys-typescale-body-medium-font, inherit);
|
|
50
50
|
font-size: var(--sp-sys-typescale-body-medium-size, 0.875rem);
|
|
51
51
|
color: var(--sp-sys-color-on-surface);
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
var(--sp-sys-motion-easing-effects, ease);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
.sp-dropdown-menu-item:focus {
|
|
59
|
+
.sp-dropdown-menu-item:focus-visible {
|
|
60
60
|
background-color: color-mix(
|
|
61
61
|
in oklch,
|
|
62
62
|
var(--sp-sys-color-on-surface) calc(var(--sp-sys-state-hover-opacity, 0.08) * 100%),
|
|
@@ -29,6 +29,12 @@
|
|
|
29
29
|
if (disabled) return
|
|
30
30
|
if (e.key === 'ArrowRight' || e.key === 'Enter' || e.key === ' ') {
|
|
31
31
|
e.preventDefault()
|
|
32
|
+
// The key is fully handled here: stop it from bubbling to ancestor menu
|
|
33
|
+
// keydown handlers (menubar-content treats ArrowRight as "close menu +
|
|
34
|
+
// open the next menubar menu", which would immediately undo openSub;
|
|
35
|
+
// Space would also leak into content-level typeahead). Same pattern as
|
|
36
|
+
// dropdown-menu-sub-content.svelte for ArrowLeft/Escape.
|
|
37
|
+
e.stopPropagation()
|
|
32
38
|
sub.openSub()
|
|
33
39
|
}
|
|
34
40
|
}
|
|
@@ -47,7 +53,8 @@
|
|
|
47
53
|
aria-expanded={sub.open}
|
|
48
54
|
aria-disabled={disabled || undefined}
|
|
49
55
|
data-disabled={disabled || undefined}
|
|
50
|
-
|
|
56
|
+
data-roving-focus-item
|
|
57
|
+
tabindex={-1}
|
|
51
58
|
onpointerenter={onPointerEnter}
|
|
52
59
|
onpointerleave={onPointerLeave}
|
|
53
60
|
onkeydown={onKeyDown}
|
|
@@ -71,11 +78,40 @@
|
|
|
71
78
|
</div>
|
|
72
79
|
|
|
73
80
|
<style>
|
|
81
|
+
/* Svelte scoped styles don't cross components: sharing the `sp-dropdown-menu-item`
|
|
82
|
+
class name inherits nothing from dropdown-menu-item.svelte. The item base styles
|
|
83
|
+
are duplicated here (same pattern as dropdown-menu-checkbox-item.svelte) and must
|
|
84
|
+
stay in sync with dropdown-menu-item.svelte. */
|
|
74
85
|
.sp-dropdown-sub-trigger {
|
|
75
86
|
display: flex;
|
|
76
87
|
align-items: center;
|
|
77
88
|
justify-content: space-between;
|
|
78
89
|
gap: 0.5rem;
|
|
90
|
+
padding: 0.5rem 0.75rem;
|
|
91
|
+
border-radius: var(--sp-sys-shape-corner-md, 12px);
|
|
92
|
+
font-family: var(--sp-sys-typescale-body-medium-font, inherit);
|
|
93
|
+
font-size: var(--sp-sys-typescale-body-medium-size, 0.875rem);
|
|
94
|
+
color: var(--sp-sys-color-on-surface);
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
user-select: none;
|
|
97
|
+
outline: none;
|
|
98
|
+
transition: background-color var(--sp-sys-motion-duration-short2, 100ms)
|
|
99
|
+
var(--sp-sys-motion-easing-effects, ease);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.sp-dropdown-sub-trigger:focus-visible,
|
|
103
|
+
.sp-dropdown-sub-trigger:hover:not([data-disabled]),
|
|
104
|
+
.sp-dropdown-sub-trigger[aria-expanded='true'] {
|
|
105
|
+
background-color: color-mix(
|
|
106
|
+
in oklch,
|
|
107
|
+
var(--sp-sys-color-on-surface) calc(var(--sp-sys-state-hover-opacity, 0.08) * 100%),
|
|
108
|
+
transparent
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.sp-dropdown-sub-trigger[data-disabled] {
|
|
113
|
+
opacity: 0.38;
|
|
114
|
+
cursor: not-allowed;
|
|
79
115
|
}
|
|
80
116
|
|
|
81
117
|
.sp-dropdown-sub-trigger-label {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dropdown-menu-sub-trigger.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/dropdown-menu/components/dropdown-menu-sub-trigger.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"dropdown-menu-sub-trigger.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/dropdown-menu/components/dropdown-menu-sub-trigger.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAA;AA+D9D,QAAA,MAAM,sBAAsB,iEAAwC,CAAC;AACrE,KAAK,sBAAsB,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACxE,eAAe,sBAAsB,CAAC"}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import DropdownMenuItem from '../../dropdown-menu/components/dropdown-menu-item.svelte'
|
|
3
3
|
import DropdownMenuSeparator from '../../dropdown-menu/components/dropdown-menu-separator.svelte'
|
|
4
|
+
import DropdownMenuSubContent from '../../dropdown-menu/components/dropdown-menu-sub-content.svelte'
|
|
5
|
+
import DropdownMenuSubTrigger from '../../dropdown-menu/components/dropdown-menu-sub-trigger.svelte'
|
|
6
|
+
import DropdownMenuSub from '../../dropdown-menu/components/dropdown-menu-sub.svelte'
|
|
4
7
|
import MenubarContent from '../components/menubar-content.svelte'
|
|
5
8
|
import MenubarMenu from '../components/menubar-menu.svelte'
|
|
6
9
|
import MenubarRoot from '../components/menubar-root.svelte'
|
|
7
10
|
import MenubarTrigger from '../components/menubar-trigger.svelte'
|
|
8
11
|
|
|
9
|
-
const {
|
|
12
|
+
const {
|
|
13
|
+
loop = true,
|
|
14
|
+
portalTo,
|
|
15
|
+
withSub = false,
|
|
16
|
+
}: { loop?: boolean; portalTo?: HTMLElement | string; withSub?: boolean } = $props()
|
|
10
17
|
</script>
|
|
11
18
|
|
|
12
19
|
<MenubarRoot {loop}>
|
|
@@ -17,6 +24,15 @@
|
|
|
17
24
|
<DropdownMenuItem>Open</DropdownMenuItem>
|
|
18
25
|
<DropdownMenuSeparator />
|
|
19
26
|
<DropdownMenuItem>Save</DropdownMenuItem>
|
|
27
|
+
{#if withSub}
|
|
28
|
+
<DropdownMenuSub>
|
|
29
|
+
<DropdownMenuSubTrigger>Share</DropdownMenuSubTrigger>
|
|
30
|
+
<DropdownMenuSubContent>
|
|
31
|
+
<DropdownMenuItem>Email</DropdownMenuItem>
|
|
32
|
+
<DropdownMenuItem>Copy Link</DropdownMenuItem>
|
|
33
|
+
</DropdownMenuSubContent>
|
|
34
|
+
</DropdownMenuSub>
|
|
35
|
+
{/if}
|
|
20
36
|
</MenubarContent>
|
|
21
37
|
</MenubarMenu>
|
|
22
38
|
<MenubarMenu value="edit">
|
|
@@ -195,4 +195,39 @@ describe('menubar', () => {
|
|
|
195
195
|
// No menu should open from hover alone
|
|
196
196
|
expect(target.querySelector('[role="menu"]')).toBeNull();
|
|
197
197
|
});
|
|
198
|
+
// --- SubTrigger inside a menubar menu ---
|
|
199
|
+
describe('subTrigger', () => {
|
|
200
|
+
function pressKey(key) {
|
|
201
|
+
const el = document.activeElement;
|
|
202
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }));
|
|
203
|
+
flushSync();
|
|
204
|
+
}
|
|
205
|
+
it('arrowRight on a plain item moves to the next menubar menu', () => {
|
|
206
|
+
setup({ withSub: true });
|
|
207
|
+
const triggers = target.querySelectorAll('[data-menubar-trigger]');
|
|
208
|
+
triggers[0].click();
|
|
209
|
+
flushSync();
|
|
210
|
+
// Autofocus lands on the first item of the opened menu
|
|
211
|
+
expect(document.activeElement?.textContent?.trim()).toBe('New');
|
|
212
|
+
pressKey('ArrowRight');
|
|
213
|
+
expect(triggers[0].getAttribute('data-state')).toBe('closed');
|
|
214
|
+
expect(triggers[1].getAttribute('data-state')).toBe('open');
|
|
215
|
+
});
|
|
216
|
+
it('arrowRight on the sub-trigger opens the submenu without switching menubar menus', () => {
|
|
217
|
+
setup({ withSub: true });
|
|
218
|
+
const triggers = target.querySelectorAll('[data-menubar-trigger]');
|
|
219
|
+
triggers[0].click();
|
|
220
|
+
flushSync();
|
|
221
|
+
const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
|
|
222
|
+
expect(subTrigger).toBeTruthy();
|
|
223
|
+
subTrigger.focus();
|
|
224
|
+
pressKey('ArrowRight');
|
|
225
|
+
// Submenu opened…
|
|
226
|
+
expect(subTrigger.getAttribute('aria-expanded')).toBe('true');
|
|
227
|
+
expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
|
|
228
|
+
// …and the menubar neither closed the current menu nor advanced to the next one
|
|
229
|
+
expect(triggers[0].getAttribute('data-state')).toBe('open');
|
|
230
|
+
expect(triggers[1].getAttribute('data-state')).toBe('closed');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
198
233
|
});
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
align-items: center;
|
|
66
66
|
gap: 0.5rem;
|
|
67
67
|
padding: 0.5rem 0.75rem;
|
|
68
|
-
border-radius: var(--sp-sys-shape-corner-
|
|
68
|
+
border-radius: var(--sp-sys-shape-corner-md, 12px);
|
|
69
69
|
font-family: var(--sp-sys-typescale-body-medium-font, inherit);
|
|
70
70
|
font-size: var(--sp-sys-typescale-body-medium-size, 0.875rem);
|
|
71
71
|
color: var(--sp-sys-color-on-surface);
|