@joewinke/jatui 0.5.1 → 0.5.2
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.
|
@@ -105,7 +105,10 @@
|
|
|
105
105
|
submenuIndex = index;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
// Accelerators route through here with a KeyboardEvent; submenu items are
|
|
109
|
+
// handled before the call in that path, so `currentTarget` is only read
|
|
110
|
+
// for the (mouse-driven) submenu toggle branch.
|
|
111
|
+
function handleItemClick(item: ActionItem, index: number, event: MouseEvent | KeyboardEvent) {
|
|
109
112
|
if (item.disabled) return;
|
|
110
113
|
if (item.submenu && item.submenu.length) {
|
|
111
114
|
// Submenu owner: open (or toggle) instead of firing onSelect/close.
|
|
@@ -184,6 +187,13 @@
|
|
|
184
187
|
return buttons.indexOf(activeBtn as HTMLButtonElement);
|
|
185
188
|
}
|
|
186
189
|
|
|
190
|
+
function isEditableTarget(target: EventTarget | null): boolean {
|
|
191
|
+
const el = target as HTMLElement | null;
|
|
192
|
+
if (!el) return false;
|
|
193
|
+
const tag = el.tagName;
|
|
194
|
+
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable;
|
|
195
|
+
}
|
|
196
|
+
|
|
187
197
|
function handleKeydown(e: KeyboardEvent) {
|
|
188
198
|
if (e.key === 'Escape') {
|
|
189
199
|
// If a submenu is open, close only the submenu on Escape; otherwise
|
|
@@ -232,6 +242,43 @@
|
|
|
232
242
|
}
|
|
233
243
|
return;
|
|
234
244
|
}
|
|
245
|
+
// Single-character accelerators (item.shortcut). No modifiers, and
|
|
246
|
+
// suppressed while focus sits in an editable element (a `content`
|
|
247
|
+
// snippet's input) so typing never fires actions.
|
|
248
|
+
if (
|
|
249
|
+
e.key.length === 1 &&
|
|
250
|
+
!e.ctrlKey &&
|
|
251
|
+
!e.metaKey &&
|
|
252
|
+
!e.altKey &&
|
|
253
|
+
!isEditableTarget(e.target)
|
|
254
|
+
) {
|
|
255
|
+
const pressed = e.key.toLowerCase();
|
|
256
|
+
const idx = items.findIndex(
|
|
257
|
+
(it) =>
|
|
258
|
+
!('divider' in it) &&
|
|
259
|
+
!it.disabled &&
|
|
260
|
+
it.shortcut?.toLowerCase() === pressed
|
|
261
|
+
);
|
|
262
|
+
if (idx >= 0) {
|
|
263
|
+
const item = items[idx] as ActionItem;
|
|
264
|
+
e.preventDefault();
|
|
265
|
+
e.stopPropagation();
|
|
266
|
+
if (item.submenu?.length) {
|
|
267
|
+
const buttons = menuItemButtons();
|
|
268
|
+
openSubmenuAt(buttons[idx], idx);
|
|
269
|
+
const el = menuEl;
|
|
270
|
+
requestAnimationFrame(() => {
|
|
271
|
+
const first = el?.querySelector(
|
|
272
|
+
':scope > [role="menu"] > button[role="menuitem"]:not(:disabled)'
|
|
273
|
+
) as HTMLButtonElement | null;
|
|
274
|
+
first?.focus();
|
|
275
|
+
});
|
|
276
|
+
} else {
|
|
277
|
+
handleItemClick(item, idx, e);
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
235
282
|
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key)) return;
|
|
236
283
|
const buttons = menuItemButtons();
|
|
237
284
|
if (!buttons.length) return;
|
|
@@ -315,6 +362,12 @@
|
|
|
315
362
|
{:else}
|
|
316
363
|
{#if item.icon}<span class="shrink-0">{item.icon}</span>{/if}
|
|
317
364
|
<span class="truncate flex-1">{item.label}</span>
|
|
365
|
+
{#if item.shortcut}
|
|
366
|
+
<kbd
|
|
367
|
+
class="shrink-0 text-[10px] leading-none px-1 py-0.5 rounded border border-base-content/20 text-base-content/50"
|
|
368
|
+
aria-hidden="true">{item.shortcut}</kbd
|
|
369
|
+
>
|
|
370
|
+
{/if}
|
|
318
371
|
{#if item.submenu?.length}
|
|
319
372
|
<span class="text-base-content/50 shrink-0" aria-hidden="true">▸</span>
|
|
320
373
|
{/if}
|
|
@@ -55,6 +55,16 @@ export type ActionItem = {
|
|
|
55
55
|
content?: Snippet;
|
|
56
56
|
variant?: 'default' | 'danger';
|
|
57
57
|
disabled?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Single-character keyboard accelerator, active while the menu is open.
|
|
60
|
+
* Pressing the key (case-insensitive, no modifiers) activates the item
|
|
61
|
+
* exactly like a click — including `submit` and closing the menu. A
|
|
62
|
+
* right-aligned <kbd> hint renders after the label. Items with a
|
|
63
|
+
* `submenu` open it (focus moves to its first item). Accelerators are
|
|
64
|
+
* suppressed while focus is inside an editable element (an `content`
|
|
65
|
+
* snippet's input, for example), so typing never triggers actions.
|
|
66
|
+
*/
|
|
67
|
+
shortcut?: string;
|
|
58
68
|
};
|
|
59
69
|
export type DividerItem = {
|
|
60
70
|
divider: true;
|