# Agent Note: 轨道搜索在展开点击到达 document 时保持展开

Status: implemented

[English](2026-08-18-rail-search-outside-click-self-dismissal.md) | 中文

## 问题

收起侧边栏的轨道搜索按钮会置位轨道手势标志（`searchOnExpand`）、展开搜索控件（`searchExpanded`）并请求侧边栏展开——设计意图是列滑开后让用户直接落在已聚焦的搜索输入框里。但在真实浏览器中这个手势从未完成：侧边栏展开了，搜索框却保持关闭且未聚焦。

发起手势的那次点击摧毁了它自己的效果。React 在冒泡中途派发轨道按钮的处理器；状态翻转渲染出宽态头部，并在同一次派发期间把 WorkspaceBrowser 的"点击外部收起搜索"监听器挂到 `document` 上。随后这次点击继续冒泡到达 `document`，其 target 是已卸载的轨道按钮——位于 `searchRoot` 之外——于是刚挂上的监听器立刻收起了它正要打开的搜索。包级测试没有抓到这个问题，因为 `fireEvent.click` 在按钮上触发时，不会像真实浏览器事件那样继续冒泡穿过派发期间新挂载的监听器。

## 决策

轨道手势进行期间不挂载"点击外部收起"监听器：其 effect 在 `searchOnExpand` 置位期间提前返回，而 `searchOnExpand` 本就精确终止于手势落定之时（列滑动结束、焦点落入输入框）。落定之后，外部点击照旧收起搜索。一个回归测试重放真实浏览器的顺序——轨道点击、宽态翻转、同一次点击到达 `document`——要求搜索在此过程中保持展开，并在下一次真正的外部点击时收起。

## 备选方案

**在轨道按钮的点击上阻止冒泡。** 在发起方抑制冒泡会让轨道按钮耦合到一个它看不见的监听器，而且其他每条展开路径——未来的键盘快捷键、另一个轨道入口——都会重新引入此缺陷。收起由监听器负责，守卫就应由监听器承载。

**将监听器挂载延迟一帧或一个定时器。** 裸延迟编码的是症状（点击来得"太早"）而非成因（手势正在进行）。`searchOnExpand` 已经是带有正确终点的显式进行中状态；帧边界两者都不是。

**改在 `pointerdown` 上收起而非 `click`。** 发起手势的 `pointerdown` 先于监听器挂载，因而不会自我收起。被否决是因为它改变了所有交互的收起语义——拖拽或按下后滑走会触发收起，而完成的点击不会——只为修复一个局限于单个手势的问题。

## 影响

轨道搜索手势在组装后的应用中端到端可用，由 `apps/web` 的真实浏览器场景钉住：真实点击穿过收起轨道、宽态翻转与 document 级冒泡，搜索保持展开且焦点落入输入框。在手势进行窗口内（约 300 ms 列滑动）外部点击不会收起搜索；该窗口在焦点落定的瞬间结束。包级回归测试另外钉住了单元层面的守卫时序。
