@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
- function handleItemClick(item: ActionItem, index: number, event: MouseEvent) {
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",