# Vue 3 原生 dialog 与 popover

在当前目录运行：

```sh
pnpm install
pnpm dev
```

使用终端打印的本机地址。`dev` 只监听 `127.0.0.1`，不调用业务 API；表单内容只保存在内存。也可以直接打开本包的 `playground.html`，它由同一份 Vue SFC 构建，内联了运行时和样式，无需安装依赖。修改源码后运行 `pnpm build:playground` 更新快照。

文章 Code Lab 的源码区是只读阅读模式，预览消费该构建快照。在线代码区不承诺直接编辑 SFC 后即时编译，实际修改和热更新使用本地 `pnpm dev`。

`pnpm build:playground` 会写入当前目录的 dist、playground.html，并同步相邻的 native-overlays-preview 目录供文章嵌入。后者是只读派生产物，copy / examples 与当前源码逐文件校验一致，不作为第二份维护源。

本例固定 Vue 与 SFC 编译器为 3.5.44，使用普通 VDOM、`defineModel`、`useTemplateRef` 和 `useId`，因此组件要求 Vue 3.5+。这里没有使用 Vapor 或全局 DOM 查询来控制业务组件。

## 先操作什么

1. 打开 dialog，留空提交，确认浏览器必填校验阻止关闭；点击取消，查看 `returnValue="cancel"`。
2. 打开后按 Esc；再勾选「阻止 Esc」重试。`cancel` 表示关闭请求，`close` 表示原生对话框已经关闭。
3. 点击「关闭后 60ms 重新打开」，确认旧动画不能关闭新窗口。
4. 打开 dialog 内的说明，先按 Esc 关闭 popover，再按 Esc 关闭 dialog。
5. 打开独立 popover，分别用内容按钮、Esc、点击外部关闭，比较事件顺序与动画。
6. 切换纯 CSS 模式，观察 `display` / `overlay` 渐进增强；在系统减少动态效果开启时再次操作。
7. 在窗口打开时卸载组件，检查模态与动画资源的清理。

事件列表按新到旧排列。面板将父层模型、真实 `open` / `:popover-open` 和动画阶段分别显示，避免把「发出关闭意图」当作「原生窗口已经关闭」。

## 迁移到项目

把 `copy/components`、`copy/composables` 和 `copy/types.ts` 迁入项目对应目录，再调整相对 import；组件自己的 scoped SCSS 跟随组件迁移。项目需要 Sass 编译器，本例使用 `sass-embedded`。`examples` 是演示页面，不属于组件必需依赖。

```vue
<script setup lang="ts">
import { ref } from 'vue';
import NativeDialog from './components/native-dialog/index.vue';
import NativePopover from './components/native-popover/index.vue';

const dialogOpen = ref(false);
const helpOpen = ref(false);
</script>

<template>
  <button @click="dialogOpen = true">编辑</button>
  <NativeDialog v-model="dialogOpen" title="编辑资料" @closed="console.log($event)">
    <template #default="{ close }">
      <button type="button" @click="close('button', 'cancel')">取消</button>
    </template>
  </NativeDialog>
  <NativePopover v-model="helpOpen" label="查看说明" title="保存说明">
    <p>这里是补充信息。</p>
  </NativePopover>
</template>
```

## 组件契约

两个组件的布尔 `v-model` 都是必传，由父组件初始化。模型表示调用方希望窗口打开还是关闭；managed 退出期间，模型已经为 false，DOM 仍可短暂保持打开。

`NativeDialog` 的 `title` 必传；`motion` 默认 `managed`，也可为 `native`；`closeOnBackdrop` 默认 true；`busy` 默认 false。`busy` 阻止关闭按钮、遮罩、Esc 和 method=dialog 提交，父组件直接把模型设为 false 仍可强制结束流程。默认插槽提供 `close(reason?, returnValue?)`。`@cancel` 接收原生事件，可以同步 `preventDefault()`；`@closed` 返回 `{ reason, returnValue }`，其中 reason 为 button、cancel-request、backdrop、form、programmatic 或 native。cancel-request 表示原生 cancel 路径，事件自身不能可靠区分 Esc、系统返回或 requestClose。

`NativePopover` 的 `label` 和 `title` 必传，默认插槽提供 `close(reason?)`，`@closed` 返回关闭原因。浮层表示非模态说明面板，所以使用 `role="dialog"` 和可访问名称；它不是完整菜单或 Tooltip。trigger 使用真正的 button，显式维护 `aria-controls` 与 `aria-expanded`，ID 由 `useId` 生成。

两个组件都提供 `@trace` 诊断事件和 `@error` 能力 / API 错误事件。`defineExpose` 暴露的 element / phase 仅供示例观察；普通业务通过模型和事件操作组件，不直接更改原生状态。

## 动画和关闭边界

managed 使用 Web Animations 播放表面动画，再调用 `close()` / `hidePopover()`。新操作使旧动画失效，取消后的 `finished` 拒绝已处理，卸载时取消动画。dialog 的 Esc 可取消默认动作，因此能统一到这条退出路径；method=dialog 的 submit 也能在通过浏览器校验后接入。

popover 保持 `popover="auto"`。组件按钮和父模型发起的关闭可以等待动画，Esc、点击外部或另一个 auto popover 引发的浏览器关闭可能直接消失。关闭时的 `beforetoggle` 不可取消，本例不会在其中重新调用开关 API，也不会改成 manual 后伪装成完整的原生 auto 行为。

native 直接关闭原生窗口，视觉退出由 CSS 的 `@starting-style`、`display` 与 `overlay` 过渡协调。它们的支持范围不同，不支持时允许动效降级。关闭事件表示原生显隐状态已关闭，不承诺纯 CSS 的像素动画也结束了。

## 正式接入前需要补什么

- 示例定位只处理上下翻转、横向边界、滚动 / resize 与尺寸变化；不覆盖 RTL、多种 placement、虚拟锚点、所有布局位移、iframe 跨文档坐标和移动键盘。复杂定位考虑 Floating UI 或单独评估 CSS Anchor Positioning。
- 示例包含滚动链约束，但不承诺所有 Safari / WebView 都完整锁住背景滚动。按目标设备验证，必要时接入项目已有的滚动管理。
- slot 内容默认保留，关闭不会清空表单。异步保存、草稿丢弃确认、路由关闭与多层弹窗栈归业务管理。
- 组件库的 Select / DatePicker 等浮层若挂到 body，可能落在原生模态 dialog 的背景；应调整挂载容器并实测键盘与屏幕阅读器。
- SSR 阶段不调用浏览器 API，真正的打开从挂载后的 post watcher 开始；服务端不能靠 open 属性预先获得原生模态。
- `close()`、`method="dialog"` 与组件自定义关闭入口并不都触发 `cancel`；通用异步 before-close 需要另外设计，不能仅监听 cancel。
- 示例没有对不支持 dialog / popover 的内核实现 polyfill，会通过 error 事件明确报告。

## 验证

```sh
pnpm typecheck
pnpm build
pnpm exec playwright install chromium
pnpm test:browser
```

跨引擎检查可先安装 `pnpm exec playwright install firefox webkit`，再运行 `pnpm test:browser --browser=firefox` 或 `pnpm test:browser --browser=webkit`。测试在本地启动临时静态服务器，读取当前 dist；尚未构建时读取随包提供的 playground 快照。也可以通过 `--url=http://127.0.0.1:5173` 检查开发服务器，通过 `--report=report.json` 保存环境和行为记录。

Playwright WebKit 与 Firefox 的结果只说明对应自动化内核，不代替真实 iOS、Safari、App WebView 或辅助技术验收。测试脚本在 `tests/browser.mjs`，发布前的实际结果见文章中的验证范围。

## 来源与许可

组件与演示代码为此示例编写，按 MIT 许可提供。原生行为依据 HTML 标准、MDN 和 Vue 官方文档；文章入口为 `/2026/11/01/vue-native-dialog-popover/`。阅读索引和兼容数据在文章资源 `data/` 中，完整文件索引见 `FILES.json`。
