Skip to content

Menu select hotkeys#3915

Closed
Kiarokh wants to merge 6 commits intomainfrom
menu-select-hotkeys
Closed

Menu select hotkeys#3915
Kiarokh wants to merge 6 commits intomainfrom
menu-select-hotkeys

Conversation

@Kiarokh
Copy link
Copy Markdown
Contributor

@Kiarokh Kiarokh commented Mar 2, 2026

fix https://github.com/Lundalogik/crm-client/issues/754

Summary by CodeRabbit

  • New Features

    • Global hotkey support: menus, selects, and options can declare hotkeys for quick keyboard selection.
    • New visual hotkey component that displays platform-aware glyphs and disabled state.
  • Style

    • Improved hotkey/kbd styling and compact example grid layout.
  • Examples & Documentation

    • New examples demonstrating hotkeys, searchable hotkeys, and select/menu integrations; docs updated with usage and reserved-key guidance.
  • Tests

    • Added unit and e2e tests for hotkey parsing and interaction.

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 2, 2026

Warning

Rate limit exceeded

@Kiarokh has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 45 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b57a246f-eb37-4914-a245-e24aa5eed4d0

📥 Commits

Reviewing files that changed from the base of the PR and between 6ff96ff and d8f1c47.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (4)
  • src/components/select/examples/select-hotkeys.tsx
  • src/components/select/option.types.ts
  • src/components/select/select.template.tsx
  • src/components/select/select.tsx
📝 Walkthrough

Walkthrough

Adds a hotkey system: utilities for tokenizing/normalizing hotkeys and mapping KeyboardEvent → hotkey; a presentational limel-hotkey component with styles and examples; device detection for platform-aware glyphs; and document-level hotkey handling integrated into Menu and Select with types, examples, and tests.

Changes

Cohort / File(s) Summary
Hotkey component & examples
src/components/hotkey/hotkey.tsx, src/components/hotkey/hotkey.scss, src/components/hotkey/examples/hotkey-basic.tsx, src/components/hotkey/examples/hotkey-basic.scss, src/components/hotkey/examples/hotkey-disabled.tsx
New presentational limel-hotkey element that renders tokenized hotkey strings as <kbd> elements, associated styles, and two example components (basic, disabled).
Hotkey utilities & tests
src/util/hotkeys.ts, src/util/hotkeys.spec.ts
New utilities: tokenizeHotkeyString, normalizeHotkeyString, hotkeyFromKeyboardEvent with canonical ordering and aliasing; comprehensive unit tests covering normalization and event mapping.
Device detection
src/util/device.ts
Refactored navigator access into safe getters and added isAppleDevice() to detect Apple platforms for glyph rendering.
Menu: types, behavior, examples, tests
src/components/menu/menu.types.ts, src/components/menu/menu.tsx, src/components/menu/menu-list-renderer.tsx, src/components/menu/menu.e2e.tsx, src/components/menu/examples/menu-hotkeys.tsx, src/components/menu/examples/menu-searchable-hotkeys.tsx
Added optional hotkey to MenuItem; document-level keydown listener while menu is open; normalization cache; input-context filtering and reserved-key rules; hotkey matching/select logic; updated examples and E2E tests for hotkey behavior.
Select: types, templates, behavior, example
src/components/select/option.types.ts, src/components/select/select.template.tsx, src/components/select/select.tsx, src/components/select/examples/select-hotkeys.tsx
Added optional hotkey to options; render hotkey as a primary component in menu items; document keydown handling when dropdown open; normalization/caching and hotkey-driven selection for single/multi-select.
List item meta
src/components/list-item/menu-item-meta/menu-item-meta.tsx
Made commandText reflected; added reflected hotkey and disabled props; renderCommandText() prefers rendering limel-hotkey when hotkey exists.
Keyboard styling partial
src/components/markdown/partial-styles/_kbd.scss
Adjusted kbd presentation (display, padding, border, box-shadow) and added ::first-letter capitalization.
Examples & layouts
src/components/hotkey/examples/hotkey-basic.scss, src/components/menu/examples/*, src/components/select/examples/*
Example layout styles and example components updated/added to demonstrate hotkey usage; searchable-menu example added.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Document as Document (keydown)
    participant HotkeyUtil as Hotkey Utilities
    participant Menu as Menu Component
    participant Item as MenuItem

    Note over User,Document: Global hotkey selection flow
    User->>Document: press key combo
    Document->>HotkeyUtil: hotkeyFromKeyboardEvent(event)
    HotkeyUtil-->>Document: normalized hotkey
    Document->>Menu: onDocumentKeydown(normalized hotkey)
    Menu->>Menu: find first visible, enabled item with matching normalized hotkey (cache lookup)
    alt match found
        Menu->>Item: select item (preventDefault, stopPropagation)
        Item-->>Menu: emits select
    else no match
        Document-->>User: no action
    end
Loading
sequenceDiagram
    participant Select as Select Component
    participant HotkeyCache as Normalized Hotkey Cache
    participant HotkeyUtil as Hotkey Utilities
    participant Device as Device Detection
    participant OptionList as Options

    Note over Select,OptionList: Select open-state hotkey handling
    Select->>Device: isAppleDevice()
    Device-->>Select: platform info
    OptionList->>Select: options (with hotkey)
    Select->>HotkeyCache: getNormalizedHotkey(option.hotkey)
    alt cache miss
        HotkeyCache->>HotkeyUtil: normalizeHotkeyString(option.hotkey)
        HotkeyUtil-->>HotkeyCache: normalized value
        HotkeyCache-->>Select: store & return
    else cache hit
        HotkeyCache-->>Select: return cached
    end
    Select->>Select: document keydown -> hotkeyFromKeyboardEvent -> find option -> select/toggle -> emit change
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • adrianschmidt
  • devbymadde
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "Menu select hotkeys" is partially related to the changeset. It refers to a real aspect of the PR (hotkey support for menus), but omits the equally significant select component changes, making it incomplete but not misleading.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch menu-select-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

github-actions Bot commented Mar 2, 2026

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

@Kiarokh Kiarokh force-pushed the menu-select-hotkeys branch from 1b3580a to 5f249c2 Compare March 13, 2026 13:53
@Kiarokh Kiarokh marked this pull request as ready for review March 13, 2026 14:43
@Kiarokh Kiarokh requested a review from a team as a code owner March 13, 2026 14:44
@Kiarokh Kiarokh marked this pull request as draft March 13, 2026 14:45
@Kiarokh Kiarokh force-pushed the menu-select-hotkeys branch from cd6eddb to 974b87d Compare March 13, 2026 14:53
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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/select/select.tsx (2)

218-264: ⚠️ Potential issue | 🔴 Critical

Remove or use the new menu-surface ref plumbing.

Line 218 wires setMenuSurfaceRef through, but Lines 262-264 drop the element on the floor. This already fails lint on Line 263, so CI stays red until the callback is deleted or the ref is actually stored and used.

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

In `@src/components/select/select.tsx` around lines 218 - 264, The
setMenuSurfaceRef passed down (setMenuSurfaceElement) is currently a no-op which
fails lint and drops the new ref plumbing; either remove the prop wiring or
implement the callback to store and use the element. Fix by adding a class
property (e.g., menuSurfaceElement: HTMLLimelMenuSurfaceElement | null) and have
setMenuSurfaceElement(_element) assign this.menuSurfaceElement = _element (and
perform any needed follow-up like syncing open state or z-index); if you choose
to remove the plumbing, delete the setMenuSurfaceRef prop usage where it is
passed so the unused callback is not declared. Ensure you update any code that
needs the stored ref (dropdownZIndex/anchor logic) to read from
menuSurfaceElement if applicable.

224-225: ⚠️ Potential issue | 🟡 Minor

The mobile guard disables hotkeys for custom multi-selects too.

Lines 224-225 only switch to the native <select> when this.isMobileDevice && !this.multiple, but Lines 399-405 bail out for every mobile device. A mobile multi-select therefore still renders the custom menu while hotkeys never work. If that is not intentional, gate on the same condition as native.

Suggested guard
-            this.isMobileDevice ||
+            (this.isMobileDevice && !this.multiple) ||

Also applies to: 399-405

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

In `@src/components/select/select.tsx` around lines 224 - 225, The mobile-only
guard that currently disables hotkeys on every mobile device should be changed
to match the same condition used for the native prop (this.isMobileDevice &&
!this.multiple) so hotkeys remain active for custom multi-selects; locate the
bail-out block that checks this.isMobileDevice (around the hotkey handling code
in the Select component) and change its condition to this.isMobileDevice &&
!this.multiple (or gate it by the same computed native value) so the component
only abandons custom hotkey logic when the native select is actually used.
🤖 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/hotkey/hotkey.tsx`:
- Around line 80-121: The switch handling key aliases should preserve actual
shortcut semantics: make the 'cmd'/'command' case behave the same as the 'meta'
case (use isApple to decide between the ⌘ glyph on macOS and a non-glyph label
on other platforms) instead of hardcoding ⌘, and split the
'delete'/'del'/'backspace' handling so on Apple devices 'delete' (forward
delete) renders as ⌦ and 'backspace' as ⌫ while non-Apple platforms keep "Del"
vs "Backspace" text; update the switch on the same lower variable and modify the
'cmd'/'command' and 'delete'/'del'/'backspace' cases accordingly.

In `@src/components/menu/examples/menu-hotkeys.tsx`:
- Around line 60-61: Replace the conflicting hotkey on the menu item with text
"Copy message" by changing its hotkey property (the object with text: 'Copy
message') from "meta+c" to a non-platform-conflicting combination such as
"meta+shift+c" (or "mod+shift+c") so it no longer collides with native
Cmd/Ctrl+C; update the hotkey value on that menu item object in the MenuHotkeys
examples accordingly.

In `@src/components/menu/examples/menu-searchable-hotkeys.tsx`:
- Around line 96-98: Mark the component's callback fields as readonly: change
the handler field declarations (for example the private handleSearch async arrow
function and the other callback handlers used in this file) to use the readonly
modifier so they cannot be reassigned; specifically update the field
declarations to be private readonly <handlerName> = (...) => ... (preserving
async/return types) for handleSearch and the other handler(s) referenced in this
component.
- Line 7: The example imports the internal helper isAppleDevice from
src/util/device which breaks the self-contained examples rule; remove that
import and either inline a small platform check (e.g. detect "Mac" in
navigator.platform or userAgent) directly inside the menu-searchable-hotkeys
example or move a shared helper into the examples folder and import it
relatively; update any references to isAppleDevice in this file to use the new
inline check or new local helper so the example only uses relative/example-local
code or public exports.

In `@src/components/menu/menu.tsx`:
- Line 249: The ref prop currently points to a no-op handler setSurfaceElement
which accepts an element but does nothing and causes an unused-argument lint;
remove the dead ref and the stub to fix this. Locate the setSurfaceElement
method and any usages (e.g., the ref={this.setSurfaceElement} at the Menu
component lines around where renderSurface is defined and the similar refs at
318-320) and delete the method and the ref attributes; if you plan to use the
surface element later, replace the ref with a meaningful handler or a
React.createRef() instance, otherwise simply drop both the ref attributes and
the setSurfaceElement function.
- Around line 583-596: This duplicate hotkey path bypasses the repeat-guard in
handleDocumentKeyDown; add the same event.repeat check (or route through the
shared hotkey-selection helper) before processing modifiers so repeated keydown
events don't re-enter handleSelect for lazy-loaded submenus—specifically, in the
block that computes hasModifier and calls hotkeyFromKeyboardEvent, early-return
if event.repeat is true (or call the existing helper used by
handleDocumentKeyDown) so findMenuItemByHotkey and handleSelect cannot be
invoked repeatedly while a key is held.

In `@src/components/select/examples/select-hotkeys.tsx`:
- Around line 64-68: The example option object with value 'closed' currently
uses a conflicting hotkey 'cmd+d'; update the object's hotkey property (the
entry where value === 'closed' / hotkey: 'cmd+d') to a neutral shortcut that
won't collide with browser/OS shortcuts (e.g., 'cmd+e' or 'cmd+n') so the
select-hotkeys example follows the project's hotkey guidance and functions
reliably across platforms.

---

Outside diff comments:
In `@src/components/select/select.tsx`:
- Around line 218-264: The setMenuSurfaceRef passed down (setMenuSurfaceElement)
is currently a no-op which fails lint and drops the new ref plumbing; either
remove the prop wiring or implement the callback to store and use the element.
Fix by adding a class property (e.g., menuSurfaceElement:
HTMLLimelMenuSurfaceElement | null) and have setMenuSurfaceElement(_element)
assign this.menuSurfaceElement = _element (and perform any needed follow-up like
syncing open state or z-index); if you choose to remove the plumbing, delete the
setMenuSurfaceRef prop usage where it is passed so the unused callback is not
declared. Ensure you update any code that needs the stored ref
(dropdownZIndex/anchor logic) to read from menuSurfaceElement if applicable.
- Around line 224-225: The mobile-only guard that currently disables hotkeys on
every mobile device should be changed to match the same condition used for the
native prop (this.isMobileDevice && !this.multiple) so hotkeys remain active for
custom multi-selects; locate the bail-out block that checks this.isMobileDevice
(around the hotkey handling code in the Select component) and change its
condition to this.isMobileDevice && !this.multiple (or gate it by the same
computed native value) so the component only abandons custom hotkey logic when
the native select is actually used.
🪄 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: 08786628-8049-43bf-9d03-23b75f550e70

📥 Commits

Reviewing files that changed from the base of the PR and between 4584cbb and 2952293.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (20)
  • src/components/hotkey/examples/hotkey-basic.scss
  • src/components/hotkey/examples/hotkey-basic.tsx
  • src/components/hotkey/examples/hotkey-disabled.tsx
  • src/components/hotkey/hotkey.scss
  • src/components/hotkey/hotkey.tsx
  • src/components/list-item/menu-item-meta/menu-item-meta.tsx
  • src/components/markdown/partial-styles/_kbd.scss
  • 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
  • src/components/select/examples/select-hotkeys.tsx
  • src/components/select/option.types.ts
  • src/components/select/select.template.tsx
  • src/components/select/select.tsx
  • src/util/device.ts
  • src/util/hotkeys.spec.ts
  • src/util/hotkeys.ts

Comment thread src/components/hotkey/hotkey.tsx
Comment thread src/components/menu/examples/menu-hotkeys.tsx
Comment thread src/components/menu/examples/menu-searchable-hotkeys.tsx Outdated
Comment thread src/components/menu/examples/menu-searchable-hotkeys.tsx
Comment thread src/components/menu/menu.tsx Outdated
Comment thread src/components/menu/menu.tsx Outdated
Comment thread src/components/select/examples/select-hotkeys.tsx
@Kiarokh Kiarokh force-pushed the menu-select-hotkeys branch 2 times, most recently from 8923b5b to 501f8f1 Compare March 13, 2026 15:02
@Kiarokh Kiarokh marked this pull request as ready for review March 13, 2026 15:03
@Kiarokh Kiarokh force-pushed the menu-select-hotkeys branch from 5348045 to b32fd91 Compare March 13, 2026 15:09
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: 4

🤖 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/menu.tsx`:
- Line 214: The field normalizedHotkeyCache is never reassigned, so mark it
readonly to signal immutability and satisfy static analysis; update the
declaration of normalizedHotkeyCache (the Map<string, string | null> field in
the menu component) to be readonly so it becomes: readonly normalizedHotkeyCache
= new Map<string, string | null>(); ensuring no other code attempts to reassign
the property.
- Around line 317-339: handleDocumentKeyDown is invoking hotkey handling even
when the user is typing into the menu's search input; update
handleDocumentKeyDown to early-return if the keyboard event originated from a
text-editable element by inspecting event.composedPath() or event.target and
checking for input, textarea, or elements with contentEditable=true (or a CSS
class/id of the menu search input). Keep the existing flow with
hotkeyFromKeyboardEvent, isReservedMenuHotkey, findMenuItemByHotkey, and
handleSelect unchanged—only add the input-origin guard at the top of
handleDocumentKeyDown so typing in the search field is not interpreted as a
hotkey.

In `@src/components/select/select.tsx`:
- Around line 394-437: Mark the event handler method handleDocumentKeyDown as
readonly and add a guard that ignores keyboard events originating from text
inputs or content-editable elements to future-proof against search fields;
specifically, change the declaration to a readonly property (private readonly
handleDocumentKeyDown = ...) and at the top of the function return early if the
event target (or first element in event.composedPath()) is an INPUT, TEXTAREA,
or an element with contentEditable not equal to "false" (or has role of
textbox), so hotkey handling only proceeds for non-text-entry elements.
- Line 128: The field normalizedHotkeyCache on the Select component is never
reassigned; mark it readonly to reflect immutability and satisfy static
analysis. Update the declaration of normalizedHotkeyCache (the Map<string,
string | null> instance) to be readonly so the compiler knows it won't be
reassigned, leaving mutation of the Map's contents unchanged.
🪄 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: 069840fe-8d24-402d-b1a0-f4cc3a08e8d0

📥 Commits

Reviewing files that changed from the base of the PR and between 2952293 and 501f8f1.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (8)
  • src/components/hotkey/examples/hotkey-basic.tsx
  • src/components/hotkey/examples/hotkey-disabled.tsx
  • src/components/hotkey/hotkey.tsx
  • src/components/menu/examples/menu-hotkeys.tsx
  • src/components/menu/menu.tsx
  • src/components/select/select.template.tsx
  • src/components/select/select.tsx
  • src/util/hotkeys.ts

Comment thread src/components/menu/menu.tsx Outdated
Comment thread src/components/menu/menu.tsx
Comment thread src/components/select/select.tsx Outdated
Comment thread src/components/select/select.tsx Outdated
@Kiarokh Kiarokh force-pushed the menu-select-hotkeys branch from 0e65414 to fe74f8e Compare March 13, 2026 15:28
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.

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

60-61: ⚠️ Potential issue | 🟡 Minor

Replace meta+c with a non-conflicting hotkey.

The documentation at lines 33-37 explicitly warns against using hotkeys that conflict with browser/OS shortcuts. meta+c maps to Cmd+C (macOS copy) and Win+C (Windows), both platform-reserved shortcuts. Consider using meta+shift+c or another modifier combination instead.

💡 Suggested fix
-        { text: 'Copy message', hotkey: 'meta+c' },
+        { text: 'Copy message', hotkey: 'meta+shift+c' },

,

🤖 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` around lines 60 - 61, The
hotkey "meta+c" assigned to the menu item with text "Copy message" conflicts
with system/browser copy; locate the menu items array in menu-hotkeys.tsx (the
entries containing { text: 'Copy link', hotkey: 'l' } and { text: 'Copy
message', hotkey: 'meta+c' }) and replace the 'meta+c' string with a
non-conflicting combo such as 'meta+shift+c' (or another modifier combo you
prefer) so the "Copy message" hotkey no longer overrides the platform copy
shortcut.
src/components/menu/examples/menu-searchable-hotkeys.tsx (1)

99-101: 🧹 Nitpick | 🔵 Trivial

Mark handler fields as readonly.

Per static analysis, these arrow function handlers are never reassigned.

♻️ Suggested fix
-    private handleSearch = async (
+    private readonly handleSearch = async (
         queryString: string
     ): Promise<Array<MenuItem | ListSeparator>> => {
-    private handleSelect = (
+    private readonly handleSelect = (
         event: LimelMenuCustomEvent<MenuItem<SuggestionValue>>
     ) => {

,

Also applies to: 144-146

🤖 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` around lines 99 -
101, Mark arrow-function handler fields as readonly to reflect that they are
never reassigned: change the class field declaration for handleSearch (private
handleSearch = async (...)) to private readonly handleSearch = async (...) and
do the same for the other arrow-function handler field in this file (the
secondary handler defined after handleSearch). This is a simple modifier
addition on the class field declarations (add readonly to those private handler
fields) and requires no other logic changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/components/menu/examples/menu-hotkeys.tsx`:
- Around line 60-61: The hotkey "meta+c" assigned to the menu item with text
"Copy message" conflicts with system/browser copy; locate the menu items array
in menu-hotkeys.tsx (the entries containing { text: 'Copy link', hotkey: 'l' }
and { text: 'Copy message', hotkey: 'meta+c' }) and replace the 'meta+c' string
with a non-conflicting combo such as 'meta+shift+c' (or another modifier combo
you prefer) so the "Copy message" hotkey no longer overrides the platform copy
shortcut.

In `@src/components/menu/examples/menu-searchable-hotkeys.tsx`:
- Around line 99-101: Mark arrow-function handler fields as readonly to reflect
that they are never reassigned: change the class field declaration for
handleSearch (private handleSearch = async (...)) to private readonly
handleSearch = async (...) and do the same for the other arrow-function handler
field in this file (the secondary handler defined after handleSearch). This is a
simple modifier addition on the class field declarations (add readonly to those
private handler fields) and requires no other logic changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b8879f6b-def2-45c4-a2ab-ad79a75d524c

📥 Commits

Reviewing files that changed from the base of the PR and between 501f8f1 and 0e65414.

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

@Lundalogik Lundalogik deleted a comment from adrianschmidt-bot Mar 16, 2026
@Kiarokh Kiarokh force-pushed the menu-select-hotkeys branch from fe74f8e to 60f0590 Compare March 18, 2026 10:53
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: 2

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

61-61: ⚠️ Potential issue | 🟡 Minor

Replace meta+c with a non-conflicting hotkey.

This hotkey conflicts with platform copy shortcuts (Cmd+C on macOS, Win+C on Windows), which contradicts the warning in the documentation at lines 33-38.

🛠️ Suggested fix
-        { text: 'Copy message', hotkey: 'meta+c' },
+        { text: 'Copy message', hotkey: 'meta+shift+c' },
🤖 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 61, The menu example
currently assigns the hotkey string 'meta+c' to the menu item with text 'Copy
message' which conflicts with platform copy shortcuts; update that hotkey value
(the hotkey property on the menu item object) to a non-conflicting combination
such as 'mod+shift+c' (or another app-specific chord) in
src/components/menu/examples/menu-hotkeys.tsx so it no longer uses 'meta+c' and
matches the docs' warning.
🤖 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-list/menu-list-renderer.tsx`:
- Around line 108-121: Remove the unnecessary (item as any) casts and use the
typed MenuItem properties directly: replace occurrences in the hasHotkey
calculation and in the primaryComponent props (commandText, hotkey, disabled) so
you read item.hotkey, item.commandText and item.disabled directly; ensure the
local variable/item is the narrowed MenuItem (as already established by the
separator check) and update hasMeta/primaryComponent to rely on those typed
properties rather than casting.

In `@src/components/select/select.tsx`:
- Around line 230-244: The component currently only attaches the document
keydown listener inside the `@Watch`('menuOpen') handler (watchOpen) so if the
component mounts with menuOpen === true the listener is never added; update the
component's connectedCallback to check this.menuOpen and, if true, add the
listener via document.addEventListener('keydown', this.handleDocumentKeyDown,
true), and ensure the listener is removed in disconnectedCallback
(document.removeEventListener(...)) to mirror the pattern used in menu.tsx and
keep behavior consistent with watchOpen.

---

Duplicate comments:
In `@src/components/menu/examples/menu-hotkeys.tsx`:
- Line 61: The menu example currently assigns the hotkey string 'meta+c' to the
menu item with text 'Copy message' which conflicts with platform copy shortcuts;
update that hotkey value (the hotkey property on the menu item object) to a
non-conflicting combination such as 'mod+shift+c' (or another app-specific
chord) in src/components/menu/examples/menu-hotkeys.tsx so it no longer uses
'meta+c' and matches the docs' warning.
🪄 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: b4107755-bc6c-426b-b354-d0949a324f98

📥 Commits

Reviewing files that changed from the base of the PR and between 0e65414 and 60f0590.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (20)
  • src/components/hotkey/examples/hotkey-basic.scss
  • src/components/hotkey/examples/hotkey-basic.tsx
  • src/components/hotkey/examples/hotkey-disabled.tsx
  • src/components/hotkey/hotkey.scss
  • src/components/hotkey/hotkey.tsx
  • src/components/list-item/menu-item-meta/menu-item-meta.tsx
  • src/components/markdown/partial-styles/_kbd.scss
  • 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
  • src/components/select/examples/select-hotkeys.tsx
  • src/components/select/option.types.ts
  • src/components/select/select.template.tsx
  • src/components/select/select.tsx
  • src/util/device.ts
  • src/util/hotkeys.spec.ts
  • src/util/hotkeys.ts

Comment on lines +108 to +121
const hasHotkey = 'hotkey' in item && !!(item as any).hotkey;
const hasMeta =
hasSubMenu ||
item.badge !== undefined ||
hasHotkey ||
(!!('commandText' in item) && !!item.commandText);

const primaryComponent = hasMeta
? {
name: 'limel-menu-item-meta',
props: {
commandText: (item as any).commandText,
hotkey: (item as any).hotkey,
disabled: !!item.disabled,
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

Consider using proper types instead of (item as any) casts.

The MenuItem interface already declares hotkey?: string (see src/components/menu/menu.types.ts:58-76), so the type casts are unnecessary. The item variable at this point is already typed as MenuItem after the separator check.

♻️ Proposed refactor to remove type casts
-        const hasHotkey = 'hotkey' in item && !!(item as any).hotkey;
+        const hasHotkey = !!item.hotkey;
         const hasMeta =
             hasSubMenu ||
             item.badge !== undefined ||
             hasHotkey ||
-            (!!('commandText' in item) && !!item.commandText);
+            !!item.commandText;

         const primaryComponent = hasMeta
             ? {
                   name: 'limel-menu-item-meta',
                   props: {
-                      commandText: (item as any).commandText,
-                      hotkey: (item as any).hotkey,
+                      commandText: item.commandText,
+                      hotkey: item.hotkey,
                       disabled: !!item.disabled,
-                      badge: (item as any).badge,
+                      badge: item.badge,
                       showChevron: hasSubMenu,
                   },
               }
             : undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/menu-list/menu-list-renderer.tsx` around lines 108 - 121,
Remove the unnecessary (item as any) casts and use the typed MenuItem properties
directly: replace occurrences in the hasHotkey calculation and in the
primaryComponent props (commandText, hotkey, disabled) so you read item.hotkey,
item.commandText and item.disabled directly; ensure the local variable/item is
the narrowed MenuItem (as already established by the separator check) and update
hasMeta/primaryComponent to rely on those typed properties rather than casting.

Comment thread src/components/select/select.tsx
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: 2

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

60-61: ⚠️ Potential issue | 🟡 Minor

Replace meta+c with a non-conflicting hotkey.

The documentation at lines 33-37 explicitly warns against using hotkeys that conflict with browser/OS shortcuts. meta+c maps to Cmd+C (copy) on macOS, which directly contradicts this guidance.

Consider using meta+shift+c or another modifier combination instead.

🤖 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` around lines 60 - 61, Replace
the conflicting hotkey 'meta+c' in the menu item for "Copy message" with a
non-conflicting combination (e.g., 'meta+shift+c'); locate the menu items array
where entries include { text: 'Copy link', hotkey: 'l' } and { text: 'Copy
message', hotkey: 'meta+c' } and update the second entry's hotkey string to
'meta+shift+c' (or another modifier+key that won't conflict with native
Cmd/Ctrl+C).
src/components/select/examples/select-hotkeys.tsx (1)

64-69: ⚠️ Potential issue | 🟡 Minor

Replace cmd+d with a non-conflicting hotkey.

Line 31 explicitly warns against using hotkeys that conflict with browser shortcuts. cmd+d maps to Cmd+D on macOS which bookmarks the page in Chrome, Firefox, and Safari.

Consider using cmd+shift+d or another combination instead.

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

In `@src/components/select/examples/select-hotkeys.tsx` around lines 64 - 69,
Hotkey "cmd+d" in the select example conflicts with browser bookmark shortcuts;
update the hotkey property in the example object (the item with value 'closed'
in src/components/select/examples/select-hotkeys.tsx) to a non-conflicting combo
such as 'cmd+shift+d' (or better use a platform-agnostic token like
'mod+shift+d' if your hotkey system supports it) and ensure the object's key
remains hotkey so the example reflects the new binding.
🤖 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/select/examples/select-hotkeys.tsx`:
- Line 42: The class has members that are never reassigned—mark the property
options and the handleChange arrow method as readonly to satisfy static
analysis: update the declaration of options (type (Option | ListSeparator)[]) to
readonly and prepend readonly to the handleChange class field declaration so
both are immutable class members (apply the same change for the other occurrence
referenced as handleChange).

In `@src/util/hotkeys.spec.ts`:
- Around line 20-158: Add a unit test to verify canonical modifier ordering for
hotkeyFromKeyboardEvent: create a KeyboardEvent-like object with metaKey,
ctrlKey, altKey, and shiftKey all true and key/code for 'a' (code 'KeyA'), call
hotkeyFromKeyboardEvent and assert the result equals 'meta+ctrl+alt+shift+a';
place this new it(...) alongside the other hotkeyFromKeyboardEvent tests so the
canonical ordering is explicitly validated.

---

Duplicate comments:
In `@src/components/menu/examples/menu-hotkeys.tsx`:
- Around line 60-61: Replace the conflicting hotkey 'meta+c' in the menu item
for "Copy message" with a non-conflicting combination (e.g., 'meta+shift+c');
locate the menu items array where entries include { text: 'Copy link', hotkey:
'l' } and { text: 'Copy message', hotkey: 'meta+c' } and update the second
entry's hotkey string to 'meta+shift+c' (or another modifier+key that won't
conflict with native Cmd/Ctrl+C).

In `@src/components/select/examples/select-hotkeys.tsx`:
- Around line 64-69: Hotkey "cmd+d" in the select example conflicts with browser
bookmark shortcuts; update the hotkey property in the example object (the item
with value 'closed' in src/components/select/examples/select-hotkeys.tsx) to a
non-conflicting combo such as 'cmd+shift+d' (or better use a platform-agnostic
token like 'mod+shift+d' if your hotkey system supports it) and ensure the
object's key remains hotkey so the example reflects the new binding.
🪄 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: b644b11a-ed62-4249-ad5a-b71d824db55e

📥 Commits

Reviewing files that changed from the base of the PR and between 60f0590 and 90bcf61.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (20)
  • src/components/hotkey/examples/hotkey-basic.scss
  • src/components/hotkey/examples/hotkey-basic.tsx
  • src/components/hotkey/examples/hotkey-disabled.tsx
  • src/components/hotkey/hotkey.scss
  • src/components/hotkey/hotkey.tsx
  • src/components/list-item/menu-item-meta/menu-item-meta.tsx
  • src/components/markdown/partial-styles/_kbd.scss
  • 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
  • src/components/select/examples/select-hotkeys.tsx
  • src/components/select/option.types.ts
  • src/components/select/select.template.tsx
  • src/components/select/select.tsx
  • src/util/device.ts
  • src/util/hotkeys.spec.ts
  • src/util/hotkeys.ts

@State()
private value: Option;

private options: (Option | ListSeparator)[] = [
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 class members as readonly per static analysis.

The options array and handleChange arrow function are never reassigned.

🔧 Suggested fix
-    private options: (Option | ListSeparator)[] = [
+    private readonly options: (Option | ListSeparator)[] = [
-    private handleChange = (event: LimelSelectCustomEvent<Option>) => {
+    private readonly handleChange = (event: LimelSelectCustomEvent<Option>) => {

Also applies to: 86-86

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=Lundalogik_lime-elements&issues=AZyvA9GSODg-M_GK9-_b&open=AZyvA9GSODg-M_GK9-_b&pullRequest=3915

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

In `@src/components/select/examples/select-hotkeys.tsx` at line 42, The class has
members that are never reassigned—mark the property options and the handleChange
arrow method as readonly to satisfy static analysis: update the declaration of
options (type (Option | ListSeparator)[]) to readonly and prepend readonly to
the handleChange class field declaration so both are immutable class members
(apply the same change for the other occurrence referenced as handleChange).

Comment thread src/util/hotkeys.spec.ts
Comment on lines +20 to +158
describe('hotkeyFromKeyboardEvent', () => {
it('includes multiple modifiers', () => {
const pressed = hotkeyFromKeyboardEvent({
key: 'a',
code: 'KeyA',
altKey: true,
ctrlKey: true,
metaKey: false,
shiftKey: false,
} as any);

expect(pressed).toBe('ctrl+alt+a');
});

it('handles function keys', () => {
expect(
hotkeyFromKeyboardEvent({
key: 'F1',
code: 'F1',
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
} as any)
).toBe('f1');

expect(
hotkeyFromKeyboardEvent({
key: 'F12',
code: 'F12',
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
} as any)
).toBe('f12');
});

it('handles arrow keys', () => {
expect(
hotkeyFromKeyboardEvent({
key: 'ArrowLeft',
code: 'ArrowLeft',
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
} as any)
).toBe('arrowleft');

expect(
hotkeyFromKeyboardEvent({
key: 'ArrowRight',
code: 'ArrowRight',
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
} as any)
).toBe('arrowright');
});

it('uses event.code for non-QWERTY letter normalization', () => {
const pressed = hotkeyFromKeyboardEvent({
key: '@',
code: 'KeyQ',
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
} as any);

// Uses code "KeyQ" to normalize to "q" (layout-independent)
expect(pressed).toBe('q');
});

it('uses event.code for digit normalization', () => {
const pressed = hotkeyFromKeyboardEvent({
key: ')',
code: 'Digit0',
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
} as any);

// Uses code "Digit0" to normalize to "0" (layout-independent)
expect(pressed).toBe('0');
});

it('does not include shift for the + key', () => {
const pressed = hotkeyFromKeyboardEvent({
key: '+',
code: 'Equal',
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: true,
} as any);

expect(pressed).toBe('+');
});

it('still includes shift for letter keys', () => {
const pressed = hotkeyFromKeyboardEvent({
key: 'A',
code: 'KeyA',
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: true,
} as any);

expect(pressed).toBe('shift+a');
});

it('keeps Backspace and Delete as distinct keys', () => {
const backspace = hotkeyFromKeyboardEvent({
key: 'Backspace',
code: 'Backspace',
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
} as any);

const del = hotkeyFromKeyboardEvent({
key: 'Delete',
code: 'Delete',
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
} as any);

expect(backspace).toBe('backspace');
expect(del).toBe('delete');
});
});
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

Consider adding a test for canonical modifier ordering.

The hotkeyFromKeyboardEvent tests are comprehensive, but there's no explicit test verifying that modifiers are output in canonical order (meta, ctrl, alt, shift). Consider adding a test case with multiple modifiers to confirm ordering:

it('outputs modifiers in canonical order', () => {
    const pressed = hotkeyFromKeyboardEvent({
        key: 'a',
        code: 'KeyA',
        altKey: true,
        ctrlKey: true,
        metaKey: true,
        shiftKey: true,
    } as any);

    expect(pressed).toBe('meta+ctrl+alt+shift+a');
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/hotkeys.spec.ts` around lines 20 - 158, Add a unit test to verify
canonical modifier ordering for hotkeyFromKeyboardEvent: create a
KeyboardEvent-like object with metaKey, ctrlKey, altKey, and shiftKey all true
and key/code for 'a' (code 'KeyA'), call hotkeyFromKeyboardEvent and assert the
result equals 'meta+ctrl+alt+shift+a'; place this new it(...) alongside the
other hotkeyFromKeyboardEvent tests so the canonical ordering is explicitly
validated.

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: 5

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

64-69: ⚠️ Potential issue | 🟡 Minor

Replace cmd+d with a non-conflicting hotkey.

The documentation at lines 24-32 explicitly warns against using hotkeys that conflict with common browser shortcuts, yet cmd+d directly conflicts with the bookmark shortcut on macOS browsers (Cmd+D) and Windows desktop (Win+D shows desktop).

Consider using cmd+shift+d or alt+d instead.

🔧 Suggested fix
         {
             text: 'Closed',
             secondaryText: 'as not planned',
             value: 'closed',
-            hotkey: 'cmd+d',
+            hotkey: 'alt+d',
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/select/examples/select-hotkeys.tsx` around lines 64 - 69, The
hotkey 'cmd+d' in the select option object (the entry with text 'Closed',
secondaryText 'as not planned', value 'closed', hotkey: 'cmd+d') conflicts with
common browser shortcuts; update that object's hotkey value to a non-conflicting
combo such as 'cmd+shift+d' or 'alt+d' (i.e., modify the hotkey property on the
'Closed' option to the chosen alternative).
src/components/menu/examples/menu-hotkeys.tsx (1)

60-61: ⚠️ Potential issue | 🟡 Minor

Replace meta+c with a non-conflicting hotkey.

The documentation at lines 33-38 warns against hotkeys conflicting with built-in browser shortcuts, yet meta+c maps to Cmd+C (copy) on macOS and Win+C on Windows—both claimed by the platform.

🔧 Suggested fix
         { text: 'Copy link', hotkey: 'l' },
-        { text: 'Copy message', hotkey: 'meta+c' },
+        { text: 'Copy message', hotkey: 'meta+shift+c' },
🤖 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` around lines 60 - 61, Replace
the conflicting hotkey string 'meta+c' used for the "Copy message" menu item
with a non-conflicting accelerator; locate the menu item object in
menu-hotkeys.tsx (the entry with text: 'Copy message' and hotkey: 'meta+c') and
change its hotkey to something that won’t override platform copy (for example
use a letters-only or modifier combination that isn’t reserved), updating any
associated hotkey handlers to match the new key string.
src/components/hotkey/hotkey.tsx (1)

80-121: ⚠️ Potential issue | 🟠 Major

Keep alias rendering aligned with normalized hotkey semantics.

cmd/command are normalized to the same key as meta, but this switch still hardcodes , so the same shortcut renders differently on non-Apple devices depending on which alias the caller used. The Apple delete branch also collapses delete and backspace into the same glyph, which makes forward-delete hints incorrect.

♻️ Suggested fix
             case 'cmd':
             case 'command': {
-                return { display: '⌘', isGlyph: true };
+                return isApple
+                    ? { display: '⌘', isGlyph: true }
+                    : { display: '⊞ Win', isGlyph: false };
             }
@@
             case 'delete':
             case 'del':
             case 'backspace': {
                 if (isApple) {
-                    return { display: '⌫', isGlyph: true };
+                    return lower === 'backspace'
+                        ? { display: '⌫', isGlyph: true }
+                        : { display: '⌦', isGlyph: true };
                 }
                 return lower === 'backspace'
                     ? { display: 'Backspace', isGlyph: false }
                     : { display: 'Del', isGlyph: false };
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/hotkey/hotkey.tsx` around lines 80 - 121, The switch handling
normalized key aliases should render aliases consistently: change the
'cmd'/'command' case so it returns the same value as the 'meta' case (i.e.,
respect isApple and return the same object used for 'meta' rather than
hardcoding '⌘'), and change the delete/del/backspace logic so on Apple platforms
you do not collapse delete and backspace into the same glyph — return '⌫'
(backspace) when lower === 'backspace' and the forward-delete glyph (e.g., '⌦')
when lower === 'delete' (and keep the non-Apple text branches for 'Backspace' vs
'Del'); update the switch cases around lower, 'meta', 'cmd', 'command', and
'delete'/'del'/'backspace' accordingly.
🤖 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/markdown/partial-styles/_kbd.scss`:
- Around line 31-33: The kbd::first-letter rule (&::first-letter {
text-transform: uppercase; }) is redundant or ineffective for limel-hotkey
because its text is wrapped in a span; decide whether to keep it for raw
markdown kbd elements or remove/relocate it: if the intent is to style only
markdown kbd, restrict the selector to target kbd elements that contain text
(e.g., kbd:not(:has(> span)) or simply keep it but document scope); if the
intent is to style hotkey spans, remove this rule from _kbd.scss and apply
text-transform to the hotkey-specific selector in hotkey.scss (e.g.,
.limel-hotkey span or the existing span selector) so the first-letter transform
actually affects rendered text.

In `@src/components/menu/menu.tsx`:
- Around line 345-373: The isFromTextInput method currently only checks
contentEditable and native INPUT/TEXTAREA/SELECT elements; update it to also
detect elements that act as text inputs by checking each path element's role
attribute for "textbox" (and optionally "search" or other relevant ARIA roles)
and return true when found; locate the isFromTextInput( event: KeyboardEvent )
function and add a role check using element.getAttribute('role') === 'textbox'
(and consider aria-multiline handling) so hotkeys won't trigger while typing in
custom accessible text components.

In `@src/components/select/select.tsx`:
- Around line 417-431: The multiselect branch in the toggle code (inside the if
(this.multiple) block that computes currentValue/nextValue and calls
this.change.emit(nextValue)) skips the scroll-preserving logic used by
handleMenuChange(); update this branch to snapshot the menu surface scrollTop
(the same .mdc-menu-surface.scrollTop read used in handleMenuChange()), emit the
change while keeping the saved scroll position, then restore the scrollTop after
emitting so the open menu doesn't jump to the top; locate symbols matchedOption,
isMultiple, this.value, this.change.emit and reuse the exact snapshot/restore
calls used in handleMenuChange().

In `@src/util/hotkeys.spec.ts`:
- Around line 20-32: Add a unit test to verify full canonical modifier ordering
for hotkeyFromKeyboardEvent: create a case calling hotkeyFromKeyboardEvent with
all four modifier booleans true (metaKey, ctrlKey, altKey, shiftKey) and key
'a'/'KeyA' and assert the returned string equals 'meta+ctrl+alt+shift+a' so the
function's canonical ordering is explicitly validated.

In `@src/util/hotkeys.ts`:
- Around line 146-193: normalizeHotkeyString currently preserves the shift
modifier when the parsed non-modifier key is '+' which is inconsistent with
hotkeyFromKeyboardEvent (which drops shift for '+'), causing bindings like
"shift++" to never match; update normalizeHotkeyString so that after determining
key (the variable named key) you detect the literal '+' and set shift = false
(or reject the combo) before calling normalizeModifiersAndKey, mirroring
hotkeyFromKeyboardEvent’s handling; also apply the same change to the analogous
logic block used elsewhere in this file (the other normalizeHotkeyString-like
path around the second occurrence noted in the review).

---

Duplicate comments:
In `@src/components/hotkey/hotkey.tsx`:
- Around line 80-121: The switch handling normalized key aliases should render
aliases consistently: change the 'cmd'/'command' case so it returns the same
value as the 'meta' case (i.e., respect isApple and return the same object used
for 'meta' rather than hardcoding '⌘'), and change the delete/del/backspace
logic so on Apple platforms you do not collapse delete and backspace into the
same glyph — return '⌫' (backspace) when lower === 'backspace' and the
forward-delete glyph (e.g., '⌦') when lower === 'delete' (and keep the non-Apple
text branches for 'Backspace' vs 'Del'); update the switch cases around lower,
'meta', 'cmd', 'command', and 'delete'/'del'/'backspace' accordingly.

In `@src/components/menu/examples/menu-hotkeys.tsx`:
- Around line 60-61: Replace the conflicting hotkey string 'meta+c' used for the
"Copy message" menu item with a non-conflicting accelerator; locate the menu
item object in menu-hotkeys.tsx (the entry with text: 'Copy message' and hotkey:
'meta+c') and change its hotkey to something that won’t override platform copy
(for example use a letters-only or modifier combination that isn’t reserved),
updating any associated hotkey handlers to match the new key string.

In `@src/components/select/examples/select-hotkeys.tsx`:
- Around line 64-69: The hotkey 'cmd+d' in the select option object (the entry
with text 'Closed', secondaryText 'as not planned', value 'closed', hotkey:
'cmd+d') conflicts with common browser shortcuts; update that object's hotkey
value to a non-conflicting combo such as 'cmd+shift+d' or 'alt+d' (i.e., modify
the hotkey property on the 'Closed' option to the chosen alternative).
🪄 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: 7534572c-d3e4-4045-a561-a6a71a7ee435

📥 Commits

Reviewing files that changed from the base of the PR and between 90bcf61 and 2d151c9.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (20)
  • src/components/hotkey/examples/hotkey-basic.scss
  • src/components/hotkey/examples/hotkey-basic.tsx
  • src/components/hotkey/examples/hotkey-disabled.tsx
  • src/components/hotkey/hotkey.scss
  • src/components/hotkey/hotkey.tsx
  • src/components/list-item/menu-item-meta/menu-item-meta.tsx
  • src/components/markdown/partial-styles/_kbd.scss
  • 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
  • src/components/select/examples/select-hotkeys.tsx
  • src/components/select/option.types.ts
  • src/components/select/select.template.tsx
  • src/components/select/select.tsx
  • src/util/device.ts
  • src/util/hotkeys.spec.ts
  • src/util/hotkeys.ts

Comment on lines +31 to 33
&::first-letter {
text-transform: uppercase;
}
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

Potential style redundancy with hotkey.scss.

The &::first-letter { text-transform: uppercase; } rule here applies to kbd elements. However, hotkey.scss (which forwards this partial) also applies the same transform to span elements inside the hotkey component (lines 22-27).

Since limel-hotkey renders text inside <span> within <kbd>, the transform on kbd::first-letter may not have the intended effect (CSS ::first-letter applies to the first letter of the element's text content, but if the text is wrapped in a span, the kbd::first-letter won't match).

Consider whether this rule is intended for markdown kbd elements only, or if it should be removed since hotkey-specific styling handles it differently.

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

In `@src/components/markdown/partial-styles/_kbd.scss` around lines 31 - 33, The
kbd::first-letter rule (&::first-letter { text-transform: uppercase; }) is
redundant or ineffective for limel-hotkey because its text is wrapped in a span;
decide whether to keep it for raw markdown kbd elements or remove/relocate it:
if the intent is to style only markdown kbd, restrict the selector to target kbd
elements that contain text (e.g., kbd:not(:has(> span)) or simply keep it but
document scope); if the intent is to style hotkey spans, remove this rule from
_kbd.scss and apply text-transform to the hotkey-specific selector in
hotkey.scss (e.g., .limel-hotkey span or the existing span selector) so the
first-letter transform actually affects rendered text.

Comment on lines +345 to +373
private isFromTextInput(event: KeyboardEvent): boolean {
const path =
typeof event.composedPath === 'function'
? event.composedPath()
: [];

for (const node of path) {
if (!('tagName' in (node as any))) {
continue;
}

const element = node as HTMLElement;

if (element.isContentEditable) {
return true;
}

const tagName = element.tagName;
if (
tagName === 'INPUT' ||
tagName === 'TEXTAREA' ||
tagName === 'SELECT'
) {
return true;
}
}

return false;
}
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

Consider adding role="textbox" check for custom text inputs.

The isFromTextInput function checks for contentEditable and native input elements, but some custom components use role="textbox" for accessibility. This could cause hotkeys to fire while typing in such elements.

🔧 Suggested enhancement
             if (element.isContentEditable) {
                 return true;
             }

             const tagName = element.tagName;
             if (
                 tagName === 'INPUT' ||
                 tagName === 'TEXTAREA' ||
                 tagName === 'SELECT'
             ) {
                 return true;
             }
+
+            if (element.getAttribute('role') === 'textbox') {
+                return true;
+            }
🤖 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 345 - 373, The isFromTextInput
method currently only checks contentEditable and native INPUT/TEXTAREA/SELECT
elements; update it to also detect elements that act as text inputs by checking
each path element's role attribute for "textbox" (and optionally "search" or
other relevant ARIA roles) and return true when found; locate the
isFromTextInput( event: KeyboardEvent ) function and add a role check using
element.getAttribute('role') === 'textbox' (and consider aria-multiline
handling) so hotkeys won't trigger while typing in custom accessible text
components.

Comment thread src/components/select/select.tsx
Comment thread src/util/hotkeys.spec.ts
Comment on lines +20 to +32
describe('hotkeyFromKeyboardEvent', () => {
it('includes multiple modifiers', () => {
const pressed = hotkeyFromKeyboardEvent({
key: 'a',
code: 'KeyA',
altKey: true,
ctrlKey: true,
metaKey: false,
shiftKey: false,
} as any);

expect(pressed).toBe('ctrl+alt+a');
});
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

Consider adding a test for full canonical modifier ordering.

The test at lines 21-32 verifies ctrl+alt ordering, but a past review suggested testing all four modifiers together to confirm the canonical order (meta+ctrl+alt+shift).

💡 Optional test addition
it('outputs modifiers in canonical order', () => {
    const pressed = hotkeyFromKeyboardEvent({
        key: 'a',
        code: 'KeyA',
        altKey: true,
        ctrlKey: true,
        metaKey: true,
        shiftKey: true,
    } as any);

    expect(pressed).toBe('meta+ctrl+alt+shift+a');
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/hotkeys.spec.ts` around lines 20 - 32, Add a unit test to verify
full canonical modifier ordering for hotkeyFromKeyboardEvent: create a case
calling hotkeyFromKeyboardEvent with all four modifier booleans true (metaKey,
ctrlKey, altKey, shiftKey) and key 'a'/'KeyA' and assert the returned string
equals 'meta+ctrl+alt+shift+a' so the function's canonical ordering is
explicitly validated.

Comment thread src/util/hotkeys.ts
Comment on lines +146 to +193
export const normalizeHotkeyString = (hotkey: string): string | null => {
if (!hotkey) {
return null;
}

const tokens = tokenizeHotkeyString(hotkey);
if (tokens.length === 0) {
return null;
}

let alt = false;
let ctrl = false;
let meta = false;
let shift = false;
let key: string | null = null;

for (const token of tokens) {
const normalized = normalizeKey(token);
if (!normalized) {
continue;
}

if (normalized === 'alt') {
alt = true;
continue;
}
if (normalized === 'ctrl') {
ctrl = true;
continue;
}
if (normalized === 'meta') {
meta = true;
continue;
}
if (normalized === 'shift') {
shift = true;
continue;
}

// Last non-modifier wins
key = normalized;
}

if (!key) {
return null;
}

return normalizeModifiersAndKey({ key, alt, ctrl, meta, 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.

⚠️ Potential issue | 🟡 Minor

Canonicalize + the same way for strings and keyboard events.

hotkeyFromKeyboardEvent() intentionally strips shift when the key is +, but normalizeHotkeyString() preserves it. That makes bindings like shift++ normalize to shift++ even though the event side can only ever produce +, so the shortcut can never match. Drop shift for + on the string path as well, or reject that form.

♻️ Suggested fix
-    return normalizeModifiersAndKey({ key, alt, ctrl, meta, shift });
+    return normalizeModifiersAndKey({
+        key,
+        alt,
+        ctrl,
+        meta,
+        shift: key === '+' ? false : shift,
+    });

Also applies to: 223-241

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

In `@src/util/hotkeys.ts` around lines 146 - 193, normalizeHotkeyString currently
preserves the shift modifier when the parsed non-modifier key is '+' which is
inconsistent with hotkeyFromKeyboardEvent (which drops shift for '+'), causing
bindings like "shift++" to never match; update normalizeHotkeyString so that
after determining key (the variable named key) you detect the literal '+' and
set shift = false (or reject the combo) before calling normalizeModifiersAndKey,
mirroring hotkeyFromKeyboardEvent’s handling; also apply the same change to the
analogous logic block used elsewhere in this file (the other
normalizeHotkeyString-like path around the second occurrence noted in the
review).

Kiarokh added 5 commits March 24, 2026 14:50
- Made it safe to import outside the browser
(removed top-level `window.navigator.userAgent` access)
- Added export function `isAppleDevice(): boolean`
for rendering hotkeys, and emitting events when
hotkeys are pressed.
@Kiarokh Kiarokh force-pushed the menu-select-hotkeys branch from 2d151c9 to 6ff96ff Compare March 24, 2026 13:50
@Kiarokh Kiarokh force-pushed the menu-select-hotkeys branch from f5df0e5 to d8f1c47 Compare March 24, 2026 13:56
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: 2

♻️ Duplicate comments (7)
src/components/menu-list/menu-list-renderer.tsx (1)

108-123: 🧹 Nitpick | 🔵 Trivial

Remove redundant any casts in narrowed MenuItem branch.

After the separator guard, these casts are unnecessary and reduce type safety/readability.

♻️ Proposed refactor
-        const hasHotkey = 'hotkey' in item && !!(item as any).hotkey;
+        const hasHotkey = !!item.hotkey;
         const hasMeta =
             hasSubMenu ||
             item.badge !== undefined ||
             hasHotkey ||
-            (!!('commandText' in item) && !!item.commandText);
+            !!item.commandText;

         const primaryComponent = hasMeta
             ? {
                   name: 'limel-menu-item-meta',
                   props: {
-                      commandText: (item as any).commandText,
-                      hotkey: (item as any).hotkey,
+                      commandText: item.commandText,
+                      hotkey: item.hotkey,
                       disabled: !!item.disabled,
-                      badge: (item as any).badge,
+                      badge: item.badge,
                       showChevron: hasSubMenu,
                   },
               }
             : undefined;
#!/bin/bash
# Verify the branch is narrowed by separator guard and locate remaining casts.
rg -n -C3 "if \\('separator' in item\\)|as any\\)\\.(hotkey|commandText|badge)|'hotkey' in item|\\('commandText' in item\\)" --type=ts --type=tsx

Expected result: separator guard appears before this block, and casted member access is confined to this code path.

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

In `@src/components/menu-list/menu-list-renderer.tsx` around lines 108 - 123, The
code is using redundant (item as any).<prop> casts inside the narrowed MenuItem
branch (see hasHotkey, hasMeta and primaryComponent/limel-menu-item-meta props);
remove these any casts and access properties directly (e.g., item.hotkey,
item.commandText, item.badge) after the existing type-narrowing guards ('hotkey'
in item / 'commandText' in item / separator guard) or cast item once to the
correct MenuItem subtype if needed, and keep explicit boolean coercions only
where required (e.g., disabled: !!item.disabled).
src/components/select/examples/select-hotkeys.tsx (2)

42-42: 🧹 Nitpick | 🔵 Trivial

Mark class members as readonly.

Both options and handleChange are never reassigned; marking them as readonly satisfies static analysis and communicates intent.

🔧 Suggested fix
-    private options: (Option | ListSeparator)[] = [
+    private readonly options: (Option | ListSeparator)[] = [
-    private handleChange = (event: LimelSelectCustomEvent<Option>) => {
+    private readonly handleChange = (event: LimelSelectCustomEvent<Option>) => {

Also applies to: 86-86

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

In `@src/components/select/examples/select-hotkeys.tsx` at line 42, Mark the class
fields that are never reassigned as readonly: add the readonly modifier to the
options property and to the handleChange class field so their declarations
become readonly (e.g., change "private options: (Option | ListSeparator)[] =
[...]" to "private readonly options: ..." and likewise for the handleChange
field), ensuring TypeScript static analysis and intent are preserved.

64-68: ⚠️ Potential issue | 🟡 Minor

Replace cmd+d with a non-conflicting hotkey.

Line 31 explicitly warns to "Choose hotkeys that do not conflict with common browser shortcuts." However, cmd+d conflicts with:

  • macOS: Cmd+D bookmarks the page in all major browsers
  • Windows: Win+D shows the desktop

Consider using cmd+c (with text "Closed") or another non-conflicting combination like cmd+e.

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

In `@src/components/select/examples/select-hotkeys.tsx` around lines 64 - 68, The
selected hotkey 'cmd+d' in the example option object (the option with text
"Closed", secondaryText "as not planned", value "closed" in select-hotkeys.tsx)
conflicts with common browser shortcuts; change the hotkey property to a
non-conflicting combination (e.g., 'cmd+c' or 'cmd+e') so it doesn't override
browser actions—update the hotkey value on that option object accordingly.
src/components/menu/examples/menu-hotkeys.tsx (1)

60-61: ⚠️ Potential issue | 🟡 Minor

Replace meta+c with a non-conflicting hotkey.

Lines 33-37 warn about browser/OS conflicts, yet meta+c maps to Cmd+C (copy) on macOS and Win+C on Windows—both platform shortcuts. This undermines the example's purpose.

Consider using meta+shift+c or another combination that doesn't conflict with system clipboard operations.

🤖 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` around lines 60 - 61, Update
the hotkey for the menu item with text 'Copy message' to a non-conflicting
combination (e.g., 'meta+shift+c') instead of 'meta+c'; locate the menu items
array where the object { text: 'Copy message', hotkey: 'meta+c' } is defined and
replace the hotkey string so it no longer collides with the system clipboard
shortcut.
src/components/hotkey/hotkey.tsx (1)

112-121: 🧹 Nitpick | 🔵 Trivial

Consider distinguishing delete and backspace on Apple devices.

On macOS, there are two distinct keys:

  • Delete (⌫) — backspace, removes character before cursor
  • Forward Delete (⌦) — removes character after cursor

Currently, all three aliases (delete, del, backspace) render as ⌫ on Apple devices. If a user specifies delete intending forward-delete behavior, the displayed glyph will be misleading.

💡 Suggested fix
             case 'delete':
             case 'del':
             case 'backspace': {
                 if (isApple) {
-                    return { display: '⌫', isGlyph: true };
+                    return lower === 'backspace'
+                        ? { display: '⌫', isGlyph: true }
+                        : { display: '⌦', isGlyph: true };
                 }
                 return lower === 'backspace'
                     ? { display: 'Backspace', isGlyph: false }
                     : { display: 'Del', isGlyph: false };
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/hotkey/hotkey.tsx` around lines 112 - 121, The switch block
handling 'delete'/'del'/'backspace' currently returns the same Apple glyph (⌫)
for all three — update the logic in the hotkey rendering function (the
switch/case in src/components/hotkey/hotkey.tsx) to distinguish forward-delete
vs backspace: when isApple is true, return '⌦' (isGlyph: true) for 'delete' and
'del' (forward delete) and return '⌫' (isGlyph: true) only for 'backspace'; for
non-Apple keep the existing string labels ('Backspace' for backspace, 'Del' for
delete/del). Ensure you check the normalized key (lower) to decide which glyph
to return.
src/components/select/select.tsx (1)

417-431: ⚠️ Potential issue | 🟠 Major

Reuse the scroll-preserving multiselect path here.

handleMenuChange() already snapshots/restores .mdc-menu-surface.scrollTop before emitting in multiselect mode, but this hotkey branch still emits and returns directly. In a long open list, each toggle can jump the menu back to the top.

♻️ Minimal fix
         if (this.multiple) {
+            const selector = `#${this.portalId} limel-menu-surface`;
+            const menuSurface = document
+                .querySelector(selector)
+                ?.shadowRoot?.querySelector('.mdc-menu-surface');
+            const scrollPosition = menuSurface?.scrollTop || 0;
+
             const currentValue = isMultiple(this.value) ? this.value : [];
             const hasSelectedOption = currentValue.some(
                 (option) => option.value === matchedOption.value
             );
@@
 
             this.change.emit(nextValue);
+
+            requestAnimationFrame(() => {
+                setTimeout(() => {
+                    if (menuSurface) {
+                        menuSurface.scrollTop = scrollPosition;
+                    }
+                });
+            });
 
             return;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/select/select.tsx` around lines 417 - 431, The hotkey branch
in the multiselect path (inside the method handling key actions where it checks
this.multiple and computes nextValue and calls this.change.emit(nextValue))
bypasses the scroll-preserving logic used in handleMenuChange(); fix it by
snapshotting the menu surface scrollTop before emitting and restoring it after
emit (reuse the same .mdc-menu-surface.scrollTop save/restore pattern or call
the existing scroll-preserving helper used by handleMenuChange()), ensuring the
matchedOption toggle (the computation of nextValue and the
this.change.emit(nextValue) call) occurs between the snapshot and restore so the
menu doesn't jump to the top when toggling via hotkey.
src/util/hotkeys.ts (1)

156-193: ⚠️ Potential issue | 🟡 Minor

Canonicalize + the same way on the string path.

hotkeyFromKeyboardEvent() already strips shift for +, but normalizeHotkeyString() still preserves it. A binding like shift++ normalizes to shift++ even though the event side can only produce +, so the shortcut never matches.

♻️ Minimal fix
-    return normalizeModifiersAndKey({ key, alt, ctrl, meta, shift });
+    return normalizeModifiersAndKey({
+        key,
+        alt,
+        ctrl,
+        meta,
+        shift: key === '+' ? false : shift,
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/hotkeys.ts` around lines 156 - 193, normalizeHotkeyString()
currently preserves the shift modifier when the literal '+' is parsed from a
string, causing mismatches with hotkeyFromKeyboardEvent() which canonicalizes
'+' without shift; after you determine the final key in normalizeHotkeyString
(the block that sets key and then returns normalizeModifiersAndKey({ key, alt,
ctrl, meta, shift })), detect the canonical plus case (key === '+') and clear
the shift flag before calling normalizeModifiersAndKey so string paths like
"shift++" canonicalize the same way as hotkeyFromKeyboardEvent(); use the same
helper functions normalizeKey, normalizeModifiersAndKey, and ensure behavior is
symmetric with hotkeyFromKeyboardEvent().
🤖 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/menu.tsx`:
- Around line 279-287: connectedCallback currently only adds the keydown
listener when the component mounts with this.open true but doesn't call
setFocus(), leaving keyboard users outside the menu; update connectedCallback so
that when this.open is true it both adds the document keydown listener
(this.handleDocumentKeyDown) and calls this.setFocus() so focus is initialized
the same way openWatcher() does.
- Around line 403-420: findMenuItemByHotkey walks this.visibleItems even when
the menu is showing a loader, allowing hotkeys to activate items that are not
rendered; update the function (findMenuItemByHotkey) to early-return null when
the menu is in a loading state by checking the component flags (this.loading or
this.loadingSubItems) before iterating visibleItems, so no hotkey lookup occurs
while renderMenuList intentionally hides items.

---

Duplicate comments:
In `@src/components/hotkey/hotkey.tsx`:
- Around line 112-121: The switch block handling 'delete'/'del'/'backspace'
currently returns the same Apple glyph (⌫) for all three — update the logic in
the hotkey rendering function (the switch/case in
src/components/hotkey/hotkey.tsx) to distinguish forward-delete vs backspace:
when isApple is true, return '⌦' (isGlyph: true) for 'delete' and 'del' (forward
delete) and return '⌫' (isGlyph: true) only for 'backspace'; for non-Apple keep
the existing string labels ('Backspace' for backspace, 'Del' for delete/del).
Ensure you check the normalized key (lower) to decide which glyph to return.

In `@src/components/menu-list/menu-list-renderer.tsx`:
- Around line 108-123: The code is using redundant (item as any).<prop> casts
inside the narrowed MenuItem branch (see hasHotkey, hasMeta and
primaryComponent/limel-menu-item-meta props); remove these any casts and access
properties directly (e.g., item.hotkey, item.commandText, item.badge) after the
existing type-narrowing guards ('hotkey' in item / 'commandText' in item /
separator guard) or cast item once to the correct MenuItem subtype if needed,
and keep explicit boolean coercions only where required (e.g., disabled:
!!item.disabled).

In `@src/components/menu/examples/menu-hotkeys.tsx`:
- Around line 60-61: Update the hotkey for the menu item with text 'Copy
message' to a non-conflicting combination (e.g., 'meta+shift+c') instead of
'meta+c'; locate the menu items array where the object { text: 'Copy message',
hotkey: 'meta+c' } is defined and replace the hotkey string so it no longer
collides with the system clipboard shortcut.

In `@src/components/select/examples/select-hotkeys.tsx`:
- Line 42: Mark the class fields that are never reassigned as readonly: add the
readonly modifier to the options property and to the handleChange class field so
their declarations become readonly (e.g., change "private options: (Option |
ListSeparator)[] = [...]" to "private readonly options: ..." and likewise for
the handleChange field), ensuring TypeScript static analysis and intent are
preserved.
- Around line 64-68: The selected hotkey 'cmd+d' in the example option object
(the option with text "Closed", secondaryText "as not planned", value "closed"
in select-hotkeys.tsx) conflicts with common browser shortcuts; change the
hotkey property to a non-conflicting combination (e.g., 'cmd+c' or 'cmd+e') so
it doesn't override browser actions—update the hotkey value on that option
object accordingly.

In `@src/components/select/select.tsx`:
- Around line 417-431: The hotkey branch in the multiselect path (inside the
method handling key actions where it checks this.multiple and computes nextValue
and calls this.change.emit(nextValue)) bypasses the scroll-preserving logic used
in handleMenuChange(); fix it by snapshotting the menu surface scrollTop before
emitting and restoring it after emit (reuse the same .mdc-menu-surface.scrollTop
save/restore pattern or call the existing scroll-preserving helper used by
handleMenuChange()), ensuring the matchedOption toggle (the computation of
nextValue and the this.change.emit(nextValue) call) occurs between the snapshot
and restore so the menu doesn't jump to the top when toggling via hotkey.

In `@src/util/hotkeys.ts`:
- Around line 156-193: normalizeHotkeyString() currently preserves the shift
modifier when the literal '+' is parsed from a string, causing mismatches with
hotkeyFromKeyboardEvent() which canonicalizes '+' without shift; after you
determine the final key in normalizeHotkeyString (the block that sets key and
then returns normalizeModifiersAndKey({ key, alt, ctrl, meta, shift })), detect
the canonical plus case (key === '+') and clear the shift flag before calling
normalizeModifiersAndKey so string paths like "shift++" canonicalize the same
way as hotkeyFromKeyboardEvent(); use the same helper functions normalizeKey,
normalizeModifiersAndKey, and ensure behavior is symmetric with
hotkeyFromKeyboardEvent().
🪄 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: 815f203d-be67-49fa-8e5b-05d0b42755ba

📥 Commits

Reviewing files that changed from the base of the PR and between 2d151c9 and 6ff96ff.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (20)
  • src/components/hotkey/examples/hotkey-basic.scss
  • src/components/hotkey/examples/hotkey-basic.tsx
  • src/components/hotkey/examples/hotkey-disabled.tsx
  • src/components/hotkey/hotkey.scss
  • src/components/hotkey/hotkey.tsx
  • src/components/list-item/menu-item-meta/menu-item-meta.tsx
  • src/components/markdown/partial-styles/_kbd.scss
  • 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
  • src/components/select/examples/select-hotkeys.tsx
  • src/components/select/option.types.ts
  • src/components/select/select.template.tsx
  • src/components/select/select.tsx
  • src/util/device.ts
  • src/util/hotkeys.spec.ts
  • src/util/hotkeys.ts

Comment on lines +279 to +287
public connectedCallback() {
if (this.open) {
document.addEventListener(
'keydown',
this.handleDocumentKeyDown,
true
);
}
}
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.

⚠️ Potential issue | 🟡 Minor

Call setFocus() for initially open menus too.

This branch mirrors the listener setup from openWatcher(), but not its focus handling. If the component mounts with open={true}, keyboard users start outside the menu until they tab in manually.

♻️ Minimal fix
     public connectedCallback() {
         if (this.open) {
             document.addEventListener(
                 'keydown',
                 this.handleDocumentKeyDown,
                 true
             );
+            this.setFocus();
         }
     }
🤖 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 279 - 287, connectedCallback
currently only adds the keydown listener when the component mounts with
this.open true but doesn't call setFocus(), leaving keyboard users outside the
menu; update connectedCallback so that when this.open is true it both adds the
document keydown listener (this.handleDocumentKeyDown) and calls this.setFocus()
so focus is initialized the same way openWatcher() does.

Comment on lines +403 to +420
private findMenuItemByHotkey(pressedHotkey: string): MenuItem | null {
for (const item of this.visibleItems) {
if (!this.isMenuItem(item) || item.disabled) {
continue;
}

const rawHotkey = item.hotkey;
if (!rawHotkey) {
continue;
}

const normalized = this.getNormalizedHotkey(rawHotkey);
if (normalized && normalized === pressedHotkey) {
return item;
}
}

return null;
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.

⚠️ Potential issue | 🟠 Major

Ignore hotkeys while the menu is showing its loader.

renderMenuList() intentionally renders no items while loadingSubItems or loading is true, but this lookup still walks visibleItems. During async search or lazy submenu loading, a quick hotkey can therefore select a stale item that is no longer rendered.

♻️ Minimal fix
     private findMenuItemByHotkey(pressedHotkey: string): MenuItem | null {
+        if (this.loadingSubItems || this.loading) {
+            return null;
+        }
+
         for (const item of this.visibleItems) {
             if (!this.isMenuItem(item) || item.disabled) {
                 continue;
             }
🤖 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 403 - 420, findMenuItemByHotkey
walks this.visibleItems even when the menu is showing a loader, allowing hotkeys
to activate items that are not rendered; update the function
(findMenuItemByHotkey) to early-return null when the menu is in a loading state
by checking the component flags (this.loading or this.loadingSubItems) before
iterating visibleItems, so no hotkey lookup occurs while renderMenuList
intentionally hides items.

@adrianschmidt
Copy link
Copy Markdown
Contributor

Closed in favour of #3976, #3977, #3978, #3989, and #3990.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants