Content Dialog 用来在继续之前向用户索取确认或补充信息(WinUI 官方描述:use a content dialog to prompt the user to confirm an action or provide additional information)。它是模态弹层:遮罩层吃掉背后的指针输入,焦点被困在弹窗内,Esc 或按钮才能结束它。
几何、状态机与动效对齐 WinUI 3 / Windows App SDK 的 ContentDialog(microsoft-ui-xaml winui3/release/2.5.1,已核对与最新 WinUI3 Gallery 发行版 v2.9.3 所用的 WindowsAppSDK 2.0.1 在目标文件上一致):
MinWidth 320 / MaxWidth 548 / MinHeight 184 / MaxHeight 756、圆角 8(OverlayCornerRadius)、描边 1、内边距 24、标题下间距 12、按钮间距 8。LayoutRoot)→ 居中表面(BackgroundElement:底色 + 描边 + 阴影)→ 两行(内容 * / 命令区 auto)→ 内容层(ContentDialogTopOverlay 底色 + 下边框)与 5 列命令区网格。scale 1.05 → 1(250ms,cubic-bezier(0,0,0,1))+ opacity 0 → 1(83ms,linear);退出 scale 1 → 1.05(167ms)+ opacity 1 → 0(83ms);遮罩层只做 83ms linear 的透明度、不缩放。v-model:open 控制开关(对齐 ShowAsync() / Hide()),closed 的 args.result 带回返回值(primary / secondary / none,对齐 ContentDialogResult)。
命令区状态只由「三个按钮文案是否为空」推导(对齐 ChangeVisualState 的 ButtonsVisibilityStates 八个分支):
状态(data-buttons) | 触发条件 | 按钮落位 |
|---|---|---|
all | 三个文案都有 | 三列各占 1/3,列间距 8 + 8(FirstSpacer = 8、SecondaryColumn = *) |
primary-secondary | 主 + 次 | 主按钮第 1 列、次按钮第 5 列,各占半宽 |
primary-close / secondary-close | 主+关 / 次+关 | 前一个按钮第 1 列、关闭按钮第 5 列 |
primary / secondary | 只有主 / 只有次 | 该按钮移到第 5 列(右半宽) |
close | 只有关闭按钮 | 关闭按钮第 5 列 |
none | 全空 | 命令区整体折叠(CommandSpace.Visibility = Collapsed) |
default-button 决定哪个按钮拿到强调样式(对应 DefaultButton + AccentButtonStyle)。焦点规则与 WinUI 逐字一致:焦点不在命令区(或正好落在该按钮上)时保留强调态;焦点移到命令区里的其它按钮时,强调态消失(NoDefaultButton 分支)。按下 Enter 时激活的是 default-button 指向的按钮,而不是当前强调的那个。
closing 的 args.cancel = true 会取消本次关闭,弹窗留在原地且不播放退出动画;三个按钮的点击事件(primaryButtonClick / secondaryButtonClick / closeButtonClick)同样可以 args.cancel 阻止后续关闭。
full-size-desired 对应 FullSizeDesired → FullDialogSizing(只把表面的纵向对齐改为 Stretch,横向仍居中);内容超出高度时由内容区内部滚动,命令区固定在底部。WinUI 的 ContentScrollViewer 把 VerticalScrollBarVisibility 设为 Disabled,因此滚动条不可见但内容仍可滚动。
| 属性(Props) | 类型 | 默认 | 说明 |
|---|---|---|---|
open | boolean | false | 是否显示(v-model:open);对齐 ShowAsync() / Hide() |
title | string | '' | 标题,20px / SemiBold,最多 2 行;对齐 Title |
primaryButtonText | string | '' | 主按钮文案;空则不渲染;对齐 PrimaryButtonText |
secondaryButtonText | string | '' | 次按钮文案;空则不渲染;对齐 SecondaryButtonText |
closeButtonText | string | '' | 关闭按钮文案;空则不渲染;对齐 CloseButtonText |
isPrimaryButtonEnabled | boolean | true | 主按钮是否可用;对齐 IsPrimaryButtonEnabled |
isSecondaryButtonEnabled | boolean | true | 次按钮是否可用;对齐 IsSecondaryButtonEnabled |
defaultButton | 'none' | 'primary' | 'secondary' | 'close' | 'none' | 默认按钮(强调样式 + Enter 目标);对齐 DefaultButton |
fullSizeDesired | boolean | false | 纵向撑满(上限 756);对齐 FullSizeDesired |
ariaLabel | string | — | 无标题时的可访问名;对齐 AutomationProperties.Name |
to | string | HTMLElement | 'body' | 门户目标(Web 侧替代 XamlRoot) |
| 事件(Emits) | 载荷 | 说明 |
|---|---|---|
update:open | boolean | v-model:open 的更新 |
opened | — | 弹窗已打开;对齐 Opened |
primaryButtonClick | { cancel } | 主按钮被点击;cancel = true 阻止后续关闭 |
secondaryButtonClick | { cancel } | 次按钮被点击;同上 |
closeButtonClick | { cancel } | 关闭按钮被点击;同上 |
closing | { result, cancel } | 请求关闭;cancel = true 取消关闭;对齐 Closing |
closed | { result } | 已关闭,result ∈ 'primary' | 'secondary' | 'none';对齐 Closed |
| 插槽(Slots) | 说明 |
|---|---|
title | 对应 Title / TitleTemplate;提供后取代 title |
default | 对应 Content / ContentTemplate |
无障碍:内容层渲染
role="dialog"+aria-modal="true"(对齐 WinUI 弹窗的IsDialog+ 模态语义),可访问名取标题(无标题时可用ariaLabel);初始焦点按 WinUI 的三级优先设置(内容区第一个可聚焦元素 → 默认按钮 → 命令区第一个可聚焦按钮);Tab在弹窗内循环,关闭后焦点回到弹出前聚焦的元素;遮罩层对 AT 隐藏。共享弹层原语:遮罩层由
FluereSmokeLayer提供(独立 Teleport + rekaPresence,83ms linear 淡入淡出、锁定 body 滚动、aria-hidden),状态机由@fluere-vue/hooks的useDisclosure提供(isOpen/isClosing/requestClose/finishClose/cancelClose,SSR 安全且尊重prefers-reduced-motion)。后续 Tooltip / Flyout 类组件复用同一组原语。实现要点
- 动效不是一条动画:reka 的
Presence以节点上第一次animationend决定卸载时机,而 WinUI 的缩放(250 / 167ms)与淡入淡出(83ms)时长不同。因此本组件把缩放放在内容层根节点(最长的一条,负责门控卸载,退出动画不会被截断),透明度放在内层表面(83ms 结束即保持 0,剩余时间里的缩放不可见),遮罩层另在独立 Teleport 层上跑自己的 83ms —— 与 WinUI「两个 Popup、同一 Storyboard 多目标」的结构同构。- 动效逐帧对齐源码:
ContentDialogOpenCloseThemeTransition::CreateStoryboardImpl(LayoutTransition_partial.cpp)里 Load 分支注册ScaleX/Y1.05 → 1.0(250ms,TimingFunctionDescription{cp3.X=0}= cubic-bezier(0,0,0,1))与Opacity0 → 1(83ms,linear),Unload 分支对称(167ms / 83ms);s_OpenScaleDuration=250/s_CloseScaleDuration=167/s_OpacityChangeDuration=83与Common_themeresources_any.xaml的ControlNormalAnimationDuration/ControlFastAnimationDuration/ControlFasterAnimationDuration一一对应。250ms 与缓动都命中 token(--durationGentle/--curveDecelerateMid),167ms / 83ms 无同值 token,落成组件局部变量并注明来源。- 遮罩层与弹窗同层:两者
z-index都是 1000,靠 Teleport 挂载顺序(遮罩先、弹窗后)决定绘制次序 —— 这是为了让弹窗内部再 Teleport 的浮层(如 Combobox 下拉,同为 1000)仍能盖在弹窗之上。- 点击遮罩不关闭:WinUI 的 ContentDialog 没有任何 light-dismiss 路径,本组件同样把
pointer-down-outside/interact-outside/focus-outside一律preventDefault(),只有Esc与三个按钮能关闭。Esc与关闭按钮:对齐ExecuteCloseAction()—— 存在可点的关闭按钮时,「程序化点击」它(先走closeButtonClick),否则直接以none关闭。- 配色映射(WinUI 资源 → token):
ContentDialogBackground←SolidBackgroundFillColorBase→colorNeutralBackground2(ΔE 2.3 / 0.6);ContentDialogTopOverlay←LayerFillColorAlt叠底后的结果 →colorNeutralBackground1(ΔE 0 / 1.4);ContentDialogBorderBrush←SurfaceStrokeColorDefault(合成后#C1C1C1/#424242)→colorNeutralStroke2(ΔE 31 / 16);ContentDialogSeparatorBorderBrush←CardStrokeColorDefault→colorNeutralStrokeAlpha;ContentDialogSmokeFill←SmokeFillColorDefault(30% 黑)→colorBackgroundOverlay(40% / 50%)。Fluent 2 Web 的 token 表没有同值档,需要严格对齐实机时覆盖局部变量:.my-dialog .fui-content-dialog__surface { --fui-content-dialog-surface: #f3f3f3; /* SolidBackgroundFillColorBase */ --fui-content-dialog-border: #c1c1c1; /* SurfaceStrokeColorDefault 合成值 */ }- 阴影是 B 级近似:
PrepareContent用ApplyElevationEffect(depth 0, baseElevation 128)投阴影,源码只给 elevation 值、没有 px 可换算,这里按浮层语义位取 Fluent 2 最大档--shadow64。prefers-reduced-motion:关闭缩放与淡入淡出(animation: none),静态形态就是最终态(scale 1/opacity 1),关闭变为立即卸载。- 窄视口:WinUI 的
MinWidth 320等绝对尺寸在 Web 上用min(..., 100%)与视口对齐(WinUI 假定桌面窗口足够宽),属响应式补充。- 与 WinUI 的差异:
ButtonClick/Closing的取消是同步标志(不做异步 Deferral);Enter在 button / a / input / textarea / select / summary / contenteditable /role=button上不接管(WinUI 靠路由事件的Handled达到同一效果);焦点还原发生在退出动画结束(WinUI 在关闭流程开始时就还焦点);不暴露PrimaryButtonCommand等 Command 属性与三个*ButtonStyle(由点击事件 / 固定样式承担);不做「同一父节点只允许一个 Popup 弹窗」的强约束;高对比度主题未实现(属已知缺口)。