Skip to content

feat(select): add hotkey prop for items & improve keyboard navigation#3979

Closed
Kiarokh wants to merge 1 commit intosplit/3.5-improve-visual-stylefrom
split/4-select-hotkeys
Closed

feat(select): add hotkey prop for items & improve keyboard navigation#3979
Kiarokh wants to merge 1 commit intosplit/3.5-improve-visual-stylefrom
split/4-select-hotkeys

Conversation

@Kiarokh
Copy link
Copy Markdown
Contributor

@Kiarokh Kiarokh commented Mar 24, 2026

Summary by CodeRabbit

  • New Features
    • Hotkey support for the select component—use keyboard shortcuts to pick options while the menu is open.
  • Style
    • Updated keyboard element styling for improved layout, shadows, padding, border, and first-letter capitalization.
  • Documentation
    • Added an example demonstrating hotkey usage with the select component.

Review:

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

Browsers tested:

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

Windows:

  • Chrome
  • Edge
  • Firefox

Linux:

  • Chrome
  • Firefox

macOS:

  • Chrome
  • Firefox
  • Safari

Mobile:

  • Chrome on Android
  • iOS

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

coderabbitai Bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

Adds hotkey support to the limel-select component (option-level hotkey properties, document-level keydown handling with normalization and caching, and UI rendering of hotkeys), a new select hotkeys example, and minor visual adjustments to the kbd styling.

Changes

Cohort / File(s) Summary
Kbd Styling
src/components/markdown/partial-styles/_kbd.scss
Adjusted kbd presentation: display, margin/background order, multi-layer box-shadow, padding and border updates, plus ::first-letter capitalization rule.
Select Types & Example
src/components/select/option.types.ts, src/components/select/examples/select-hotkeys.tsx
Added optional hotkey?: string to Option and introduced a new Stencil example component demonstrating options with hotkeys and change handling.
Select Template & Logic
src/components/select/select.template.tsx, src/components/select/select.tsx
Template now includes isOpen when creating menu items and renders a limel-hotkey primary component for options with hotkeys; component adds document-level keydown listener, normalized-hotkey caching, reserved-key checks, and selection behavior for matching hotkeys (single and multiple modes).

Sequence Diagram

sequenceDiagram
    participant User
    participant Document
    participant Select
    participant Options
    participant Menu

    User->>Document: press key
    Document->>Select: document keydown
    Select->>Select: normalize hotkey (cache)
    Select->>Select: validate (menu open, not mobile, not repeat, not reserved)
    Select->>Options: find option matching hotkey
    alt match found and enabled
        Options-->>Select: matched option
        Select->>Select: update selection (single/multiple)
        Select->>Menu: emit change
        Select->>Select: close menu (if single) and restore focus
    else no match / invalid
        Select-->>Document: ignore
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

visual design, 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 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: adding hotkey support to select items and improving keyboard navigation, which aligns with the main objectives of the PR.

✏️ 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/4-select-hotkeys

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

❤️ Share

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

@github-actions
Copy link
Copy Markdown

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

@adrianschmidt adrianschmidt force-pushed the split/3-menu-hotkeys branch 3 times, most recently from e6762e5 to 7326c80 Compare March 30, 2026 15:19
Base automatically changed from split/3-menu-hotkeys to main March 30, 2026 15:24
@adrianschmidt adrianschmidt force-pushed the split/4-select-hotkeys branch from 2d151c9 to 6b9083e Compare March 30, 2026 15:28
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@src/components/markdown/partial-styles/_kbd.scss`:
- Around line 31-33: The rule using &::first-letter in
src/components/markdown/partial-styles/_kbd.scss uppercases the first character
of <kbd> labels and can misrepresent keyboard shortcuts; remove the
&::first-letter { text-transform: uppercase; } block (or adjust scope so it does
not apply to kbd elements) so that <kbd> content is rendered exactly as authored
rather than being style-transformed.

In `@src/components/select/examples/select-hotkeys.tsx`:
- Around line 86-88: The property handleChange is defined as an arrow function
and never reassigned, so mark it readonly to satisfy static analysis: add the
readonly modifier to the handleChange property declaration (private readonly
handleChange = (event: LimelSelectCustomEvent<Option>) => { this.value =
event.detail; }); to ensure immutability and silence the linter while keeping
the existing behavior.
- Line 42: The class field options is never reassigned so mark it readonly to
express immutability; update the declaration of options (the private options:
(Option | ListSeparator)[] = [...]) to private readonly options: (Option |
ListSeparator)[] = [...] so the compiler and readers know it won't be reassigned
and to satisfy the static analysis warning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 37274e04-81bc-4478-a385-72704443191d

📥 Commits

Reviewing files that changed from the base of the PR and between 7c5cc57 and 6b9083e.

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

Comment on lines +31 to 33
&::first-letter {
text-transform: uppercase;
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 30, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid transforming <kbd> content with ::first-letter

Uppercasing the first letter changes the rendered key label and can misrepresent actual shortcuts/commands (e.g., case-sensitive keys or multi-key sequences). Prefer rendering the intended label directly from content instead of style-based mutation.

Suggested fix
-    &::first-letter {
-        text-transform: uppercase;
-    }
📝 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
&::first-letter {
text-transform: uppercase;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/markdown/partial-styles/_kbd.scss` around lines 31 - 33, The
rule using &::first-letter in src/components/markdown/partial-styles/_kbd.scss
uppercases the first character of <kbd> labels and can misrepresent keyboard
shortcuts; remove the &::first-letter { text-transform: uppercase; } block (or
adjust scope so it does not apply to kbd elements) so that <kbd> content is
rendered exactly as authored rather than being style-transformed.

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 Thoughts?

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 explained this in details in the original PR once. But can't find it right now to link to it. Maybe @coderabbitai remembers?

Short answer (which I hope is enough):
I don't think the feedback is valid. All physical keyboards have the capital letter printed on their keys. Apple keybaords write modifier key names and esc and such with small letters. But that's alright too if we visualize their first letters with capital letters as well. We render glyphs anywas.

@coderabbitai what do you mean by "case-sensitive keys or multi-key sequences" how rendering capital letters can be misinterpreted by users?

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, I wasn't able to locate the original PR discussion where this was explained in detail, sorry about that.

To clarify what I meant by the original concern:

  • "Case-sensitive keys" — if an author intentionally writes <kbd>a</kbd> vs <kbd>A</kbd> to visually distinguish a lowercase key from an uppercase one (e.g., to describe a case-sensitive terminal shortcut), the ::first-letter rule would render both as A, erasing that distinction. That was the scenario I had in mind.
  • "Multi-key sequences" — if someone wrote a sequence like <kbd>ctrl+shift+a</kbd> in a single element, only the first letter c would be capitalized, yielding Ctrl+shift+a. However, that's a misuse of <kbd> (the spec recommends nesting <kbd> elements for sequences), so it's more of an edge case.

That said, your reasoning is compelling: the visual metaphor here is a physical key cap, and physical keyboards universally display uppercase letters. Capitalizing ctrlCtrl, shiftShift, escEsc is entirely consistent with that metaphor. Given this context and the fact that the ::first-letter rule only uppercases the first character (not the full label), I agree my concern was overstated for this particular use case. I'm happy to withdraw it.


✏️ Learnings added
Learnt from: Kiarokh
Repo: Lundalogik/lime-elements PR: 3979
File: src/components/markdown/partial-styles/_kbd.scss:31-33
Timestamp: 2026-03-30T16:57:04.211Z
Learning: In lime-elements, `kbd` styling in markdown (src/components/markdown/partial-styles/_kbd.scss) intentionally uses `&::first-letter { text-transform: uppercase; }` to mirror physical keyboard key cap styling, where keys are displayed with uppercase labels. This is a deliberate visual design decision and not a bug — the metaphor is a physical key cap, not raw text content.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Kiarokh
Repo: Lundalogik/lime-elements PR: 3658
File: src/components/profile-picture/profile-picture.scss:120-124
Timestamp: 2025-09-03T07:17:23.267Z
Learning: Issue `#3666` in the lime-elements repository tracks the removal of a CSS hack in profile-picture component that overrides icon color via --limel-theme-on-surface-color, to be replaced with proper icon color support in limel-icon-button.

Learnt from: adrianschmidt
Repo: Lundalogik/lime-elements PR: 3960
File: src/style-utilities.md:31-33
Timestamp: 2026-03-19T19:28:52.281Z
Learning: When reviewing Dart Sass (SCSS) imports, distinguish between the two package-resolution mechanisms:
1) Use the `pkg:` URL form (e.g., `use "pkg:scope/name"`) only when the Sass plugin/CLI is configured with `NodePackageImporter`, which uses the target package’s `package.json` `sass`/`exports` fields.
2) Prefer/expect `includePaths` for load-path resolution: if build config sets `includePaths` (e.g., `['node_modules']`), then plain `use 'scope/name'` should resolve via standard file resolution within the resolved directory—typically `index.scss` or `_index.scss` (e.g., `node_modules/scope/name/index.scss`)—without needing `NodePackageImporter` or the `pkg:` prefix.

Copy link
Copy Markdown
Contributor Author

@Kiarokh Kiarokh Mar 31, 2026

Choose a reason for hiding this comment

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

@coderabbitai I see what you mean. Since this style is used in limel-markdown as well (not just limel-hotkey) we should be more careful. I'm gonna add a fixup

Comment thread src/components/select/examples/select-hotkeys.tsx Outdated
Comment thread src/components/select/examples/select-hotkeys.tsx Outdated
@adrianschmidt adrianschmidt force-pushed the split/4-select-hotkeys branch from 6b9083e to b0743eb Compare March 30, 2026 15:51
@adrianschmidt
Copy link
Copy Markdown
Contributor

Consolidated PR Review -- 6 Dimensions

PR Summary

PR #3979 adds hotkey support to the limel-select component's Option type, wiring up document-level keydown handling with hotkey matching, normalization caching, and visual display via limel-hotkey. It also improves the visual styling of <kbd> elements in the markdown component. 6 files changed, +265/-11 lines.


1. Backward Compatibility -- SAFE ✅

All changes are purely additive. The new hotkey?: string property on Option is optional — existing consumers require no changes. The document-level keydown listener is completely inert unless options explicitly define hotkeys. The kbd CSS changes are visual-only within the markdown component's shadow DOM.

  • [Low] The ::first-letter { text-transform: uppercase } on kbd elements will capitalize the first letter of any <kbd> in markdown content. This is conventional for keyboard keys but could be surprising for other uses of <kbd>.
  • Positives: API report correctly updated. Optional property ensures zero impact on existing consumers. Defensive cleanup in disconnectedCallback.

2. Code Quality -- ACCEPTABLE ✅

Well-implemented following patterns established in PR #3978 (menu hotkeys). The main concerns are duplicated logic and missing tests.

  • [Medium] No test coverage for select hotkey behavior. The menu component has comprehensive e2e tests for hotkeys (menu.e2e.tsx lines 127-556), but the select component has none. The new logic covers keyboard event interception, reserved-key filtering, multi-select toggling, and disabled-option skipping — all worth testing.
  • [Medium] getNormalizedHotkey + normalizedHotkeyCache pattern is character-for-character identical between select.tsx and menu.tsx. This could be extracted to a shared utility in src/util/hotkeys.ts.
  • [Low] isReservedSelectHotkey intentionally differs from isReservedMenuHotkey (arrows always reserved in select for navigation). A brief comment explaining why the logic differs from menu would help future maintainers.
  • Positives: Good early-exit guards. Example component has thorough JSDoc documentation including platform-specific notes. Cache invalidation via @Watch('options') is correct.

3. Architecture -- ACCEPTABLE ✅

The select component follows the same hotkey architecture as the menu component, reusing the shared src/util/hotkeys.ts utilities. The main concern is growing duplication.

  • [Medium] Duplicated imperative hotkey-handling pattern across menu and select: getNormalizedHotkey, cache field, findByHotkey, isReservedHotkey, document listener add/remove. For two components this is tolerable, but extracting a shared DropdownHotkeyHandler utility before it spreads further would be advisable.
  • [Low] Divergent rendering paths: menu uses limel-menu-item-meta to wrap hotkey display (with normalization, badge, chevron), while select constructs primaryComponent inline in select.template.tsx with a hardcoded style: { order: '2' }. The inline style is a layout concern baked into data flow code.
  • [Low] No isFromTextInput guard. The menu component has this to avoid intercepting keypresses from its search field. The select doesn't need it today, but it would be needed if a searchable select is ever added.
  • Positives: Proper reuse of shared hotkey utilities. Clean listener lifecycle management. Correct handling of the multiple select variant with toggle semantics.

4. Security -- SAFE ✅

No significant security issues found. The hotkey string is developer-provided configuration (not user input), used only for keyboard event matching and passed as a prop to a Stencil component (no raw HTML injection). Event listeners are properly scoped and cleaned up.

  • Positives: No XSS vectors. No injection risks. No information disclosure. Proper event handler cleanup.

5. Observability -- GOOD ✅

No significant observability concerns. Appropriate for a UI component library.

  • [Low] Invalid hotkey strings (e.g., "ctrl+++") silently normalize to null and are cached, effectively disabling the hotkey without feedback. A console.warn in dev mode could help consumers debug configuration mistakes.
  • Positives: Cache invalidation is present. Event listener cleanup prevents leaks. Silent no-ops on unmatched keys are correct behavior for keyboard shortcuts.

6. Performance -- GOOD ✅

No performance blockers. The caching strategy is sound.

  • [Low] getOptionsExcludingSeparators() calls this.options.filter(...) on every keydown while the menu is open, allocating a new array each time. For typical select sizes this is negligible, but could be cached alongside the hotkey cache if needed.
  • Positives: normalizedHotkeyCache avoids re-running normalization on repeated keystrokes. Early-exit guards are well-ordered (cheapest checks first). Arrow-function handler ensures stable reference for removeEventListener. Bundle impact is minimal since hotkey utilities are already loaded by menu/hotkey components.

Overall Verdicts

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

Top Recommendations

  1. [Medium from Code Quality] Add e2e tests for select hotkey behavior — at minimum: hotkey selects correct option, disabled options are skipped, reserved keys are not intercepted, and multi-select toggling works. The menu component's menu.e2e.tsx hotkey tests provide a good template.
  2. [Medium from Architecture/Code Quality] Extract the duplicated getNormalizedHotkey + cache pattern into a shared utility (e.g., a HotkeyCache class or factory function in src/util/hotkeys.ts). The isReserved* and findByHotkey methods have intentional differences, but the normalization + caching is identical.
  3. [Low from Architecture] Add a brief comment to isReservedSelectHotkey explaining why it differs from isReservedMenuHotkey (arrow keys are always reserved in select because they're used for dropdown navigation, unlike in menu where they're only reserved without modifiers).

@adrianschmidt adrianschmidt force-pushed the split/4-select-hotkeys branch from b0743eb to abcf148 Compare March 31, 2026 09:38
@adrianschmidt
Copy link
Copy Markdown
Contributor

@Kiarokh Small observation on isReservedSelectHotkey vs isReservedMenuHotkey:

In menu, the logic is:

if (hasModifiers) {
    return false; // any modified key is allowed
}
return key === 'arrowup' || ... || key === 'tab' || key === 'enter' || key === 'space' || key === 'escape';

In select, arrow keys and tab are reserved regardless of modifiers:

if (key === 'arrowup' || ... || key === 'arrowright') {
    return true; // always reserved, even with modifiers
}
if (key === 'tab') {
    return true; // always reserved, even with modifiers
}
const hasModifiers = tokens.length > 1;
return !hasModifiers && (key === 'enter' || key === 'space' || key === 'escape');

This means a hotkey like ctrl+arrowdown would work in a menu but be blocked in a select. Since unmodified arrows are used for dropdown navigation in both components equally, it seems like the logic should be the same — i.e., the select version should also do the early if (hasModifiers) return false check, matching the menu.

Was this divergence intentional, or should they be unified?

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@src/components/select/examples/select-hotkeys.tsx`:
- Around line 64-69: The demo option object that sets hotkey: 'cmd+d' (the entry
with value 'closed' / text 'Closed') uses a meta-based shortcut which normalizes
to OS-level shortcuts on Windows/Linux; update that hotkey to a portable,
non-meta combination (for example 'ctrl+shift+d' or another ctrl/alt-based
combo) so the example works consistently across platforms and avoids OS shortcut
collisions—locate the object with value 'closed' in select-hotkeys.tsx and
replace its hotkey property accordingly.

In `@src/components/select/select.tsx`:
- Around line 424-438: The multi-select branch currently calls
this.change.emit(nextValue) directly (inside the this.multiple / isMultiple /
matchedOption block), which bypasses the scroll snapshot/restore logic in
handleMenuChange() and causes the menu to jump; instead, route the update
through the existing handleMenuChange flow (call the appropriate
handleMenuChange path or helper with nextValue so the snapshot/restore logic
runs) rather than emitting nextValue directly, ensuring you reference the same
symbols (this.multiple, isMultiple(this.value), matchedOption, nextValue) when
making the change.
- Around line 473-485: findOptionByHotkey currently iterates
getOptionsExcludingSeparators() and may match options that createMenuItems
intentionally omits when this.selectProps.required is true (e.g.,
empty-text/placeholder options). Update findOptionByHotkey to apply the same
filtering logic used in createMenuItems: skip any option that would be filtered
out when this.selectProps.required is true (for example, options with empty
text/value or the placeholder option), then normalize and compare hotkeys as
before; reference the findOptionByHotkey, getOptionsExcludingSeparators, and
createMenuItems logic to ensure the filters are identical.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5fc9624d-e830-4634-9a11-e22f65630dbf

📥 Commits

Reviewing files that changed from the base of the PR and between 6b9083e and abcf148.

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

Comment on lines +64 to +69
{
text: 'Closed',
secondaryText: 'as not planned',
value: 'closed',
hotkey: 'cmd+d',
},
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

Replace cmd+d with a portable demo shortcut.

cmd+d normalizes to meta+d, which becomes Win+D on Windows/Linux and collides with an OS-owned shortcut. That makes the example unreliable outside macOS; please use a non-meta combination here.

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

In `@src/components/select/examples/select-hotkeys.tsx` around lines 64 - 69, The
demo option object that sets hotkey: 'cmd+d' (the entry with value 'closed' /
text 'Closed') uses a meta-based shortcut which normalizes to OS-level shortcuts
on Windows/Linux; update that hotkey to a portable, non-meta combination (for
example 'ctrl+shift+d' or another ctrl/alt-based combo) so the example works
consistently across platforms and avoids OS shortcut collisions—locate the
object with value 'closed' in select-hotkeys.tsx and replace its hotkey property
accordingly.

Comment on lines +424 to +438
if (this.multiple) {
const currentValue = isMultiple(this.value) ? this.value : [];
const hasSelectedOption = currentValue.some(
(option) => option.value === matchedOption.value
);

const nextValue = hasSelectedOption
? currentValue.filter(
(option) => option.value !== matchedOption.value
)
: [...currentValue, matchedOption];

this.change.emit(nextValue);

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.

⚠️ Potential issue | 🟠 Major

Keep the multi-select hotkey path on the existing scroll-preserving flow.

handleMenuChange() already snapshots and restores the menu surface scroll position for multi-select updates because rerendering otherwise jumps the menu back to the top. This new branch emits nextValue directly, so toggling items with hotkeys will regress that behavior in long dropdowns.

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

In `@src/components/select/select.tsx` around lines 424 - 438, The multi-select
branch currently calls this.change.emit(nextValue) directly (inside the
this.multiple / isMultiple / matchedOption block), which bypasses the scroll
snapshot/restore logic in handleMenuChange() and causes the menu to jump;
instead, route the update through the existing handleMenuChange flow (call the
appropriate handleMenuChange path or helper with nextValue so the
snapshot/restore logic runs) rather than emitting nextValue directly, ensuring
you reference the same symbols (this.multiple, isMultiple(this.value),
matchedOption, nextValue) when making the change.

Comment on lines +473 to +485
private findOptionByHotkey(pressedHotkey: string): Option | null {
for (const option of this.getOptionsExcludingSeparators()) {
if (option.disabled || !option.hotkey) {
continue;
}

const normalized = this.getNormalizedHotkey(option.hotkey);
if (normalized && normalized === pressedHotkey) {
return option;
}
}

return null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hotkeys can still select options that the required menu intentionally hides.

findOptionByHotkey() scans every non-separator option, but createMenuItems() filters out empty-text options when required is true. A hidden “empty option” can therefore still be selected via hotkey, which breaks the required-select invariant. Please apply the same filter before matching here.

🔧 Suggested direction
     private findOptionByHotkey(pressedHotkey: string): Option | null {
-        for (const option of this.getOptionsExcludingSeparators()) {
+        const options = this.required
+            ? this.getOptionsExcludingSeparators().filter((option) => option.text)
+            : this.getOptionsExcludingSeparators();
+
+        for (const option of options) {
             if (option.disabled || !option.hotkey) {
                 continue;
             }
📝 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 findOptionByHotkey(pressedHotkey: string): Option | null {
for (const option of this.getOptionsExcludingSeparators()) {
if (option.disabled || !option.hotkey) {
continue;
}
const normalized = this.getNormalizedHotkey(option.hotkey);
if (normalized && normalized === pressedHotkey) {
return option;
}
}
return null;
private findOptionByHotkey(pressedHotkey: string): Option | null {
const options = this.required
? this.getOptionsExcludingSeparators().filter((option) => option.text)
: this.getOptionsExcludingSeparators();
for (const option of options) {
if (option.disabled || !option.hotkey) {
continue;
}
const normalized = this.getNormalizedHotkey(option.hotkey);
if (normalized && normalized === pressedHotkey) {
return option;
}
}
return null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/select/select.tsx` around lines 473 - 485, findOptionByHotkey
currently iterates getOptionsExcludingSeparators() and may match options that
createMenuItems intentionally omits when this.selectProps.required is true
(e.g., empty-text/placeholder options). Update findOptionByHotkey to apply the
same filtering logic used in createMenuItems: skip any option that would be
filtered out when this.selectProps.required is true (for example, options with
empty text/value or the placeholder option), then normalize and compare hotkeys
as before; reference the findOptionByHotkey, getOptionsExcludingSeparators, and
createMenuItems logic to ensure the filters are identical.

@adrianschmidt adrianschmidt changed the base branch from main to split/3.5-improve-visual-style March 31, 2026 11:46
@adrianschmidt adrianschmidt changed the title fix(markdown): improve visual style of kbd elements feat(select): add hotkey prop for items & improve keyboard navigation Mar 31, 2026
@adrianschmidt adrianschmidt changed the title feat(select): add hotkey prop for items & improve keyboard navigation feat(select): add hotkey prop for items & improve keyboard navigation Mar 31, 2026
@adrianschmidt adrianschmidt marked this pull request as draft March 31, 2026 11:49
@adrianschmidt
Copy link
Copy Markdown
Contributor

This is messy with reviews for commits no longer part of the PR. I'll close this so we can open a new fresh PR instead.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants