Skip to content

feat(menu): add hotkey prop for items & improve keyboard navigation#3978

Merged
adrianschmidt merged 1 commit intomainfrom
split/3-menu-hotkeys
Mar 30, 2026
Merged

feat(menu): add hotkey prop for items & improve keyboard navigation#3978
adrianschmidt merged 1 commit intomainfrom
split/3-menu-hotkeys

Conversation

@Kiarokh
Copy link
Copy Markdown
Contributor

@Kiarokh Kiarokh commented Mar 24, 2026

Summary by CodeRabbit

  • New Features

    • Menu items can declare hotkeys shown in item metadata (respects disabled state); command text now reflects to the DOM. Hotkeys are scoped to the open menu, support modifier-aware matching, and choose the first matching item when duplicates exist.
  • Examples

    • Added searchable-hotkeys example with per-result hotkeys and an “Apply all” action; updated demo trigger and layout.
  • Documentation

    • Expanded hotkey docs covering reserved keys, modifiers, scope, and platform notes.
  • Tests

    • Added e2e tests for hotkey selection, modifiers, search interaction, reserved/disabled behavior, and repeat-key handling.

Review:

  • Commits are atomic
  • Commits have the correct type for the changes made
  • Commits with breaking changes are marked as such

Browsers tested:

(Check any that applies, it's ok to leave boxes unchecked if testing something didn't seem relevant.)

Windows:

  • Chrome
  • Edge
  • Firefox

Linux:

  • Chrome
  • Firefox

macOS:

  • Chrome
  • Firefox
  • Safari

Mobile:

  • Chrome on Android
  • iOS

@Kiarokh Kiarokh requested a review from a team as a code owner March 24, 2026 21:04
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 24, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds first-class hotkey support across the menu system: new hotkey and reflected disabled props on menu items/meta, document-level keydown handling with normalization/matching against visible items, renderer/meta wiring to surface hotkeys, examples, and E2E tests for hotkey behavior.

Changes

Cohort / File(s) Summary
Types
src/components/menu/menu.types.ts
Add optional hotkey?: string to MenuItem<T> with JSDoc describing semantics and reserved-key rules.
Menu Core
src/components/menu/menu.tsx
Add document-level keydown handling (connected/disconnected callbacks), hotkey normalization/matching (with cache), visible-items memoization, reserved-key filtering, input gating, and replace previous Escape-capture path.
Renderer Wiring
src/components/menu/menu-list-renderer.tsx
Include item.hotkey in meta detection and forward hotkey and disabled (as boolean) to limel-menu-item-meta; remove prior 'commandText' in item check and avoid casting.
Meta Rendering
src/components/list-item/menu-item-meta/menu-item-meta.tsx
Reflect commandText; add reflected hotkey and reflected disabled props; render <limel-hotkey value=... disabled=... /> when hotkey normalizes to a truthy value, otherwise fall back to commandText.
Examples
src/components/menu/examples/menu-hotkeys.tsx, src/components/menu/examples/menu-searchable-hotkeys.tsx
Replace commandText-only example with hotkey-driven items and updated trigger; add searchable hotkeys example that assigns primary/apply-all hotkeys and demonstrates platform differences; update render structure.
E2E Tests
src/components/menu/menu.e2e.tsx
Add comprehensive hotkey test suite covering selection, reserved-key rejection, modified-key allowances, search-focused hotkeys, disabled-item suppression, duplicate-hotkey determinism, submenu scoping, and held-key suppression.

Sequence Diagram

sequenceDiagram
    participant User
    participant Document as Document (global)
    participant Menu as limel-menu
    participant Utils as HotkeyUtils
    participant Item as MenuItem (visible)

    User->>Document: keydown
    Document->>Menu: deliver keydown
    activate Menu
    Menu->>Menu: exit if closed / defaultPrevented / repeat
    Menu->>Menu: ignore if typing in input and no Ctrl/Meta
    Menu->>Utils: normalize hotkey from event
    Utils-->>Menu: normalizedHotkey
    Menu->>Item: compare normalizedHotkey to visible, enabled items (cached)
    alt matched
      Menu->>Document: preventDefault & stopPropagation
      Menu->>Menu: handleSelect(matched item)
      Menu-->>User: emit select, close menu
    else no match
      Menu-->>User: no action
    end
    deactivate Menu
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

maintenance

Suggested reviewers

  • devbymadde
  • omaralweli
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: adding a hotkey prop to menu items and enhancing keyboard navigation, matching the core functionality across multiple files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch split/3-menu-hotkeys

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

Documentation has been published to https://lundalogik.github.io/lime-elements/versions/PR-3978/

@adrianschmidt adrianschmidt force-pushed the split/2-hotkey-component branch 4 times, most recently from d0d36ff to 66462bb Compare March 26, 2026 10:33
@adrianschmidt adrianschmidt force-pushed the split/2-hotkey-component branch 2 times, most recently from 2db801c to bb97913 Compare March 30, 2026 07:37
Base automatically changed from split/2-hotkey-component to main March 30, 2026 08:01
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/list-item/menu-item-meta/menu-item-meta.tsx`:
- Around line 59-63: The hotkey string is passed raw into renderCommandText and
into <limel-hotkey>, so rendered shortcut hints can be incorrect; update
renderCommandText to normalize this.hotkey before rendering by calling the same
normalization utility used in src/components/menu/menu.tsx (or the normalization
function documented in menu.types.ts) and pass the normalized value to
<limel-hotkey> (keep the check on this.hotkey but use the normalizedHotkey when
rendering and consider handling empty/invalid results).

In `@src/components/menu/examples/menu-hotkeys.tsx`:
- Around line 18-20: The comment notes the documentation in menu-hotkeys.tsx
wrongly states arrow keys are "always reserved"; update the text to match the
implementation in menu.tsx which only rejects unmodified arrow keys and allows
modifier combinations (e.g. alt+ArrowDown) as verified by menu.e2e.tsx. Edit the
explanatory bullet to say that Tab and unmodified arrow keys are reserved, and
that Enter, Escape, and Space are reserved unless used with a modifier (and
explicitly mention that arrows can be used with modifiers like alt+arrowdown);
also add or update a short example in this file demonstrating a modifier+arrow
hotkey to follow the guideline for documenting new prop/behavior.

In `@src/components/menu/menu.tsx`:
- Around line 328-329: The hotkey interception currently treats any modifier
(including Alt/AltGraph) as blocking typing; update the modifier check used by
the keyboard handler (the call sites using this.isFromTextInput(event) &&
!this.hasModifier(event)) so that hasModifier only returns true for
event.ctrlKey or event.metaKey and explicitly rejects
event.getModifierState('AltGraph') and event.altKey; adjust the implementation
of hasModifier (or replace its use) to allow Alt/Option/AltGraph for text input
and ensure the same change is applied at the second occurrence around the other
keyboard handler (the block that mirrors the check at the later occurrence).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6dee05e4-adbb-48a2-9ac4-fa015f13cbee

📥 Commits

Reviewing files that changed from the base of the PR and between befd449 and d792051.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (7)
  • src/components/list-item/menu-item-meta/menu-item-meta.tsx
  • src/components/menu-list/menu-list-renderer.tsx
  • src/components/menu/examples/menu-hotkeys.tsx
  • src/components/menu/examples/menu-searchable-hotkeys.tsx
  • src/components/menu/menu.e2e.tsx
  • src/components/menu/menu.tsx
  • src/components/menu/menu.types.ts

Comment thread src/components/list-item/menu-item-meta/menu-item-meta.tsx Outdated
Comment thread src/components/menu/examples/menu-hotkeys.tsx Outdated
Comment on lines +328 to +329
if (this.isFromTextInput(event) && !this.hasModifier(event)) {
return;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/components/menu/menu.tsx | sed -n '320,340p'

Repository: Lundalogik/lime-elements

Length of output: 736


🏁 Script executed:

cat -n src/components/menu/menu.tsx | sed -n '375,390p'

Repository: Lundalogik/lime-elements

Length of output: 564


🏁 Script executed:

cat -n src/components/menu/menu.tsx | sed -n '320,345p'

Repository: Lundalogik/lime-elements

Length of output: 900


🏁 Script executed:

# Find the hasModifier method definition
rg -n "hasModifier" src/components/menu/menu.tsx -A 3

Repository: Lundalogik/lime-elements

Length of output: 467


🌐 Web query:

macOS Alt Option key text input special characters how it works

💡 Result:

On macOS, the Option key (also labeled Alt on some keyboards, symbolized as ⌥) acts as a modifier key similar to AltGr on other systems. It enables access to special characters, symbols, and diacritical marks when combined with other keys. This works primarily through direct character mappings and dead key functionality, varying by keyboard layout (e.g., US English ABC layout). Key mechanisms: - Direct symbols: Option + key produces a special character (e.g., Option + 2 = ™, Option + 8 = •, Option + - = –, Option + G = ©). - Option + Shift often produces variants (e.g., Option + Shift + 8 = °, Option + Shift + 2 = €, Option + Shift + - = —). - Dead keys for accents: Option + accent key (e.g., Option + e = acute ´), then letter (e.g., a → á). Orange-highlighted keys in Keyboard Viewer indicate dead keys. - Unicode Hex Input (optional): Add via System Settings > Keyboard > Text Input > Input Sources > + > Unicode Hex Input. Switch to it, then Option + hex code (e.g., Option + 00A9 = ©). To view mappings: 1. Enable Input menu: System Settings > Keyboard > Text Input > Edit > Show Input menu in menu bar. 2. Click Input menu icon > Show Keyboard Viewer. 3. Press Option (and/or Shift) on physical keyboard; viewer shows changed characters/orange dead keys. Examples (US layout): - Currency: Option + 3 = £, Option + Y = ¥, Option + Shift + 2 = €. - Punctuation/Math: Option + ; = …, Option + / = ÷, Option + X = ≈. - Accents: Option + then e = è, Option + i then a = â. Note: Exact output depends on input source/layout. Press-and-hold letters shows accent popups separately (not Option-specific). Use Character Viewer (Control + Command + Space) for any symbol. This is consistent across recent macOS versions including Tahoe 26.

Citations:


🌐 Web query:

AltGraph keyboard event how it reports in JavaScript ctrlKey altKey

💡 Result:

In JavaScript KeyboardEvent objects, the AltGraph (AltGr) key's reporting on ctrlKey and altKey varies by browser and platform but follows consistent patterns in modern browsers. When the AltGraph key is pressed alone or as a modifier: - event.key === "AltGraph" (key code 225, code: "AltRight", location: 2) - event.altKey is true across all major browsers (Chrome, Firefox, Safari, Edge) - event.ctrlKey is typically true in modern browsers (Chrome/WebKit, Firefox, Edge/IE10+), but was false in older Firefox and IE8 This is because AltGr is often implemented as a right Alt key that logically combines Alt + Ctrl effects for character input (e.g., € from AltGr+E), making it indistinguishable from simultaneous Ctrl+Alt in many cases. Implementations are encouraged to expose AltGraph separately via getModifierState("AltGraph"), which returns true specifically for AltGr (supported in Chrome 48+, Firefox 15+, Safari 10.1+). To detect AltGr reliably: - Check event.key === "AltGraph" for the key itself - Use event.ctrlKey && event.altKey && !event.getModifierState("Control") && !event.getModifierState("Alt") && event.getModifierState("AltGraph") for AltGr-modified keys (platform-dependent) - Note: No reliable way to distinguish physical AltGr from Ctrl+Alt in all cases due to OS-level synthesis (Windows treats AltGr as Ctrl+Alt). Example event dump for AltGraph press: {key: "AltGraph", code: "AltRight", location: 2, altKey: true, ctrlKey: false (varies), getModifierState("AltGraph"): true}

Citations:


🌐 Web query:

international keyboard layouts Alt modifier text input

💡 Result:

In international keyboard layouts, the Alt modifier (often the right Alt key, functioning as AltGr) enables text input of accented characters, special symbols, and diacritics, particularly for Latin-based languages. This is distinct from the left Alt used for shortcuts. Key mechanisms: - Right Alt/AltGr + key: Direct input of characters like € (AltGr+5), æ (AltGr+a in US-Intl), ç (AltGr+, in some layouts). - Dead keys: AltGr activates diacritics (e.g., AltGr+' then e → é), which combine with following letters without inserting the accent alone unless followed by space. Platform specifics: Windows (US-International layout): - Right Alt (AltGr) for symbols: AltGr+2=², AltGr+E=€, AltGr+N=ñ. - Dead keys on ', , ^, ~, " for accents (e.g., ' then e → é). Press twice or +space for literal accent. - Official: Microsoft docs confirm layout supports European languages via right Alt and dead keys. Linux (us variant altgr-intl or intl with AltGr dead keys): - setxkbmap us(altgr-intl): Hides dead keys behind AltGr (AltGr+' then e → é; normal ' types apostrophe). - Preferred for programmers: Avoids dead key interference in code. macOS (Option key ≡ Alt): - Option + key for dead keys: Option+e then a → é; Option+u then a → ä. - ABC - Extended layout: Comprehensive diacritics. - Press-and-hold vowels for accent menus. Common issues: - Shortcuts using right Alt may fail as it's AltGr. - Ctrl+Alt often emulates AltGr on Windows/Linux. To enable: - Windows: Settings > Time & Language > Language > Add English (US) > Options > Add keyboard > United States-International. - Linux: Settings or setxkbmap -layout us -variant altgr-intl. - macOS: System Settings > Keyboard > Input Sources > Add ABC Extended or target language.

Citations:


Restrict hotkey interception to Ctrl and Meta modifiers only; do not allow Alt/Option/AltGraph.

When typing in the searchable input, bare Alt/Option and AltGraph are essential for text input (special characters on macOS, accented letters and symbols on international layouts). Treating these as keyboard shortcuts causes accidental menu item selection. The proposed fix correctly removes event.altKey and explicitly rejects AltGraph, allowing only Ctrl and Meta combinations for hotkeys.

🛠️ Proposed fix
     private hasModifier(event: KeyboardEvent): boolean {
-        return event.ctrlKey || event.metaKey || event.altKey;
+        if (event.getModifierState?.('AltGraph')) {
+            return false;
+        }
+
+        return event.ctrlKey || event.metaKey;
     }

Also applies to: 381-382

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/menu/menu.tsx` around lines 328 - 329, The hotkey interception
currently treats any modifier (including Alt/AltGraph) as blocking typing;
update the modifier check used by the keyboard handler (the call sites using
this.isFromTextInput(event) && !this.hasModifier(event)) so that hasModifier
only returns true for event.ctrlKey or event.metaKey and explicitly rejects
event.getModifierState('AltGraph') and event.altKey; adjust the implementation
of hasModifier (or replace its use) to allow Alt/Option/AltGraph for text input
and ensure the same change is applied at the second occurrence around the other
keyboard handler (the block that mirrors the check at the later occurrence).

✅ Addressed in commit 0a67799

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai: Alt/Option and AltGraph ought to be usable as long as they are only used in combination with Ctrl and/or Meta though, right? (Just like Shift.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adrianschmidt Yes, that's correct! And it's already handled by the proposed fix — here's why:

The hasModifier check is used as a guard:

if (this.isFromTextInput(event) && !this.hasModifier(event)) {
    return; // let normal typing happen
}

With the proposed fix:

private hasModifier(event: KeyboardEvent): boolean {
    if (event.getModifierState?.('AltGraph')) {
        return false;
    }
    return event.ctrlKey || event.metaKey;
}
  • Alt+Ctrl+key: event.ctrlKey === truehasModifier returns true → the text-input guard is bypassed → hotkey fires. ✅
  • Alt+Meta+key: same via metaKey. ✅
  • Standalone Alt+key (no Ctrl/Meta): hasModifier returns false → the guard is not bypassed → normal text input proceeds (e.g. typing é or © on macOS). ✅

The only tricky edge case is AltGraph on Windows, which synthesizes ctrlKey: true in the KeyboardEvent. Without the getModifierState('AltGraph') check, pressing AltGr+e on an international layout (to type ) would look like a Ctrl combo and accidentally trigger a hotkey. The explicit check neutralizes that: AltGr-modified key events always return false from hasModifier, so they always fall back to normal text input even though ctrlKey appears true.

So the proposed fix is accurate as written — Alt in combination with Ctrl/Meta remains fully usable as a hotkey modifier, since those combinations still pass the hasModifier check.

(ᵔᴥᵔ)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might have been a mistake to make this change, because we are typically not listening to hotkeys when we're in text-fields… and single characters like o and p can be hotkeys too…

@adrianschmidt

This comment was marked as outdated.

@adrianschmidt adrianschmidt self-assigned this Mar 30, 2026
@adrianschmidt
Copy link
Copy Markdown
Contributor

CodeRabbit feedback and review recommendations addressed in 0a67799:

CodeRabbit findings:

  1. Normalize hotkey before renderingmenu-item-meta.tsx now calls normalizeHotkeyString() before passing to <limel-hotkey>, ensuring correct modifier ordering and alias resolution for display (e.g. "k+shift" renders as ⇧ k, not k ⇧).

  2. Arrow-key docs inaccurate — Updated the docs in menu-hotkeys.tsx to correctly state that arrow keys are reserved only without modifiers, not "always reserved". Modified arrows like alt+arrowdown are valid hotkeys.

  3. Alt/AltGraph modifier issuehasModifier() now only treats Ctrl and Meta as real modifiers for the text-input bypass, and explicitly rejects AltGraph. This prevents accidental hotkey activation when typing special characters on international keyboards (e.g. Option+e for é on macOS, AltGr+e for on Windows).

Our review recommendations:

  1. Remove (item as any) casts — Removed unnecessary casts in menu-list-renderer.tsx. After the separator guard, TypeScript narrows to MenuItem which has all the needed properties.

  2. Add disabled-item hotkey test — New e2e test verifies that a hotkey on a disabled menu item is correctly ignored.

  3. Memoize visibleItems — Added reference-equality cache for the sub-menu path to avoid re-allocating spread objects on every visibleItems access (which now happens on every keydown via findMenuItemByHotkey).

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@adrianschmidt

This comment was marked as outdated.

@adrianschmidt
Copy link
Copy Markdown
Contributor

Second review recommendations addressed in 9e8fdd9:

  1. [Low from Code Quality] hasModifier documented and tested — Added a 7-line comment on hasModifier explaining why Alt/AltGraph are excluded (international keyboard support), and a new test verifying alt+x does NOT fire while the search input is focused.

  2. [Low from Architecture] stopPropagation — Left as-is per reviewer judgement (hard to assess impact of changing).

  3. [Low from Code Quality] Sub-menu scoping and duplicate hotkey tests — Added two new tests:

    • Duplicate hotkeys: verifies first match wins when two items share the same hotkey.
    • Sub-menu scoping: verifies hotkeys only match against the currently visible menu level (child hotkey c doesn't fire at root; root item with same hotkey c fires instead).

@adrianschmidt

This comment was marked as outdated.

@adrianschmidt
Copy link
Copy Markdown
Contributor

Final review recommendations addressed in 0b21ff8:

Test fix: The does not trigger alt-only hotkeys while search input is focused test was failing because it dispatched the KeyboardEvent on limel-input-field (a custom element), but isFromTextInput checks composedPath() for native INPUT/TEXTAREA/SELECT tag names. Now dispatches from the native <input> inside limel-input-field's shadow DOM, matching real browser behavior.

Recommendations:

  1. (node as any)node instanceof HTMLElement — Replaced the cast in isFromTextInput with a proper instanceof check, which provides cleaner type narrowing and eliminates both the as any and the subsequent as HTMLElement cast.

  2. event.repeat suppression test — New test verifies that held-down keys (repeat: true) are correctly ignored by the hotkey handler.

  3. Docs fix — Corrected the example docs: tab is reserved "unless used with a modifier" (same rule as other navigation keys), not "always reserved". This matches the actual behavior in isReservedMenuHotkey.

Comment on lines +15 to +17
* 2. The `hotkey` works only when the menu is open. Adding a hotkey does not
* trigger the action which is tied to a menu item selectable from
* anywhere in the application.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure I only understand what this is trying to say because I already know how the hotkeys are supposed to work… 😅

I'll see if I can formulate it better, possibly with some AI help 🙂

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to:

  1. Hotkeys only work while the menu is open. They do not register
    global keyboard shortcuts — the shortcut has no effect when the
    menu is closed.

* anywhere in the application.
* 3. Some keys are reserved for menu interaction and will be ignored as hotkeys:
* `tab`, `enter`, `escape`, `space`, and arrow keys are reserved unless
* used with a modifier (e.g. `alt+enter`, `alt+arrowdown`).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The examples need updating, because alt is not recognised as a modifier, because it would interfere with text entry on macOS. See this CodeRabbit comment: #3978 (comment)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to ctrl+enter, and meta+arrowdown respectively. That showcases both ctrl and meta as well 😊

@adrianschmidt
Copy link
Copy Markdown
Contributor

adrianschmidt commented Mar 30, 2026

Consolidated PR Review -- 6 Dimensions (fourth review)

PR Summary

PR #3978 adds a hotkey property to the MenuItem interface, enabling keyboard shortcut selection while a menu is open. It includes a unified document-level capture-phase keydown handler, hotkey normalization with caching, AltGraph-safe text-input detection (documented), reserved key filtering, sub-menu item memoization, and ~435 lines of e2e tests covering 11 scenarios. 8 files changed, 893 additions, 32 deletions.


1. Backward Compatibility -- SAFE ✅

No backward compatibility concerns. All changes are additive or internal.

  • Positives: New hotkey?: string is fully optional. handleEscapeCapture merge preserves exact semantics. connectedCallback/disconnectedCallback fix a latent listener-leak bug. (item as any) removal improves type safety without runtime change. hasMeta simplification is semantically equivalent. API report correctly reflects all additions.

2. Code Quality -- GOOD ✅

Well-structured code with comprehensive test coverage. No blocking issues.

  • [Low] visibleItems getter mutates cachedSubMenuSource/cachedSubMenuItems as a memoization side effect. While functionally correct, a getter with side effects can be surprising. Consider a @Watch('currentSubMenu') handler or an explicit method instead.
  • [Low] isFromTextInput returns true for all <input> types including non-text inputs like checkbox, radio, range. In practice unlikely to matter for menus, but a false positive is possible.
  • Positives: 11 e2e tests covering all key scenarios (reserved keys, disabled items, duplicates, sub-menu scoping, event.repeat, alt-only from native input in shadow DOM). Clean instanceof HTMLElement narrowing. Well-documented hasModifier with AltGraph rationale. Thorough hotkey utility tests (264 lines). Type-safe renderer with (item as any) casts removed.

3. Architecture -- GOOD ✅

Sound design with clean separation of concerns.

  • [Low] Multiple simultaneously-open menus would both have active capture-phase handlers. First match calls stopPropagation; which menu "wins" depends on registration order. Unlikely in practice but worth acknowledging as a known limitation.
  • [Low] normalizedHotkeyCache is cleared on items change but not on searchResults change. Search result hotkeys accumulate in the cache. Functionally correct (same string always normalizes the same way) but an asymmetry in invalidation triggers.
  • [Low] commandText JSDoc on MenuItem doesn't mention that hotkey takes precedence when both are provided. The example demonstrates it but the type docs don't.
  • Positives: Clean three-layer separation (utility, display, event handling). Canonical normalization with event.code for layout independence. Proper listener lifecycle. isFromTextInput + hasModifier correctly handles searchable-menu scenarios. Reserved key filtering respects MDC contract. Sub-menu memoization well-placed.

4. Security -- SAFE ✅

No security issues found.

  • Positives: All rendering through Stencil JSX text interpolation. normalizeHotkeyString effectively sanitizes by decomposing to tokens, lowercasing, and reassembling. hotkeyFromKeyboardEvent uses strict regex on event.code. AltGraph correctly rejected. Disabled items skipped. Strict === comparison for matching. No innerHTML anywhere.

5. Observability -- SAFE ✅

No significant issues found.

  • Positives: Silent early returns appropriate for high-frequency event handler. Cache invalidation is explicit. Documentation-as-code in hasModifier comment aids future debugging.

6. Performance -- GOOD ✅

Well-optimized for the expected workload. One minor reordering opportunity.

  • [Low] isFromTextInput calls composedPath() (allocates an array) on every non-repeat keydown while the menu is open, including keys that will be rejected by later guards. Moving the isFromTextInput check after hotkeyFromKeyboardEvent and isReservedMenuHotkey would defer the allocation until a key might actually match a hotkey.
  • Positives: Handler only active while open. event.repeat rejected early. Normalization cache with proper invalidation. Sub-menu memoization via reference equality. Arrow-function class fields avoid closure re-allocation. isAppleDevice() cached in componentWillLoad.

Overall Verdicts

Dimension Verdict
Backward Compatibility SAFE
Code Quality GOOD
Architecture GOOD
Security SAFE
Observability SAFE
Performance GOOD

Top Recommendations

No high or medium severity issues. All remaining items are low severity / nice-to-have:

  1. [Low from Code Quality] Consider replacing the visibleItems getter's memoization side effect with a @Watch('currentSubMenu') handler or explicit method, to avoid the getter-with-side-effects pattern.
  2. [Low from Performance] Reorder guards in handleDocumentKeyDown to defer the composedPath() allocation: move isFromTextInput after hotkeyFromKeyboardEvent and isReservedMenuHotkey.
  3. [Low from Architecture] Add a note to commandText's JSDoc on MenuItem that hotkey takes precedence when both are provided.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/components/menu/examples/menu-hotkeys.tsx (1)

69-69: ⚠️ Potential issue | 🟡 Minor

alt is not recognized as a modifier—this hotkey won't work.

Per the component implementation, alt and altGraph are intentionally excluded from modifier detection to support international keyboard input on macOS (where Alt/Option is used for special characters). This example will not trigger as expected.

Consider using a different modifier combination, such as shift+m or ctrl+m.

🔧 Suggested fix
-                { text: 'Mute', hotkey: 'alt+m' },
+                { text: 'Mute', hotkey: 'ctrl+m' },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/menu/examples/menu-hotkeys.tsx` at line 69, The hotkey string
'alt+m' used for the menu item with text 'Mute' is invalid because the component
excludes 'alt'/'altGraph' as modifiers; update that menu item's hotkey (the
entry with text: 'Mute') to a supported modifier combination such as 'shift+m'
or 'ctrl+m' so the hotkey will be recognized and trigger as expected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/menu/examples/menu-searchable-hotkeys.tsx`:
- Line 144: Mark the private class field handleSelect as readonly to match
handleSearch and prevent accidental reassignment; change the declaration from
the current "private handleSelect = (…)" to "private readonly handleSelect =
(…)" so the callback is immutable and consistent with handleSearch's pattern in
the same component.

---

Duplicate comments:
In `@src/components/menu/examples/menu-hotkeys.tsx`:
- Line 69: The hotkey string 'alt+m' used for the menu item with text 'Mute' is
invalid because the component excludes 'alt'/'altGraph' as modifiers; update
that menu item's hotkey (the entry with text: 'Mute') to a supported modifier
combination such as 'shift+m' or 'ctrl+m' so the hotkey will be recognized and
trigger as expected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8d28c0f8-a8f8-48bb-a34a-f8ec3682cd16

📥 Commits

Reviewing files that changed from the base of the PR and between 0b21ff8 and e6762e5.

📒 Files selected for processing (2)
  • src/components/menu/examples/menu-hotkeys.tsx
  • src/components/menu/examples/menu-searchable-hotkeys.tsx

return [...menuItems, { separator: true }, applyAllMenuItem];
};

private handleSelect = (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Mark handleSelect as readonly for consistency.

The handleSearch callback is already marked as readonly. Apply the same pattern here for consistency and to prevent accidental reassignment.

♻️ Proposed fix
-    private handleSelect = (
+    private readonly handleSelect = (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private handleSelect = (
private readonly handleSelect = (
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 144-144: Member 'handleSelect' is never reassigned; mark it as readonly.

See more on https://sonarcloud.io/project/issues?id=Lundalogik_lime-elements&issues=AZ0hq9upshhUnMiqDkqH&open=AZ0hq9upshhUnMiqDkqH&pullRequest=3978

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/menu/examples/menu-searchable-hotkeys.tsx` at line 144, Mark
the private class field handleSelect as readonly to match handleSearch and
prevent accidental reassignment; change the declaration from the current
"private handleSelect = (…)" to "private readonly handleSelect = (…)" so the
callback is immutable and consistent with handleSearch's pattern in the same
component.

Copy link
Copy Markdown
Contributor

@adrianschmidt adrianschmidt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@adrianschmidt adrianschmidt enabled auto-merge (rebase) March 30, 2026 15:20
@adrianschmidt adrianschmidt merged commit 7d49081 into main Mar 30, 2026
9 of 10 checks passed
@adrianschmidt adrianschmidt deleted the split/3-menu-hotkeys branch March 30, 2026 15:24
@lime-opensource
Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 39.10.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants