Skip to content

chore(util): add helpers for detecting & using keyboard hotkeys#3976

Merged
adrianschmidt merged 2 commits intomainfrom
split/1-hotkey-utils
Mar 25, 2026
Merged

chore(util): add helpers for detecting & using keyboard hotkeys#3976
adrianschmidt merged 2 commits intomainfrom
split/1-hotkey-utils

Conversation

@Kiarokh
Copy link
Copy Markdown
Contributor

@Kiarokh Kiarokh commented Mar 24, 2026

Summary by CodeRabbit

  • New Features

    • Hotkey normalization system for consistent shortcut strings, including literal "+" support.
    • Improved Apple-device detection to better recognize iPhone, iPad, and Mac.
  • Bug Fixes / Compatibility

    • More robust device/platform checks that safely handle non-browser and edge environments.
  • Tests

    • Comprehensive tests for hotkey tokenization, normalization, and keyboard-event-derived hotkeys.
  • Documentation

    • Clarified comments about single-key keycode usage.

Review:

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

Browsers tested:

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

Windows:

  • Chrome
  • Edge
  • Firefox

Linux:

  • Chrome
  • Firefox

macOS:

  • Chrome
  • Firefox
  • Safari

Mobile:

  • Chrome on Android
  • iOS

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 24, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e3421deb-5231-4427-a293-5a0f69286133

📥 Commits

Reviewing files that changed from the base of the PR and between b4f4f9a and df5620a.

📒 Files selected for processing (2)
  • src/util/hotkeys.spec.ts
  • src/util/hotkeys.ts

📝 Walkthrough

Walkthrough

Safe navigator accessors replaced a module-level user-agent; device detection functions updated and new isAppleDevice() added. A new hotkey normalization module and Jest tests were introduced. Two explanatory comment lines were added to keycodes.ts.

Changes

Cohort / File(s) Summary
Device Detection Utilities
src/util/device.ts
Removed module-level userAgent; added getUserAgent and getPlatform helpers that safely read navigator/navigator.userAgentData and return empty strings when unavailable; updated isIOSDevice() and isAndroidDevice() to use helpers and globalThis.MSStream; added exported isAppleDevice() combining UA and platform checks.
Hotkey Utilities
src/util/hotkeys.ts
New module exporting tokenizeHotkeyString, normalizeHotkeyString, and hotkeyFromKeyboardEvent. Implements alias maps, deterministic modifier order (meta,ctrl,alt,shift), literal + handling (including ++), last-non-modifier precedence, and layout-independent normalization using event.code for letters/digits.
Hotkey Tests
src/util/hotkeys.spec.ts
Added Jest suite covering tokenization edge cases, normalization rules (aliases, modifier-only filtering, ordering, + handling), and KeyboardEvent→hotkey expectations (modifier inclusion, function/arrow keys, layout-independence, Backspace vs Delete).
Keycodes Comment Update
src/util/keycodes.ts
Inserted two comment lines clarifying these constants target single-key event.key comparisons and referencing hotkeys.ts for multi-key hotkey handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding hotkey detection and handling utilities plus device detection improvements.

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

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

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.

coderabbitai[bot]

This comment was marked as resolved.

@github-actions
Copy link
Copy Markdown

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

@adrianschmidt adrianschmidt self-assigned this Mar 25, 2026
@adrianschmidt adrianschmidt self-requested a review March 25, 2026 12:24
@adrianschmidt

This comment was marked as outdated.

@adrianschmidt
Copy link
Copy Markdown
Contributor

Review follow-up: all 4 recommendations addressed

Pushed 4 fixup commits targeting chore(util): add helpers for detecting & using keyboard hotkeys:

  1. [Medium] KEY_ALIASES lookup — switched to Object.hasOwn(KEY_ALIASES, normalized) (55868e4)
  2. [Medium] Test coverage — added tests for tokenizeHotkeyString (empty, null, whitespace, ++, +++), normalizeHotkeyString (aliases, modifier ordering, modifier-only → null, null/undefined), and hotkeyFromKeyboardEvent (pure modifier press → null) (7d6cee0)
  3. [Low] "Last non-modifier wins" docs — added to normalizeHotkeyString JSDoc (c3b879f)
  4. [Low] Cross-reference comments — added to both hotkeys.ts and keycodes.ts (285e48d)

All are fixup! commits — squash with git rebase -i --autosquash when ready.

coderabbitai[bot]

This comment was marked as resolved.

@adrianschmidt
Copy link
Copy Markdown
Contributor

Consolidated PR Review -- 6 Dimensions

PR Summary

PR #3976 adds hotkey normalization utilities (tokenizeHotkeyString, normalizeHotkeyString, hotkeyFromKeyboardEvent) and refactors device detection to support SSR/test environments with lazy navigator access plus a new isAppleDevice() function. 4 files changed: +550 / -2 lines. This is part 1 of a 4-part split adding hotkey support to lime-elements.


1. Backward Compatibility -- SAFE ✅

All changes are additive or internal refactors with no signature changes. Existing isIOSDevice, isAndroidDevice, and isMobileDevice maintain their exact exported signatures and behavior.

  • [Low] device.ts: user agent evaluation changed from eager (module load) to lazy (per call). navigator.userAgent doesn't change during a page session, so behavior is identical. The lazy approach is strictly better for SSR/test contexts.
  • [Low] isIOSDevice now checks globalThis.MSStream instead of window.MSStream. Equivalent in browsers (globalThis === window), safer in non-browser environments.
  • Positives: No public APIs removed or changed. New exports are purely additive. keycodes.ts change is comment-only. All 13 existing consumers of keycodes.ts and 3 consumers of device.ts are unaffected.

2. Code Quality -- GOOD ✅

Well-structured utility code with clear documentation and solid test coverage.

  • [Low] isAppleDevice() in device.ts: isIPadOS13Plus is redundant since it requires isMacLike, which is already a standalone return condition. The three-way || suggests three independent conditions when there are really two. Consider simplifying or adding a clarifying comment.
  • [Low] (navigator as any).maxTouchPoints in device.ts: maxTouchPoints is a standard Navigator property in TypeScript's DOM lib. The as any cast is unnecessary (though the one for userAgentData is correct).
  • [Low] No unit tests for device.ts. The refactored helpers and new isAppleDevice() have no test coverage, while hotkeys.ts has excellent coverage. Given subsequent PRs depend on isAppleDevice(), tests would increase confidence.
  • [Low] normalizeHotkeyString silently drops extra non-modifier keys ("last non-modifier wins"). While documented, "ctrl+a+b" becoming "ctrl+b" could mask user errors.
  • Positives: Excellent module-level documentation in hotkeys.ts explaining canonical format, examples, and relationship with keycodes.ts. The tokenizer handles the + key edge case thoughtfully. normalizeEventKey correctly uses event.code for layout-independent key detection. Thorough test coverage including non-QWERTY layouts, null inputs, and modifier-only presses.

3. Architecture -- GOOD ✅

Clean separation of concerns with a well-designed, stateless, pure-function module.

  • [Low] Subtle inconsistency: keycodes.ts uses SPACE = 'Space' (matching event.key), while hotkeys.ts normalizes to lowercase 'space'. This is intentional (different use cases) and the cross-reference comments mitigate confusion.
  • [Low] isAppleDevice() partially overlaps with isIOSDevice() (both check iPad|iPhone|iPod regex). Could reuse isIOSDevice() internally, though the duplication is minor given function simplicity.
  • Positives: Clean module boundary between keycodes.ts (single-key constants) and hotkeys.ts (multi-key combination normalization), with explicit cross-reference comments. Both normalizeHotkeyString and hotkeyFromKeyboardEvent funnel through the shared normalizeModifiersAndKey, ensuring consistent canonical output. Pure functional design with no side effects. Lazy navigator access improves SSR safety and testability.

4. Security -- SAFE ✅

No significant security issues found. Pure stateless utility functions with no DOM manipulation, network access, or dynamic code execution.

  • Positives: Defensive null/undefined handling throughout. No eval, innerHTML, or dynamic execution. All lookups use Object.hasOwn on a hardcoded alias map, avoiding prototype pollution. No secrets or authentication logic.

5. Observability -- SAFE ✅

Not applicable for these pure, deterministic utility functions. Adding instrumentation here would generate noise with no diagnostic value.

  • Positives: Functions handle invalid inputs gracefully by returning null or empty arrays, providing a clean contract for callers. Clear module boundaries aid future debugging.

6. Performance -- SAFE ✅

No meaningful performance concerns. Well-structured for keydown hot-path usage.

  • [Low] getUserAgent() is now called per function invocation instead of being cached at module load. Impact is negligible since current call sites are lifecycle hooks, not hot paths.
  • [Low] Regex literals in isIOSDevice()/isAppleDevice() are re-created per call rather than hoisted. Inconsequential at current call frequencies.
  • Positives: hotkeyFromKeyboardEvent is allocation-light and O(1) -- suitable for keydown handlers. Works directly with boolean modifier flags rather than string parsing. KEY_ALIASES is properly hoisted as a module-level constant. ~250 lines with no external dependencies means minimal bundle size impact.

Overall Verdicts

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

Top Recommendations

  1. [Low from Code Quality] Consider adding unit tests for device.ts, particularly isAppleDevice() and the SSR-safety guards in the refactored helpers, since subsequent PRs (chore(hotkey): add new @private component #3977-feat(select): add hotkey prop for items & improve keyboard navigation #3979) depend on them.
  2. [Low from Code Quality] Remove the unnecessary as any cast on navigator.maxTouchPoints in device.ts:20 -- it's a standard DOM property.
  3. [Low from Code Quality] Simplify the isAppleDevice() return expression or add a comment clarifying why isIPadOS13Plus is listed separately despite being subsumed by isMacLike.

Overall this is a clean, well-tested foundation PR. No issues that should block merge.


🤖 Generated with Claude Code

@adrianschmidt
Copy link
Copy Markdown
Contributor

Architectural Review

Positive Aspects

  1. Clean separation of concerns. The module-level doc comment at hotkeys.ts:1-21 clearly explains the boundary between keycodes.ts (single-key event.key comparisons) and hotkeys.ts (multi-key combinations with modifier normalization). This is a well-drawn line.

  2. Canonical format is well-defined. Fixed modifier order (meta+ctrl+alt+shift+key) ensures consistent string comparison — no need for set-based matching later. This is the right design choice for a hotkey system.

  3. Layout-independence via event.code. normalizeEventKey correctly uses event.code for Key[A-Z] and Digit[0-9] patterns, falling back to event.key for everything else. This means Ctrl+Q works regardless of whether the user has a QWERTY, AZERTY, or Dvorak layout.

  4. The + key handling is thorough. The tokenizer handles the ambiguity of + being both a separator and a key (+, meta++, ctrl+++). The shift-stripping in hotkeyFromKeyboardEvent ensures meta++ matches keyboard events correctly. Well thought through.

  5. SSR/non-browser safety in device.ts. Replacing the top-level window.navigator.userAgent access with lazy functions is a good refactor — makes the module safely importable in SSR or test contexts.

  6. Good test coverage. 252 lines of tests covering edge cases: null/undefined, whitespace, modifier-only inputs, layout normalization, function keys, arrow keys, and the + key ambiguity.

  7. Series structure is well-scoped. This PR is purely foundational utils with no component changes. The split/1-4 naming makes the dependency chain clear for reviewers.


Minor Issues (non-blocking)

  1. hotkeys.ts:149 — "Last non-modifier wins" is silently lossy.
    normalizeHotkeyString("ctrl+a+b")"ctrl+b", silently discarding a. The JSDoc mentions this, but it might be worth logging a dev-mode warning. A user writing "ctrl+a+b" is almost certainly making a mistake, and silent acceptance makes debugging harder.

  2. device.ts:57-59isIPadOS13Plus check is redundant in the return.
    The return is isIPadIPhoneIPod || isIPadOS13Plus || isMacLike. Since isIPadOS13Plus is defined as isMacLike && getMaxTouchPoints() > 1, and isMacLike is already in the || chain, isIPadOS13Plus can never be the deciding factor. The three-way OR is logically equivalent to just isIPadIPhoneIPod || isMacLike. Consider simplifying, or add a comment if the three-way form is preferred for readability.

  3. device.ts:20navigator.maxTouchPoints is standard, not any.
    (navigator as any).maxTouchPointsmaxTouchPoints is part of the Navigator interface in TypeScript's lib.dom. The as any cast is only needed for userAgentData. Minor type hygiene.

  4. hotkeys.ts:99Object.hasOwn browser support.
    Object.hasOwn requires Chrome 93+ / Firefox 92+ / Safari 15.4+. Worth double-checking against lime-elements' browser support matrix. If older browsers need support, normalized in KEY_ALIASES would be safer.


Nitpicks

  1. hotkeys.ts:22NORMALIZED_HOTKEY_SEPARATOR is only used once (line 140). A constant for a single '+' character used in one place adds indirection without much value.

  2. hotkeys.spec.ts:38-39 — test description is slightly misleading. "handles +++ as modifier + literal + key" implies three tokens, but the result is ['ctrl', '+'] (two). The trailing + is treated as a separator with no following token. Maybe "handles +++ as modifier + literal +" would be clearer.


Summary

Architecture is well-designed: clean separation from existing keycodes.ts, good canonical format, layout-independent event handling, and proper SSR safety. The minor issues are worth considering but don't block approval. 👍

coderabbitai[bot]

This comment was marked as resolved.

Comment thread src/util/device.ts
Comment on lines -1 to +3
const userAgent = window.navigator.userAgent;
function getUserAgent(): string {
return typeof navigator === 'undefined' ? '' : (navigator.userAgent ?? '');
}
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.

Really nice improvement!

@adrianschmidt
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Kiarokh added 2 commits March 25, 2026 17:11
- Made it safe to import outside the browser
(removed top-level `window.navigator.userAgent` access)
- Added export function `isAppleDevice(): boolean`
@adrianschmidt adrianschmidt enabled auto-merge (rebase) March 25, 2026 16:13
Copy link
Copy Markdown
Contributor

@adrianschmidt adrianschmidt left a comment

Choose a reason for hiding this comment

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

👍

@adrianschmidt adrianschmidt merged commit 394ea55 into main Mar 25, 2026
9 checks passed
@adrianschmidt adrianschmidt deleted the split/1-hotkey-utils branch March 25, 2026 16:15
@lime-opensource
Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 39.9.4 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants