fix(candidate-interaction): unify active candidate navigation for pointer and keyboard interactions across candidate lists

This commit is contained in:
chinboy
2026-08-19 13:43:38 +08:00
parent 79319f93fc
commit f1c19298ca
27 changed files with 933 additions and 126 deletions
@@ -2,6 +2,16 @@
本文档记录 AutoComplete 控件级设计、API、主题契约、Token 和实现结构的变化。它不替代仓库根目录 `CHANGELOG.md`,也不作为正式版本发布说明。
## 2026-08-19
- Behavior
- Unify pointer and keyboard candidate navigation through the shared `CandidateList` active candidate owner.
- Keep pointer movement non-committing and non-scrolling while ensuring `Enter` commits the single visual active candidate.
- Theme
- Stop inherited `:pointerover` styling from rendering a second candidate highlight; committed selection remains visually dominant.
- Tests
- Add popup integration coverage for pointer migration followed by `Enter` commit.
## 2026-06-26
- Docs
@@ -1,6 +1,6 @@
# AutoComplete 桌面版实现原理
本文档描述 AutoComplete 桌面版的内部实现范围、源码职责、状态流、生命周期、资源边界和维护规则。公共设计与 API 契约见 [AutoComplete 桌面版架构设计](overview.md),变化记录见 [AutoComplete Changelog](changelog.md)。涉及控件 Token 的实现应同时阅读 [AutoComplete Token 设计](token.md)。
本文档描述 AutoComplete 桌面版的内部实现范围、源码职责、状态流、生命周期、资源边界和维护规则。公共设计与 API 契约见 [AutoComplete 桌面版架构设计](overview.md),候选列表状态契约见 [候选列表统一交互设计](../select/candidate-interaction-design.md),变化记录见 [AutoComplete Changelog](changelog.md)。涉及控件 Token 的实现应同时阅读 [AutoComplete Token 设计](token.md)。
## 1. 实现定位
@@ -126,6 +126,8 @@ AutoComplete 的交互事件应从输入源收敛到控件级语义事件:
- ItemsSource、selection、checked、expanded、filter、paging 或 upload task 的集合同步。
- 动效启停、初始加载阶段 transition 抑制和卸载取消。
候选交互必须遵循统一 active candidate 流:`CandidateList` 保存唯一候选 owner,鼠标命中可用项时迁移该状态但不滚动,键盘导航复用同一写入路径并可滚动到可见位置;容器回收、过滤或 source 重建时只恢复 owner 状态,不把 `:pointerover` 当作独立候选来源。`Enter` 从 active candidate 读取提交目标,不能从旧的 `SelectedItem` 或指针命中状态重新推导。
实现文档不逐行解释私有方法。若某个私有算法成为稳定维护入口,应在本节补充算法不变量,而不是把代码复述为说明书。
## 8. 资源、性能与 AOT 边界
@@ -1,6 +1,6 @@
# AutoComplete 桌面版架构设计
本文档定义 `AutoComplete` 桌面版的最新设计定位、公共契约、状态模型、视觉主题关系和兼容边界。通用控件研发约束见 [控件研发标准](../../../../engineering/development/control-development-guidelines.md),内部实现原理见 [AutoComplete 桌面版实现原理](implementation.md),AutoComplete Token 的专项设计见 [AutoComplete Token 设计](token.md),设计和契约变化记录见 [AutoComplete Changelog](changelog.md)。
本文档定义 `AutoComplete` 桌面版的最新设计定位、公共契约、状态模型、视觉主题关系和兼容边界。通用控件研发约束见 [控件研发标准](../../../../engineering/development/control-development-guidelines.md),候选列表统一交互见 [候选列表统一交互设计](../select/candidate-interaction-design.md),内部实现原理见 [AutoComplete 桌面版实现原理](implementation.md),AutoComplete Token 的专项设计见 [AutoComplete Token 设计](token.md),设计和契约变化记录见 [AutoComplete Changelog](changelog.md)。
## 1. 控件定位
@@ -140,7 +140,7 @@ AutoComplete 与同分类控件共享尺寸、状态、Token、Gallery 展示和
### 8.1 选择与当前项模型
AutoComplete 的当前项状态必须由单一 owner 推导。public 选择属性、集合项容器和伪类之间只能做单向同步,集合替换、清空和模板重套用时必须回放当前状态。
AutoComplete 的候选当前项由弹层中的 `CandidateList` 作为单一 active candidate owner。鼠标移动和 `Up` / `Down` 都只迁移这个候选,不提前改变输入值;`Enter` 使用同一个 active candidate 提交,提交后才更新 `Value` / `SelectionChanged` 并按控件契约关闭弹层。候选项容器只投影 active、selected 和 disabled 状态,`:pointerover` 不再形成第二个候选高亮。过滤结果、异步加载、集合替换、清空和模板重套用时,失效的 active candidate 必须被清除或重新从当前视图解析。
### 8.2 弹层与宿主模型
@@ -3,6 +3,17 @@
本文档记录 Cascader 控件级设计、API、主题契约、Token 和实现结构的变化。
它不替代仓库根目录 CHANGELOG.md,也不作为正式版本发布说明。
## 2026-08-19
- Behavior
- Make pointer movement and keyboard navigation share one active candidate in both ordinary tree columns and filtered path results.
- Preserve hover-triggered expansion while excluding disabled, hidden, and loading nodes from active candidate ownership.
- Make `Enter` commit the filtered or tree candidate represented by the single active visual state.
- Theme
- Remove independent pointer-over candidate highlighting while preserving expanded and selected visual precedence.
- Tests
- Add mixed pointer/keyboard and pointer-to-`Enter` coverage for tree and filtered modes.
## 2026-07-05
- API
@@ -1,6 +1,6 @@
# Cascader 桌面版实现原理
本文档描述 Cascader 桌面版的输入壳体、选项集合、级联展开、选择同步、勾选、过滤、异步加载、绑定型选项和资源生命周期。公共设计与 API 契约见 [Cascader 桌面版架构设计](overview.md),Token 语义见 [Cascader Token 设计](token.md),变化记录见 [Cascader Changelog](changelog.md)。
本文档描述 Cascader 桌面版的输入壳体、选项集合、级联展开、选择同步、勾选、过滤、异步加载、绑定型选项和资源生命周期。公共设计与 API 契约见 [Cascader 桌面版架构设计](overview.md),候选列表状态契约见 [候选列表统一交互设计](../select/candidate-interaction-design.md),Token 语义见 [Cascader Token 设计](token.md),变化记录见 [Cascader Changelog](changelog.md)。
## 1. 实现定位
@@ -149,7 +149,7 @@ container clear / detach -> dispose attachment and subscriptions
`CascaderView.OnApplyTemplate()` 获取 `PART_ItemsPanel`、`PART_RootLevelList` 和 `PART_FilterList`。Root level list 设置 `Level=1` 和 `OwnerView=this`,filter list 必须解除旧 selection 订阅再订阅新实例。
`CascaderView` 分别持有树形 `_keyboardCandidateItem` 与 `CascaderViewFilterList` 的过滤候选。过滤结果重建或清空、filter list 重套模板、以及 pointer 在过滤列表内移动时清除过滤候选;单选 popup 关闭时 `ResetInteractionState()` 清除两类候选并折叠展开路径。虚拟化树容器回收时必须清除 `IsCandidateSelectedProperty`,不能让候选视觉进入下一个 option 的复用容器。
`CascaderView` 分别持有树列和过滤列的 active candidate owner。普通树列的 pointer move 先把 enabled item 提升为 active candidate,再按 `ExpandTrigger` 执行展开;过滤列 pointer move 同样迁移过滤候选,不再无条件清除。过滤结果重建或清空、filter list 重套模板、popup 关闭时 `ResetInteractionState()` 清除两类候选并折叠展开路径。虚拟化树容器回收时必须清除 `IsCandidateSelectedProperty`,不能让候选视觉进入下一个 option 的复用容器。
`CascaderViewLevelList.ContainerForItemPreparedOverride()` 对每个 `ICascaderOption` 调用 `CascaderViewItem.PrepareCascaderOptionData()`。如果 option 是 `BindableCascaderOption`,容器创建同一生命周期的 `CompositeDisposable`,用于保存 resource host attachment、属性订阅和 children 集合订阅。
@@ -167,6 +167,7 @@ Pointer 路径:
- popup 内 pointer 事件不触发外层关闭。
- tag close button 点击不触发外层 popup toggle。
- `CascaderViewLevelList` 点击 option 时先触发 item click,再按 `ExpandTrigger` 决定是否展开。
- `CascaderViewLevelList` / `CascaderViewFilterList` 的 pointer move 只负责把命中的 enabled item 迁移为 active candidate;普通列的鼠标路径不滚动,过滤列不因 pointer move 无条件清除候选。
键盘路径:
@@ -1,6 +1,6 @@
# Cascader 桌面版架构设计
本文档定义 `AtomUI.Desktop.Controls.Cascader` 的最新设计定位、公共契约、状态模型、视觉主题关系和兼容边界。通用控件研发约束见 [控件研发标准](../../../../engineering/development/control-development-guidelines.md),内部实现原理见 [Cascader 桌面版实现原理](implementation.md),Cascader Token 的专项设计见 [Cascader Token 设计](token.md),设计和契约变化记录见 [Cascader Changelog](changelog.md)。
本文档定义 `AtomUI.Desktop.Controls.Cascader` 的最新设计定位、公共契约、状态模型、视觉主题关系和兼容边界。通用控件研发约束见 [控件研发标准](../../../../engineering/development/control-development-guidelines.md),候选列表统一交互见 [候选列表统一交互设计](../select/candidate-interaction-design.md),内部实现原理见 [Cascader 桌面版实现原理](implementation.md),Cascader Token 的专项设计见 [Cascader Token 设计](token.md),设计和契约变化记录见 [Cascader Changelog](changelog.md)。
## 1. 控件定位
@@ -181,11 +181,13 @@ input display + Form value
- `FilterValue` 非空且控件 loaded 时,CascaderView 收集所有叶子路径并按 `Filter` 过滤。
- 过滤结果显示完整路径文本,选中过滤结果后回写目标 option。
- 过滤模式下,`Up` / `Down` 在可用结果间循环移动内部候选高亮,不修改 `SelectedOption`;`Enter` 提交当前候选,尚无候选时提交第一个可用结果,没有可用结果时保持选择和 popup 状态不变。路径中任一祖先 disabled 时,该过滤结果也不可作为候选或提交。
- 过滤列表拥有独立于树列的 active candidate owner;过滤结果重建、过滤清空、popup 关闭和容器回收时清除旧候选。树列与过滤列不会同时保留两个候选视觉。
- 清空过滤值或关闭 popup 后,过滤列表、过滤计数和缓存路径会被清理。
树形键盘导航:
- popup 打开且未过滤时,`Up` / `Down` 在当前已展开列的可见 enabled item 间循环移动内部候选;候选高亮与真实选择相互独立。
- 普通树列的 active candidate 由 `CascaderView` 单一持有;鼠标移动到 enabled item 时迁移该候选并继续执行 `ExpandTrigger=Hover` 的展开逻辑,但不提前提交选择、不滚动列表。`Enter` 使用同一 active candidate 作为选择或展开目标。
- `Right` 从当前候选或第一个可见 enabled item 开始,展开可展开节点并把候选移到下一列的第一个 enabled child。
- `Left` 优先把子级候选移回父级;候选已位于展开的根级非叶节点时折叠该节点。
- `Enter` 提交 enabled、非 loading 的叶子候选;`IsAllowSelectParent=true` 时也可提交父节点,否则沿用 `Right` 的展开并进入子级行为。
@@ -220,7 +222,7 @@ Cascader 的默认视觉由 Cascader 根主题、输入壳体、PopupHost、Casc
- `OptionTemplate` 的 DataContext 必须保持为 `ICascaderOption`,不能改为 header 文本。
- Popup 宽度和空状态宽度匹配语义必须保持:普通级联列使用列宽,空状态需要匹配输入宽度。
- 选项行的 checkbox、icon、header、expand / loading icon 间距由 Token 管理,不应在单个模板节点中写死。
- disabled、expanded、pointerover、checked 和 loading 状态 selector 不能被绕过。
- disabled、expanded、pointerover、checked、loading 和 active candidate 状态 selector 不能被绕过;`pointerover` 只触发 active candidate 迁移,不独立绘制第二个候选背景。
## 6. 控件家族或集成关系
@@ -8,6 +8,16 @@
- 不记录临时讨论、纯格式化或没有长期价值的实现细节。
- 架构文档始终描述最新设计状态;历史变化记录在本文档。
## 2026-08-19
- Behavior
- Unify pointer and keyboard candidate navigation through the shared `CandidateList` active candidate owner.
- Keep the current mention trigger context intact while `Enter` inserts the single visual active candidate.
- Theme
- Stop inherited `:pointerover` styling from rendering a second candidate highlight; committed selection remains visually dominant.
- Tests
- Add trigger-popup integration coverage for pointer migration followed by mention insertion.
## 2026-07-06
- API
@@ -1,6 +1,6 @@
# Mentions 桌面版实现原理
本文档描述 Mentions 桌面版的内部模板组合、触发符识别、候选弹层、同步/异步加载、过滤、候选插入、Form 和 Token 资源边界。公共设计与 API 契约见 [Mentions 桌面版架构设计](overview.md),Token 语义见 [Mentions Token 设计](token.md),变化记录见 [Mentions Changelog](changelog.md)。
本文档描述 Mentions 桌面版的内部模板组合、触发符识别、候选弹层、同步/异步加载、过滤、候选插入、Form 和 Token 资源边界。公共设计与 API 契约见 [Mentions 桌面版架构设计](overview.md),候选列表状态契约见 [候选列表统一交互设计](../select/candidate-interaction-design.md),Token 语义见 [Mentions Token 设计](token.md),变化记录见 [Mentions Changelog](changelog.md)。
## 1. 实现定位
@@ -27,7 +27,7 @@ Mentions 的实现以 `MentionTextArea` 为输入内核,`Popup` 和 `Candidate
`MentionTextArea` 是文本输入和 trigger 检测边界。它继承 `TextArea`,复用 TextArea 的多行输入、清除、状态、Form feedback 和尺寸能力,并增加 `TriggerPrefix`、`FilterValue`、`IsDropDownOpen`、`CandidateOpenRequest` 和 `CandidateCloseRequest`。
`CandidateList` 是候选选择边界。Mentions 只依赖 `ICandidateList` 的 `ItemsSource`、`SelectedItem`、`Commit`、`Cancel` 和 `HandleKeyDown()`,不直接管理候选项容器。
`CandidateList` 是候选选择边界和 active candidate owner。Mentions 只依赖 `ICandidateList` 的 `ItemsSource`、`SelectedItem`、`Commit`、`Cancel` 和 `HandleKeyDown()`,不直接管理候选项容器;鼠标候选迁移、键盘导航和 `Enter` 提交必须最终读取同一 active candidate。
`IMentionOptionsAsyncLoader` 是异步数据边界。控件只调用 `LoadAsync(context, token)`,不假设远程协议、缓存策略或错误显示方式。
@@ -122,6 +122,8 @@ Popup 打开时创建 `_subscriptionsOnOpen`,订阅自身 `IsVisible`、`IsEna
- 弹层打开时点击非文本区域会关闭弹层。
- 普通按下状态通过标准 `:pressed` 伪类表达。
候选项的 `:pointerover` 只表示指针命中事实,不单独绘制候选 active 背景。候选列表把鼠标移动到可用项转换为 active candidate 迁移;键盘路径复用同一 owner,且只有键盘路径负责滚动到可见区域。
候选提交:
```text
@@ -1,6 +1,6 @@
# Mentions 桌面版架构设计
本文档定义 `AtomUI.Desktop.Controls.Mentions` 的最新设计定位、公共契约、状态模型、视觉主题关系和兼容边界。通用控件研发约束见 [控件研发标准](../../../../engineering/development/control-development-guidelines.md),内部实现原理见 [Mentions 桌面版实现原理](implementation.md),Token 专项设计见 [Mentions Token 设计](token.md),设计和契约变化记录见 [Mentions Changelog](changelog.md)。
本文档定义 `AtomUI.Desktop.Controls.Mentions` 的最新设计定位、公共契约、状态模型、视觉主题关系和兼容边界。通用控件研发约束见 [控件研发标准](../../../../engineering/development/control-development-guidelines.md),候选列表统一交互见 [候选列表统一交互设计](../select/candidate-interaction-design.md),内部实现原理见 [Mentions 桌面版实现原理](implementation.md),Token 专项设计见 [Mentions Token 设计](token.md),设计和契约变化记录见 [Mentions Changelog](changelog.md)。
## 1. 控件定位
@@ -225,7 +225,7 @@ Mentions 属于 Data Entry 文本输入控件家族,与 LineEdit/TextArea 共
Mentions 将 `OptionsSource` 缓存为 `List<IMentionOption>`,再根据 `FilterValue` 构造当前候选视图。默认过滤器是 contains。`FilterValueSelector` 存在时使用 selector 返回值;否则按 `Header`、`Value`、`Key` 的顺序选择过滤文本。
过滤过程通过 `_filterInAction` 和 `_cancelRequested` 避免重入时生成过期视图。刷新完成后,候选列表自动选中第一项。
过滤过程通过 `_filterInAction` 和 `_cancelRequested` 避免重入时生成过期视图。刷新完成后,`CandidateList` 从当前有效视图建立 active candidate;鼠标移动和键盘导航共享同一候选状态,`Enter` 使用该状态插入 mention。过滤、异步结果替换或候选列表回收时,旧候选必须失效,不能由 pointer-over 视觉继续保留。
### 8.2 异步加载模型
@@ -1,10 +1,10 @@
# Select 候选交互设计
# 候选列表统一交互设计
本文档定义 Select 候选弹层中鼠标、键盘、已确认选择和虚拟化容器之间的统一候选交互模型。控件总体设计见 [Select 桌面版架构设计](overview.md),源码职责与生命周期见 [Select 桌面版实现原理](implementation.md)。
本文档定义 AtomUI 下拉候选列表家族中鼠标、键盘、已确认选择和虚拟化容器之间的统一候选交互模型。Select 是该契约的最早实现;AutoComplete、Mentions、ComboBox 和 Cascader 的候选列表复用同一状态语义,但由各自控件负责把 active candidate 映射到自己的提交和展开流程。Select 的总体设计见 [Select 桌面版架构设计](overview.md),源码职责与生命周期见 [Select 桌面版实现原理](implementation.md)。
## 1. 设计定位
Select 使用 active candidate 表达用户在候选弹层中的当前操作目标。active candidate 可以由鼠标移动或键盘导航产生,是 `Enter` 提交或切换选择的唯一候选目标。
候选列表使用 active candidate 表达用户在候选弹层中的当前操作目标。active candidate 可以由鼠标移动或键盘导航产生,是 `Enter` 提交、切换选择或继续展开的唯一候选目标。
该模型覆盖:
@@ -15,9 +15,23 @@ Select 使用 active candidate 表达用户在候选弹层中的当前操作目
- 候选项虚拟化、容器回收和重新准备。
- 候选弹层关闭、过滤上下文变化和选项源重建时的状态失效。
### 1.1 适用边界
该契约适用于同时满足以下条件的弹层内容:方向键移动的是尚未提交的候选目标,鼠标命中项能够成为同一次 `Enter` 操作的目标,并且候选高亮与已确认选择具有独立状态。当前纳入该契约的控件如下:
| 控件 | 候选 owner |
| --- | --- |
| Select | `SelectCandidateList` |
| AutoComplete、Mentions | 公共 `CandidateList` |
| ComboBox | ComboBox internal candidate index 与 item projection |
| Cascader 普通树列 | `CascaderView` 当前树候选 |
| Cascader 过滤结果 | `CascaderViewFilterList` 当前过滤候选 |
TreeSelect、DatePicker/Calendar 和 TimePicker 不机械套用本契约:TreeSelect 的 TreeView 方向键状态属于树选择与焦点契约;日期范围 hover、日历焦点和时间列 current value 还承担范围预览、日期焦点或时间值语义,不是独立的下拉列表 active candidate。它们仍需保证各自状态模型只有一个 owner,但不能通过关闭通用 hover 视觉来替代本身的选择、焦点和预览契约。
## 2. 设计原则
1. `SelectCandidateList` 是 active candidate 的唯一 owner;候选项容器只投影 owner 状态。
1. 每个候选列表实例只有一个 active candidate owner;候选项容器只投影 owner 状态。Select 使用 `SelectCandidateList`,AutoComplete 和 Mentions 使用公共 `CandidateList`,ComboBox 与 Cascader 在各自 view 中维护等价的 internal owner。
2. 鼠标和键盘共享同一个 active candidate,不维护相互独立的 hover candidate 和 keyboard candidate。
3. `:pointerover` 只表达 Avalonia 指针命中事实,不独立决定 Select 候选高亮。
4. active candidate 与已确认选择相互独立;候选迁移不能改变 `SelectedOption` 或 `SelectedOptions`。
@@ -33,14 +47,14 @@ Select 使用 active candidate 表达用户在候选弹层中的当前操作目
| 术语 | 定义 |
| --- | --- |
| Active candidate | 当前可以由 `Enter` 提交或切换选择的唯一候选项。 |
| Active candidate | 当前可以由 `Enter` 提交、切换选择或继续展开的唯一候选项。 |
| Committed selection | 已写入 `SelectedOption` 或 `SelectedOptions` 的确认选择。 |
| Pointer hit | Avalonia 通过 `IsPointerOver` / `:pointerover` 表达的当前指针命中事实。 |
| Source index | 候选项在有效候选源中的索引,是 `CandidateSelectedIndex` 的索引语义。 |
| View index | 过滤和分组后候选项在当前 ListView 投影中的位置。 |
| Candidate projection | 已准备容器上的 `IsCandidateSelected` 状态。 |
active candidate 使用以下 internal 状态表达:
active candidate 使用以下 internal 状态表达(具体控件可使用等价的私有字段或 internal 属性):
```text
CandidateSelectedIndex
@@ -61,8 +75,8 @@ CandidateSelectedItem
| 状态 | Owner | 作用 |
| --- | --- | --- |
| Active candidate | `SelectCandidateList` | 导航、提交目标和候选高亮。 |
| Committed selection | Select 公共选择属性与 ListView selection | 已确认值、selected 视觉和 Form 值。 |
| Active candidate | 候选列表 owner | 导航、提交/展开目标和候选高亮。 |
| Committed selection | 各控件公共选择属性与列表 selection | 已确认值、selected 视觉和 Form 值。 |
| Pointer hit | Avalonia input system | 产生鼠标候选迁移请求,不直接绘制候选视觉。 |
同一候选项可以同时是 active candidate 和 committed selection。此时 selected 视觉保持优先,active 状态仍作为 `Enter` 的交互目标存在。
@@ -88,20 +102,20 @@ CandidateSelectedItem
| 组件 | 职责 | 不负责 |
| --- | --- | --- |
| `Select` | 公共选择属性、候选源、过滤、模式和 popup 生命周期协调 | 不直接维护容器 active 视觉 |
| `SelectCandidateList` | active candidate owner、输入映射、索引转换、键盘滚动和提交/取消 | 不把 active 状态写入业务 option |
| `SelectCandidateListItem` | 投影 `IsCandidateSelected`、`IsSelected`、启用和分组状态 | 不持有跨回收周期的候选状态 |
| `CandidateVirtualizingStackPanel` | 键盘候选滚动到可见区域 | 不决定 active candidate |
| `SelectCandidateListItemTheme` | 把 active、selected、disabled 和隐藏状态映射为视觉 | 不从 `:pointerover` 推导独立候选 |
| 下拉控件 | 公共选择属性、候选源、过滤、模式和 popup 生命周期协调 | 不直接维护容器 active 视觉 |
| 候选列表 owner | active candidate owner、输入映射、索引转换、键盘滚动和提交/取消 | 不把 active 状态写入业务 option |
| 候选项容器 | 投影 `IsCandidateSelected`、`IsSelected`、启用、分组和控件特有状态 | 不持有跨回收周期的候选状态 |
| 虚拟化列表面板 | 键盘候选滚动到可见区域 | 不决定 active candidate |
| 候选项主题 | 把 active、selected、disabled、expanded 或 checked 状态映射为视觉 | 不从 `:pointerover` 推导独立候选 |
状态流保持单向:
```text
pointer move / Up / Down
-> resolve valid candidate source index
-> SelectCandidateList active candidate
-> realized container IsCandidateSelected
-> SelectCandidateListItemTheme active visual
-> candidate-list owner active candidate
-> realized container candidate projection
-> item theme active visual
Enter / pointer click
-> active or clicked candidate
@@ -112,7 +126,7 @@ Enter / pointer click
## 6. Theme 与组合契约
`SelectCandidateListItemTheme` 基于 `ListViewItemTheme`,但 Select 的候选视觉必须由统一 active candidate 状态控制。
候选项主题基于 `ListViewItemTheme`,但候选视觉必须由统一 active candidate 状态控制。各控件可以有不同的 selected、expanded 或 checked 视觉,但不能恢复独立的候选 hover owner。
主题遵守以下层次:
@@ -133,12 +147,12 @@ Enter / pointer click
```text
PointerMoved
-> 从事件源解析最近的 SelectCandidateListItem
-> 从事件源解析最近的候选项容器
-> 验证非 group、可见、启用且允许选择
-> view index 映射为 source index
-> 与 CandidateSelectedIndex 比较
-> 相同则返回
-> 更新 active candidate,不调用 ScrollCandidateItemIntoView
-> 更新 active candidate,不调用候选滚动方法
```
鼠标在同一候选项内部移动是稳态热路径,必须在索引比较后直接返回。目标变化时只清理旧候选容器并设置新候选容器,不扫描全部候选项。
@@ -3,6 +3,14 @@
本文档记录 Select 控件级设计、API、主题契约、Token 和实现结构的变化。
它不替代仓库根目录 CHANGELOG.md,也不作为正式版本发布说明。
## 2026-08-19
- Design
- Generalize `candidate-interaction-design.md` from the Select-specific implementation into the shared contract for Select, AutoComplete, Mentions, ComboBox, and Cascader candidate lists.
- Record the explicit boundary for TreeSelect, DatePicker/Calendar, and TimePicker state models so their tree selection, range preview, focus, and current-value semantics are not replaced by list hover suppression.
- Tests
- Align family-level verification with pointer-to-keyboard, keyboard-to-pointer, `Enter`, disabled candidate, lifecycle, and projection invariants.
## 2026-08-18
- Design
@@ -2,6 +2,16 @@
本文档记录 ComboBox 控件级设计、API、主题契约、Token 和实现结构的变化。它不替代仓库根目录 `CHANGELOG.md`,也不作为正式版本发布说明。
## 2026-08-19
- Behavior
- Make pointer movement and keyboard navigation update the same internal active candidate without committing `SelectedItem`.
- Keep pointer migration non-scrolling and make `Enter` commit the current visual candidate.
- Theme
- Neutralize inherited pointer-over candidate styling and preserve selected-item visual precedence.
- Tests
- Add mixed pointer/keyboard navigation and pointer-to-`Enter` commit regression coverage.
## 2026-08-18
- Behavior
@@ -1,6 +1,6 @@
# ComboBox 桌面版实现原理
本文档描述 ComboBox 桌面版的内部实现范围、源码职责、状态流、生命周期、资源边界和维护规则。公共设计与 API 契约见 [ComboBox 桌面版架构设计](overview.md),变化记录见 [ComboBox Changelog](changelog.md)。涉及控件 Token 的实现应同时阅读 [ComboBox Token 设计](token.md)。
本文档描述 ComboBox 桌面版的内部实现范围、源码职责、状态流、生命周期、资源边界和维护规则。公共设计与 API 契约见 [ComboBox 桌面版架构设计](overview.md),候选列表状态契约见 [候选列表统一交互设计](../../data-entry/select/candidate-interaction-design.md),变化记录见 [ComboBox Changelog](changelog.md)。涉及控件 Token 的实现应同时阅读 [ComboBox Token 设计](token.md)。
## 1. 实现定位
@@ -68,6 +68,7 @@ Public API / ItemsSource / Command / Event
- `IFormItemAware` 的值读写直接映射到 `SelectedItem`:`SetFormValue(value)` 保留对象实例并设置选择,`GetFormValue()` 返回选择对象,`ClearFormValue()` 清空选择。
- 非编辑态选中内容溢出提示由 `OverflowTip` 托管,只在 `SelectedContentPresenter` 视觉溢出时写入 `ToolTip.Tip`,延迟和位置分别映射到 `ToolTip.ShowDelay` 与 `ToolTip.Placement`;非编辑态显示节点以外层 `AddOnDecoratedBox` 作为 `PlacementTarget`,避免 tooltip 左边按内部文本 padding 对齐;编辑态输入文本仍由 `PART_EditableTextBox` 自己承载,不自动开启该提示。
- 伪类和 internal state 必须从单一 owner 推导,避免双向同步导致循环更新。
- popup 候选必须由单一 active candidate owner 驱动:鼠标命中可用 `ComboBoxItem` 时只迁移候选,不滚动、不提交;`Up` / `Down` 复用同一状态并允许滚动,`Enter` 从 active candidate 写入真实 `SelectedItem`。容器 recycle、ItemsSource / filter 变化和 popup close 必须清理旧投影。
- overview.md 的 API 契约说明应与源码实际状态流一致。
## 5. 生命周期与模板接入
@@ -1,6 +1,6 @@
# ComboBox 桌面版架构设计
本文档定义 `ComboBox` 桌面版的最新设计定位、公共契约、状态模型、视觉主题关系和兼容边界。通用控件研发约束见 [控件研发标准](../../../../engineering/development/control-development-guidelines.md),内部实现原理见 [ComboBox 桌面版实现原理](implementation.md),ComboBox Token 的专项设计见 [ComboBox Token 设计](token.md),设计和契约变化记录见 [ComboBox Changelog](changelog.md)。
本文档定义 `ComboBox` 桌面版的最新设计定位、公共契约、状态模型、视觉主题关系和兼容边界。通用控件研发约束见 [控件研发标准](../../../../engineering/development/control-development-guidelines.md),候选列表统一交互见 [候选列表统一交互设计](../../data-entry/select/candidate-interaction-design.md),内部实现原理见 [ComboBox 桌面版实现原理](implementation.md),ComboBox Token 的专项设计见 [ComboBox Token 设计](token.md),设计和契约变化记录见 [ComboBox Changelog](changelog.md)。
## 1. 控件定位
@@ -85,6 +85,7 @@ Public API / inherited command / item source / user input
- Disabled 或不可交互状态优先屏蔽 pointer、keyboard、motion 和提交类反馈。
- open/close、collection/filter、input/value、motion、visual option 状态由控件实例或明确的数据 owner 推导,不能在 template part 之间双向竞争。
- 单选结果以继承的 `SelectedItem` / `SelectedIndex` 为准;AtomUI Form 集成使用 `SelectedItem` 作为 ComboBox 的默认表单值,`SetFormValue`、`GetFormValue` 和 `ClearFormValue` 不应转换为字符串或读取展示文本。
- 下拉项的 active candidate 与 `SelectedItem` / `SelectedIndex` 分离。鼠标移动和键盘 `Up` / `Down` 共享同一个 active candidate;迁移只改变候选视觉,`Enter` 才提交 `SelectedItem`。`:pointerover` 不得形成第二个候选高亮,selected 视觉优先于 active 视觉。
- 模板重套用时必须把 public API 对应状态回放到新的 part、伪类和主题变量。
- 集合、弹层、异步、动效或窗口相关状态必须能处理 reset、close、cancel、detach 和 owner 释放。
@@ -144,6 +145,8 @@ ComboBox 与同分类控件共享尺寸、状态、Token、Gallery 展示和验
ComboBox 涉及弹层、窗口或 overlay 宿主时,打开状态、取消事件、定位和宿主释放必须保持一致。重复打开、关闭、窗口失活和 template reapply 都必须释放旧宿主引用。
ComboBox 的 active candidate 归属于当前 popup 的候选视图。popup 关闭、ItemsSource / filter 重建或容器回收时清除失效候选;重新打开后从当前视图重新建立候选,不继承旧容器的 active 投影。
### 8.2 集合与数据同步模型
ComboBox 的集合状态必须能处理 source replace、reset、clear 和 container recycle。业务数据对象不应反向持有视觉对象,虚拟化或懒创建路径必须在容器回收时清理旧状态。