Skip to content

Add keyboard hotkey support to menu items#3818

Closed
Kiarokh wants to merge 5 commits intomainfrom
menu-hotkeys
Closed

Add keyboard hotkey support to menu items#3818
Kiarokh wants to merge 5 commits intomainfrom
menu-hotkeys

Conversation

@Kiarokh
Copy link
Copy Markdown
Contributor

@Kiarokh Kiarokh commented Feb 2, 2026

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

Summary by CodeRabbit

  • New Features

    • Menus now support keyboard shortcuts: items show platform-aware shortcut glyphs and can be triggered by hotkeys. New hotkey component + examples (basic, disabled, duplicates, prevent-default, searchable menu).
  • Bug Fixes

    • Hotkey handling ignores text inputs and reserved navigation keys, respects disabled items, handles duplicate handlers, and supports preventing browser defaults.
  • Tests

    • Added unit and e2e tests for hotkey parsing and menu interactions.
  • Style

    • Improved keyboard (kbd) styling for clearer appearance.

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 2, 2026

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

Adds a hotkey subsystem: normalization utilities, a visual/interactive limel-hotkey component with global key handling and duplicate detection, menu integration to trigger items by hotkey (with reserved-key filtering and caching), examples, styles, tests, and a small device util refinement.

Changes

Cohort / File(s) Summary
Hotkey utilities & tests
src/util/hotkeys.ts, src/util/hotkeys.spec.ts
New tokenizer/normalizer, KeyboardEvent→canonical hotkey mapping, text-input detection, and unit tests validating normalization and event mapping.
Hotkey component, types & styles
src/components/hotkey/hotkey.tsx, src/components/hotkey/hotkey.types.ts, src/components/hotkey/hotkey.scss, src/components/markdown/partial-styles/_kbd.scss
New limel-hotkey component: renders tokens, global keydown handling, duplicate detection/warn, emits typed hotkeyTrigger, supports disabled and preventBrowserDefault; styles and event-detail type added.
Hotkey examples & example styles
src/components/hotkey/examples/hotkey-basic.tsx, src/components/hotkey/examples/hotkey-disabled.tsx, src/components/hotkey/examples/hotkey-duplicates.tsx, src/components/hotkey/examples/hotkey-prevent-default.tsx, src/components/hotkey/examples/hotkey-basic.scss
New example components demonstrating basic usage, disabled state, duplicate handling, and prevent-default control; layout SCSS added.
Menu integration & examples/tests
src/components/menu/menu.tsx, src/components/menu/menu.types.ts, src/components/menu/examples/menu-hotkeys.tsx, src/components/menu/examples/menu-searchable-hotkeys.tsx, src/components/menu/menu.e2e.tsx
Adds hotkey?: string to MenuItem, wires menu surface to listen for hotkeyTrigger, normalizes/caches hotkeys, filters reserved navigation keys, finds matching enabled visible item and triggers selection; includes searchable example and E2E tests.
Menu-list / meta rendering
src/components/list-item/menu-item-meta/menu-item-meta.tsx, src/components/menu-list/menu-list-renderer.tsx
Meta now reflects hotkey and renders limel-hotkey when present; menu-list forwards item.hotkey and treats hotkey as meta info alongside badge/commandText.
Device util
src/util/device.ts
Refactors UA/platform/touch helpers and adds isAppleDevice() heuristic; updates iOS/Android checks to use helpers.
Project manifest & package
manifest_file, package.json
Manifest/package updates to account for new files and exports.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Document
    participant Hotkey as limel-hotkey
    participant Registry as HotkeyRegistry
    participant Consumer as EventConsumer

    User->>Document: keydown
    Document->>Hotkey: global keydown listener (capture)
    Hotkey->>Hotkey: normalize hotkey (cache)
    Hotkey->>Registry: lookup enabled handlers for canonical hotkey
    alt single handler
        Registry-->>Hotkey: handler found
        Hotkey->>Document: preventDefault / stopPropagation (if configured)
        Hotkey->>Consumer: emit hotkeyTrigger(detail)
    else duplicates
        Registry-->>Hotkey: multiple handlers
        Hotkey->>Hotkey: console.warn, pick first enabled
        Hotkey->>Consumer: emit hotkeyTrigger(detail for first)
    else none
        Hotkey-->>Document: allow default
    end
Loading
sequenceDiagram
    participant User
    participant UI as Menu UI
    participant Menu as limel-menu
    participant Surface as MenuSurface
    participant MenuHotkey as MenuHotkeyHandler
    participant Items as MenuItems

    User->>UI: open menu
    UI->>Menu: opens
    Menu->>Surface: attach surface element (setSurfaceElement)
    User->>Surface: press hotkey (e.g., Alt+C)
    Surface->>MenuHotkey: receives hotkeyTrigger / keydown
    MenuHotkey->>MenuHotkey: normalize hotkey, ignore reserved keys
    MenuHotkey->>Items: find visible & enabled item by hotkey
    alt match found
        Items-->>MenuHotkey: matching item
        MenuHotkey->>Menu: trigger select(item)
        Menu->>UI: close menu, emit select event
    else no match or reserved
        MenuHotkey-->>Surface: ignore
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

accessibility

Suggested reviewers

  • jgroth
  • LucyChyzhova
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% 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 Title check skipped as CodeRabbit has written the PR title.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 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.

@Kiarokh Kiarokh force-pushed the menu-hotkeys branch 2 times, most recently from e2c148e to 530a80c Compare February 2, 2026 16:00
@Kiarokh Kiarokh marked this pull request as ready for review February 2, 2026 16:00
@Kiarokh Kiarokh requested a review from a team as a code owner February 2, 2026 16:00
@Kiarokh Kiarokh self-assigned this Feb 2, 2026
@Kiarokh Kiarokh added feature New feature or request usability Things that make it harder or easier for users to understand or use elements labels Feb 2, 2026
@coderabbitai coderabbitai Bot changed the title @coderabbitai Add keyboard hotkey support to menu items Feb 2, 2026
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

Caution

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

⚠️ Outside diff range comments (1)
src/components/menu/examples/menu-hotkeys.tsx (1)

73-88: 🛠️ Refactor suggestion | 🟠 Major

Wrap multiple JSX elements in <Host> instead of an array literal.

Per the coding guidelines for StencilJS .tsx files, when returning multiple top-level JSX elements from the render method, they should be wrapped in the special <Host> element rather than an array literal.

♻️ Proposed fix
+import { Host } from '@stencil/core';
     public render() {
-        return [
+        return (
+            <Host>
+                <limel-menu items={this.items} onSelect={this.handleSelect}>
+                    <limel-icon-button
+                        label="Menu"
+                        icon="menu_2"
+                        elevated={true}
+                        slot="trigger"
+                    />
+                </limel-menu>
+                <limel-example-value
+                    label="Last selected item"
+                    value={this.lastSelectedItem}
+                />
+            </Host>
+        );
-            <limel-menu items={this.items} onSelect={this.handleSelect}>
-                <limel-icon-button
-                    label="Menu"
-                    icon="menu_2"
-                    elevated={true}
-                    slot="trigger"
-                />
-            </limel-menu>,
-            <limel-example-value
-                label="Last selected item"
-                value={this.lastSelectedItem}
-            />,
-        ];
     }

As per coding guidelines: "When returning multiple JSX elements from the render method, never wrap them in an array literal. Instead, always wrap them in the special <Host> element."

🤖 Fix all issues with AI agents
In `@src/components/menu/menu.tsx`:
- Around line 367-385: In findMenuItemByHotkey, remove the redundant type cast
by relying on the isMenuItem type guard: replace the line that assigns rawHotkey
= (item as MenuItem).hotkey with a direct access const rawHotkey = item.hotkey;
(keep the existing checks using this.isMenuItem, this.getNormalizedHotkey,
item.disabled and this.visibleItems unchanged).

In `@src/util/hotkeys.ts`:
- Around line 47-55: The return logic in the normalization branch is redundant:
after checking if /^f\d{1,2}$/.test(normalized) and returning normalized, the
subsequent if (normalized.length === 1) branch also returns normalized and the
final return returns normalized again; simplify by removing the length check and
directly returning normalized if the regex test fails. Update the hotkey
normalization function to keep the /^f\d{1,2}$/ early return and replace the
remaining branches with a single return normalized to preserve existing behavior
(refer to the normalized variable and the regex check).

Comment thread src/components/menu/menu.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

🤖 Fix all issues with AI agents
In `@src/components/menu/menu.e2e.ts`:
- Around line 265-306: Tests use fixed waits (page.waitForTimeout(200/50)),
causing flakiness; replace those with deterministic checks: after
limelMenu.setProperty('items') and trigger.click(), wait for the menu to be open
or the focused item to appear (for example, await page.waitForFunction to assert
trigger.getAttribute('aria-expanded') === 'true' or wait for a focused menu item
via page.find('li[aria-selected="true"], li:focus') ) before pressing keys with
page.keyboard.press('Enter'/'ArrowDown'), and remove the waitForTimeout calls so
selection assertions (spy on 'select') run only after the menu/focus state is
confirmed.

In `@src/util/hotkeys.ts`:
- Around line 203-231: The isKeyboardEventFromTextInput function currently
treats missing composedPath() as an empty path and can miss text inputs; update
isKeyboardEventFromTextInput to fall back to a single-node path using
event.target (and/or event.srcElement for legacy IE) when composedPath is not
available, then run the same checks (isContentEditable, tagName
INPUT/TEXTAREA/SELECT, role="textbox") against that fallback node so hotkeys
won't fire while typing in unsupported browsers.

Comment thread src/components/menu/menu.e2e.ts Outdated
Comment thread src/util/hotkeys.ts
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

🤖 Fix all issues with AI agents
In `@src/components/hotkey/hotkey.tsx`:
- Around line 66-73: normalizeHotkeyString currently splits on "+" and drops
empty tokens so a lone this.value of "+" becomes null and can never match
hotkeyFromKeyboardEvent; fix by special-casing the exact string "+" inside
normalizeHotkeyString (or the caller before normalizing) to return the canonical
token that normalizeKey expects (e.g., map "+" to the supported token name) so
pressed comparisons work, or alternatively update the docs/examples to use the
supported token instead of "+"; ensure the mapping uses the same normalized form
returned by normalizeKey so the pressed === expected check succeeds.

Comment thread src/components/hotkey/hotkey.tsx Outdated
@Kiarokh Kiarokh force-pushed the menu-hotkeys branch 3 times, most recently from dd96fcb to 1a3d2c4 Compare February 3, 2026 14:07
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 3, 2026

@Kiarokh, I apologize for the confusion. Let me trigger the complete fresh review now. I'll review all the current changes in the PR with a clean perspective on the new code structure and strategy.

✅ Actions performed

Full review triggered.

@Lundalogik Lundalogik deleted a comment from coderabbitai Bot Feb 3, 2026
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

🤖 Fix all issues with AI agents
In `@src/components/hotkey/hotkey.tsx`:
- Around line 55-61: The mapped <kbd> elements rendered in parts.map(...) lack a
key which breaks the useJsxKeyInIterable lint rule; update the map callback in
the render (the parts.map producing <kbd> and calling this.formatDisplayToken)
to provide a unique key for each <kbd> — e.g., use the part value or an index
fallback (key={part} or key={`${part}-${i}`}) so each <kbd> inside Host has a
stable key.

In `@src/util/device.ts`:
- Around line 49-60: The return expression in isAppleDevice() is redundant
because isIPadOS13Plus is defined as (isMacLike && getMaxTouchPoints() > 1), so
including it separately is unnecessary; update the function to simplify the
final return to "isIPadIPhoneIPod || isMacLike" (or remove isIPadOS13Plus usage
while keeping the variable for documentation if preferred) and ensure variables
getUserAgent(), getPlatform(), isIPadIPhoneIPod, isMacLike, and isIPadOS13Plus
are left intact for clarity.

In `@src/util/hotkeys.spec.ts`:
- Around line 14-40: Add unit tests for hotkeyFromKeyboardEvent to cover
modifier combinations, function keys, arrow keys, and code-based normalization:
add a test like "includes multiple modifiers" that asserts
hotkeyFromKeyboardEvent returns "ctrl+alt+a" for an event with key 'a' and
ctrlKey/altKey true; add "handles function keys" asserting 'f1' and 'f12' for
events with key 'F1'/'F12'; add "handles arrow keys" asserting
'arrowleft'/'arrowright' for ArrowLeft/ArrowRight keys; and add "uses event.code
for non-QWERTY" that simulates a non-letter key where key differs from code
(e.g., key='@' code='Quote' or code-based letter) to ensure the function
normalizes using event.code when appropriate by comparing expected normalized
output from hotkeyFromKeyboardEvent.

Comment thread src/util/device.ts
Comment thread src/util/hotkeys.spec.ts
@Kiarokh Kiarokh force-pushed the menu-hotkeys branch 2 times, most recently from a69816e to bf75cb7 Compare February 4, 2026 10:19
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

🤖 Fix all issues with AI agents
In `@src/components/hotkey/hotkey.tsx`:
- Around line 55-61: The kbd elements rendered in the Host mapping currently set
aria-hidden="true" which triggers the a11y lint rule; remove the aria-hidden
attribute from the <kbd> elements in the parts.map render soScreen readers can
access the keys while keeping the Host's aria-label (refer to Host, parts.map
and formatDisplayToken in hotkey.tsx to locate the code). Ensure no other
aria-hidden is added and that formatDisplayToken continues to produce the
visible label for each kbd.

In `@src/components/menu/menu.e2e.ts`:
- Around line 362-381: The test dispatches the KeyboardEvent to
limel-menu-surface immediately after clicking the trigger, which can race if the
surface hasn't opened; after clicking the trigger (the variable trigger and the
call to trigger.click()/page.waitForChanges()), wait deterministically for the
surface to exist and be open/visible (e.g., use page.waitForSelector or a short
polling loop to confirm document.querySelector('limel-menu-surface') is present
and has the open/visible state) before executing the evaluated block that
dispatches the KeyboardEvent; update the code that finds limel-menu-surface and
dispatches the event (the evaluate callback) to run only once that deterministic
wait completes.

Comment thread src/components/hotkey/hotkey.tsx Outdated
Comment thread src/components/menu/menu.e2e.ts Outdated
Comment thread src/components/hotkey/examples/hotkey-basic.tsx
Comment thread src/components/hotkey/hotkey.tsx Outdated
Comment thread src/components/hotkey/hotkey.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: 1

🤖 Fix all issues with AI agents
In `@src/components/hotkey/hotkey.tsx`:
- Around line 78-83: The UI shows 'meta' as 'Ctrl' for non-Apple (case 'meta' /
'cmd' / 'command' returning isApple ? '⌘' : 'Ctrl') but matching still expects
Meta, so fix by normalizing the incoming hotkey tokens when not on Apple: in the
hotkey normalization function (the code that processes 'lower' tokens before
matching, e.g., where you switch on lower) map 'meta'/'cmd'/'command' → 'ctrl'
for non-Apple platforms so both display and matching align (alternatively, if
you prefer to keep matching as Meta, change the display branch to render
'Meta'/'Win' instead—choose one consistent approach and apply it in the switch
handling and the hotkey normalization logic).

Comment thread src/components/hotkey/hotkey.tsx
Comment thread src/components/hotkey/hotkey.tsx Outdated
);
}

private formatDisplayToken(token: string, isApple: boolean): string {
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.

Does this need to live inside the component? We can move it to a separate function for easier testing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it could, but I also wanted to use it to add a class to render the glyphs a bit larger than the letters. added here: 195e489

Is it a big deal?

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.

No it's not 😄 Just separation of concerns

Comment on lines +120 to +122
if ((event as any)[Hotkey.handledFlag]) {
return;
}
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.

Why do we need this check? Do we expect the event to propagate though multiple hotkeys? It does not look like they will ever be nested inside each other

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added it because of some discussions I had with a 🤖 friend, when trying to mitigate buggy behaviours, due to having multiple instances of the same component potentially rendered all at once.

Asked it to explain, and it said:

That check is there to prevent duplicate handling of the same physical keypress when there are multiple limel-hotkey instances on the page, not because they’re nested.

What happens otherwise:

Every limel-hotkey registers @Listen('keydown', { target: 'document', capture: true }).
A single keydown will invoke handleKeyDown() once per instance.
If you render several hotkeys (like in the example, or a menu with many items), each instance will evaluate the event; without a guard, more than one instance can emit if they happen to match (or if you accidentally have duplicates).
Why a flag on the event:

stopPropagation() isn’t ideal here because we’re listening on document in capture and we don’t want to interfere with other global handlers in the app.
The per-event flag makes it “handled once” across all limel-hotkey listeners, regardless of render order.
So yes: we do expect the same event to “reach” multiple hotkey components because they all listen globally, even though they aren’t nested.

If you’re confident you’ll never have duplicate hotkeys active at the same time, you could remove it—but in menus/examples it’s very easy to have multiple limel-hotkey elements present, so it’s a safety net.

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.

But with this check, it will be unclear which of those hotkeys will be triggered. There is no way for the consumer to know if their instance will be allowed or blocked by this check. Is that the behavior we want? The way it will work now will most likely be the component that renders first is the one that will receive the event

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hm… u r right.
what's a better approach? Just remove it?

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.

Yeah I think we should remove it. What does the 🤖 say?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Now I've kept the mechanics to trigger a console warning when a duplicate hotkey is identified. ec3a5c0
This way, the limel-hotkey component will only work for the first instance, and reduces a little bit of the confusion risk for the end-user. And the consumer can hopefully see a warning in the console.

Most of this responsibility is on the consumer anyway. But it's good to have some error prevention in the component itself as well.

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 (14)
src/util/device.ts (2)

1-21: Explicit return-type annotations on private helpers (pre-existing)

The three unexported helpers carry explicit : string / : number annotations that TypeScript infers correctly on its own. This was already raised in a previous review round and remains unaddressed.

Based on learnings: "prefer omitting explicit return type annotations for functions when TypeScript inference is sufficient; only add an explicit return type when inference would be insufficient or incorrect."

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

In `@src/util/device.ts` around lines 1 - 21, Remove the redundant explicit
return-type annotations from the three unexported helper functions so TypeScript
can infer them automatically: delete ": string" from getUserAgent and
getPlatform and ": number" from getMaxTouchPoints; keep the existing runtime
guards and returns intact and do not change the function bodies or exports
(these are private helpers so inferred types are sufficient).

49-60: Redundant isIPadOS13Plus in the return expression (pre-existing)

isIPadOS13Plus is defined as isMacLike && getMaxTouchPoints() > 1, making it a strict subset of isMacLike. Including it in isIPadIPhoneIPod || isIPadOS13Plus || isMacLike is therefore redundant. This was already flagged in a previous review round.

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

In `@src/util/device.ts` around lines 49 - 60, The isIPadOS13Plus local is
redundant because it is defined as isMacLike && getMaxTouchPoints() > 1 and then
OR'd with isMacLike in isAppleDevice(); update isAppleDevice() to return
isIPadIPhoneIPod || isMacLike (and remove the unused isIPadOS13Plus variable and
its getMaxTouchPoints check), keeping helper calls getUserAgent and getPlatform
as-is so only the redundant branch is eliminated.
src/components/menu-list/menu-list-renderer.tsx (1)

108-108: Redundant 'hotkey' in item guard — still flagged from the previous review.

After the separator early-return TypeScript narrows item to MenuItem, making the in check unnecessary. The previous suggestion to simplify to !!(item as MenuItem).hotkey remains unaddressed.

🤖 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` at line 108, The guard
"'hotkey' in item" is redundant after the early-return narrow to MenuItem;
replace the check for hasHotkey with a simple truthy cast like !!(item as
MenuItem).hotkey to rely on TypeScript narrowing and remove the unnecessary 'in'
operator; update the declaration that defines hasHotkey (the const hasHotkey
variable in menu-list-renderer.tsx) and ensure any references assume item is a
MenuItem.
src/components/hotkey/hotkey.types.ts (1)

1-5: LimelHotkeyTriggerDetail is still missing JSDoc.

The type is part of the public API (imported from @limetech/lime-elements) but has no description or property-level documentation.

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

In `@src/components/hotkey/hotkey.types.ts` around lines 1 - 5, Add JSDoc to the
public type LimelHotkeyTriggerDetail describing its purpose and each property: a
short description for the type itself (e.g., "Details provided when a Limel
hotkey is triggered"), then property tags for hotkey (string identifier for the
hotkey), value (string payload or label associated with the trigger), and
keyboardEvent (the original KeyboardEvent instance). Place the JSDoc immediately
above the exported type definition so the documentation appears in the public
API and tooling.
src/components/markdown/partial-styles/_kbd.scss (1)

26-29: 0.375px blur radius still inconsistent with the surrounding rem values.

All other offsets in the box-shadow declaration use rem. The past review comment flagging this remains unaddressed.

🤖 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 26 - 29, The
box-shadow blur radius uses a pixel value (0.375px) while all other offsets use
rems; update the blur radius in the box-shadow declaration in _kbd.scss to an
equivalent rem value (e.g., 0.0234375rem if using 16px root) so units are
consistent with the other offsets, keeping the rest of the box-shadow entries
and var(--button-shadow-normal) unchanged.
src/util/hotkeys.spec.ts (1)

1-130: Existing test cases are correct; missing coverage from prior review still outstanding.

isKeyboardEventFromTextInput, normalizeHotkeyString edge cases (empty/null input, modifier-only strings such as "ctrl+shift"), and direct tokenizeHotkeyString tests were requested in the previous review cycle and remain absent.

🤖 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 1 - 130, Add unit tests to cover the
missing hotkeys edge cases: create new specs in src/util/hotkeys.spec.ts that
(1) call normalizeHotkeyString with empty string and null and assert it returns
the normalized empty result (e.g., ''), and assert modifier-only inputs like
"ctrl+shift" produce the normalized modifier-only token sequence (e.g.,
"ctrl+shift"); (2) directly test tokenizeHotkeyString with cases including
single "+" and "++" and mixed modifier+key strings to verify tokenization; and
(3) add tests for isKeyboardEventFromTextInput covering typical text-input
events versus non-text events (e.g., key 'a' on input/textarea targets vs
function keys or modifier-only events) to assert the boolean result; if any test
fails, update the corresponding functions (normalizeHotkeyString,
tokenizeHotkeyString, isKeyboardEventFromTextInput) to conform to these expected
behaviors.
src/components/menu/menu.tsx (1)

308-337: Inline type duplicates LimelHotkeyTriggerDetail; import the shared type instead.

The inline type on lines 309–313 is identical to the LimelHotkeyTriggerDetail type in hotkey.types.ts. Using the shared type avoids drift if the type changes.

🤖 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 308 - 337, The inline event detail
type used in the handleHotkeyTrigger method duplicates the shared
LimelHotkeyTriggerDetail; replace the inline type with an import of
LimelHotkeyTriggerDetail from hotkey.types and annotate the event parameter as
CustomEvent<LimelHotkeyTriggerDetail> in the handleHotkeyTrigger signature,
adding the import at the top of the file and leaving the method body unchanged
(method name: handleHotkeyTrigger).
src/components/list-item/menu-item-meta/menu-item-meta.tsx (1)

59-64: Hotkeys in closed menus may still steal keyboard events from the rest of the application.

limel-hotkey registers a document-level keydown listener and can call preventDefault() + set the handledFlag when a matching key is pressed. If limel-portal keeps the DOM alive (hidden but attached) when visible=false, these limel-hotkey instances remain active and will intercept keypresses even when the menu is closed.

The menu's handleHotkeyTrigger has a !this.open guard, but that only prevents selection — it doesn't prevent the limel-hotkey component itself from consuming the keyboard event before it reaches other handlers.

Consider threading the menu's open state into disabled here:

-<limel-hotkey value={this.hotkey} disabled={this.disabled} />
+<limel-hotkey value={this.hotkey} disabled={this.disabled || !this.menuOpen} />

(This would require a new menuOpen prop propagated from the menu.)

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

In `@src/components/list-item/menu-item-meta/menu-item-meta.tsx` around lines 59 -
64, renderCommandText currently renders <limel-hotkey value={this.hotkey}
disabled={this.disabled} /> which leaves hotkeys active even when the menu is
closed (limel-hotkey listens at document level); update the component to accept
a new boolean prop (e.g., menuOpen) and pass combinedDisabled = this.disabled ||
!this.menuOpen into renderCommandText so the <limel-hotkey> receives disabled
when the menu is closed, add the new menuOpen prop to the menu item component
and ensure the parent menu propagates its open state down to this component.
src/components/hotkey/examples/hotkey-basic.tsx (1)

51-52: meta+enter and cmd+enter normalize to the same hotkey, causing a duplicate-hotkey console warning.

Since cmd is an alias for meta, both entries register under the same normalized key. This will trigger the duplicate-detection console.warn every time the hotkey is pressed in this basic example, which is confusing for readers treating this as a reference. Consider removing one.

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

In `@src/components/hotkey/examples/hotkey-basic.tsx` around lines 51 - 52, The
example registers the same hotkey twice because "cmd" aliases "meta", causing
duplicate-normalization warnings; remove one of the duplicate <limel-hotkey>
entries (either the limel-hotkey with value="meta+enter" or the one with
value="cmd+enter") in the hotkey-basic example so only a single <limel-hotkey>
element registers the Enter shortcut; update the JSX in the hotkey-basic example
where the <limel-hotkey> elements are declared to eliminate the duplicate.
src/components/hotkey/hotkey.scss (1)

16-20: Possible unit mismatch in box-shadow: 0.375px may be a typo for 0.375rem.

The blur radius uses px while the surrounding values use rem, which seems inconsistent.

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

In `@src/components/hotkey/hotkey.scss` around lines 16 - 20, The box-shadow
declaration contains a likely unit mismatch: change the blur radius in the rule
that sets "box-shadow" (currently "0 0.625rem 0.375px -0.5rem ...") to use rem
instead of px so it matches the other values (e.g., "0.375rem"); update the
box-shadow value used in the hotkey component's stylesheet accordingly.
src/components/hotkey/hotkey.tsx (1)

133-183: Missing win and windows cases in formatDisplayToken.

normalizeKey in hotkeys.ts maps win/windows to meta, but formatDisplayToken doesn't handle these tokens. If a consumer passes value="win+c", the "win" token renders as raw text instead of ⊞ Win or .

🤖 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 133 - 183, formatDisplayToken
is missing handling for the "win" and "windows" tokens so keys like "win+c"
render raw; update the switch in formatDisplayToken to treat 'win' and 'windows'
the same as 'meta' (or call normalizeKey before switching) so they return the
platform-specific glyph/display (e.g., ⊞ Win or ⌘) — reference the
formatDisplayToken function and the existing 'meta' case to add 'win' and
'windows' alternatives.
src/util/hotkeys.ts (3)

100-103: Redundant function-key guard — the branch is a no-op.

Both the if body and the fallthrough at line 104 return normalized; the regex check is dead code.

♻️ Proposed fix
-    if (/^f\d{1,2}$/.test(normalized)) {
-        return normalized;
-    }
-
     return normalized;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/hotkeys.ts` around lines 100 - 103, The regex branch checking
/^f\d{1,2}$/ is redundant because it returns the same value as the fallthrough;
remove the entire if block (the /^f\d{1,2}$/.test(normalized) check and its
return) from the hotkey normalization function so there is a single return
normalized; path, or if special handling for function keys was intended,
implement the intended behavior inside the function (e.g., transform or prefix
function keys) instead of a no-op; refer to the normalized variable and the
regex /^f\d{1,2}$/ in hotkeys.ts to locate the dead branch.

29-30: backspace: 'delete' alias collapses two physically distinct keys — document the intent.

On Windows/Linux, Backspace (event.key = "Backspace") and Delete (event.key = "Delete") are distinct keys. With this alias both map to the canonical string "delete", making them indistinguishable to consumers. The JSDoc at line 15 shows the output but not the rationale. If this cross-platform unification is intentional, add a brief comment so future maintainers don't inadvertently "fix" it.

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

In `@src/util/hotkeys.ts` around lines 29 - 30, Summary: The mapping that sets
backspace: 'delete' collapses two distinct physical keys into one canonical
"delete" value without explanation; either document this cross-platform
unification or separate them. Fix: in src/util/hotkeys.ts, update the hotkey
canonicalization mapping (the entry backspace: 'delete' and the related del:
'delete') — if the unification is intentional, add a one-line comment above the
mapping (and update the JSDoc near the existing doc comment around the mapping)
explaining that Backspace and Delete are normalized to "delete" for
cross-platform consistency; if it was unintentional, change the mapping so
Backspace maps to a distinct canonical value (e.g., 'backspace') and adjust any
consumer code that relies on the current canonical string. Ensure you reference
the mapping keys backspace and del when making the change.

20-20: 🧹 Nitpick | 🔵 Trivial

KEY_ALIASES should be Readonly to prevent accidental property mutation.

const prevents reference reassignment but leaves the entries mutable — KEY_ALIASES['x'] = 'y' compiles and silently corrupts normalization for all callers. const freezes the reference; readonly freezes the contents.

♻️ Proposed fix
-const KEY_ALIASES: Record<string, string> = {
+const KEY_ALIASES: Readonly<Record<string, string>> = {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/hotkeys.ts` at line 20, Change the KEY_ALIASES declaration to an
immutable type so its entries can’t be mutated: update the declaration of
KEY_ALIASES to use Readonly<Record<string, string>> (e.g. const KEY_ALIASES:
Readonly<Record<string, string>> = { ... }) and, if you want runtime
enforcement, call Object.freeze(KEY_ALIASES) after initialization; this prevents
accidental writes like KEY_ALIASES['x'] = 'y' while keeping the rest of
normalization logic intact.
🤖 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.e2e.tsx`:
- Around line 177-219: The test "does not allow reserved keys (Enter) to be used
as item hotkeys" currently only asserts that no select events match the "Second"
item, but this can be confusing because Enter may still activate the focused
item; update the test (or its description) to clarify intent by adding a short
comment or adjusting the it(...) title to state it only verifies that Enter does
not trigger hotkey-based selection for the "Second" item (e.g., mention that
native activation of the focused item may still fire a select for "First"), and
keep all existing logic (handler, hotkeyItems, surface dispatchEvent) unchanged
so readers know this test is scoped to hotkey matching rather than general Enter
behavior.

In `@src/components/menu/menu.types.ts`:
- Around line 64-65: The JSDoc above the hotkey property currently reads
"Hotkey(s)..." but the TypeScript type is a single string; update the comment
for the property (the doc for the hotkey/Hotkey field in menu.types.ts) to
singular wording — e.g., "Hotkey that can be used to select this item while the
menu is open." — so the documentation matches the type signature.

---

Duplicate comments:
In `@src/components/hotkey/examples/hotkey-basic.tsx`:
- Around line 51-52: The example registers the same hotkey twice because "cmd"
aliases "meta", causing duplicate-normalization warnings; remove one of the
duplicate <limel-hotkey> entries (either the limel-hotkey with
value="meta+enter" or the one with value="cmd+enter") in the hotkey-basic
example so only a single <limel-hotkey> element registers the Enter shortcut;
update the JSX in the hotkey-basic example where the <limel-hotkey> elements are
declared to eliminate the duplicate.

In `@src/components/hotkey/hotkey.scss`:
- Around line 16-20: The box-shadow declaration contains a likely unit mismatch:
change the blur radius in the rule that sets "box-shadow" (currently "0 0.625rem
0.375px -0.5rem ...") to use rem instead of px so it matches the other values
(e.g., "0.375rem"); update the box-shadow value used in the hotkey component's
stylesheet accordingly.

In `@src/components/hotkey/hotkey.tsx`:
- Around line 133-183: formatDisplayToken is missing handling for the "win" and
"windows" tokens so keys like "win+c" render raw; update the switch in
formatDisplayToken to treat 'win' and 'windows' the same as 'meta' (or call
normalizeKey before switching) so they return the platform-specific
glyph/display (e.g., ⊞ Win or ⌘) — reference the formatDisplayToken function and
the existing 'meta' case to add 'win' and 'windows' alternatives.

In `@src/components/hotkey/hotkey.types.ts`:
- Around line 1-5: Add JSDoc to the public type LimelHotkeyTriggerDetail
describing its purpose and each property: a short description for the type
itself (e.g., "Details provided when a Limel hotkey is triggered"), then
property tags for hotkey (string identifier for the hotkey), value (string
payload or label associated with the trigger), and keyboardEvent (the original
KeyboardEvent instance). Place the JSDoc immediately above the exported type
definition so the documentation appears in the public API and tooling.

In `@src/components/list-item/menu-item-meta/menu-item-meta.tsx`:
- Around line 59-64: renderCommandText currently renders <limel-hotkey
value={this.hotkey} disabled={this.disabled} /> which leaves hotkeys active even
when the menu is closed (limel-hotkey listens at document level); update the
component to accept a new boolean prop (e.g., menuOpen) and pass
combinedDisabled = this.disabled || !this.menuOpen into renderCommandText so the
<limel-hotkey> receives disabled when the menu is closed, add the new menuOpen
prop to the menu item component and ensure the parent menu propagates its open
state down to this component.

In `@src/components/markdown/partial-styles/_kbd.scss`:
- Around line 26-29: The box-shadow blur radius uses a pixel value (0.375px)
while all other offsets use rems; update the blur radius in the box-shadow
declaration in _kbd.scss to an equivalent rem value (e.g., 0.0234375rem if using
16px root) so units are consistent with the other offsets, keeping the rest of
the box-shadow entries and var(--button-shadow-normal) unchanged.

In `@src/components/menu-list/menu-list-renderer.tsx`:
- Line 108: The guard "'hotkey' in item" is redundant after the early-return
narrow to MenuItem; replace the check for hasHotkey with a simple truthy cast
like !!(item as MenuItem).hotkey to rely on TypeScript narrowing and remove the
unnecessary 'in' operator; update the declaration that defines hasHotkey (the
const hasHotkey variable in menu-list-renderer.tsx) and ensure any references
assume item is a MenuItem.

In `@src/components/menu/menu.tsx`:
- Around line 308-337: The inline event detail type used in the
handleHotkeyTrigger method duplicates the shared LimelHotkeyTriggerDetail;
replace the inline type with an import of LimelHotkeyTriggerDetail from
hotkey.types and annotate the event parameter as
CustomEvent<LimelHotkeyTriggerDetail> in the handleHotkeyTrigger signature,
adding the import at the top of the file and leaving the method body unchanged
(method name: handleHotkeyTrigger).

In `@src/util/device.ts`:
- Around line 1-21: Remove the redundant explicit return-type annotations from
the three unexported helper functions so TypeScript can infer them
automatically: delete ": string" from getUserAgent and getPlatform and ":
number" from getMaxTouchPoints; keep the existing runtime guards and returns
intact and do not change the function bodies or exports (these are private
helpers so inferred types are sufficient).
- Around line 49-60: The isIPadOS13Plus local is redundant because it is defined
as isMacLike && getMaxTouchPoints() > 1 and then OR'd with isMacLike in
isAppleDevice(); update isAppleDevice() to return isIPadIPhoneIPod || isMacLike
(and remove the unused isIPadOS13Plus variable and its getMaxTouchPoints check),
keeping helper calls getUserAgent and getPlatform as-is so only the redundant
branch is eliminated.

In `@src/util/hotkeys.spec.ts`:
- Around line 1-130: Add unit tests to cover the missing hotkeys edge cases:
create new specs in src/util/hotkeys.spec.ts that (1) call normalizeHotkeyString
with empty string and null and assert it returns the normalized empty result
(e.g., ''), and assert modifier-only inputs like "ctrl+shift" produce the
normalized modifier-only token sequence (e.g., "ctrl+shift"); (2) directly test
tokenizeHotkeyString with cases including single "+" and "++" and mixed
modifier+key strings to verify tokenization; and (3) add tests for
isKeyboardEventFromTextInput covering typical text-input events versus non-text
events (e.g., key 'a' on input/textarea targets vs function keys or
modifier-only events) to assert the boolean result; if any test fails, update
the corresponding functions (normalizeHotkeyString, tokenizeHotkeyString,
isKeyboardEventFromTextInput) to conform to these expected behaviors.

In `@src/util/hotkeys.ts`:
- Around line 100-103: The regex branch checking /^f\d{1,2}$/ is redundant
because it returns the same value as the fallthrough; remove the entire if block
(the /^f\d{1,2}$/.test(normalized) check and its return) from the hotkey
normalization function so there is a single return normalized; path, or if
special handling for function keys was intended, implement the intended behavior
inside the function (e.g., transform or prefix function keys) instead of a
no-op; refer to the normalized variable and the regex /^f\d{1,2}$/ in hotkeys.ts
to locate the dead branch.
- Around line 29-30: Summary: The mapping that sets backspace: 'delete'
collapses two distinct physical keys into one canonical "delete" value without
explanation; either document this cross-platform unification or separate them.
Fix: in src/util/hotkeys.ts, update the hotkey canonicalization mapping (the
entry backspace: 'delete' and the related del: 'delete') — if the unification is
intentional, add a one-line comment above the mapping (and update the JSDoc near
the existing doc comment around the mapping) explaining that Backspace and
Delete are normalized to "delete" for cross-platform consistency; if it was
unintentional, change the mapping so Backspace maps to a distinct canonical
value (e.g., 'backspace') and adjust any consumer code that relies on the
current canonical string. Ensure you reference the mapping keys backspace and
del when making the change.
- Line 20: Change the KEY_ALIASES declaration to an immutable type so its
entries can’t be mutated: update the declaration of KEY_ALIASES to use
Readonly<Record<string, string>> (e.g. const KEY_ALIASES:
Readonly<Record<string, string>> = { ... }) and, if you want runtime
enforcement, call Object.freeze(KEY_ALIASES) after initialization; this prevents
accidental writes like KEY_ALIASES['x'] = 'y' while keeping the rest of
normalization logic intact.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cbd4667 and 909b857.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (18)
  • src/components/hotkey/examples/hotkey-basic.scss
  • src/components/hotkey/examples/hotkey-basic.tsx
  • src/components/hotkey/examples/hotkey-disabled.tsx
  • src/components/hotkey/examples/hotkey-duplicates.tsx
  • src/components/hotkey/examples/hotkey-prevent-default.tsx
  • src/components/hotkey/hotkey.scss
  • src/components/hotkey/hotkey.tsx
  • src/components/hotkey/hotkey.types.ts
  • 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/menu.e2e.tsx
  • src/components/menu/menu.tsx
  • src/components/menu/menu.types.ts
  • src/util/device.ts
  • src/util/hotkeys.spec.ts
  • src/util/hotkeys.ts

Comment thread src/components/menu/menu.e2e.tsx
Comment thread src/components/menu/menu.types.ts
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 (19)
src/util/device.ts (1)

1-21: 🧹 Nitpick | 🔵 Trivial

Explicit return-type annotations on unexported helpers are still present.

getUserAgent(), getPlatform(), and getMaxTouchPoints() are unexported; TypeScript infers their return types exactly. The annotations add no contract value.

♻️ Proposed simplification
-function getUserAgent(): string {
+function getUserAgent() {

-function getPlatform(): string {
+function getPlatform() {

-function getMaxTouchPoints(): number {
+function getMaxTouchPoints() {

Based on learnings: "prefer omitting explicit return type annotations for functions when TypeScript inference is sufficient."

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

In `@src/util/device.ts` around lines 1 - 21, Remove the redundant explicit
return-type annotations from the unexported helper functions getUserAgent,
getPlatform, and getMaxTouchPoints: let TypeScript infer the return types by
deleting the ": string" and ": number" annotations on those function
declarations while keeping their bodies and runtime behavior unchanged (no
changes to exported APIs).
src/components/menu-list/menu-list-renderer.tsx (1)

108-108: 🧹 Nitpick | 🔵 Trivial

Redundant 'hotkey' in item guard — simplify to a truthiness check.

After the separator early-return at line 89, TypeScript narrows item to MenuItem, which means accessing an optional property that is absent returns undefined. The in check is superfluous.

♻️ Proposed simplification
-        const hasHotkey = 'hotkey' in item && !!(item as any).hotkey;
+        const hasHotkey = !!(item as MenuItem).hotkey;
🤖 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` at line 108, The `'hotkey'
in item` check is redundant because `item` is already narrowed to MenuItem after
the earlier separator early-return; replace the current declaration of hasHotkey
(const hasHotkey = 'hotkey' in item && !!(item as any).hotkey;) with a simple
truthiness check like const hasHotkey = !!(item as MenuItem).hotkey or const
hasHotkey = Boolean(item.hotkey) to remove the unnecessary `in` guard and keep
proper typing.
src/components/markdown/partial-styles/_kbd.scss (1)

26-29: ⚠️ Potential issue | 🟡 Minor

0.375px blur radius — same sub-pixel issue as in hotkey.scss.

Since hotkey.scss @forwards this file, this is the source of the mismatch. All other shadow values use rem; the blur should be 0.375rem.

🐛 Proposed fix
-        0 0.625rem 0.375px -0.5rem rgb(var(--color-black), 0.02),
+        0 0.625rem 0.375rem -0.5rem rgb(var(--color-black), 0.02),
🤖 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 26 - 29, The
box-shadow in _kbd.scss uses a sub-pixel blur "0.375px" which causes a unit
mismatch (hotkey.scss forwards this file); change that blur value to "0.375rem"
so it matches the other shadow values and the rest of the codebase. Locate the
box-shadow declaration in src/components/markdown/partial-styles/_kbd.scss and
replace the "0.375px" blur term with "0.375rem" (ensure consistency with
hotkey.scss which forwards this file).
src/components/hotkey/hotkey.scss (1)

16-20: ⚠️ Potential issue | 🟡 Minor

0.375px blur radius — still effectively zero in most browsers.

The second shadow layer mixes rem values everywhere except the blur radius (0.375px). A sub-pixel blur rounds to 0–1 px; the intended value is likely 0.375rem (~6 px).

🐛 Proposed fix
-        0 0.625rem 0.375px -0.5rem rgb(var(--color-black), 0.02),
+        0 0.625rem 0.375rem -0.5rem rgb(var(--color-black), 0.02),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/hotkey/hotkey.scss` around lines 16 - 20, The box-shadow
declaration's second shadow layer uses a sub-pixel blur "0.375px", which is
effectively zero in many browsers; update the blur radius in the box-shadow
property (the line starting with "0 0.625rem 0.375px -0.5rem rgb(...)") to use
"0.375rem" instead of "0.375px" so the blur scales with rem units and produces
the intended visible blur.
src/components/menu/menu.types.ts (1)

64-66: ⚠️ Potential issue | 🟡 Minor

"Hotkey(s)" — plural is misleading for a string type.

The parenthetical (s) suggests string | string[], but the property only accepts a single string.

📝 Proposed fix
-     * Hotkey(s) that can be used to select this item while the menu is open.
+     * A hotkey that can be used to select this item while the menu is open.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/menu/menu.types.ts` around lines 64 - 66, The JSDoc "Hotkey(s)
that can be used..." is misleading because the property it documents is typed as
a single string; update the JSDoc above the hotkey property in
src/components/menu/menu.types.ts to use singular wording (e.g., "Hotkey that
can be used to select this item while the menu is open.") or alternatively
change the property's type to string | string[] if multiple hotkeys were
intended; adjust either the comment or the property type for consistency with
the property name (hotkey/hotkeys) used in the file.
src/components/hotkey/hotkey.tsx (2)

61-62: @Event options are redundant — StencilJS defaults to bubbles: true, composed: true.

bubbles defaults to true and composed defaults to true in StencilJS, so the explicit object { bubbles: true, composed: true } is a no-op. This was already noted by a human reviewer. If you want to keep the options explicit for documentation clarity that's fine, but otherwise @Event() alone is sufficient.

🤖 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 61 - 62, The `@Event` decorator
on the hotkeyTrigger EventEmitter is using redundant options ({ bubbles: true,
composed: true }) because StencilJS already defaults those to true; remove the
explicit options and change the decorator to `@Event`() for the hotkeyTrigger
field (symbol: hotkeyTrigger) to eliminate the no-op, or keep the explicit
object only if you intentionally want to preserve inline documentation clarity.

133-182: formatDisplayToken is missing 'win' and 'windows' aliases.

The documentation (e.g., menu-hotkeys.tsx line 26) explicitly lists win and windows as aliases for meta. normalizeKey in hotkeys.ts already maps them to meta for matching, but formatDisplayToken's switch has no case 'win': case 'windows':, so they fall through to return { display: trimmed, isGlyph: false } and render as the raw strings "win" / "windows" instead of the proper platform glyph.

🔧 Proposed fix
             case 'meta': {
                 return isApple
                     ? { display: '⌘', isGlyph: true }
                     : { display: '⊞ Win', isGlyph: false };
             }

+            case 'win':
+            case 'windows': {
+                return isApple
+                    ? { display: '⌘', isGlyph: true }
+                    : { display: '⊞ Win', isGlyph: false };
+            }
+
             case 'cmd':
🤖 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 133 - 182, formatDisplayToken
fails to special-case the 'win' and 'windows' aliases so those tokens render as
raw text instead of mapping to the platform meta glyph; update the switch in
formatDisplayToken to include case 'win': and case 'windows': (grouped with the
existing 'meta' branch) so they return the same { display: '⌘' / '⊞ Win',
isGlyph: ... } behavior as 'meta' (respecting isApple), keeping normalizeKey
behavior unchanged.
src/util/hotkeys.spec.ts (1)

20-158: Good additions; unresolved coverage gaps remain.

The new hotkeyFromKeyboardEvent tests covering multiple modifiers, function keys, arrow keys, non-QWERTY code normalization, and Backspace/Delete are all correct and appreciated. Tests for isKeyboardEventFromTextInput, edge cases for normalizeHotkeyString (empty/null input, modifier-only strings like "ctrl+shift" that should return null), and direct tokenizeHotkeyString tests for complex inputs (e.g. "ctrl++a") are still absent.

🤖 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 missing unit tests to
close coverage gaps: create tests for isKeyboardEventFromTextInput verifying
text input elements and non-text elements; add edge-case tests for
normalizeHotkeyString to assert it returns null for empty/null inputs and for
modifier-only strings like "ctrl+shift"; and add direct tokenizeHotkeyString
tests for complex inputs such as "ctrl++a" (and similar cases) to ensure tokens
are parsed correctly; place these in src/util/hotkeys.spec.ts alongside existing
hotkeyFromKeyboardEvent tests and reference the functions
isKeyboardEventFromTextInput, normalizeHotkeyString, and tokenizeHotkeyString
when asserting expected outputs.
src/components/menu/examples/menu-hotkeys.tsx (1)

63-71: 'Organize' hotkey 'o' navigates into the sub-menu rather than selecting — clarify in a comment.

When 'o' is pressed, the menu's handleSelect branches on Array.isArray(items) && items.length > 0, navigating into the sub-menu instead of emitting a select event. This is intentional behaviour worth documenting inline so example readers aren't confused.

🤖 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 63 - 71, The
'Organize' item defines hotkey 'o' but pressing it opens the sub-menu because
handleSelect checks Array.isArray(items) && items.length > 0 and navigates
rather than emitting select; add a concise inline comment either on the
'Organize' menu entry or directly above the handleSelect implementation noting
that hotkeys on items with a non-empty items array will open the submenu (not
emit select), so readers understand this intentional behavior and won't be
surprised by the navigation.
src/components/list-item/menu-item-meta/menu-item-meta.tsx (2)

59-64: limel-hotkey instances in closed menus remain registered and active.

disabled={this.disabled} reflects only the menu item's disabled state, not the menu's open state. When the menu is closed but its portal DOM stays attached, any enabled limel-hotkey rendered here stays in the global registry, intercepts matching keypresses, calls event.preventDefault(), and sets handledFlag — stealing events from other global handlers even though the menu is not open.

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

In `@src/components/list-item/menu-item-meta/menu-item-meta.tsx` around lines 59 -
64, renderCommandText currently always renders a <limel-hotkey> when this.hotkey
is set, which registers global key handlers even when the menu is closed; update
renderCommandText to only render the <limel-hotkey> when the menu is actually
open (e.g. check a boolean like this.open or this.menuOpen propagated into this
component) in addition to this.hotkey and this.disabled so the hotkey is not
registered while the menu is closed, or alternatively move hotkey rendering to
the parent that controls open state; ensure you reference renderCommandText,
limel-hotkey, this.hotkey and this.disabled when making the change.

22-23: reflect: true added to commandText is an unconfirmed behavioral change.

Previously, commandText was not reflected to the DOM attribute. Adding reflect: true now serializes the value as an HTML attribute on every render. Please confirm this is intentional (e.g., a CSS [command-text] selector is needed) and not an accidental side-effect.

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

In `@src/components/list-item/menu-item-meta/menu-item-meta.tsx` around lines 22 -
23, The change adds reflect: true to the `@Prop` decorator on commandText (public
commandText?: string) which causes the property to be serialized to an HTML
attribute on every render—confirm intent or revert: if unintentional, remove
reflect: true from the `@Prop` on commandText to restore previous behavior; if
intentional (e.g., relying on a CSS attribute selector like [command-text] or
external consumers reading the attribute), leave reflect: true and add a short
inline comment above the property explaining why reflection is required and add
a test or note in the component docs verifying the attribute is relied upon.
src/components/hotkey/examples/hotkey-basic.tsx (1)

51-52: meta+enter and cmd+enter normalize to the same key — will always trigger a console.warn.

cmd is an alias for meta, so both entries register as meta+enter. Every keypress will fire the duplicate-detection warning in hotkey.tsx, making the basic example noisy for anyone opening the console.

Either remove one of the two lines, or move the alias-pairing into the hotkey-duplicates example where the warning is the intended teaching point.

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

In `@src/components/hotkey/examples/hotkey-basic.tsx` around lines 51 - 52, The
example registers both "meta+enter" and its alias "cmd+enter" in
src/components/hotkey/examples/hotkey-basic.tsx which normalize to the same
hotkey and triggers the duplicate-detection in hotkey.tsx on every keypress; fix
this by removing one of the duplicate limel-hotkey entries (either the
"meta+enter" or "cmd+enter" line) or move the alias pair into the
hotkey-duplicates example so the warning remains only where it’s intended,
ensuring the basic example registers a single unique hotkey.
src/components/menu/menu.e2e.tsx (1)

127-175: Good coverage; note unresolved stale portal concern.

The new hotkey selection test looks correct. However, document.querySelector('limel-menu-surface') at Line 153 (and similarly at Lines 198, 239) can return a surface from a prior test's un-torn-down portal, silently misdirecting the dispatched event.

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

In `@src/components/menu/menu.e2e.tsx` around lines 127 - 175, The test can pick
up a stale limel-menu-surface from a previous test; before rendering/opening the
menu (or before querying for the surface) remove any existing portal surfaces by
calling document.querySelectorAll('limel-menu-surface') and removing them, then
proceed with render/open and query document.querySelector('limel-menu-surface')
for the current test; update the test around the
render/root/waitForChanges/trigger flow that interacts with <limel-menu> and
limel-menu-surface so it cleans prior surfaces first to ensure the dispatched
KeyboardEvent targets the surface created by this test only.
src/components/menu/menu.tsx (3)

309-334: Clean hotkey dispatch with proper guard clauses.

Previous review concerns (no-op preventDefault on the CustomEvent, inline type duplication) are both addressed. The early-return chain is readable and correctly funnels only valid, non-reserved matches into handleSelect.

🤖 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 309 - 334, The handleHotkeyTrigger
implementation looks correct and the previous problems are resolved; keep the
early-return guard chain as-is in handleHotkeyTrigger, ensure you continue to
use isReservedMenuHotkey(pressedHotkey) to skip reserved keys,
findMenuItemByHotkey(pressedHotkey) to locate actionable items, and then call
handleSelect(matchedItem) after stopping propagation and preventing default on
event.detail.keyboardEvent; no further changes required.

36-37: Good: shared type imported, relative paths used.

The import of LimelHotkeyTriggerDetail addresses the previously flagged inline type duplication. Both imports correctly use relative paths per coding guidelines.

🤖 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 36 - 37, The imports for shared
symbols are correct: keep the relative imports of normalizeHotkeyString and
LimelHotkeyTriggerDetail as they replace the previous inline duplicated type; no
code changes needed — ensure the import lines referencing normalizeHotkeyString
and LimelHotkeyTriggerDetail remain unchanged.

363-381: LGTM — clean iteration with caching.

Previous redundant cast concern is resolved. The search correctly restricts to visible, enabled menu items and leverages the normalized hotkey cache.

🤖 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 363 - 381, The implementation of
findMenuItemByHotkey correctly iterates this.visibleItems, filters non-menu or
disabled items, uses getNormalizedHotkey(rawHotkey) and returns the matching
MenuItem; no code changes are required—mark this as approved and remove the
duplicated review comment to avoid noise in the PR.
src/util/hotkeys.ts (3)

89-100: LGTM — concise normalization with alias lookup.

Previous redundant function-key guard has been removed. The function is straightforward.

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

In `@src/util/hotkeys.ts` around lines 89 - 100, The normalizeKey function is
concise and correct; no code change required—keep the implementation of
normalizeKey (which trims, lowercases, returns null on empty, and consults
KEY_ALIASES) as-is and ensure KEY_ALIASES remains defined and populated
elsewhere in the module.

20-35: Good: backspace alias removed, keys are now distinguishable.

The previously flagged backspace: 'delete' alias has been removed. Backspace and Delete are now distinct canonical keys, and the module JSDoc (line 15) confirms this.

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

In `@src/util/hotkeys.ts` around lines 20 - 35, KEY_ALIASES should no longer map
"backspace" to "delete"—confirm that the KEY_ALIASES constant no longer contains
a backspace alias and that the module JSDoc (the comment around hotkeys.ts)
documents that Backspace and Delete are distinct; search for any references to
KEY_ALIASES or code that expects a "backspace"→"delete" mapping and update those
usages/tests to handle "backspace" and "delete" separately.

102-137: LGTM — canonical modifier ordering is clear and consistent.

The controlctrl alias is now centralized in KEY_ALIASES, so the modifier check on line 115 correctly uses only canonical names.

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

In `@src/util/hotkeys.ts` around lines 102 - 137, No functional change is needed:
the normalizeModifiersAndKey function correctly normalizes keys, ignores pure
modifier presses, and uses canonical modifier names (meta, ctrl, alt, shift) as
intended; simply close out the PR as approved and remove any duplicate review
comment threads if present to avoid confusion (refer to normalizeModifiersAndKey
and KEY_ALIASES for context).
🤖 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 336-361: The isReservedMenuHotkey function misparses hotkeys
containing the literal '+' key because it uses hotkey.split('+') and filters
empty parts; update isReservedMenuHotkey to derive the final key using the
existing tokenizer instead of naive split: import or reference
tokenizeHotkeyString (already present via normalizeHotkeyString import) and call
tokenizeHotkeyString(hotkey) to get tokens, then use the last token as key for
the reserved checks (preserving '+' when it's the actual key); keep the rest of
the reserved-key logic unchanged.

In `@src/util/device.ts`:
- Line 49: Remove the redundant explicit return type on isAppleDevice(): boolean
so it matches the other exported helpers (isIOSDevice, isAndroidDevice,
isMobileDevice) that rely on TypeScript return-type inference; locate the
isAppleDevice function and delete the ": boolean" annotation from its signature
so the function signature becomes just export function isAppleDevice() and keeps
behavior unchanged.

---

Duplicate comments:
In `@src/components/hotkey/examples/hotkey-basic.tsx`:
- Around line 51-52: The example registers both "meta+enter" and its alias
"cmd+enter" in src/components/hotkey/examples/hotkey-basic.tsx which normalize
to the same hotkey and triggers the duplicate-detection in hotkey.tsx on every
keypress; fix this by removing one of the duplicate limel-hotkey entries (either
the "meta+enter" or "cmd+enter" line) or move the alias pair into the
hotkey-duplicates example so the warning remains only where it’s intended,
ensuring the basic example registers a single unique hotkey.

In `@src/components/hotkey/hotkey.scss`:
- Around line 16-20: The box-shadow declaration's second shadow layer uses a
sub-pixel blur "0.375px", which is effectively zero in many browsers; update the
blur radius in the box-shadow property (the line starting with "0 0.625rem
0.375px -0.5rem rgb(...)") to use "0.375rem" instead of "0.375px" so the blur
scales with rem units and produces the intended visible blur.

In `@src/components/hotkey/hotkey.tsx`:
- Around line 61-62: The `@Event` decorator on the hotkeyTrigger EventEmitter is
using redundant options ({ bubbles: true, composed: true }) because StencilJS
already defaults those to true; remove the explicit options and change the
decorator to `@Event`() for the hotkeyTrigger field (symbol: hotkeyTrigger) to
eliminate the no-op, or keep the explicit object only if you intentionally want
to preserve inline documentation clarity.
- Around line 133-182: formatDisplayToken fails to special-case the 'win' and
'windows' aliases so those tokens render as raw text instead of mapping to the
platform meta glyph; update the switch in formatDisplayToken to include case
'win': and case 'windows': (grouped with the existing 'meta' branch) so they
return the same { display: '⌘' / '⊞ Win', isGlyph: ... } behavior as 'meta'
(respecting isApple), keeping normalizeKey behavior unchanged.

In `@src/components/list-item/menu-item-meta/menu-item-meta.tsx`:
- Around line 59-64: renderCommandText currently always renders a <limel-hotkey>
when this.hotkey is set, which registers global key handlers even when the menu
is closed; update renderCommandText to only render the <limel-hotkey> when the
menu is actually open (e.g. check a boolean like this.open or this.menuOpen
propagated into this component) in addition to this.hotkey and this.disabled so
the hotkey is not registered while the menu is closed, or alternatively move
hotkey rendering to the parent that controls open state; ensure you reference
renderCommandText, limel-hotkey, this.hotkey and this.disabled when making the
change.
- Around line 22-23: The change adds reflect: true to the `@Prop` decorator on
commandText (public commandText?: string) which causes the property to be
serialized to an HTML attribute on every render—confirm intent or revert: if
unintentional, remove reflect: true from the `@Prop` on commandText to restore
previous behavior; if intentional (e.g., relying on a CSS attribute selector
like [command-text] or external consumers reading the attribute), leave reflect:
true and add a short inline comment above the property explaining why reflection
is required and add a test or note in the component docs verifying the attribute
is relied upon.

In `@src/components/markdown/partial-styles/_kbd.scss`:
- Around line 26-29: The box-shadow in _kbd.scss uses a sub-pixel blur "0.375px"
which causes a unit mismatch (hotkey.scss forwards this file); change that blur
value to "0.375rem" so it matches the other shadow values and the rest of the
codebase. Locate the box-shadow declaration in
src/components/markdown/partial-styles/_kbd.scss and replace the "0.375px" blur
term with "0.375rem" (ensure consistency with hotkey.scss which forwards this
file).

In `@src/components/menu-list/menu-list-renderer.tsx`:
- Line 108: The `'hotkey' in item` check is redundant because `item` is already
narrowed to MenuItem after the earlier separator early-return; replace the
current declaration of hasHotkey (const hasHotkey = 'hotkey' in item && !!(item
as any).hotkey;) with a simple truthiness check like const hasHotkey = !!(item
as MenuItem).hotkey or const hasHotkey = Boolean(item.hotkey) to remove the
unnecessary `in` guard and keep proper typing.

In `@src/components/menu/examples/menu-hotkeys.tsx`:
- Around line 63-71: The 'Organize' item defines hotkey 'o' but pressing it
opens the sub-menu because handleSelect checks Array.isArray(items) &&
items.length > 0 and navigates rather than emitting select; add a concise inline
comment either on the 'Organize' menu entry or directly above the handleSelect
implementation noting that hotkeys on items with a non-empty items array will
open the submenu (not emit select), so readers understand this intentional
behavior and won't be surprised by the navigation.

In `@src/components/menu/menu.e2e.tsx`:
- Around line 127-175: The test can pick up a stale limel-menu-surface from a
previous test; before rendering/opening the menu (or before querying for the
surface) remove any existing portal surfaces by calling
document.querySelectorAll('limel-menu-surface') and removing them, then proceed
with render/open and query document.querySelector('limel-menu-surface') for the
current test; update the test around the render/root/waitForChanges/trigger flow
that interacts with <limel-menu> and limel-menu-surface so it cleans prior
surfaces first to ensure the dispatched KeyboardEvent targets the surface
created by this test only.

In `@src/components/menu/menu.tsx`:
- Around line 309-334: The handleHotkeyTrigger implementation looks correct and
the previous problems are resolved; keep the early-return guard chain as-is in
handleHotkeyTrigger, ensure you continue to use
isReservedMenuHotkey(pressedHotkey) to skip reserved keys,
findMenuItemByHotkey(pressedHotkey) to locate actionable items, and then call
handleSelect(matchedItem) after stopping propagation and preventing default on
event.detail.keyboardEvent; no further changes required.
- Around line 36-37: The imports for shared symbols are correct: keep the
relative imports of normalizeHotkeyString and LimelHotkeyTriggerDetail as they
replace the previous inline duplicated type; no code changes needed — ensure the
import lines referencing normalizeHotkeyString and LimelHotkeyTriggerDetail
remain unchanged.
- Around line 363-381: The implementation of findMenuItemByHotkey correctly
iterates this.visibleItems, filters non-menu or disabled items, uses
getNormalizedHotkey(rawHotkey) and returns the matching MenuItem; no code
changes are required—mark this as approved and remove the duplicated review
comment to avoid noise in the PR.

In `@src/components/menu/menu.types.ts`:
- Around line 64-66: The JSDoc "Hotkey(s) that can be used..." is misleading
because the property it documents is typed as a single string; update the JSDoc
above the hotkey property in src/components/menu/menu.types.ts to use singular
wording (e.g., "Hotkey that can be used to select this item while the menu is
open.") or alternatively change the property's type to string | string[] if
multiple hotkeys were intended; adjust either the comment or the property type
for consistency with the property name (hotkey/hotkeys) used in the file.

In `@src/util/device.ts`:
- Around line 1-21: Remove the redundant explicit return-type annotations from
the unexported helper functions getUserAgent, getPlatform, and
getMaxTouchPoints: let TypeScript infer the return types by deleting the ":
string" and ": number" annotations on those function declarations while keeping
their bodies and runtime behavior unchanged (no changes to exported APIs).

In `@src/util/hotkeys.spec.ts`:
- Around line 20-158: Add missing unit tests to close coverage gaps: create
tests for isKeyboardEventFromTextInput verifying text input elements and
non-text elements; add edge-case tests for normalizeHotkeyString to assert it
returns null for empty/null inputs and for modifier-only strings like
"ctrl+shift"; and add direct tokenizeHotkeyString tests for complex inputs such
as "ctrl++a" (and similar cases) to ensure tokens are parsed correctly; place
these in src/util/hotkeys.spec.ts alongside existing hotkeyFromKeyboardEvent
tests and reference the functions isKeyboardEventFromTextInput,
normalizeHotkeyString, and tokenizeHotkeyString when asserting expected outputs.

In `@src/util/hotkeys.ts`:
- Around line 89-100: The normalizeKey function is concise and correct; no code
change required—keep the implementation of normalizeKey (which trims,
lowercases, returns null on empty, and consults KEY_ALIASES) as-is and ensure
KEY_ALIASES remains defined and populated elsewhere in the module.
- Around line 20-35: KEY_ALIASES should no longer map "backspace" to
"delete"—confirm that the KEY_ALIASES constant no longer contains a backspace
alias and that the module JSDoc (the comment around hotkeys.ts) documents that
Backspace and Delete are distinct; search for any references to KEY_ALIASES or
code that expects a "backspace"→"delete" mapping and update those usages/tests
to handle "backspace" and "delete" separately.
- Around line 102-137: No functional change is needed: the
normalizeModifiersAndKey function correctly normalizes keys, ignores pure
modifier presses, and uses canonical modifier names (meta, ctrl, alt, shift) as
intended; simply close out the PR as approved and remove any duplicate review
comment threads if present to avoid confusion (refer to normalizeModifiersAndKey
and KEY_ALIASES for context).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 909b857 and 7cab23f.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (18)
  • src/components/hotkey/examples/hotkey-basic.scss
  • src/components/hotkey/examples/hotkey-basic.tsx
  • src/components/hotkey/examples/hotkey-disabled.tsx
  • src/components/hotkey/examples/hotkey-duplicates.tsx
  • src/components/hotkey/examples/hotkey-prevent-default.tsx
  • src/components/hotkey/hotkey.scss
  • src/components/hotkey/hotkey.tsx
  • src/components/hotkey/hotkey.types.ts
  • 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/menu.e2e.tsx
  • src/components/menu/menu.tsx
  • src/components/menu/menu.types.ts
  • src/util/device.ts
  • src/util/hotkeys.spec.ts
  • src/util/hotkeys.ts

Comment thread src/components/menu/menu.tsx
Comment thread src/util/device.ts
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 (7)
src/components/menu-list/menu-list-renderer.tsx (1)

108-108: 🛠️ Refactor suggestion | 🟠 Major

Redundant 'hotkey' in item guard — still unaddressed.

After the 'separator' in item early-return at line 89, TypeScript narrows item to MenuItem. Accessing an undefined optional property already returns undefined, making the 'hotkey' in item prefix unnecessary.

♻️ Proposed simplification
-        const hasHotkey = 'hotkey' in item && !!(item as any).hotkey;
+        const hasHotkey = !!(item as MenuItem).hotkey;
🤖 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` at line 108, The check
"'hotkey' in item" is redundant after the earlier "'separator' in item"
early-return which narrows item to MenuItem; replace the hasHotkey expression so
it simply coerces the optional property (e.g., use Boolean(item.hotkey) or
!!item.hotkey) and remove the (item as any) cast so hasHotkey is defined as a
direct, typed check against item.hotkey in the menu-list-renderer.tsx code
(referencing the const hasHotkey and the earlier "'separator' in item" branch).
src/components/hotkey/hotkey.scss (1)

16-20: ⚠️ Potential issue | 🟡 Minor

0.375px blur radius — still unaddressed.

Same unit mismatch as in _kbd.scss line 28: the blur value uses px while all surrounding values use rem. Since hotkey.scss defines its own override kbd block (separate from the @forwarded styles), this must be fixed here independently.

🔧 Proposed fix
-        0 0.625rem 0.375px -0.5rem rgb(var(--color-black), 0.02),
+        0 0.625rem 0.375rem -0.5rem rgb(var(--color-black), 0.02),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/hotkey/hotkey.scss` around lines 16 - 20, The box-shadow in
the kbd override inside hotkey.scss uses a mismatched unit "0.375px" among rem
values; update the blur radius to the rem equivalent (e.g., replace 0.375px with
the proper rem value consistent with surrounding tokens) in the box-shadow
declaration so all components use rem units, ensuring the kbd override mirrors
the unit usage from _kbd.scss; edit the box-shadow line in the kbd/block within
hotkey.scss to use the calculated rem instead of 0.375px.
src/components/menu/menu.e2e.tsx (1)

153-154: ⚠️ Potential issue | 🟡 Minor

Stale document.querySelector('limel-menu-surface') — portal cleanup risk still unresolved.

Since each render() generates a new portalId via createRandomString(), multiple limel-menu-surface elements can coexist in document.body across tests if portal teardown hasn't completed. document.querySelector returns the first match, which may belong to a prior test — silently misdirecting event dispatches and making assertions unreliable.

The same risk applies to Lines 198-199 and Lines 239-240.

Consider scoping the surface lookup to the specific portal container, e.g.:

const portalContainer = document.getElementById(portalId);
const surface = portalContainer?.querySelector('limel-menu-surface');

or adding explicit cleanup between tests.

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

In `@src/components/menu/menu.e2e.tsx` around lines 153 - 154, The test is
querying document.querySelector('limel-menu-surface') which can return a surface
from a previous render because render() generates a new portalId via
createRandomString(); change the lookup to scope to the test's portal container
using the portalId (e.g. get the portal container by
document.getElementById(portalId) and then
portalContainer.querySelector('limel-menu-surface')) or ensure explicit portal
cleanup between tests; update all occurrences (the checks around surface lookup
and event dispatchs) that currently use
document.querySelector('limel-menu-surface') to use the scoped portalContainer
lookup referencing portalId and render().
src/components/markdown/partial-styles/_kbd.scss (1)

26-29: ⚠️ Potential issue | 🟡 Minor

0.375px blur radius — still unaddressed.

The second shadow layer uses px for blur while all other values use rem. A sub-pixel blur is effectively a no-op in most browsers.

🔧 Proposed fix
-        0 0.625rem 0.375px -0.5rem rgb(var(--color-black), 0.02),
+        0 0.625rem 0.375rem -0.5rem rgb(var(--color-black), 0.02),
🤖 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 26 - 29, The
second box-shadow layer uses a sub-pixel blur value "0.375px" which is
inconsistent with the rem-based values and effectively ignored by browsers;
update the blur to a rem value (use the rem equivalent 0.0234375rem or pick a
sensible rem like 0.025rem/0.03rem) in the box-shadow definition in _kbd.scss
(the second shadow layer inside the box-shadow declaration) so all offsets/blur
values consistently use rem units.
src/components/menu/menu.types.ts (1)

64-65: ⚠️ Potential issue | 🟡 Minor

Hotkey(s) wording still misleads consumers — property accepts a single string.

The parenthetical (s) implies string | string[].

📝 Proposed fix
-     * Hotkey(s) that can be used to select this item while the menu is open.
+     * A hotkey that can be used to select this item while the menu is open.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/menu/menu.types.ts` around lines 64 - 65, The JSDoc for the
menu item hotkey currently reads "Hotkey(s)" which incorrectly implies an array;
update the comment above the hotkey property in
src/components/menu/menu.types.ts (the JSDoc immediately preceding the
hotkey/hotkeys property) to read "Hotkey that can be used to select this item
while the menu is open." and ensure the property type remains a single string
(not string[]), so consumers aren't misled.
src/components/hotkey/examples/hotkey-basic.tsx (1)

51-52: ⚠️ Potential issue | 🟡 Minor

Remove duplicated normalized hotkey from the basic example.
Line 51 and Line 52 normalize to the same hotkey (meta+enter), which triggers duplicate warnings and "first handler wins" behavior in a file meant to demonstrate basic usage.

Suggested fix
                 <limel-hotkey value="meta+enter" />
-                <limel-hotkey value="cmd+enter" />
                 <limel-hotkey value="ctrl+shift+c" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/hotkey/examples/hotkey-basic.tsx` around lines 51 - 52, The
example registers two limel-hotkey elements whose values normalize to the same
shortcut (meta+enter), causing duplicate warnings and first-handler-wins
behavior; fix by removing or changing one of the <limel-hotkey> entries in
hotkey-basic.tsx (the elements with value="meta+enter" and value="cmd+enter") so
the example only registers unique normalized hotkeys (e.g., remove the cmd+enter
element or replace it with a different shortcut like ctrl+enter).
src/components/hotkey/hotkey.tsx (1)

133-143: ⚠️ Potential issue | 🟡 Minor

Add win / windows alias handling in token display.
Line 133-143 maps meta/cmd/command, but win and windows currently fall through to raw text despite being aliases of meta in hotkey normalization/docs.

Suggested fix
-            case 'meta': {
+            case 'meta':
+            case 'win':
+            case 'windows': {
                 return isApple
                     ? { display: '⌘', isGlyph: true }
                     : { display: '⊞ Win', 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 133 - 143, The switch over
token lowercase values (switch (lower)) currently handles 'meta', 'cmd', and
'command' but misses 'win' and 'windows', causing those aliases to render as raw
text; add case 'win' and case 'windows' grouped with 'meta' (or map them to the
same return logic) so they return { display: '⌘', isGlyph: true } on Apple and {
display: '⊞ Win', isGlyph: false } otherwise—update the switch in the function
that renders key tokens (the switch (lower) block) to include these cases.
🤖 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 339-363: The isReservedMenuHotkey function currently treats 'tab'
and arrow keys as reserved regardless of modifiers, preventing valid combos like
'alt+arrowdown'; update isReservedMenuHotkey (and its use of
tokenizeHotkeyString/tokens and hasModifiers) so that arrows and 'tab' are only
reserved when there are no modifiers (i.e., when tokens.length === 1), and keep
'enter', 'space', and 'escape' reserved only when unmodified as already
implemented; ensure the final return logic checks modifiers before classifying
arrows/tab as reserved.

---

Duplicate comments:
In `@src/components/hotkey/examples/hotkey-basic.tsx`:
- Around line 51-52: The example registers two limel-hotkey elements whose
values normalize to the same shortcut (meta+enter), causing duplicate warnings
and first-handler-wins behavior; fix by removing or changing one of the
<limel-hotkey> entries in hotkey-basic.tsx (the elements with value="meta+enter"
and value="cmd+enter") so the example only registers unique normalized hotkeys
(e.g., remove the cmd+enter element or replace it with a different shortcut like
ctrl+enter).

In `@src/components/hotkey/hotkey.scss`:
- Around line 16-20: The box-shadow in the kbd override inside hotkey.scss uses
a mismatched unit "0.375px" among rem values; update the blur radius to the rem
equivalent (e.g., replace 0.375px with the proper rem value consistent with
surrounding tokens) in the box-shadow declaration so all components use rem
units, ensuring the kbd override mirrors the unit usage from _kbd.scss; edit the
box-shadow line in the kbd/block within hotkey.scss to use the calculated rem
instead of 0.375px.

In `@src/components/hotkey/hotkey.tsx`:
- Around line 133-143: The switch over token lowercase values (switch (lower))
currently handles 'meta', 'cmd', and 'command' but misses 'win' and 'windows',
causing those aliases to render as raw text; add case 'win' and case 'windows'
grouped with 'meta' (or map them to the same return logic) so they return {
display: '⌘', isGlyph: true } on Apple and { display: '⊞ Win', isGlyph: false }
otherwise—update the switch in the function that renders key tokens (the switch
(lower) block) to include these cases.

In `@src/components/markdown/partial-styles/_kbd.scss`:
- Around line 26-29: The second box-shadow layer uses a sub-pixel blur value
"0.375px" which is inconsistent with the rem-based values and effectively
ignored by browsers; update the blur to a rem value (use the rem equivalent
0.0234375rem or pick a sensible rem like 0.025rem/0.03rem) in the box-shadow
definition in _kbd.scss (the second shadow layer inside the box-shadow
declaration) so all offsets/blur values consistently use rem units.

In `@src/components/menu-list/menu-list-renderer.tsx`:
- Line 108: The check "'hotkey' in item" is redundant after the earlier
"'separator' in item" early-return which narrows item to MenuItem; replace the
hasHotkey expression so it simply coerces the optional property (e.g., use
Boolean(item.hotkey) or !!item.hotkey) and remove the (item as any) cast so
hasHotkey is defined as a direct, typed check against item.hotkey in the
menu-list-renderer.tsx code (referencing the const hasHotkey and the earlier
"'separator' in item" branch).

In `@src/components/menu/menu.e2e.tsx`:
- Around line 153-154: The test is querying
document.querySelector('limel-menu-surface') which can return a surface from a
previous render because render() generates a new portalId via
createRandomString(); change the lookup to scope to the test's portal container
using the portalId (e.g. get the portal container by
document.getElementById(portalId) and then
portalContainer.querySelector('limel-menu-surface')) or ensure explicit portal
cleanup between tests; update all occurrences (the checks around surface lookup
and event dispatchs) that currently use
document.querySelector('limel-menu-surface') to use the scoped portalContainer
lookup referencing portalId and render().

In `@src/components/menu/menu.types.ts`:
- Around line 64-65: The JSDoc for the menu item hotkey currently reads
"Hotkey(s)" which incorrectly implies an array; update the comment above the
hotkey property in src/components/menu/menu.types.ts (the JSDoc immediately
preceding the hotkey/hotkeys property) to read "Hotkey that can be used to
select this item while the menu is open." and ensure the property type remains a
single string (not string[]), so consumers aren't misled.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7cab23f and 1f0986d.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (18)
  • src/components/hotkey/examples/hotkey-basic.scss
  • src/components/hotkey/examples/hotkey-basic.tsx
  • src/components/hotkey/examples/hotkey-disabled.tsx
  • src/components/hotkey/examples/hotkey-duplicates.tsx
  • src/components/hotkey/examples/hotkey-prevent-default.tsx
  • src/components/hotkey/hotkey.scss
  • src/components/hotkey/hotkey.tsx
  • src/components/hotkey/hotkey.types.ts
  • 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/menu.e2e.tsx
  • src/components/menu/menu.tsx
  • src/components/menu/menu.types.ts
  • src/util/device.ts
  • src/util/hotkeys.spec.ts
  • src/util/hotkeys.ts

Comment thread src/components/menu/menu.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.

♻️ Duplicate comments (4)
src/components/markdown/partial-styles/_kbd.scss (1)

26-29: ⚠️ Potential issue | 🟡 Minor

Unit mismatch: 0.375px should likely be 0.375rem.

Line 28 uses 0.375px for the blur radius while all other shadow values use rem units. This appears to be a typo that was flagged previously but not yet addressed.

🔧 Proposed fix
     box-shadow:
         var(--button-shadow-normal),
-        0 0.625rem 0.375px -0.5rem rgb(var(--color-black), 0.02),
+        0 0.625rem 0.375rem -0.5rem rgb(var(--color-black), 0.02),
         0 0.025rem 0.5rem 0 rgb(var(--contrast-100)) inset;
🤖 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 26 - 29, The
box-shadow declaration in _kbd.scss uses a mismatched unit "0.375px" (in the
box-shadow list) which should be "0.375rem"; update the blur radius in the
box-shadow tuple inside the box-shadow property (the line with "0 0.625rem
0.375px -0.5rem") to "0 0.625rem 0.375rem -0.5rem" so all shadow measurements
consistently use rem units.
src/components/menu/menu.types.ts (1)

64-77: ⚠️ Potential issue | 🟡 Minor

Align hotkey JSDoc with its single-string type.

Line 65 says “Hotkey(s)”, but Line 77 defines hotkey?: string (single value). Please switch to singular wording to avoid implying array support.

Proposed fix
-     * Hotkey(s) that can be used to select this item while the menu is open.
+     * A hotkey that can be used to select this item while the menu is open.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/menu/menu.types.ts` around lines 64 - 77, Update the JSDoc for
the hotkey property to match its single-string type: change the header and any
plural wording (e.g., "Hotkey(s)" and references implying arrays) to singular
wording like "Hotkey" and clarify it accepts a single string (examples remain
fine); locate the documentation above the hotkey?: string declaration in
menu.types.ts and adjust the text to avoid implying array support.
src/components/menu-list/menu-list-renderer.tsx (1)

108-113: 🧹 Nitpick | 🔵 Trivial

Remove redundant hotkey guard and any cast.

After the separator early-return, item is already a MenuItem, so Line 108 can be simplified and kept type-safe.

Proposed simplification
-        const hasHotkey = 'hotkey' in item && !!(item as any).hotkey;
+        const hasHotkey = !!item.hotkey;
🤖 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 - 113,
Simplify and make the hotkey check type-safe: remove the redundant "'hotkey' in
item" guard and the "(item as any)" cast used when computing hasHotkey, and
instead directly test the MenuItem's hotkey property for truthiness; then update
hasMeta to use that new hasHotkey variable (leave the other checks—hasSubMenu,
item.badge, commandText—unchanged). This keeps types intact after the separator
early-return where item is already a MenuItem and removes the unnecessary any
cast from hasHotkey and hasMeta.
src/components/menu/menu.e2e.tsx (1)

153-154: ⚠️ Potential issue | 🟡 Minor

Scope menu-surface lookup to the current portal instance.

Using document.querySelector('limel-menu-surface') (Line 153, Line 198, Line 239, Line 274) can pick a stale surface from another test instance.

Proposed fix pattern
- const surface = document.querySelector('limel-menu-surface');
+ const trigger = root.querySelector('button[slot="trigger"]') as HTMLElement;
+ const portalId = trigger.getAttribute('aria-controls');
+ const surface = document
+     .getElementById(portalId!)
+     ?.querySelector('limel-menu-surface');

Also applies to: 198-199, 239-240, 274-275

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

In `@src/components/menu/menu.e2e.tsx` around lines 153 - 154, Tests currently use
document.querySelector('limel-menu-surface') which can return a stale surface
from another test; update each occurrence to query the surface from the current
portal instance instead (e.g., replace
document.querySelector('limel-menu-surface') with
portal.querySelector('limel-menu-surface') or the local portal variable used in
the test setup) so the lookup is scoped to the test's portal element (adjust any
variable name to match the existing portal variable in the test).
🤖 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/markdown/partial-styles/_kbd.scss`:
- Around line 26-29: The box-shadow declaration in _kbd.scss uses a mismatched
unit "0.375px" (in the box-shadow list) which should be "0.375rem"; update the
blur radius in the box-shadow tuple inside the box-shadow property (the line
with "0 0.625rem 0.375px -0.5rem") to "0 0.625rem 0.375rem -0.5rem" so all
shadow measurements consistently use rem units.

In `@src/components/menu-list/menu-list-renderer.tsx`:
- Around line 108-113: Simplify and make the hotkey check type-safe: remove the
redundant "'hotkey' in item" guard and the "(item as any)" cast used when
computing hasHotkey, and instead directly test the MenuItem's hotkey property
for truthiness; then update hasMeta to use that new hasHotkey variable (leave
the other checks—hasSubMenu, item.badge, commandText—unchanged). This keeps
types intact after the separator early-return where item is already a MenuItem
and removes the unnecessary any cast from hasHotkey and hasMeta.

In `@src/components/menu/menu.e2e.tsx`:
- Around line 153-154: Tests currently use
document.querySelector('limel-menu-surface') which can return a stale surface
from another test; update each occurrence to query the surface from the current
portal instance instead (e.g., replace
document.querySelector('limel-menu-surface') with
portal.querySelector('limel-menu-surface') or the local portal variable used in
the test setup) so the lookup is scoped to the test's portal element (adjust any
variable name to match the existing portal variable in the test).

In `@src/components/menu/menu.types.ts`:
- Around line 64-77: Update the JSDoc for the hotkey property to match its
single-string type: change the header and any plural wording (e.g., "Hotkey(s)"
and references implying arrays) to singular wording like "Hotkey" and clarify it
accepts a single string (examples remain fine); locate the documentation above
the hotkey?: string declaration in menu.types.ts and adjust the text to avoid
implying array support.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f0986d and baed4c0.

⛔ 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/markdown/partial-styles/_kbd.scss
  • src/components/menu-list/menu-list-renderer.tsx
  • src/components/menu/examples/menu-hotkeys.tsx
  • src/components/menu/menu.e2e.tsx
  • src/components/menu/menu.tsx
  • src/components/menu/menu.types.ts

Comment thread src/components/menu/examples/menu-searchable-hotkeys.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

🤖 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 7: The example imports isAppleDevice from an internal util which couples
the example to implementation details; make the example self-contained by
removing the import and inlining the device detection logic directly inside the
example (e.g., replace the import of isAppleDevice and any calls to
isAppleDevice with a small local helper or constant within the
menu-searchable-hotkeys.tsx example), or alternatively expose isAppleDevice via
the public API and update the import to use that public export; locate usages of
isAppleDevice in the example and update them to use the new local helper or the
public export accordingly.
- Around line 69-91: The render method currently returns an array of JSX
elements; update the component's render (in the class containing render, which
uses limel-menu and limel-example-value and references handleSearch,
handleSelect, and lastSelectedItem) to wrap these multiple elements inside a
single <Host> element instead of returning an array literal so the JSX follows
StencilJS guidelines; locate the render method and replace the array return with
a single Host wrapper containing the limel-menu (with
searcher={this.handleSearch} and onSelect={this.handleSelect}) and the
limel-example-value (using this.lastSelectedItem).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between baed4c0 and e9397e7.

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

Comment thread src/components/menu/examples/menu-searchable-hotkeys.tsx
Comment thread src/components/menu/examples/menu-searchable-hotkeys.tsx Outdated
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 (5)
src/components/menu/examples/menu-searchable-hotkeys.tsx (1)

32-36: ⚠️ Potential issue | 🟡 Minor

Fix typo in documentation.

Line 33 has a grammatical error: "they focus" should be "the focus".

📝 Proposed fix
-* Consider that when the menu is opened, they focus is on the search input,
+* Consider that when the menu is opened, the focus is on the search input,
🤖 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 32 -
36, In the comment block inside
src/components/menu/examples/menu-searchable-hotkeys.tsx (above the example in
the MenuSearchableHotkeys component), fix the typo "they focus" to "the focus"
so the sentence reads correctly; locate the multi-line comment describing focus
behavior when the menu opens and replace the incorrect word to correct the
documentation grammar.
src/components/markdown/partial-styles/_kbd.scss (1)

28-28: ⚠️ Potential issue | 🟡 Minor

Likely unit typo in box-shadow blur radius.

Line 28 uses 0.375px, which is inconsistent with the surrounding rem-based shadow values and likely unintended.

Proposed fix
-        0 0.625rem 0.375px -0.5rem rgb(var(--color-black), 0.02),
+        0 0.625rem 0.375rem -0.5rem rgb(var(--color-black), 0.02),
🤖 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` at line 28, In _kbd.scss
update the box-shadow value that currently uses a pixel unit (0.375px) to use
the consistent rem unit (0.375rem) so it aligns with the surrounding rem-based
shadow values; locate the box-shadow declaration in the kbd styling (the line
containing "0 0.625rem 0.375px -0.5rem") and change the blur radius unit from px
to rem.
src/components/menu/menu.e2e.tsx (1)

153-155: ⚠️ Potential issue | 🟡 Minor

Use portal-scoped surface queries to avoid stale element selection.

At Line 154 and the other listed occurrences, document.querySelector('limel-menu-surface') can resolve a stale surface from another portal instance, making these tests flaky. Scope the lookup to the current menu’s portal container (via its portalId) before dispatching events.

Suggested direction
-const surface = document.querySelector('limel-menu-surface');
+const portalId = (root as any).portalId;
+const portal = document.getElementById(portalId);
+const surface = portal?.querySelector('limel-menu-surface');
 expect(surface).toBeTruthy();

Also applies to: 199-200, 240-241, 275-276, 313-315

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

In `@src/components/menu/menu.e2e.tsx` around lines 153 - 155, The tests currently
query document.querySelector('limel-menu-surface') which can return a stale
surface from another portal; instead, use the menu's portal container (via the
menu instance's portalId) to scope the lookup before interacting—e.g. get the
portal element by its id (constructed from portalId) and run
querySelector('limel-menu-surface') on that container so each test targets its
own surface; update all occurrences that use
document.querySelector('limel-menu-surface') (the lookups around the surface
variable and subsequent event dispatches) to perform the scoped query against
the portal container retrieved from the menu's portalId.
src/components/hotkey/examples/hotkey-basic.tsx (1)

51-52: ⚠️ Potential issue | 🟡 Minor

Avoid normalized duplicate hotkeys in the basic example.

At Line 51–52, meta+enter and cmd+enter normalize to the same hotkey, so this basic example will emit duplicate-hotkey warnings and blur the intended “basic usage” path.

Proposed fix
                 <limel-hotkey value="meta+enter" />
-                <limel-hotkey value="cmd+enter" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/hotkey/examples/hotkey-basic.tsx` around lines 51 - 52, The
example includes duplicate normalized hotkeys: the two limel-hotkey elements
with value="meta+enter" and value="cmd+enter" resolve to the same binding and
cause duplicate-hotkey warnings; fix by removing or changing one of them (e.g.,
keep value="meta+enter" and delete the limel-hotkey with value="cmd+enter", or
replace it with a different combo like value="ctrl+enter") so the basic example
demonstrates a single unique hotkey.
src/components/hotkey/hotkey.tsx (1)

133-143: ⚠️ Potential issue | 🟡 Minor

Align alias rendering with meta semantics across platforms.

On Line 140–Line 143, cmd/command always render , while meta is platform-aware, and win/windows fall through to raw text. This creates misleading hints on non-Apple platforms for aliases that normalize to meta.

🔧 Proposed fix
         switch (lower) {
             case 'meta': {
                 return isApple
                     ? { display: '⌘', isGlyph: true }
                     : { display: '⊞ Win', isGlyph: false };
             }

+            case 'win':
+            case 'windows':
             case 'cmd':
             case 'command': {
-                return { display: '⌘', isGlyph: true };
+                return isApple
+                    ? { display: '⌘', isGlyph: true }
+                    : { display: '⊞ Win', 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 133 - 143, The switch handling
key aliases treats 'cmd'/'command' differently from 'meta' and causes incorrect
glyphs on non-Apple platforms; update the switch in the function that maps keys
(the block switching on variable lower) so that 'cmd' and 'command' alias to the
same platform-aware logic as 'meta' (i.e., return { display: '⌘', isGlyph: true
} when isApple is true, otherwise return the Windows text like { display: '⊞
Win', isGlyph: false }), and ensure 'win'/'windows' continue to produce the
Windows text variant rather than always falling through to raw text.
🤖 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/util/device.ts`:
- Around line 26-29: isIOSDevice incorrectly returns false for modern iPadOS
Safari because the UA can be "Macintosh"; update isIOSDevice (and callers like
isMobileDevice) to detect iPadOS by checking for either the existing
/iPad|iPhone|iPod/i pattern OR a Macintosh UA combined with touch capability
(e.g., navigator.maxTouchPoints > 1), while preserving the existing !(globalThis
as any).MSStream exclusion and using getUserAgent() to obtain the UA.

---

Duplicate comments:
In `@src/components/hotkey/examples/hotkey-basic.tsx`:
- Around line 51-52: The example includes duplicate normalized hotkeys: the two
limel-hotkey elements with value="meta+enter" and value="cmd+enter" resolve to
the same binding and cause duplicate-hotkey warnings; fix by removing or
changing one of them (e.g., keep value="meta+enter" and delete the limel-hotkey
with value="cmd+enter", or replace it with a different combo like
value="ctrl+enter") so the basic example demonstrates a single unique hotkey.

In `@src/components/hotkey/hotkey.tsx`:
- Around line 133-143: The switch handling key aliases treats 'cmd'/'command'
differently from 'meta' and causes incorrect glyphs on non-Apple platforms;
update the switch in the function that maps keys (the block switching on
variable lower) so that 'cmd' and 'command' alias to the same platform-aware
logic as 'meta' (i.e., return { display: '⌘', isGlyph: true } when isApple is
true, otherwise return the Windows text like { display: '⊞ Win', isGlyph: false
}), and ensure 'win'/'windows' continue to produce the Windows text variant
rather than always falling through to raw text.

In `@src/components/markdown/partial-styles/_kbd.scss`:
- Line 28: In _kbd.scss update the box-shadow value that currently uses a pixel
unit (0.375px) to use the consistent rem unit (0.375rem) so it aligns with the
surrounding rem-based shadow values; locate the box-shadow declaration in the
kbd styling (the line containing "0 0.625rem 0.375px -0.5rem") and change the
blur radius unit from px to rem.

In `@src/components/menu/examples/menu-searchable-hotkeys.tsx`:
- Around line 32-36: In the comment block inside
src/components/menu/examples/menu-searchable-hotkeys.tsx (above the example in
the MenuSearchableHotkeys component), fix the typo "they focus" to "the focus"
so the sentence reads correctly; locate the multi-line comment describing focus
behavior when the menu opens and replace the incorrect word to correct the
documentation grammar.

In `@src/components/menu/menu.e2e.tsx`:
- Around line 153-155: The tests currently query
document.querySelector('limel-menu-surface') which can return a stale surface
from another portal; instead, use the menu's portal container (via the menu
instance's portalId) to scope the lookup before interacting—e.g. get the portal
element by its id (constructed from portalId) and run
querySelector('limel-menu-surface') on that container so each test targets its
own surface; update all occurrences that use
document.querySelector('limel-menu-surface') (the lookups around the surface
variable and subsequent event dispatches) to perform the scoped query against
the portal container retrieved from the menu's portalId.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9397e7 and daf8388.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (19)
  • src/components/hotkey/examples/hotkey-basic.scss
  • src/components/hotkey/examples/hotkey-basic.tsx
  • src/components/hotkey/examples/hotkey-disabled.tsx
  • src/components/hotkey/examples/hotkey-duplicates.tsx
  • src/components/hotkey/examples/hotkey-prevent-default.tsx
  • src/components/hotkey/hotkey.scss
  • src/components/hotkey/hotkey.tsx
  • src/components/hotkey/hotkey.types.ts
  • 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/util/device.ts
  • src/util/hotkeys.spec.ts
  • src/util/hotkeys.ts

Comment thread src/util/device.ts
Comment on lines 26 to 29
export function isIOSDevice() {
return /iPad|iPhone|iPod/i.test(userAgent) && !(window as any).MSStream;
const userAgent = getUserAgent();
return /iPad|iPhone|iPod/i.test(userAgent) && !(globalThis as any).MSStream;
}
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

iPadOS 13+ is misclassified as non-iOS, breaking mobile detection

Line 28 only matches iPad|iPhone|iPod. On newer iPadOS Safari (Mac-like UA), isIOSDevice() returns false, which makes isMobileDevice() on Line 43 return false too.

💡 Suggested fix
 export function isIOSDevice() {
     const userAgent = getUserAgent();
-    return /iPad|iPhone|iPod/i.test(userAgent) && !(globalThis as any).MSStream;
+    const platform = getPlatform();
+    const isIPadIPhoneIPod = /iPad|iPhone|iPod/i.test(userAgent);
+    const isIPadOS13Plus = /Mac/i.test(platform) && getMaxTouchPoints() > 1;
+
+    return (
+        (isIPadIPhoneIPod || isIPadOS13Plus) &&
+        !(globalThis as any).MSStream
+    );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/device.ts` around lines 26 - 29, isIOSDevice incorrectly returns
false for modern iPadOS Safari because the UA can be "Macintosh"; update
isIOSDevice (and callers like isMobileDevice) to detect iPadOS by checking for
either the existing /iPad|iPhone|iPod/i pattern OR a Macintosh UA combined with
touch capability (e.g., navigator.maxTouchPoints > 1), while preserving the
existing !(globalThis as any).MSStream exclusion and using getUserAgent() to
obtain the UA.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@adrianschmidt-bot there are some comments in this PR about improvements that can be done in src/util/device.ts Even though there are some changes in the current PR in the same file, this file is originally too old and its internal logics can be certainly better. Once the current PR is merged, make a new PR and improve this helper file. I am not sure where this file is used in this library. We need to make sure we don't break anything. Therefore, I want this to be done and tested separately (but including the content from this current PR of course).

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.

Got it! I'll create a separate PR to improve src/util/device.ts once this PR is merged. Will make sure to audit all usages across the library and test thoroughly. Added it to my task list. 👍

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.

@Kiarokh Brilliant use of the bot! I noticed that it had checked if this PR was still open as part of its recurring todo-list check, and was curious to see what that was about. And finding this thread was the origin made me smile!

I've had similar cases, where I wanted something fixed in a follow-up, but never thought about actually asking the bot to keep track of when the PR would be closed, so I just had it create an issue that I had to come back to myself later on. But this is so much more convenient!

(Also, this was good timing, because I set up the automatic checking of the todo-list just a few days ago, so before then, this wouldn't have worked 😄)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

great @adrianschmidt I had no idea whether this would work or not. But I thought it was worth testing. 😉

@Kiarokh Kiarokh removed their assignment Feb 27, 2026
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 (10)
src/util/device.ts (1)

26-29: ⚠️ Potential issue | 🟠 Major

iPadOS 13+ is still missed in iOS detection (breaks mobile classification).

Line 28 only checks iPad|iPhone|iPod, so iPadOS Safari with Mac-like UA still returns false in isIOSDevice(), which also makes isMobileDevice() return false for those devices.

💡 Suggested fix
 export function isIOSDevice() {
     const userAgent = getUserAgent();
-    return /iPad|iPhone|iPod/i.test(userAgent) && !(globalThis as any).MSStream;
+    const platform = getPlatform();
+    const isIPadIPhoneIPod = /iPad|iPhone|iPod/i.test(userAgent);
+    const isIPadOS13Plus = /Mac/i.test(platform) && getMaxTouchPoints() > 1;
+
+    return (
+        (isIPadIPhoneIPod || isIPadOS13Plus) &&
+        !(globalThis as any).MSStream
+    );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/device.ts` around lines 26 - 29, isIOSDevice() currently only tests
/iPad|iPhone|iPod/i so modern iPadOS 13+ (which reports a Mac-like UA) is
missed; update isIOSDevice() to also detect iPadOS by checking getUserAgent()
plus platform/touch capabilities (e.g., detect navigator.platform === 'MacIntel'
and navigator.maxTouchPoints > 1 or similar via globalThis) so that
isIOSDevice() (and consequently isMobileDevice()) returns true for iPadOS
Safari; modify the function that calls getUserAgent() (isIOSDevice) to include
this additional platform+touch check while preserving the existing regex path.
src/components/markdown/partial-styles/_kbd.scss (1)

26-29: ⚠️ Potential issue | 🟡 Minor

Unit mismatch: 0.375px likely intended to be 0.375rem.

The box-shadow blur radius at line 28 uses px while all other shadow values use rem. A sub-pixel blur (0.375px) effectively rounds to 0-1px in most browsers, which is probably not the intended effect.

🔧 Proposed fix
     box-shadow:
         var(--button-shadow-normal),
-        0 0.625rem 0.375px -0.5rem rgb(var(--color-black), 0.02),
+        0 0.625rem 0.375rem -0.5rem rgb(var(--color-black), 0.02),
         0 0.025rem 0.5rem 0 rgb(var(--contrast-100)) inset;
🤖 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 26 - 29, The
box-shadow declaration uses a mismatched unit "0.375px" for the blur radius;
update the box-shadow value in the _kbd.scss rule (the box-shadow line
containing "0 0.625rem 0.375px -0.5rem") to use rem instead of px (i.e., change
0.375px → 0.375rem) so all shadow offsets/blur/spread use consistent rem units.
src/components/hotkey/hotkey.tsx (2)

133-182: ⚠️ Potential issue | 🟡 Minor

Add missing win and windows cases to formatDisplayToken.

The documentation states win and windows are aliases for meta, and normalizeKey in hotkeys.ts maps them accordingly. However, formatDisplayToken lacks these cases, causing them to render as raw text instead of the proper glyph.

🔧 Proposed fix
             case 'meta': {
                 return isApple
                     ? { display: '⌘', isGlyph: true }
                     : { display: '⊞ Win', isGlyph: false };
             }

+            case 'win':
+            case 'windows': {
+                return isApple
+                    ? { display: '⌘', isGlyph: true }
+                    : { display: '⊞ Win', isGlyph: false };
+            }
+
             case 'cmd':
             case 'command': {
🤖 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 133 - 182, formatDisplayToken
is missing handling for the 'win' and 'windows' aliases so those keys fall
through to raw text; add case 'win' and case 'windows' alongside the existing
'meta' handling in function formatDisplayToken so they return the same object
(use the isApple conditional to return { display: '⌘', isGlyph: true } for Apple
and { display: '⊞ Win', isGlyph: false } otherwise), keeping the same structure
used for the 'meta' branch.

140-143: ⚠️ Potential issue | 🟡 Minor

Missing platform awareness for cmd/command display tokens.

These cases unconditionally return , but on Windows/Linux a cmd+s hotkey would display ⌘ + S while actually triggering on the Windows/Meta key. This creates a visual mismatch. Consider aligning with the meta case's platform-aware rendering.

🔧 Proposed fix
             case 'cmd':
             case 'command': {
-                return { display: '⌘', isGlyph: true };
+                return isApple
+                    ? { display: '⌘', isGlyph: true }
+                    : { display: '⊞ Win', 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 140 - 143, The 'cmd' /
'command' switch branch is returning an unconditional '⌘' glyph which causes
incorrect visuals on non-mac platforms; update the case for 'cmd' and 'command'
(in the hotkey token/rendering switch inside the Hotkey component / hotkey
rendering function) to be platform-aware and mirror the logic used by the 'meta'
case (use the same isMac/platform detection and return either { display: '⌘',
isGlyph: true } for mac or the platform-appropriate string (e.g., 'Win'/'Ctrl'
or the same display as the 'meta' branch) for non-mac). Ensure the returned
shape still matches { display, isGlyph } so downstream rendering is unchanged.
src/components/hotkey/hotkey.scss (1)

16-19: ⚠️ Potential issue | 🟡 Minor

Same 0.375px unit inconsistency as in _kbd.scss.

This local box-shadow override has the same 0.375px blur value. Since this file @forwards _kbd.scss, consider fixing both locations for consistency, or rely solely on the fix in the source partial.

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

In `@src/components/hotkey/hotkey.scss` around lines 16 - 19, Replace the
inconsistent "0.375px" blur value in the box-shadow declaration with the correct
unit to match surrounding values (e.g., "0.375rem") and make the same change in
the corresponding declaration in _kbd.scss (or remove the local override in
hotkey.scss and rely on the forwarded _kbd.scss partial) so both box-shadow
entries are consistent; locate the box-shadow line in hotkey.scss and the
matching box-shadow in _kbd.scss to apply the fix.
src/components/hotkey/examples/hotkey-basic.tsx (1)

51-52: ⚠️ Potential issue | 🟡 Minor

Avoid duplicate normalized hotkeys in the basic example.

At Line 51–52, meta+enter and cmd+enter normalize to the same hotkey, so this example can trigger duplicate-hotkey warnings.

🛠️ Proposed fix
                 <limel-hotkey value="meta+enter" />
-                <limel-hotkey value="cmd+enter" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/hotkey/examples/hotkey-basic.tsx` around lines 51 - 52, The
example contains duplicate normalized hotkeys: the <limel-hotkey
value="meta+enter" /> and <limel-hotkey value="cmd+enter" /> entries normalize
to the same hotkey and cause duplicate-hotkey warnings; fix it by removing or
replacing one of these tags in the hotkey-basic.tsx example (e.g., remove the
<limel-hotkey value="cmd+enter" /> line or replace it with a different
combination such as <limel-hotkey value="ctrl+enter" />) so each <limel-hotkey>
in the example has a unique normalized value.
src/components/menu/examples/menu-searchable-hotkeys.tsx (2)

7-7: 🧹 Nitpick | 🔵 Trivial

Make the example self-contained instead of importing internal device utilities.

At Line 7, importing isAppleDevice from ../../../util/device couples this example to internals.

♻️ Proposed direction
-import { isAppleDevice } from '../../../util/device';
+const isAppleDevice = () =>
+    /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent);

Based on learnings: "For lime-elements, example files should be self-contained and avoid importing internal implementation details."

🤖 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 7, The
example imports isAppleDevice from internal util/device; remove that external
import and make the example self-contained by implementing a small local
isAppleDevice helper inside menu-searchable-hotkeys.tsx (or the same example
module) and use it where referenced (replace usages of isAppleDevice with the
local function); the helper can check navigator.platform or navigator.userAgent
for Apple identifiers (e.g., /Mac|iPhone|iPad|iPod|Macintosh/) and return a
boolean so the example no longer depends on internal utilities.

33-34: ⚠️ Potential issue | 🟡 Minor

Fix small documentation typo in the example description.

At Line 33, “they focus” should be “the focus”.

📝 Proposed fix
- * Consider that when the menu is opened, they focus is on the search input,
+ * Consider that when the menu is opened, the focus is on the search input,
🤖 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 33 -
34, Fix the typo in the example description comment inside the
MenuSearchableHotkeys example: change the phrase "they focus is on the search
input" to "the focus is on the search input" in the comment above the component
(look for the menu-searchable-hotkeys.tsx example / MenuSearchableHotkeys) so
the sentence reads correctly.
src/components/menu/menu.e2e.tsx (1)

153-155: ⚠️ Potential issue | 🟠 Major

Scope menu-surface lookup to the current test instance to avoid portal flakiness.

Using document.querySelector('limel-menu-surface') can target a stale portal surface from another test run. Query via the current menu’s portal container instead.

🔧 Proposed pattern
- const surface = document.querySelector('limel-menu-surface');
+ const portalId = (root as any).portalId;
+ const portal = document.getElementById(portalId);
+ const surface = portal?.querySelector('limel-menu-surface');
  expect(surface).toBeTruthy();

Also applies to: 199-201, 240-242, 275-277, 313-315

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

In `@src/components/menu/menu.e2e.tsx` around lines 153 - 155, The test is
querying the global document for 'limel-menu-surface', which can pick up a stale
portal from another test; instead scope the lookup to the current menu
instance's portal container (use the menu's portal/container element reference
used in the test to query for 'limel-menu-surface' rather than
document.querySelector), updating each occurrence that sets const surface =
document.querySelector('limel-menu-surface') (and the similar checks at the
other locations) to query from the current menu's portal container so the
surface is tied to this test instance.
src/components/menu/menu.types.ts (1)

64-66: ⚠️ Potential issue | 🟡 Minor

Use singular wording for hotkey documentation.

At Line 65, “Hotkey(s)” implies multiple values, but hotkey is typed as a single string.

📝 Proposed fix
-     * Hotkey(s) that can be used to select this item while the menu is open.
+     * A hotkey that can be used to select this item while the menu is open.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/menu/menu.types.ts` around lines 64 - 66, The doc comment for
the hotkey property uses plural wording "Hotkey(s)" but the symbol hotkey is
typed as a single string; update the comment to use singular wording (e.g.,
"Hotkey") and clarify that this string represents one keyboard shortcut used to
select the item while the menu is open, distinguishing it from commandText which
is purely visual.
🤖 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 314-339: Extract the duplicated hotkey selection flow used in
handleHotkeyTrigger and handleInputKeyDown into a single helper (e.g.,
selectMenuItemByHotkey) that accepts the hotkey string and optional
KeyboardEvent; this helper should perform the shared checks (ensure this.open,
ensure hotkey is present, call isReservedMenuHotkey, call findMenuItemByHotkey)
and, when a match is found, call handleSelect(matchedItem) and return a flag
indicating whether it handled the event so callers can
stopPropagation/preventDefault; replace the duplicated logic in
handleHotkeyTrigger and handleInputKeyDown to delegate to this helper and only
run event.stopPropagation()/preventDefault() when the helper reports the hotkey
was handled.
- Around line 314-337: In handleHotkeyTrigger, the code uses
event.detail.keyboardEvent without guaranteeing it exists; add a guard for
event.detail?.keyboardEvent (or store it as a local const keyboardEvent =
event.detail?.keyboardEvent) and only call stopPropagation() and
preventDefault() if that keyboardEvent is present, leaving existing early
returns intact and keeping the remaining logic (isReservedMenuHotkey,
findMenuItemByHotkey) unchanged.

---

Duplicate comments:
In `@src/components/hotkey/examples/hotkey-basic.tsx`:
- Around line 51-52: The example contains duplicate normalized hotkeys: the
<limel-hotkey value="meta+enter" /> and <limel-hotkey value="cmd+enter" />
entries normalize to the same hotkey and cause duplicate-hotkey warnings; fix it
by removing or replacing one of these tags in the hotkey-basic.tsx example
(e.g., remove the <limel-hotkey value="cmd+enter" /> line or replace it with a
different combination such as <limel-hotkey value="ctrl+enter" />) so each
<limel-hotkey> in the example has a unique normalized value.

In `@src/components/hotkey/hotkey.scss`:
- Around line 16-19: Replace the inconsistent "0.375px" blur value in the
box-shadow declaration with the correct unit to match surrounding values (e.g.,
"0.375rem") and make the same change in the corresponding declaration in
_kbd.scss (or remove the local override in hotkey.scss and rely on the forwarded
_kbd.scss partial) so both box-shadow entries are consistent; locate the
box-shadow line in hotkey.scss and the matching box-shadow in _kbd.scss to apply
the fix.

In `@src/components/hotkey/hotkey.tsx`:
- Around line 133-182: formatDisplayToken is missing handling for the 'win' and
'windows' aliases so those keys fall through to raw text; add case 'win' and
case 'windows' alongside the existing 'meta' handling in function
formatDisplayToken so they return the same object (use the isApple conditional
to return { display: '⌘', isGlyph: true } for Apple and { display: '⊞ Win',
isGlyph: false } otherwise), keeping the same structure used for the 'meta'
branch.
- Around line 140-143: The 'cmd' / 'command' switch branch is returning an
unconditional '⌘' glyph which causes incorrect visuals on non-mac platforms;
update the case for 'cmd' and 'command' (in the hotkey token/rendering switch
inside the Hotkey component / hotkey rendering function) to be platform-aware
and mirror the logic used by the 'meta' case (use the same isMac/platform
detection and return either { display: '⌘', isGlyph: true } for mac or the
platform-appropriate string (e.g., 'Win'/'Ctrl' or the same display as the
'meta' branch) for non-mac). Ensure the returned shape still matches { display,
isGlyph } so downstream rendering is unchanged.

In `@src/components/markdown/partial-styles/_kbd.scss`:
- Around line 26-29: The box-shadow declaration uses a mismatched unit "0.375px"
for the blur radius; update the box-shadow value in the _kbd.scss rule (the
box-shadow line containing "0 0.625rem 0.375px -0.5rem") to use rem instead of
px (i.e., change 0.375px → 0.375rem) so all shadow offsets/blur/spread use
consistent rem units.

In `@src/components/menu/examples/menu-searchable-hotkeys.tsx`:
- Line 7: The example imports isAppleDevice from internal util/device; remove
that external import and make the example self-contained by implementing a small
local isAppleDevice helper inside menu-searchable-hotkeys.tsx (or the same
example module) and use it where referenced (replace usages of isAppleDevice
with the local function); the helper can check navigator.platform or
navigator.userAgent for Apple identifiers (e.g.,
/Mac|iPhone|iPad|iPod|Macintosh/) and return a boolean so the example no longer
depends on internal utilities.
- Around line 33-34: Fix the typo in the example description comment inside the
MenuSearchableHotkeys example: change the phrase "they focus is on the search
input" to "the focus is on the search input" in the comment above the component
(look for the menu-searchable-hotkeys.tsx example / MenuSearchableHotkeys) so
the sentence reads correctly.

In `@src/components/menu/menu.e2e.tsx`:
- Around line 153-155: The test is querying the global document for
'limel-menu-surface', which can pick up a stale portal from another test;
instead scope the lookup to the current menu instance's portal container (use
the menu's portal/container element reference used in the test to query for
'limel-menu-surface' rather than document.querySelector), updating each
occurrence that sets const surface =
document.querySelector('limel-menu-surface') (and the similar checks at the
other locations) to query from the current menu's portal container so the
surface is tied to this test instance.

In `@src/components/menu/menu.types.ts`:
- Around line 64-66: The doc comment for the hotkey property uses plural wording
"Hotkey(s)" but the symbol hotkey is typed as a single string; update the
comment to use singular wording (e.g., "Hotkey") and clarify that this string
represents one keyboard shortcut used to select the item while the menu is open,
distinguishing it from commandText which is purely visual.

In `@src/util/device.ts`:
- Around line 26-29: isIOSDevice() currently only tests /iPad|iPhone|iPod/i so
modern iPadOS 13+ (which reports a Mac-like UA) is missed; update isIOSDevice()
to also detect iPadOS by checking getUserAgent() plus platform/touch
capabilities (e.g., detect navigator.platform === 'MacIntel' and
navigator.maxTouchPoints > 1 or similar via globalThis) so that isIOSDevice()
(and consequently isMobileDevice()) returns true for iPadOS Safari; modify the
function that calls getUserAgent() (isIOSDevice) to include this additional
platform+touch check while preserving the existing regex path.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between daf8388 and 263e1c3.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (19)
  • src/components/hotkey/examples/hotkey-basic.scss
  • src/components/hotkey/examples/hotkey-basic.tsx
  • src/components/hotkey/examples/hotkey-disabled.tsx
  • src/components/hotkey/examples/hotkey-duplicates.tsx
  • src/components/hotkey/examples/hotkey-prevent-default.tsx
  • src/components/hotkey/hotkey.scss
  • src/components/hotkey/hotkey.tsx
  • src/components/hotkey/hotkey.types.ts
  • 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/util/device.ts
  • src/util/hotkeys.spec.ts
  • src/util/hotkeys.ts

Comment thread src/components/menu/menu.tsx
Comment on lines +314 to +337
private readonly handleHotkeyTrigger = (
event: CustomEvent<LimelHotkeyTriggerDetail>
) => {
if (!this.open) {
return;
}

const pressedHotkey = event.detail?.hotkey;
if (!pressedHotkey) {
return;
}

if (this.isReservedMenuHotkey(pressedHotkey)) {
return;
}

const matchedItem = this.findMenuItemByHotkey(pressedHotkey);
if (!matchedItem) {
return;
}

event.stopPropagation();
event.detail.keyboardEvent.stopPropagation();
event.detail.keyboardEvent.preventDefault();
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

Guard keyboardEvent before dereference in hotkey trigger handler.

event.detail?.hotkey is guarded, but event.detail.keyboardEvent is still used unguarded. A malformed/custom hotkeyTrigger event can throw at runtime.

🛠️ Proposed fix
     private readonly handleHotkeyTrigger = (
         event: CustomEvent<LimelHotkeyTriggerDetail>
     ) => {
         if (!this.open) {
             return;
         }

         const pressedHotkey = event.detail?.hotkey;
-        if (!pressedHotkey) {
+        const keyboardEvent = event.detail?.keyboardEvent;
+        if (!pressedHotkey || !keyboardEvent) {
             return;
         }

         if (this.isReservedMenuHotkey(pressedHotkey)) {
             return;
         }

         const matchedItem = this.findMenuItemByHotkey(pressedHotkey);
         if (!matchedItem) {
             return;
         }

         event.stopPropagation();
-        event.detail.keyboardEvent.stopPropagation();
-        event.detail.keyboardEvent.preventDefault();
+        keyboardEvent.stopPropagation();
+        keyboardEvent.preventDefault();
         this.handleSelect(matchedItem);
     };
📝 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 readonly handleHotkeyTrigger = (
event: CustomEvent<LimelHotkeyTriggerDetail>
) => {
if (!this.open) {
return;
}
const pressedHotkey = event.detail?.hotkey;
if (!pressedHotkey) {
return;
}
if (this.isReservedMenuHotkey(pressedHotkey)) {
return;
}
const matchedItem = this.findMenuItemByHotkey(pressedHotkey);
if (!matchedItem) {
return;
}
event.stopPropagation();
event.detail.keyboardEvent.stopPropagation();
event.detail.keyboardEvent.preventDefault();
private readonly handleHotkeyTrigger = (
event: CustomEvent<LimelHotkeyTriggerDetail>
) => {
if (!this.open) {
return;
}
const pressedHotkey = event.detail?.hotkey;
const keyboardEvent = event.detail?.keyboardEvent;
if (!pressedHotkey || !keyboardEvent) {
return;
}
if (this.isReservedMenuHotkey(pressedHotkey)) {
return;
}
const matchedItem = this.findMenuItemByHotkey(pressedHotkey);
if (!matchedItem) {
return;
}
event.stopPropagation();
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
this.handleSelect(matchedItem);
};
🤖 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 314 - 337, In handleHotkeyTrigger,
the code uses event.detail.keyboardEvent without guaranteeing it exists; add a
guard for event.detail?.keyboardEvent (or store it as a local const
keyboardEvent = event.detail?.keyboardEvent) and only call stopPropagation() and
preventDefault() if that keyboardEvent is present, leaving existing early
returns intact and keeping the remaining logic (isReservedMenuHotkey,
findMenuItemByHotkey) unchanged.

Comment on lines 19 to +29
/**
* Use to display optional keyboard shortcut or command hint, e.g. `⌘ + K`
*/
@Prop()
@Prop({ reflect: true })
public commandText?: string;

/**
* Hotkey to display. When provided, `commandText` is ignored.
*/
@Prop({ reflect: true })
public hotkey?: string;
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.

If I recall correctly, you said that the hotkey only works once you've opened a menu, it's basically a way to navigate the menu.

commandText on the other hand, was, when we added it, meant for showing a keyboard command that would activate the underlying function (like "copy"), regardless of whether the menu was open or not. It's a way to communicate a separate way to trigger the same command, not a way to navigate the menu, right?

What's the plan for commandText? Are we going to use it ourselves still? Do we intend to keep it around, or should it be deprecated? The use cases aren't exactly the same, but given that commandText will always lose out to hotkey, it feels like you've kind of staked out hotkey as the new direction? 🙂

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah, this was a tricky decision.

The builtin hotkeys are visualized as physical keybaord keys, while the command text, is just a grey string.

I thought if a consumer uses the inbuilt hotkeys, they would have to follow the intentional design, which (in case limel-menu or limel-select) means: the hotkeys only work if they menu is open. This approach is healthy I'd say, as it also reduces the chance of having several identical hotkeys defined in the same view/ pane/ page, doing different things.

For these cases, I think displaying the grey commandText is overkill. So I chose to hide it, and replace it with the visuals for the hotkey instead. Can't say that's a 100% good choice. But we can display both simultaneously if consumers complain later.

Why not deprecate commmandText?
A consumer may want to define global hotkey which should work even when the menu is closed; or they may want to mae the hotkeys run when the menu is open, but maintain the entire logic of how their hotkeys work on their side. In this case, I think it is good that they still have the commmandText to use, and give their users some tips about their existing hotkeys, and make them more discoverable.

Discoverability
Is having a hidden but globally accessible and active hotkey a good design? I doubt it!

If the UI doesn't visibly and clearly communicate that they are hotkeys implemented and globally available in this view/ pane /page, associated with some action, then I think that's a questionable design.

In the web client, I want to ensure that:

  1. what we call "unpromoted actions" will run, only when the menu is open.
  2. what we call "promoted actions" will run, only when the floating action bar is visible.
  3. other actions tied to buttons like Post, Send, Save, Cancel are run, only when their respective buttons are displayed and are enabled.

The only global hotkeys would be those that are used for global navigation, e.g. going Home, navigating to Search results, opening the Add new dialog, etc…

Not sure if I answered all your questions. Please ask or call on Slack if easier 😊

@Kiarokh
Copy link
Copy Markdown
Contributor Author

Kiarokh commented Mar 2, 2026

a potential upcoming PR showing how hotkey prop can work in limel-select #3915

Kiarokh added 5 commits March 6, 2026 12:02
- 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.
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/hotkey/hotkey.scss (1)

16-20: ⚠️ Potential issue | 🟡 Minor

Possible unit mismatch: 0.375px likely a typo for 0.375rem.

The box-shadow mixes rem for y-offset (0.625rem) and spread (-0.5rem) with px for blur (0.375px). A sub-pixel blur radius is effectively 0-1px after browser rounding, which is probably not intended.

🔧 Proposed fix
-        0 0.625rem 0.375px -0.5rem rgb(var(--color-black), 0.02),
+        0 0.625rem 0.375rem -0.5rem rgb(var(--color-black), 0.02),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/hotkey/hotkey.scss` around lines 16 - 20, The box-shadow in
hotkey.scss mixes px and rem units (specifically "0.375px") which is likely a
typo; update the blur radius in the box-shadow declaration inside the
hotkey.scss file (the rule containing "box-shadow: var(--button-shadow-pressed),
0 0.625rem 0.375px -0.5rem ...") to use rem (e.g., "0.375rem") so units are
consistent and the blur is not sub-pixel.
src/components/hotkey/examples/hotkey-basic.tsx (1)

51-52: ⚠️ Potential issue | 🟡 Minor

Duplicate hotkeys will trigger a console warning.

cmd is an alias for meta, so both meta+enter and cmd+enter normalize to the same hotkey. This will cause the duplicate-detection logic to emit console.warn on every keypress, which may confuse readers using this as a reference example.

Consider removing one of them, or moving this pairing to the hotkey-duplicates example where duplicate behavior is explicitly demonstrated.

🔧 Proposed fix: Keep only one
                 <limel-hotkey value="meta+enter" />
-                <limel-hotkey value="cmd+enter" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/hotkey/examples/hotkey-basic.tsx` around lines 51 - 52, The
example registers two identical hotkeys ("meta+enter" and "cmd+enter") which
normalize to the same binding and trigger console.warn; remove one of the
limel-hotkey elements (either the <limel-hotkey value="meta+enter" /> or the
<limel-hotkey value="cmd+enter" />) from hotkey-basic.tsx, or move the pair into
the hotkey-duplicates example so the duplicate behavior is shown intentionally.
src/components/menu/examples/menu-searchable-hotkeys.tsx (2)

7-7: 🧹 Nitpick | 🔵 Trivial

Internal utility import in example file.

Importing isAppleDevice from an internal path ('../../../util/device') couples this example to implementation details. Example files should be self-contained or import from the public API.

♻️ Inline the logic to make the example self-contained
-import { isAppleDevice } from '../../../util/device';
+
+const isAppleDevice = (): boolean => {
+    return /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent);
+};
🤖 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 7, The
example imports an internal helper isAppleDevice from '../../../util/device',
which couples the example to internal implementation; inline the minimal
device-detection logic directly in the example (replace references to
isAppleDevice) so the file is self-contained: add a small function (e.g., const
isAppleDevice = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform || '') ||
navigator.userAgent.includes('Mac') ) inside the menu-searchable-hotkeys
component (MenuSearchableHotkeys) and remove the internal import; ensure all
uses of isAppleDevice in the file now call the local inline function.

32-36: ⚠️ Potential issue | 🟡 Minor

Fix typo in documentation.

Line 33 contains a grammatical error: "they focus" should be "the focus".

📝 Proposed fix
- * Consider that when the menu is opened, they focus is on the search input,
+ * Consider that when the menu is opened, the focus is on the search input,
🤖 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 32 -
36, Fix the typo in the example comment that reads "they focus" — change it to
"the focus" in the comment starting "Consider that when the menu is opened, they
focus is on the search input..." so the sentence reads "Consider that when the
menu is opened, the focus is on the search input..." which will correct the
grammar in the menu-searchable-hotkeys example comment.
src/components/markdown/partial-styles/_kbd.scss (1)

26-29: ⚠️ Potential issue | 🟡 Minor

Unit mismatch in box-shadow: 0.375px should likely be 0.375rem.

Line 28 uses 0.375px for the blur radius while all other shadow values use rem. This appears to be a typo that creates visual inconsistency.

🔧 Proposed fix
     box-shadow:
         var(--button-shadow-normal),
-        0 0.625rem 0.375px -0.5rem rgb(var(--color-black), 0.02),
+        0 0.625rem 0.375rem -0.5rem rgb(var(--color-black), 0.02),
         0 0.025rem 0.5rem 0 rgb(var(--contrast-100)) inset;
🤖 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 26 - 29, The
box-shadow declaration in _kbd.scss uses a mismatched unit — change the blur
radius "0.375px" to "0.375rem" in the box-shadow list (the rule containing
var(--button-shadow-normal), 0 0.625rem 0.375px -0.5rem rgb(...), 0 0.025rem
0.5rem 0 rgb(...)) so all shadow offsets/blur/spread use rem units for visual
consistency.
src/components/menu/menu.tsx (1)

321-337: ⚠️ Potential issue | 🟡 Minor

Guard keyboardEvent before dereferencing it.

pressedHotkey is null-checked, but Lines 336-337 still assume event.detail.keyboardEvent exists. Since this handler is wired through addEventListener, a malformed/custom hotkeyTrigger event can still reach it and throw at runtime.

Suggested fix
         const pressedHotkey = event.detail?.hotkey;
-        if (!pressedHotkey) {
+        const keyboardEvent = event.detail?.keyboardEvent;
+        if (!pressedHotkey || !keyboardEvent) {
             return;
         }
@@
         event.stopPropagation();
-        event.detail.keyboardEvent.stopPropagation();
-        event.detail.keyboardEvent.preventDefault();
+        keyboardEvent.stopPropagation();
+        keyboardEvent.preventDefault();
         this.handleSelect(matchedItem);
🤖 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 321 - 337, The handler currently
dereferences event.detail.keyboardEvent without guarding it; update the hotkey
handler (around pressedHotkey, isReservedMenuHotkey, findMenuItemByHotkey) to
check that event.detail and event.detail.keyboardEvent are defined before
calling event.detail.keyboardEvent.stopPropagation() and .preventDefault(); if
keyboardEvent is missing, still call event.stopPropagation() and bail or handle
appropriately so malformed/custom hotkeyTrigger events cannot throw.
src/components/hotkey/hotkey.tsx (1)

133-180: ⚠️ Potential issue | 🟡 Minor

Render win / windows the same way they normalize.

normalizeKey() accepts both aliases as meta, but formatDisplayToken() falls through to raw text for them. That makes value="win+k" render win + k even though it triggers the Meta/Windows-key combo.

Suggested fix
             case 'meta': {
                 return isApple
                     ? { display: '⌘', isGlyph: true }
                     : { display: '⊞ Win', isGlyph: false };
             }

+            case 'win':
+            case 'windows': {
+                return isApple
+                    ? { display: '⌘', isGlyph: true }
+                    : { display: '⊞ Win', isGlyph: false };
+            }
+
             case 'cmd':
             case 'command': {
                 return { display: '⌘', isGlyph: true };
             }
🤖 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 133 - 180, formatDisplayToken
currently doesn't handle the 'win'/'windows' aliases that normalizeKey maps to
'meta', causing "win+k" to render raw text; update the switch in
formatDisplayToken to treat 'win' and 'windows' like 'meta' (use the same
returned object based on isApple) so the Windows key is displayed as the
Meta/Win glyph/text consistently with normalizeKey (refer to the switch handling
of 'meta', the local variables lower and isApple, and normalizeKey for expected
normalization).
🤖 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.e2e.tsx`:
- Around line 154-155: Tests for limel-menu are leaving portal elements
(limel-menu-surface) in the document which accumulate across runs; add an
afterEach cleanup that removes any portal elements created by render() (e.g.,
querySelectorAll('limel-menu-surface') and remove each) or explicitly remove the
surface element after each test so stale portals don't pollute subsequent tests;
update the test file to include an afterEach hook that locates
limel-menu-surface nodes and removes them to ensure clean DOM between runs.

In `@src/util/hotkeys.ts`:
- Around line 163-194: The loop currently lets the "last non-modifier wins" (so
"a+b" becomes "b"); instead, detect multiple non-modifier tokens and return null
for malformed inputs: while iterating tokens (using normalizeKey) track whether
a non-modifier key has already been seen (the variable key) and if you encounter
a second non-modifier, immediately return null. Keep the existing modifier
handling (alt/ctrl/meta/shift) and ultimately call normalizeModifiersAndKey({
key, alt, ctrl, meta, shift }) only when exactly one non-modifier key exists.

---

Duplicate comments:
In `@src/components/hotkey/examples/hotkey-basic.tsx`:
- Around line 51-52: The example registers two identical hotkeys ("meta+enter"
and "cmd+enter") which normalize to the same binding and trigger console.warn;
remove one of the limel-hotkey elements (either the <limel-hotkey
value="meta+enter" /> or the <limel-hotkey value="cmd+enter" />) from
hotkey-basic.tsx, or move the pair into the hotkey-duplicates example so the
duplicate behavior is shown intentionally.

In `@src/components/hotkey/hotkey.scss`:
- Around line 16-20: The box-shadow in hotkey.scss mixes px and rem units
(specifically "0.375px") which is likely a typo; update the blur radius in the
box-shadow declaration inside the hotkey.scss file (the rule containing
"box-shadow: var(--button-shadow-pressed), 0 0.625rem 0.375px -0.5rem ...") to
use rem (e.g., "0.375rem") so units are consistent and the blur is not
sub-pixel.

In `@src/components/hotkey/hotkey.tsx`:
- Around line 133-180: formatDisplayToken currently doesn't handle the
'win'/'windows' aliases that normalizeKey maps to 'meta', causing "win+k" to
render raw text; update the switch in formatDisplayToken to treat 'win' and
'windows' like 'meta' (use the same returned object based on isApple) so the
Windows key is displayed as the Meta/Win glyph/text consistently with
normalizeKey (refer to the switch handling of 'meta', the local variables lower
and isApple, and normalizeKey for expected normalization).

In `@src/components/markdown/partial-styles/_kbd.scss`:
- Around line 26-29: The box-shadow declaration in _kbd.scss uses a mismatched
unit — change the blur radius "0.375px" to "0.375rem" in the box-shadow list
(the rule containing var(--button-shadow-normal), 0 0.625rem 0.375px -0.5rem
rgb(...), 0 0.025rem 0.5rem 0 rgb(...)) so all shadow offsets/blur/spread use
rem units for visual consistency.

In `@src/components/menu/examples/menu-searchable-hotkeys.tsx`:
- Line 7: The example imports an internal helper isAppleDevice from
'../../../util/device', which couples the example to internal implementation;
inline the minimal device-detection logic directly in the example (replace
references to isAppleDevice) so the file is self-contained: add a small function
(e.g., const isAppleDevice = () =>
/Mac|iPhone|iPad|iPod/.test(navigator.platform || '') ||
navigator.userAgent.includes('Mac') ) inside the menu-searchable-hotkeys
component (MenuSearchableHotkeys) and remove the internal import; ensure all
uses of isAppleDevice in the file now call the local inline function.
- Around line 32-36: Fix the typo in the example comment that reads "they focus"
— change it to "the focus" in the comment starting "Consider that when the menu
is opened, they focus is on the search input..." so the sentence reads "Consider
that when the menu is opened, the focus is on the search input..." which will
correct the grammar in the menu-searchable-hotkeys example comment.

In `@src/components/menu/menu.tsx`:
- Around line 321-337: The handler currently dereferences
event.detail.keyboardEvent without guarding it; update the hotkey handler
(around pressedHotkey, isReservedMenuHotkey, findMenuItemByHotkey) to check that
event.detail and event.detail.keyboardEvent are defined before calling
event.detail.keyboardEvent.stopPropagation() and .preventDefault(); if
keyboardEvent is missing, still call event.stopPropagation() and bail or handle
appropriately so malformed/custom hotkeyTrigger events cannot throw.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ffd5d0c7-c2c5-4e32-9636-d347e2237c8e

📥 Commits

Reviewing files that changed from the base of the PR and between 263e1c3 and b5cb6c2.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (19)
  • src/components/hotkey/examples/hotkey-basic.scss
  • src/components/hotkey/examples/hotkey-basic.tsx
  • src/components/hotkey/examples/hotkey-disabled.tsx
  • src/components/hotkey/examples/hotkey-duplicates.tsx
  • src/components/hotkey/examples/hotkey-prevent-default.tsx
  • src/components/hotkey/hotkey.scss
  • src/components/hotkey/hotkey.tsx
  • src/components/hotkey/hotkey.types.ts
  • 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/util/device.ts
  • src/util/hotkeys.spec.ts
  • src/util/hotkeys.ts

Comment on lines +154 to +155
const surface = document.querySelector('limel-menu-surface');
expect(surface).toBeTruthy();
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check test setup/teardown and portal handling
echo "=== Checking for afterEach/beforeEach/cleanup ==="
rg -n "afterEach|beforeEach|cleanup" src/components/menu/menu.e2e.tsx

echo ""
echo "=== Examining test structure around lines 118-155 ==="
sed -n '100,170p' src/components/menu/menu.e2e.tsx

Repository: Lundalogik/lime-elements

Length of output: 2816


🏁 Script executed:

#!/bin/bash
# Get more of the test file to see all hotkeys tests
sed -n '131,250p' src/components/menu/menu.e2e.tsx

Repository: Lundalogik/lime-elements

Length of output: 4371


🏁 Script executed:

#!/bin/bash
# Find the end of hotkeys describe block to count tests
echo "=== Finding end of hotkeys block ==="
rg -n "describe\|^\s*}\);" src/components/menu/menu.e2e.tsx | tail -20

echo ""
echo "=== Count it blocks in hotkeys ==="
sed -n '131,$p' src/components/menu/menu.e2e.tsx | rg -c "^\s+it\("

Repository: Lundalogik/lime-elements

Length of output: 142


Add cleanup hook to prevent portal accumulation across tests.

The hotkeys tests will accumulate stale portal elements in the DOM across 4 test runs without explicit cleanup (lines 118-121 already document this limitation). While document.querySelector('limel-menu-surface') will find the most recent surface, stale portals persist, polluting subsequent test runs.

Add an afterEach hook to clean up portals created by the render() calls, or explicitly remove the surface element after each test.

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

In `@src/components/menu/menu.e2e.tsx` around lines 154 - 155, Tests for
limel-menu are leaving portal elements (limel-menu-surface) in the document
which accumulate across runs; add an afterEach cleanup that removes any portal
elements created by render() (e.g., querySelectorAll('limel-menu-surface') and
remove each) or explicitly remove the surface element after each test so stale
portals don't pollute subsequent tests; update the test file to include an
afterEach hook that locates limel-menu-surface nodes and removes them to ensure
clean DOM between runs.

Comment thread src/util/hotkeys.ts
Comment on lines +163 to +194
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

Reject multiple non-modifier tokens instead of silently taking the last one.

The JSDoc says invalid strings return null, but a+b currently normalizes to b and ctrl+a+b to ctrl+b. That makes malformed configs bind the wrong shortcut instead of being ignored.

Suggested fix
     for (const token of tokens) {
         const normalized = normalizeKey(token);
         if (!normalized) {
             continue;
         }
@@
-        // Last non-modifier wins
+        if (key) {
+            return null;
+        }
+
         key = normalized;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/hotkeys.ts` around lines 163 - 194, The loop currently lets the
"last non-modifier wins" (so "a+b" becomes "b"); instead, detect multiple
non-modifier tokens and return null for malformed inputs: while iterating tokens
(using normalizeKey) track whether a non-modifier key has already been seen (the
variable key) and if you encounter a second non-modifier, immediately return
null. Keep the existing modifier handling (alt/ctrl/meta/shift) and ultimately
call normalizeModifiersAndKey({ key, alt, ctrl, meta, shift }) only when exactly
one non-modifier key exists.

@adrianschmidt
Copy link
Copy Markdown
Contributor

adrianschmidt commented Mar 6, 2026

Architectural Analysis of Hotkey Implementation

(by Claude Code with Opus 4.6, effort: high)

I've reviewed the PR, the issue, the review discussion, and the existing webclient keyboard shortcut system. Here's an analysis of the concerns Adrian raised, and a suggestion for how to move forward.

Two Distinct Concepts Being Conflated

There are two fundamentally different things at play here:

  1. Menu-scoped quick navigation — single-key accelerators that only work while a menu is open (like pressing d to jump to "Delete" in an open context menu). This is what the Slack screenshot in the issue shows.

  2. Global keyboard shortcuts — work regardless of UI state (like Cmd+S to save). These are already handled by the KeybindingRegistry in lime-web-components, configured via keyboard_shortcuts.py in the webclient, and bound by components like action-button and action-menu in lime-crm-components.

The PR's limel-hotkey component treats both of these as the same thing, which creates several problems.

What the Current Implementation Does

Each <limel-hotkey> instance:

  • Listens at the document level in the capture phase (@Listen('keydown', { target: 'document', capture: true }))
  • Always listens, though the menu's handleHotkeyTrigger ignores events when !this.open
  • Each menu item renders its own <limel-hotkey> instance, each adding its own global listener

So the architecture is: N global keydown listeners (one per menu item with a hotkey), all evaluating every keypress, with the menu filtering out events when closed.

Problems

1. Architectural mismatch with the existing system.
The webclient already has a KeybindingRegistry service for global shortcuts. If someone uses limel-hotkey with e.g. Cmd+K, it will conflict with the webclient's existing Cmd+K → global search binding. There's no coordination between the two systems.

2. The component does too much.
limel-hotkey is simultaneously:

  • A visual renderer (displays ⌘K as styled kbd elements)
  • A global keyboard event listener
  • A hotkey-to-command dispatcher

For the menu-scoped use case, the visual rendering is great, but the global listening is architecturally wrong — even though the menu happens to suppress it when closed. The menu should own its keyboard handling internally.

3. Performance at scale.
Each <limel-hotkey> adds a global document-level listener. A menu with 10 items = 10 global listeners. Multiple menus in the app multiplies this further. This works fine at small scale but is architecturally wasteful when the menu could handle this with a single listener.

4. The "handled flag" fragility.
The review discussion about the __limelHotkeyHandled flag highlights the problem. When multiple limel-hotkey instances exist for the same key, only the first one to process wins, and which one depends on DOM render order. This is non-deterministic.

Suggested Separation

A. Visual component (display-only)

Keep the limel-hotkey component (or rename to limel-hotkey-badge) as display-only:

  • Renders keyboard shortcut glyphs (, , , etc.) with platform awareness
  • No event listeners whatsoever
  • Used by menus, tooltips, buttons, etc. to visually indicate a shortcut
  • This part of the PR is solid and valuable

B. Menu-internal keyboard navigation

Built into the menu component itself:

  • A single keydown listener on the menu (or document, but only one listener total)
  • Only active when the menu is open
  • Iterates over visible items, checks their hotkey property, dispatches selection
  • No new external events — the menu already emits select when an item is chosen
  • This is essentially what handleHotkeyTrigger in the PR already does — it just doesn't need limel-hotkey as middleware
// Single listener, internal to menu
@Listen('keydown')
private handleKeydown(event: KeyboardEvent) {
    if (!this.open) return;
    
    const pressed = hotkeyFromKeyboardEvent(event);
    const matchedItem = this.findMenuItemByHotkey(pressed);
    if (matchedItem) {
        this.handleSelect(matchedItem);
        event.preventDefault();
    }
}

C. Global shortcuts (already exists, separate concern)

The KeybindingRegistry in lime-web-components handles this. If it needs improvements, that's a separate effort. lime-elements doesn't need to replicate it.

Communication to the User

With this separation, the convention becomes self-reinforcing:

  • Single-character hotkeys (n, d, o) shown inside a menu → obviously only work when that menu is open
  • Modifier hotkeys (Cmd+S, Ctrl+K) shown in tooltips/buttons → work globally (handled by the webclient's registry)

Single keys in menus can't work globally anyway (they'd fire while typing), and modifier keys shown outside menus obviously work globally. No special notation needed.

Recommendation

  1. Keep the visual rendering work — the glyph rendering, platform detection, and kbd styling is solid
  2. Make limel-hotkey display-only — remove the global listener
  3. Move keyboard handling into the menu — a single listener, internal to the menu
  4. Don't touch the webclient's KeybindingRegistry — that's a separate system for a separate concern
  5. Don't expose a general-purpose event-handling hotkey component — it invites misuse where people build global shortcuts in lime-elements when they should use the webclient's registry

What Can Be Kept From This PR

A large portion of the work here is reusable:

  • The hotkey normalization utilities (hotkeys.ts) — excellent
  • The platform-aware glyph rendering — excellent
  • The MenuItem.hotkey type extension — good
  • The menu item rendering with hotkey badges — good
  • The e2e tests for menu hotkey selection — good (just need to test the menu's internal handling instead)

The main change would be removing the event-listening responsibility from limel-hotkey and moving the menu's hotkey matching logic from handleHotkeyTrigger (which catches bubbled events from child limel-hotkey instances) into a direct keydown handler on the menu.

@Kiarokh
Copy link
Copy Markdown
Contributor Author

Kiarokh commented Mar 6, 2026

@adrianschmidt I agree with everything, and have no issues with moving out the listener to the menu. I actually started like this, and think that approach is better. Having a hotkey visualizer component is way better than leaving the event emitting too, to the visualizer. This will make it really hard for the hotkey to be used inside a tooltip surface for instance.

However, one thing that I think is unnecessarily in the way is how hotkeys are handled in the web-client today. I really dislike that the are globally available all the time. I think, a hotkey, must be scoped/limited to its container. In my opinions, a hotkey should:

  • either be limited to
    • the visual presence of a trigger of an action (such as a button)
    • to the enability / disability of the trigger (a button which is disable shouldn't trigger the action it's tied to, therefore, its hotkey shouldn't trigger the action neither)
  • or be limited to having focus in a certain region of the UI. For example in a conversation UI, the CmdEnter hotkey for sending / posting a message shouldn't work, if the focus is not within the composer or within the text editor where the user is typing.

So having a globally active hotkey is a bad idea. Without this approach that I explained above, we can never use the same hotkey combination for two different things, in two different places within the same app-screen.

What to do about webclient's existing Cmd+K → global search binding then (or similar global hotkeys)?
Well, the only reason they seem logically correct is because the Dock (main navigation component in the UI) is always visible, and the triggers (Start page, Search, Browser menu, Etc…) are always active. That's why they automatically fall into the description that I mentioned above. I can't imagine scenarios where you want to offer undiscoverable hotkeys that are globally present. I think we have made a bad choice about how hotkeys work in the web client. We should refactor and for example make the Dock (like the menu) have its own Hotkey support.

I hope I could clearly explain how I think and reason about this.

Shall I now move out the event emitting logic from limel-hotkey and refactor in this PR?

@Kiarokh
Copy link
Copy Markdown
Contributor Author

Kiarokh commented Mar 13, 2026

closing this PR in favor of #3915 where the requested changes above are applied, but we also have a new implementation of hotkeys in limel-select. So both limel-menu and limel-select implement the hotkey. This gives better understanding of how this feature is expected to work.

I think we should do some refactoring regarding how hotkeys are designed to work in the web client. We should adress that issue there instead.

@adrianschmidt-bot I asked you to make a PR once this PR is merged, as explained here: #3818 (comment)

But now since I'm closing this PR without merging, I want you to keep an eye on the other pr instead: #3915

Hope it works

@Kiarokh Kiarokh closed this Mar 13, 2026
@adrianschmidt-bot
Copy link
Copy Markdown
Contributor

Got it! 👍 I've updated my notes — I'll watch PR #3915 instead and create the src/util/device.ts improvement PR once that one is merged.

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

Labels

feature New feature or request usability Things that make it harder or easier for users to understand or use elements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants