DropdownMenuFilter 完整指南:在下拉菜单中实现搜索过滤输入框)
reka-uiradix-vueDropdownMenuFilter 完整指南在下拉菜单中实现搜索过滤输入框【免费下载链接】radix-vueAn open-source UI component library for building high-quality, accessible design systems and web apps for Vue. Previously Radix Vue项目地址: https://gitcode.com/GitHub_Trending/ra/radix-vueDropdownMenuFilter是 radix-vue现 reka-ui下拉菜单DropdownMenu组件族中专门用于菜单内搜索过滤的输入框部件把它放进DropdownMenuContent后用户可以边输入边筛选菜单项同时键盘导航、焦点管理和 IME 输入法组合处理都由组件接管。本文基于官方 API 元数据 DropdownMenuFilter.md 与仓库源码 DropdownMenuFilter.vue完整给出该部件的 Props/Events/Slots 参考、可直接运行的用法示例以及从源码层面解析它的搜索同步、键盘交互、无障碍属性和中文输入法兼容实现读完即可在自己的 Vue 项目里落地一个可用的可搜索下拉菜单。组件定位它是 DropdownMenu 的一个内置搜索框DropdownMenuFilter与其他部件一起从核心包统一导出见 index.ts 中的DropdownMenuFilter、DropdownMenuFilterEmits、DropdownMenuFilterProps导出项。它的设计定位是渲染在DropdownMenuContent内部、通常位于所有DropdownMenuItem之前组件本身不直接负责过滤逻辑它只负责维护当前搜索文本modelValue并同步给菜单内容上下文MenuContent的searchRef实际的条目过滤由开发者根据这个受控值决定例如用v-if/computed过滤列表接管菜单内容区的键盘行为当焦点在过滤框上时方向键/Enter/Esc 的语义被重新解释为在过滤后的菜单项中导航/选中/清空。它属于 DropdownMenu 文档 所描述的组件族的一部分遵循 Menu Button 的 WAI-ARIA 设计模式并使用 roving tabindex 管理焦点。API 参考完整继承自官方元数据以下为 DropdownMenuFilter.md 中定义的完整 API 表。PropsNameDescriptionTypeRequiredDefaultasThe element or component this component should render as. Can be overwritten by asChild.AsTag \| ComponentNoinputasChildChange the default rendered element for the one passed as a child, merging their props and behavior.Composition 组合模式booleanNo-autoFocusFocus on element when mounted.booleanNo-disabledWhen true, prevents the user from interacting with itembooleanNo-modelValueThe controlled value of the filter. Can be binded with v-model.stringNo-对应源码中的 TypeScript 接口定义DropdownMenuFilter.vue L11-L28export interface DropdownMenuFilterProps extends PrimitiveProps { /** The controlled value of the filter. Can be binded with v-model. */ modelValue?: string /** Focus on element when mounted. */ autoFocus?: boolean /** When true, prevents the user from interacting with item */ disabled?: boolean }几个结合源码可以补充的默认行为as默认值是inputwithDefaults(..., { as: input })L26-L28即默认渲染原生input且模板中写死了typetext与rolesearchboxmodelValue使用useVModel来自vueuse/core实现defaultValue为空字符串未绑定v-model时自动进入非受控的被动模式L38-L41disabled为true时输入元素上会同时写入disabled、data-disabled和aria-disabled三个属性L125-L127便于 CSS 选择器与辅助技术双重识别handleInput与handleKeyDown开头都有if (disabled.value) return的短路测试用例 with disabled filter 也验证了disabled与data-disabled的渲染DropdownMenuFilter.test.ts L124-L142。EventsNameDescriptionTypeupdate:modelValueEvent handler called when the value changes.[string]update:modelValue在以下时机触发用户键入input事件、IME 组合结束compositionend、以及按下 Esc 清空时。源码声明见 L20-L22。SlotsNameDescriptionTypemodelValueCurrent input valuesstring \| undefineddefineSlots的签名L31-L36表明默认插槽接收一个包含modelValue的对象参数可用于在插槽内回读当前过滤值DropdownMenuFilter v-modelfilterText template #default{ modelValue } !-- 在作用域插槽中可以拿到实时过滤文本 -- /template /DropdownMenuFilter基础用法可过滤的下拉菜单仓库 Storybook 示例 DropdownMenuFilter.story.vue 展示了最典型的接入方式一个根菜单加一个带独立过滤文本的子菜单所有条目用v-ifmatches(...)按过滤值显隐script setup langts import { ref } from vue import { DropdownMenuContent, DropdownMenuFilter, DropdownMenuItem, DropdownMenuPortal, DropdownMenuRoot, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from reka-ui const toggleState ref(false) const filterText ref() const subFilterText ref() function matches(text: string, filter: string) { return !filter || text.toLowerCase().includes(filter.toLowerCase()) } /script template DropdownMenuRoot v-model:opentoggleState DropdownMenuTrigger aria-labelOpen menuOpen/DropdownMenuTrigger DropdownMenuPortal DropdownMenuContent :side-offset4 !-- 过滤输入框v-model 绑定过滤文本auto-focus 打开后聚焦 -- DropdownMenuFilter v-modelfilterText placeholderFilter items... auto-focus / DropdownMenuItem v-ifmatches(New Tab, filterText) valueNew Tab New Tab /DropdownMenuItem DropdownMenuSub v-ifmatches(More Tools, filterText) DropdownMenuSubTrigger valuemore toolsMore Tools →/DropdownMenuSubTrigger DropdownMenuPortal DropdownMenuSubContent !-- 子菜单可再放一个独立的 Filter -- DropdownMenuFilter v-modelsubFilterText auto-focus / DropdownMenuItem v-ifmatches(Save Page As…, subFilterText) Save Page As… /DropdownMenuItem /DropdownMenuSubContent /DropdownMenuPortal /DropdownMenuSub DropdownMenuItem v-ifmatches(New Window, filterText) valueNew Window New Window /DropdownMenuItem /DropdownMenuContent /DropdownMenuPortal /DropdownMenuRoot /template与之等价的测试夹具 _DropdownMenuWithFilter.vue 则演示了计算属性过滤的写法script setup langts const filterText ref() const items [ { value: new-tab, label: New Tab }, { value: new-window, label: New Window }, { value: bookmarks, label: Show Bookmarks }, { value: history, label: Show History }, ] const filteredItems computed(() { if (!filterText.value) return items return items.filter(item item.label.toLowerCase().includes(filterText.value.toLowerCase()), ) }) /script template DropdownMenuContent DropdownMenuFilter v-modelfilterText placeholderFilter items... / DropdownMenuItem v-foritem in filteredItems :keyitem.value {{ item.label }} /DropdownMenuItem /DropdownMenuContent /template两种写法逐条v-if或用computed生成过滤后的数组都能工作测试用例 should filter menu items based on input 验证了初始渲染 4 个[rolemenuitem]输入New后只剩New Tab与New Window两项DropdownMenuFilter.test.ts L51-L67。源码解析搜索文本如何同步进菜单系统DropdownMenuFilter的核心价值在于它通过 inject 拿到了三层上下文并参与菜单的搜索状态机L43-L48const rootContext injectMenuRootContext() const contentContext injectMenuContentContext() const subContext injectMenuSubContext(null) // Keep searchRef in sync with modelValue changes watch(modelValue, (v) { contentContext.searchRef.value v ?? }, { immediate: true })1.searchRef菜单级搜索缓冲searchRef定义在菜单内容实现 MenuContentImpl.vue L129并通过provideMenuContentContext提供给所有子部件L301-L340。它的两个作用抑制正在输入时的误触MenuItem的 keydown 处理中会检查isTypingAhead contentContext.searchRef.value ! 当搜索框非空时忽略空格选中避免用户输入的空格意外激活高亮项MenuItem.vue L69-L83失焦清空菜单内容区 blur 且焦点离开内容时handleBlur会清掉searchRefMenuContentImpl.vue L265-L272防止上一轮的搜索词残留影响下一次打开菜单。DropdownMenuFilter侧则保证三个方向的同步watch(modelValue)把外部对受控值的修改写回searchRefhandleInput中把用户输入同时写入modelValue与searchRefL82-L90。2. 注册 filter 元素鼠标进入菜单即聚焦过滤框挂载时组件把自身元素注册进内容上下文的filterElementL56-L57onMounted(() { contentContext.onFilterElementChange(currentElement.value) ... }) onUnmounted(() { contentContext.onFilterElementChange(undefined) // Clean up search when unmounting contentContext.searchRef.value })MenuContentImpl的handlePointerEnter利用这个引用鼠标进入菜单内容区时直接filterElement.value.focus()L292-L299。也就是说只要菜单里存在过滤框指针一旦进入菜单就会把焦点交给它——这正是可搜索菜单与普通菜单在交互上的分水岭。3.autoFocus的时机细节autoFocus的聚焦被包裹在setTimeout(..., 1)中源码注释解释为make sure all DOM was flush then only capture the focusL58-L65。另一个值得注意的分支const isSubmenu !!subContext if (!isSubmenu || rootContext.isUsingKeyboardRef.value) currentElement.value?.focus()从源码结构看子菜单中的过滤框只有在键盘操作isUsingKeyboardRef时才自动抢焦点鼠标打开子菜单则不会——这避免了鼠标用户被强制夺焦。Story 示例里主菜单与子菜单的过滤框都显式使用了auto-focus。键盘交互方向键、Enter 与 Esc 的特殊语义handleKeyDownL92-L116重新解释了过滤框上的按键按键行为源码依据ArrowDown/ArrowUp/Home/EndpreventDefault后转交内容上下文的onKeydownNavigation在菜单项之间移动高亮而非移动焦点L102-L105EnterpreventDefault后调用onKeydownEnter点击当前高亮项L106-L109Escape仅当过滤文本非空仅清空过滤框并stopPropagation不会关闭菜单L110-L115Esc 的这条设计在测试中有明确断言输入test后按 EscfilterText变回空串而菜单仍然打开should handle Escape key to clear filter when not emptyDropdownMenuFilter.test.ts L94-L108。只有过滤框为空时的 Esc 才会继续冒泡给DismissableLayer走关闭菜单并把焦点还给 Trigger的标准菜单行为。导航动作本身发生在 MenuContentImpl.vue 的onKeydownNavigation基于useArrowNavigation在[data-reka-collection-item]:not([data-disabled])中选择器范围内垂直移动高亮元素并scrollIntoView({ block: nearest })保证高亮项可见。测试验证了 ArrowDown 高亮第一项、ArrowUp 高亮最后一项L69-L92。无障碍属性searchbox 与 aria-activedescendant模板L119-L139渲染的属性如下Primitive :asas :as-childasChild :valuemodelValue :disableddisabled ? : undefined :data-disableddisabled ? : undefined :aria-disableddisabled ? true : undefined :aria-activedescendantactivedescendant typetext rolesearchbox inputhandleInput keydownhandleKeyDown compositionstarthandleCompositionStart compositionupdatehandleCompositionUpdate compositionendhandleCompositionEnd rolesearchboxtypetext向辅助技术声明这是一个搜索框测试断言了两个属性should have correct type and role attributesL36-L40aria-activedescendant由于焦点始终停留在输入框上、仅靠高亮表示当前项组件用watchSyncEffect把高亮项的id实时写进该属性L53-L54。测试 should sync aria-activedescendant with highlighted item 断言了按 ArrowDown 后aria-activedescendant恰好等于第一项的idL110-L121。这是 roving tabindex 菜单中焦点不移动、activedescendant 移动模式的典型实现。输入法IME组合输入处理这是该组件最容易踩坑、也最能体现实现深度的部分。中文/日文/韩文输入法在compositionend之前产生的是中间态拼音或假名若此时实时过滤会出现先按拼音筛、再按汉字筛的闪烁与错误结果。组件用共享工具 useComposing.ts 解决const { isComposing, shouldDeferInput, handleCompositionStart, handleCompositionUpdate, handleCompositionEnd } useComposing((event) { const el event.target as HTMLInputElement if (el) { modelValue.value el.value contentContext.searchRef.value el.value } })判定逻辑useComposing.ts L1-L59用 Unicode 脚本正则[\p{ScriptHan}\p{ScriptHiragana}\p{ScriptKatakana}\p{ScriptHangul}\p{ScriptBopomofo}]判断compositionupdate的event.data是否属于 CJK 类文字shouldDeferInput isComposing isImeComposition只有真正的 IME 组合才把input事件推迟到compositionendAndroid 例外Android 软键盘Gboard 等会把普通英文词保持为组合中做自动纠错如果一律推迟实时过滤会冻结到单词提交。因此纯英文的 Android 组合被识别为非 IME 组合允许实时更新源码注释引用了 nuxt/ui#6717一旦会话中出现过 IME 文字sawImeScript粘性置位后续纯文本更新不能把会话降级回实时更新——这是为了覆盖罗马字模式日语ka → か、越南语 Telex、泰语转写等先出拉丁字母的场景源码注释明确记录了这一已知限制。测试文件专门有一组 handle IME composition 用例覆盖这些分支DropdownMenuFilter.test.ts L144-L270组合中不更新搜索输入拼音xiang期间条目数不变compositionend后以提交值zzzzz过滤0 条命中桌面拼音预编辑compositionupdate携带拉丁文本保持推迟Android UA 下自动纠错的纯英文组合实时更新而假名かんじ组合直到compositionend才生效组合期间的方向键只做 IME 候选项导航菜单必须既不移动高亮也不抢走焦点event.stopPropagation实现见 handleKeyDown L95-L101组合结束后导航恢复正常。与 Typeahead 的协同菜单内容实现还内置了 typeahead 能力MenuContentImpl.vue L244-L251在非文本输入框上快速敲击字符时按 useTypeahead.ts 的getNextMatch前缀匹配算法在条目间跳转搜索缓冲 1 秒后自动重置refAutoReset(, 1000)L6。handleKeyDown中的守卫if (!isModifierKey isCharacterKey !isKeyDownInTextField)保证当焦点在DropdownMenuFilter这个input上时字符按键不会触发 typeahead只会更新过滤文本typeahead 仅在焦点位于菜单内容区本体时生效。两者互补——过滤框管精确输入筛选typeahead 管焦点不在输入框时的快捷跳转。测试矩阵速览DropdownMenuFilter.test.ts 共覆盖了以下可回归行为可用作自研封装的验收清单场景断言打开菜单渲染rolesearchbox输入框且typetext输入文本v-model同步更新filterText New过滤4 项 → 输入New后剩 2 项ArrowDown / ArrowUp高亮第一/最后一项data-highlightedEsc非空时清空过滤文本菜单不关闭高亮同步aria-activedescendant等于高亮项iddisabled渲染disabled与data-disabled属性IME 组合组合中不更新、组合结束后更新、Android 分支、组合中方向键不导航实践要点小结过滤逻辑在消费方DropdownMenuFilter只维护搜索文本条目显隐用v-ifmatches(...)或computed过滤数组实现两种写法见 Story 示例 与测试夹具打开即聚焦交互体验好的菜单应给过滤框加auto-focus配合鼠标进入菜单即聚焦过滤框的内建行为键盘与指针路径都从搜索开始Esc 双语义非空时 Esc 清空搜索空时 Esc 关闭菜单——这与普通菜单Esc 关闭不同交互文档中值得向用户说明受控/非受控皆可v-model绑定即受控不传modelValue时useVModel以空串为默认值进入非受控模式无障碍已内置rolesearchbox、aria-activedescendant、IME 期间屏蔽导航都是组件默认行为封装时无需重复实现子菜单可再放 Filter主菜单与子菜单各自独立的过滤文本filterText/subFilterText互不干扰且子菜单过滤框仅在键盘操作时自动聚焦。关键源码入口实现 DropdownMenuFilter.vue、菜单内容上下文与搜索缓冲 MenuContentImpl.vue、IME 组合工具 useComposing.ts、typeahead 匹配 useTypeahead.ts、完整测试 DropdownMenuFilter.test.ts。【免费下载链接】radix-vueAn open-source UI component library for building high-quality, accessible design systems and web apps for Vue. Previously Radix Vue项目地址: https://gitcode.com/GitHub_Trending/ra/radix-vue创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考