@joewinke/jatui 0.5.0 → 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.
|
|
@@ -125,7 +128,13 @@
|
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
function handleItemPointerEnter(item: ActionItem, index: number, event: PointerEvent) {
|
|
128
|
-
|
|
131
|
+
// Entering an item WITHOUT a submenu must close any open submenu —
|
|
132
|
+
// otherwise hover-scrolling past a submenu parent (e.g. "Classify
|
|
133
|
+
// sender") leaves its panel stuck open over the rest of the menu.
|
|
134
|
+
if (item.disabled || !item.submenu?.length) {
|
|
135
|
+
closeSubmenu();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
129
138
|
const el = event.currentTarget as HTMLElement;
|
|
130
139
|
openSubmenuAt(el, index);
|
|
131
140
|
}
|
|
@@ -178,6 +187,13 @@
|
|
|
178
187
|
return buttons.indexOf(activeBtn as HTMLButtonElement);
|
|
179
188
|
}
|
|
180
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
|
+
|
|
181
197
|
function handleKeydown(e: KeyboardEvent) {
|
|
182
198
|
if (e.key === 'Escape') {
|
|
183
199
|
// If a submenu is open, close only the submenu on Escape; otherwise
|
|
@@ -226,6 +242,43 @@
|
|
|
226
242
|
}
|
|
227
243
|
return;
|
|
228
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
|
+
}
|
|
229
282
|
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key)) return;
|
|
230
283
|
const buttons = menuItemButtons();
|
|
231
284
|
if (!buttons.length) return;
|
|
@@ -289,7 +342,7 @@
|
|
|
289
342
|
aria-expanded={item.submenu?.length ? submenuIndex === i : undefined}
|
|
290
343
|
aria-label={item.content ? item.label : undefined}
|
|
291
344
|
class="w-full flex items-center gap-2 text-left px-3 py-1.5 text-sm transition-colors
|
|
292
|
-
{item.variant === 'danger' ? 'text-error hover:bg-error/10' : 'hover:bg-base-
|
|
345
|
+
{item.variant === 'danger' ? 'text-error hover:bg-error/10' : 'hover:bg-base-300'}
|
|
293
346
|
{item.disabled ? 'opacity-50 cursor-not-allowed' : ''}"
|
|
294
347
|
disabled={item.disabled}
|
|
295
348
|
onclick={(e) => handleItemClick(item, i, e)}
|
|
@@ -297,6 +350,10 @@
|
|
|
297
350
|
onfocus={(e) => {
|
|
298
351
|
if (item.submenu?.length && !item.disabled) {
|
|
299
352
|
openSubmenuAt(e.currentTarget as HTMLElement, i);
|
|
353
|
+
} else {
|
|
354
|
+
// Arrow-navigating onto a submenu-less item closes any
|
|
355
|
+
// open submenu, same as pointer hover.
|
|
356
|
+
closeSubmenu();
|
|
300
357
|
}
|
|
301
358
|
}}
|
|
302
359
|
>
|
|
@@ -305,6 +362,12 @@
|
|
|
305
362
|
{:else}
|
|
306
363
|
{#if item.icon}<span class="shrink-0">{item.icon}</span>{/if}
|
|
307
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}
|
|
308
371
|
{#if item.submenu?.length}
|
|
309
372
|
<span class="text-base-content/50 shrink-0" aria-hidden="true">▸</span>
|
|
310
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;
|