Tauri (22)——让 `Esc` 快捷键一层层退出的分层关闭方案

背景:为什么 Esc 会变得"不可控"

在 Coco 里,Esc 同时承担了很多"退出/关闭"的职责:

  • 退出输入(Input/Textarea)编辑态
  • 关闭弹层(Popover / 菜单)
  • 关闭历史面板(History Panel)
  • 最后隐藏窗口(Tauri hideWindow)

问题在于 :这些层级的 UI 往往同时存在。如果不做事件隔离,Esc 可能 "一键关闭所有",或者被某一层吞掉导致 "该关的不关"。

本次改动的核心,就是把 Esc 做成可预期的优先级链路 ,并通过 stopPropagation + DOM 状态判定让各层各司其职。


设计目标:Esc 的层级优先级(从近到远)

我们把 Esc 定义为 "从用户当前操作点开始逐层退出",优先级如下:

  1. 如果正在输入:先 blur(退出输入态),不做其它关闭
  2. 如果在 Popover 里 :关闭当前 Popover(但第一下 Esc 若在输入框里,仍先 blur)
  3. 如果上下文菜单打开:关闭上下文菜单
  4. 如果 History Panel 打开:关闭 History Panel
  5. 如果 Extension View 打开:不隐藏窗口(交给 Extension 自己处理或保持现状)
  6. 否则 :隐藏窗口(hideWindow())

这条链路的关键点是:"内层 UI 必须能拦截并消费 Esc,避免冒泡到全局导致直接 hideWindow"。


全局 Esc:统一入口与"最后兜底"

全局 Esc 由 LayoutOutlet 初始化(src/routes/outlet.tsx 调用 useEscape()),核心逻辑在 src/hooks/useEscape.ts:

  • 先做 event.preventDefault() + event.stopPropagation()(src/hooks/useEscape.ts)
  • 再按优先级处理:
    • 输入 blur(src/hooks/useEscape.ts)
    • 关闭右键菜单(src/hooks/useEscape.ts)
    • 关闭 History Panel(src/hooks/useEscape.ts)
    • Extension 打开时直接 return,避免误隐藏(src/hooks/useEscape.ts)
    • 最后隐藏窗口(src/hooks/useEscape.ts)

这里有个重要的小优化:回调里用 useSearchStore.getState() 取最新状态(src/hooks/useEscape.ts),避免快捷键回调捕获旧状态导致 "按了没反应"。


Popover 的 Esc:把"关闭弹层"从全局剥离出来

真正决定 "Esc 是否一层层退出" 的关键,是 Popover 对 Esc 的消费。

在 Radix Popover 中,PopoverContent 提供了 onEscapeKeyDown。本次在组件封装层统一加入:

  • 文件:src/components/ui/popover.tsx
  • 逻辑:src/components/ui/popover.tsx

行为是:

  1. stopPropagation + preventDefault(避免 Esc 冒泡到全局 useEscape,也避免浏览器默认行为)
  2. 如果焦点在输入框/文本域:只 blur(src/components/ui/popover.tsx)
    • 这保证了"第一下 Esc 退出输入",而不是直接关掉 popover
  3. 否则:找到当前打开的 popover trigger 并 click(),从而走 Radix 的正常关闭流程(src/components/ui/popover.tsx)

为了找到 "当前打开的 trigger",新增了选择器常量:

  • OPENED_POPOVER_TRIGGER_SELECTOR:src/constants/index.ts

为什么要改选择器:从"自定义标记"到 Radix 的真实 DOM

这次对 Popover 的识别方式也做了统一调整:

  • POPOVER_PANEL_SELECTOR 改为 Radix wrapper:src/constants/index.ts
    • 变成 "[data-radix-popper-content-wrapper]",用于更可靠地判断"现在是否存在 popover"
  • HISTORY_PANEL_ID / CONTEXT_MENU_PANEL_ID 从 headlessui-... 命名迁移到 popover-panel:...(src/constants/index.ts)
    • 配合当前实际渲染结构,避免 ID 不一致导致关闭逻辑失效

对应的 History 关闭动作走的是点击 trigger:

  • closeHistoryPanel():src/utils/index.ts
  • 它通过 [aria-controls="${HISTORY_PANEL_ID}"] 找到按钮并点击(src/utils/index.ts)
  • useEscape 用 document.getElementById(HISTORY_PANEL_ID) 判断历史面板是否存在(src/hooks/useEscape.ts)

VisibleKey 与 "在 Popover 里显示快捷键提示"

VisibleKey 需要知道 "当前快捷键提示是否应该显示",尤其是在 popover 打开时,只在 popover 内的元素上显示。

它通过:

  • POPOVER_PANEL_SELECTOR 获取 popover 面板 wrapper(src/components/Common/VisibleKey.tsx)
  • OPENED_POPOVER_TRIGGER_SELECTOR 获取打开的 trigger(src/components/Common/VisibleKey.tsx)
  • 判断当前组件是否在 panel 或 trigger 内(src/components/Common/VisibleKey.tsx)

这样一来,"打开 popover 后,快捷键提示只在当前 popover 的交互区域出现",不会污染外层 UI。


输入框组件收敛:删除 PopoverInput,统一用 shadcn Input

src/components/Common/PopoverInput.tsx 被删除,相关位置改为直接使用 src/components/ui/input:

  • SearchPopover:src/components/Search/SearchPopover.tsx
  • MCPPopover:src/components/Search/MCPPopover.tsx
  • AssistantList:src/components/Assistant/AssistantList.tsx

同时统一加了 autoCorrect="off",减少输入联想带来的干扰(例如英文关键字/ID 搜索场景)。


小结

本次改动将 Esc 从"不可控的一键退出"重构为有明确优先级的逐层退出机制:

输入态 → Popover → 菜单 / History → Extension → 窗口隐藏

核心做法是:

  • 全局 useEscape 只作为最后兜底,按优先级处理关闭逻辑
  • Popover 内部消费 Esc(优先 blur 输入,其次关闭弹层),防止事件冒泡到全局
  • 基于 Radix 实际 DOM 统一判断 popover / history 是否打开
  • VisibleKey 只在当前 popover 交互区域内显示
  • 统一输入组件,减少干扰

结果是:Esc 行为稳定、可预期,不再误关、不再漏关。

开源

如果你对我们的技术细节感兴趣,或者想体验一下这款全能的生产力工具,欢迎访问我们的开源仓库和官网:

相关推荐
jktantan3 小时前
Vue keep-alive 切标签后滚动位置回顶?因为 onDeactivated 里读到的 scrollTop 是 0
前端·javascript·vue.js
2601_962885724 小时前
quotes 字段全解:涨跌幅、振幅、换手率的口径与单位
java·前端·spring
excel5 小时前
encodeURI 和 encodeURIComponent 有什么区别?
前端
前端精髓5 小时前
调用openrouter的免费API模型
java·前端·javascript
赛博守夜人5 小时前
AI安全攻防与治理之十四:AI 安全测试不能脱离传统安全,Web、API、移动端与模型攻击面的组合拳
前端·人工智能·安全
开开心心_Every5 小时前
快速隐藏程序工具自定义快捷键一键切换
java·开发语言·前端·spring·智能手机·word·intellij-idea
networknomad6 小时前
OpenReplay:自己搭的会话回放工具,给前端排错用的
前端·其他
缓慢更新7 小时前
AI 辅助生成单测:从接口定义到用例的流水线
java·服务器·前端·网络·数据库·人工智能
陈随易7 小时前
AI 运维服务器,安全又可靠的方法
前端·后端·程序员
风骏时光牛马8 小时前
AI稳定性治理体系构建与疑难问题深度分析
前端