Skip to content

Conversation

@wouterlms
Copy link

@wouterlms wouterlms commented Jan 16, 2026

πŸ”— Linked issue


❓ Type of change

  • πŸ“– Documentation (updates to the documentation, readme or JSdoc annotations)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • πŸ‘Œ Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

πŸ“š Description

This PR adds filter support documentation and requirements to the DropdownMenu component, aligning it with the existing Listbox filter behavior.

The DropdownMenu previously did not define filtering or search behavior. This change documents an optional filter input that provides keyboard navigation, accessibility guarantees, and submenu awareness.

  • Optional filter input for DropdownMenu
  • Full keyboard navigation support
  • Search support across submenus
  • Focused index is preserved when opening and closing submenus
  • Listbox filter behavior used as a reference
  • Non-filtering by design (structure and accessibility only)

πŸ“Έ Screenshots (if appropriate)

image

πŸ“ Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a filter input component for dropdown menus, enabling users to search and filter menu items in real-time with keyboard navigation and accessibility support.
  • Chores

    • Updated menu context to expose additional navigation controls and state management.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

πŸ“ Walkthrough

Walkthrough

This pull request introduces a new DropdownMenuFilter component that enables filtering of dropdown menu items. The implementation includes the filter component itself, updates to the menu context system to support filter element tracking and keyboard navigation coordination, enhancements to arrow navigation utilities to handle edge cases, and integration with the component registration and plugin systems.

Changes

Cohort / File(s) Summary
DropdownMenuFilter Component
packages/core/src/DropdownMenu/DropdownMenuFilter.vue
New Vue component implementing a filterable input for dropdown menus. Defines DropdownMenuFilterProps (with modelValue, autoFocus, disabled) and DropdownMenuFilterEmits (update:modelValue). Integrates with menu contexts for focus management, ARIA accessibility, and key navigation. Handles input updates, focus/blur lifecycle, and Escape/Enter key behaviors.
DropdownMenu Exports & Registration
packages/core/src/DropdownMenu/index.ts, packages/core/constant/components.ts, packages/core/src/DropdownMenu/story/DropdownMenuFilter.story.vue
Exports new DropdownMenuFilter component and types (DropdownMenuFilterProps, DropdownMenuFilterEmits). Registers component in the public components list. Includes comprehensive story demonstrating filter usage with nested menus, checkboxes, and conditional item rendering.
Menu Context Enhancements
packages/core/src/Menu/MenuContentImpl.vue, packages/core/src/Menu/MenuItemImpl.vue, packages/core/src/Menu/MenuSubContent.vue, packages/core/src/Menu/MenuSubTrigger.vue
Extended MenuContentContext with highlightedElement, keyboard navigation handlers (onKeydownNavigation, onKeydownEnter), filter element tracking, and activeSubmenuContext management. MenuItemImpl updated to track highlight state via contentContext. MenuSubContent refocuses filter element on close. MenuSubTrigger synchronizes activeSubmenuContext on open/close. Enhanced focus management to avoid stealing focus from inputs.
Navigation Utilities & Tests
packages/core/src/shared/useArrowNavigation.ts, packages/core/src/shared/useArrowNavigation.test.ts
Modified findNextFocusableElement to handle navigation when currentElement is not in the elements array (e.g., external filter element). Adjusts iterations and starts from first/last element based on direction. Added unit tests for edge cases: navigation from external element with single/multiple items.
Plugin Integration
packages/plugins/src/namespaced/index.ts
Added Filter member to DropdownMenu namespace, exposing DropdownMenuFilter in the runtime and type-safe plugin API.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Filter as DropdownMenuFilter
    participant Content as MenuContentImpl
    participant Item as MenuItemImpl
    participant Template as Template

    User->>Filter: Type characters
    Filter->>Content: Update searchRef
    Content->>Template: Trigger reactivity
    Template->>Item: Re-render items (filtered)
    
    User->>Filter: Press ArrowDown
    Filter->>Content: Call onKeydownNavigation
    Content->>Content: Update highlightedElement
    Item->>Item: Recompute isHighlighted
    Template->>Template: Render highlight
    
    User->>Filter: Press Escape (with value)
    Filter->>Content: Stop propagation, reset search
    Content->>Template: Clear filter, maintain menu open
    Template->>Template: Show all items again
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A filter for the menu's delight,
Where dropdowns dance left and right,
The rabbit hops through items with glee,
Searching and sifting what ought to be,
A whisker of logic, a dash of care! ✨

πŸš₯ Pre-merge checks | βœ… 3
βœ… Passed checks (3 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title 'feat(DropdownMenu): add DropdownMenuFilter component' directly and accurately summarizes the main changeβ€”adding a new DropdownMenuFilter component to the DropdownMenu suite.
Docstring Coverage βœ… Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • πŸ“ Generate docstrings

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

Copy link

@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 `@packages/core/src/DropdownMenu/DropdownMenuFilter.vue`:
- Line 4: The typeahead state (searchRef) can fall out of sync when the parent
initializes or updates modelValue because searchRef is only changed on user
input, Escape, and unmount; add a watcher for modelValue with immediate: true to
set searchRef.value = modelValue so the internal typeahead state reflects
external updates. Import watch from 'vue' (in addition to watchSyncEffect) and
add watch(modelValue, v => { searchRef.value = v ?? '' }, { immediate: true })
inside the DropdownMenuFilter component so searchRef stays synchronized on mount
and whenever modelValue changes.
🧹 Nitpick comments (3)
packages/core/src/Menu/MenuSubTrigger.vue (1)

26-36: Consider clearing activeSubmenuContext on unmount.

The watcher correctly synchronizes the active submenu context. However, if this component unmounts while the submenu is open, activeSubmenuContext may still reference the stale trigger.

πŸ”§ Suggested enhancement
 onUnmounted(() => {
   clearOpenTimer()
+  if (contentContext.activeSubmenuContext.value?.trigger.value === subContext.trigger.value) {
+    contentContext.activeSubmenuContext.value = undefined
+  }
 })
packages/core/src/DropdownMenu/story/DropdownMenuFilter.story.vue (1)

133-136: Consider separator visibility logic.

The separator's visibility is tied only to "Developer Tools" matching. Typically, a separator should remain visible when any items exist on both sides of it. Consider showing the separator when any of the items above it match OR the "Developer Tools" item matches.

♻️ Suggested improvement
 <DropdownMenuSeparator
-  v-if="matches('Developer Tools', subFilterText)"
+  v-if="(matches('Save Page As…', subFilterText) || matches('Create Shortcut…', subFilterText) || matches('Name Window…', subFilterText)) && matches('Developer Tools', subFilterText)"
   :class="separator"
 />
packages/core/src/Menu/MenuContentImpl.vue (1)

162-166: Consider adding null check or early return.

The onKeydownEnter handler clicks the highlighted element, but consider whether the calling code handles cases where no item is highlighted (e.g., when the filter yields no results). If highlightedElement.value is undefined, this is a no-op, which is fine, but documenting this behavior or adding a guard would improve clarity.

πŸ“œ Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 07c5d94 and 4492d9c.

πŸ“’ Files selected for processing (11)
  • packages/core/constant/components.ts
  • packages/core/src/DropdownMenu/DropdownMenuFilter.vue
  • packages/core/src/DropdownMenu/index.ts
  • packages/core/src/DropdownMenu/story/DropdownMenuFilter.story.vue
  • packages/core/src/Menu/MenuContentImpl.vue
  • packages/core/src/Menu/MenuItemImpl.vue
  • packages/core/src/Menu/MenuSubContent.vue
  • packages/core/src/Menu/MenuSubTrigger.vue
  • packages/core/src/shared/useArrowNavigation.test.ts
  • packages/core/src/shared/useArrowNavigation.ts
  • packages/plugins/src/namespaced/index.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/core/src/shared/useArrowNavigation.test.ts (1)
packages/core/src/shared/useArrowNavigation.ts (1)
  • useArrowNavigation (76-145)
πŸ”‡ Additional comments (19)
packages/core/src/shared/useArrowNavigation.test.ts (2)

116-127: LGTM! Good edge case coverage.

This test correctly validates that navigating forward (ArrowDown) from an external element lands on the first item in the collection.


129-145: LGTM! Complements the forward navigation test.

This test correctly validates that navigating backward (ArrowUp) from an external element lands on the last item in the collection.

packages/core/src/shared/useArrowNavigation.ts (1)

172-184: LGTM! Clean handling of external element navigation.

The logic correctly handles the case where currentElement is not in the collection:

  • Forward navigation starts from the first element
  • Backward navigation starts from the last element
  • The extra iteration (elements.length + 1) ensures all elements can be checked even when starting from outside the collection
packages/core/constant/components.ts (1)

178-182: LGTM! Consider reordering for consistency with Listbox.

The component is correctly registered. For consistency with listbox (where ListboxFilter appears near the top of the list after ListboxContent), you might consider placing DropdownMenuFilter closer to DropdownMenuContent. This is purely a stylistic preference.

packages/core/src/DropdownMenu/index.ts (1)

15-19: Export structure is correct and follows established conventions.

The DropdownMenuFilter export properly matches the component definition, with both DropdownMenuFilterProps and DropdownMenuFilterEmits correctly exported. The placement is alphabetically ordered and consistent with the pattern used by all other components in this barrel file.

packages/core/src/Menu/MenuItemImpl.vue (3)

36-36: LGTM! Good separation of highlight state from focus.

The computed isHighlighted correctly combines local focus state with the shared highlightedElement context, enabling items to appear highlighted while focus remains on the filter input.


50-54: LGTM! Proper focus management with input awareness.

The logic correctly prevents stealing focus from filter inputs by checking if an INPUT or TEXTAREA is currently focused before calling focus(). The optional chaining on getActiveElement()?.tagName handles the null case appropriately.


84-90: LGTM! Focus handler correctly updates highlighted element.

The focus handler properly synchronizes highlightedElement with the focused item, maintaining consistency between the highlight and focus states.

packages/core/src/DropdownMenu/story/DropdownMenuFilter.story.vue (2)

39-41: LGTM! Clean filter helper function.

The matches helper is concise and handles the empty filter case correctly by returning true when no filter is applied.


67-72: LGTM! Good demonstration of nested filter usage.

The story effectively showcases the DropdownMenuFilter in both the main menu and submenu contexts with proper v-model binding and the auto-focus prop.

Also applies to: 106-111

packages/plugins/src/namespaced/index.ts (1)

312-312: LGTM! Consistent namespace registration.

The DropdownMenuFilter is correctly added to both the runtime object and type assertion, following the established pattern for other components in this file.

Also applies to: 330-330

packages/core/src/Menu/MenuSubContent.vue (1)

84-91: LGTM! Proper submenu close handling with filter awareness.

The implementation correctly maintains focus on the parent's filter input while visually highlighting the submenu trigger, providing good UX for keyboard navigation. The scrollIntoView({ block: 'nearest' }) ensures the trigger remains visible without unnecessary scrolling.

One minor consideration: menuSubContext.trigger.value could theoretically be undefined, but since this code path only executes when closing an open submenu, the trigger should always exist at that point.

packages/core/src/Menu/MenuContentImpl.vue (5)

143-160: LGTM! Well-structured arrow navigation handler.

The onKeydownNavigation function correctly uses useArrowNavigation with focus: false to calculate the next element without immediately focusing it, then updates highlightedElement and ensures visibility with scrollIntoView. This enables keyboard navigation while keeping focus on the filter input.


171-176: LGTM! Smart submenu auto-close on navigation.

The watcher correctly closes active submenus when the highlighted element moves away from the submenu trigger. This prevents "orphaned" open submenus during keyboard navigation and improves UX.


243-244: LGTM! Correct typeahead exclusion for input elements.

Adding the check for INPUT and TEXTAREA tag names prevents typeahead search from interfering when users are typing in the filter input.


297-301: LGTM! Consistent input focus preservation.

The onItemLeave handler now correctly checks if an input is focused before refocusing the content element, maintaining consistency with the focus management pattern used elsewhere in this PR.


312-319: LGTM! Context properly exposes new filter-related state.

All new context fields are correctly provided, enabling child components to interact with the filter element, highlighted state, and navigation handlers.

packages/core/src/DropdownMenu/DropdownMenuFilter.vue (2)

88-103: Keyboard routing looks consistent with menu navigation.

Arrow/home/end routing plus Enter handling and Escape-to-clear behavior are coherent with expected menu UX.


106-125: ARIA wiring and disabled semantics look solid.

role="searchbox", aria-activedescendant, and disabled attributes are well covered.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

<script lang="ts">
import type { PrimitiveProps } from '@/Primitive'
import { useVModel } from '@vueuse/core'
import { computed, onMounted, onUnmounted, ref, watchSyncEffect } from 'vue'
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/core/src/DropdownMenu/DropdownMenuFilter.vue

Repository: unovue/reka-ui

Length of output: 4605


🏁 Script executed:

rg -A 5 "searchRef" packages/core/src/Menu/ --type ts --type vue | head -100

Repository: unovue/reka-ui

Length of output: 85


🏁 Script executed:

rg "searchRef" packages/core/src/Menu/ -A 3 -B 1 | head -150

Repository: unovue/reka-ui

Length of output: 2359


🏁 Script executed:

rg -B 5 -A 10 "searchRef.value" packages/core/src/Menu/MenuContentImpl.vue | head -80

Repository: unovue/reka-ui

Length of output: 631


🏁 Script executed:

rg -B 5 -A 10 "typeahead|search.*filter|searchRef" packages/core/src/Menu/MenuContentImpl.vue | grep -A 15 -B 5 "typeahead\|isValidTypeaheadKey"

Repository: unovue/reka-ui

Length of output: 40


🏁 Script executed:

cat -n packages/core/src/Menu/MenuContentImpl.vue | grep -A 5 -B 5 "searchRef"

Repository: unovue/reka-ui

Length of output: 1803


🏁 Script executed:

rg "searchRef\.value" packages/core/src/Menu/ -B 3 -A 3 | head -200

Repository: unovue/reka-ui

Length of output: 1652


🏁 Script executed:

rg -B 10 -A 10 "onKeydownNavigation|onKeydownEnter" packages/core/src/Menu/MenuContentImpl.vue | head -150

Repository: unovue/reka-ui

Length of output: 2748


🏁 Script executed:

rg "useTypeahead" packages/core/src/ -A 20 | head -100

Repository: unovue/reka-ui

Length of output: 7764


🏁 Script executed:

find packages/core/src -name "*useTypeahead*" -o -name "*typeahead*" | head -20

Repository: unovue/reka-ui

Length of output: 98


🏁 Script executed:

cat -n packages/core/src/shared/useTypeahead.ts

Repository: unovue/reka-ui

Length of output: 3802


🏁 Script executed:

cat -n packages/core/src/DropdownMenu/DropdownMenuFilter.vue | sed -n '37,45p'

Repository: unovue/reka-ui

Length of output: 398


Sync searchRef whenever modelValue changes to prevent typeahead desync.

searchRef only updates on user input, Escape key, and unmount. If modelValue is initialized non-empty or updated by the parent, the menu's typeahead state can drift from the input value, causing incorrect space-key behavior. Add a watch(modelValue) with immediate: true to keep searchRef in sync on mount and whenever the parent updates the value.

πŸ”§ Suggested fix
-import { computed, onMounted, onUnmounted, ref, watchSyncEffect } from 'vue'
+import { computed, onMounted, onUnmounted, ref, watch, watchSyncEffect } from 'vue'
@@
 const rootContext = injectMenuRootContext()
 const contentContext = injectMenuContentContext()
 const subContext = injectMenuSubContext(null)
+
+watch(modelValue, (value) => {
+  contentContext.searchRef.value = value ?? ''
+}, { immediate: true })
@@
 function handleInput(event: InputEvent) {
   const target = event.target as HTMLInputElement
   modelValue.value = target.value
-  // Update the menu's search ref to help with filtering
-  contentContext.searchRef.value = target.value
 }
πŸ€– Prompt for AI Agents
In `@packages/core/src/DropdownMenu/DropdownMenuFilter.vue` at line 4, The
typeahead state (searchRef) can fall out of sync when the parent initializes or
updates modelValue because searchRef is only changed on user input, Escape, and
unmount; add a watcher for modelValue with immediate: true to set
searchRef.value = modelValue so the internal typeahead state reflects external
updates. Import watch from 'vue' (in addition to watchSyncEffect) and add
watch(modelValue, v => { searchRef.value = v ?? '' }, { immediate: true })
inside the DropdownMenuFilter component so searchRef stays synchronized on mount
and whenever modelValue changes.

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.

1 participant