@dorsk/tsumikit 0.64.1 → 0.65.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/README.md
CHANGED
|
@@ -276,7 +276,8 @@ and an `attrs` object for `data-*`/test ids on the row; a checkable `pressed` it
|
|
|
276
276
|
its icon leading and shows its check trailing, or uses the leading check slot when it has
|
|
277
277
|
no icon; selecting closes the menu unless `closeOnSelect={false}` or the item sets `keepOpen`;
|
|
278
278
|
rest props land on the trigger, `count`/`countMax` paint its corner badge and `panelStyle`
|
|
279
|
-
sizes the panel
|
|
279
|
+
sizes the panel; an item with a `control` snippet renders as a non-button row hosting a native
|
|
280
|
+
`<select>` or nested Popover trigger, keeping the icon column, row height and ↑/↓ navigation),
|
|
280
281
|
Tabs (omit `panel` for a strip-only tablist of document tabs: `closable` + `onclose(id)`
|
|
281
282
|
close through the ✕, Delete and middle-click, per-tab `leading` snippet, `title`,
|
|
282
283
|
trailing `actions` snippet; `--tab-max-width` truncates long labels),
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
2
|
export interface MenuItem {
|
|
3
3
|
label: string;
|
|
4
|
-
|
|
4
|
+
/** Required for button rows; a `control` row's hosted control acts on its own. */
|
|
5
|
+
onselect?: () => void;
|
|
5
6
|
icon?: import('../atoms/Icon.svelte').IconName;
|
|
6
7
|
danger?: boolean;
|
|
7
8
|
disabled?: boolean;
|
|
@@ -18,6 +19,13 @@
|
|
|
18
19
|
keepOpen?: boolean;
|
|
19
20
|
/** Custom row content (a rename Input, a slider…) replacing icon/label/tag. */
|
|
20
21
|
content?: import('svelte').Snippet<[MenuItem]>;
|
|
22
|
+
/**
|
|
23
|
+
* A row that is itself a control (a native `<select>`, a nested Popover trigger):
|
|
24
|
+
* rendered as a non-button row with the menu's icon column and row height, its
|
|
25
|
+
* first focusable element joining ↑/↓ navigation. Replaces label/tag; `onselect`,
|
|
26
|
+
* `pressed` and `keepOpen` do not apply.
|
|
27
|
+
*/
|
|
28
|
+
control?: import('svelte').Snippet<[MenuItem, { close: () => void }]>;
|
|
21
29
|
/**
|
|
22
30
|
* Extra attributes for the rendered row — `data-*`, `title`, `aria-describedby`…
|
|
23
31
|
* The row's own semantics (`role`, `aria-checked`, `disabled`, `class`, `onclick`)
|
|
@@ -117,18 +125,27 @@
|
|
|
117
125
|
|
|
118
126
|
let listEl = $state<HTMLDivElement | null>(null);
|
|
119
127
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
const ROW_TARGET = ':is(select, input, textarea, button):not(:disabled), a[href], [tabindex]:not([tabindex="-1"])';
|
|
129
|
+
|
|
130
|
+
function targets(): HTMLElement[] {
|
|
131
|
+
if (!listEl) return [];
|
|
132
|
+
const rows = listEl.querySelectorAll<HTMLElement>(
|
|
133
|
+
':scope > [role="menuitem"]:not(:disabled), :scope > [role="menuitemcheckbox"]:not(:disabled), :scope > .menu-row:not([inert])'
|
|
134
|
+
);
|
|
135
|
+
return Array.from(rows).flatMap((row) => {
|
|
136
|
+
if (!row.classList.contains('menu-row')) return [row];
|
|
137
|
+
const control = row.querySelector<HTMLElement>(ROW_TARGET);
|
|
138
|
+
return control ? [control] : [];
|
|
139
|
+
});
|
|
124
140
|
}
|
|
125
141
|
function focusAt(i: number) {
|
|
126
|
-
const b =
|
|
142
|
+
const b = targets();
|
|
127
143
|
if (b.length) b[(i + b.length) % b.length].focus();
|
|
128
144
|
}
|
|
129
145
|
function onkeydown(e: KeyboardEvent) {
|
|
130
|
-
|
|
131
|
-
const
|
|
146
|
+
if ((e.target as Element).closest('[popover]') !== listEl?.closest('[popover]')) return;
|
|
147
|
+
const b = targets();
|
|
148
|
+
const i = b.indexOf(document.activeElement as HTMLElement);
|
|
132
149
|
if (e.key === 'ArrowDown') {
|
|
133
150
|
e.preventDefault();
|
|
134
151
|
focusAt(i + 1);
|
|
@@ -146,7 +163,7 @@
|
|
|
146
163
|
function select(item: MenuItem, close: () => void) {
|
|
147
164
|
if (item.disabled) return;
|
|
148
165
|
if (menuItemCloses(item, closeOnSelect)) close();
|
|
149
|
-
item.onselect();
|
|
166
|
+
item.onselect?.();
|
|
150
167
|
}
|
|
151
168
|
</script>
|
|
152
169
|
|
|
@@ -186,6 +203,12 @@
|
|
|
186
203
|
{#snippet children({ close }: { close: () => void })}
|
|
187
204
|
<div bind:this={listEl} role="none" class="menu" data-tsu="Menu" tabindex="-1" {onkeydown}>
|
|
188
205
|
{#each items as item (item.label)}
|
|
206
|
+
{#if item.control}
|
|
207
|
+
<div {...item.attrs as HTMLAttributes<HTMLDivElement>} role="none" class="menu-item menu-row" class:danger={item.danger} inert={item.disabled}>
|
|
208
|
+
{#if item.icon}<Icon name={item.icon} />{/if}
|
|
209
|
+
{@render item.control(item, { close })}
|
|
210
|
+
</div>
|
|
211
|
+
{:else}
|
|
189
212
|
<button
|
|
190
213
|
type="button"
|
|
191
214
|
{...item.attrs}
|
|
@@ -216,6 +239,7 @@
|
|
|
216
239
|
{#if glyphs.trailingCheck}<span class="menu-check menu-check-trailing"><Icon name="check" /></span>{/if}
|
|
217
240
|
{/if}
|
|
218
241
|
</button>
|
|
242
|
+
{/if}
|
|
219
243
|
{/each}
|
|
220
244
|
</div>
|
|
221
245
|
{/snippet}
|
|
@@ -241,8 +265,12 @@
|
|
|
241
265
|
font-size: var(--fs-sm);
|
|
242
266
|
white-space: nowrap;
|
|
243
267
|
}
|
|
268
|
+
.menu-row {
|
|
269
|
+
position: relative;
|
|
270
|
+
}
|
|
244
271
|
.menu-item:hover:not(:disabled),
|
|
245
|
-
.menu-item:focus-visible
|
|
272
|
+
.menu-item:focus-visible,
|
|
273
|
+
.menu-row:focus-within {
|
|
246
274
|
background: var(--bg-elevated-2);
|
|
247
275
|
outline: none;
|
|
248
276
|
}
|
|
@@ -273,7 +301,8 @@
|
|
|
273
301
|
margin-inline-start: 0;
|
|
274
302
|
padding-inline-start: 0;
|
|
275
303
|
}
|
|
276
|
-
.menu-item:disabled
|
|
304
|
+
.menu-item:disabled,
|
|
305
|
+
.menu-row[inert] {
|
|
277
306
|
opacity: 0.45;
|
|
278
307
|
cursor: not-allowed;
|
|
279
308
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface MenuItem {
|
|
2
2
|
label: string;
|
|
3
|
-
|
|
3
|
+
/** Required for button rows; a `control` row's hosted control acts on its own. */
|
|
4
|
+
onselect?: () => void;
|
|
4
5
|
icon?: import('../atoms/Icon.svelte').IconName;
|
|
5
6
|
danger?: boolean;
|
|
6
7
|
disabled?: boolean;
|
|
@@ -17,6 +18,15 @@ export interface MenuItem {
|
|
|
17
18
|
keepOpen?: boolean;
|
|
18
19
|
/** Custom row content (a rename Input, a slider…) replacing icon/label/tag. */
|
|
19
20
|
content?: import('svelte').Snippet<[MenuItem]>;
|
|
21
|
+
/**
|
|
22
|
+
* A row that is itself a control (a native `<select>`, a nested Popover trigger):
|
|
23
|
+
* rendered as a non-button row with the menu's icon column and row height, its
|
|
24
|
+
* first focusable element joining ↑/↓ navigation. Replaces label/tag; `onselect`,
|
|
25
|
+
* `pressed` and `keepOpen` do not apply.
|
|
26
|
+
*/
|
|
27
|
+
control?: import('svelte').Snippet<[MenuItem, {
|
|
28
|
+
close: () => void;
|
|
29
|
+
}]>;
|
|
20
30
|
/**
|
|
21
31
|
* Extra attributes for the rendered row — `data-*`, `title`, `aria-describedby`…
|
|
22
32
|
* The row's own semantics (`role`, `aria-checked`, `disabled`, `class`, `onclick`)
|
package/package.json
CHANGED