Modal 弹窗
宽度三档:sm = 480 / md = 640 / lg = 960。
标题、说明、正文、底部操作各占一个插槽。弹窗初始是关闭的:把 open 设为 true 或调用 show() 才会出现,这里只展示结构。
弹窗默认关闭,页面上看不到它;下面的代码就是它的全部结构。
<pt-modal>
<span slot="title">确认删除</span>
<span slot="description">删除后不可恢复。</span>
<p>正文内容。</p>
<pt-button slot="footer" variant="secondary">取消</pt-button>
<pt-button slot="footer" variant="destructive">删除</pt-button>
</pt-modal>打开与关闭都走 open 属性(show() / hide() 只是它的糖)。关闭后会发 ptClose, detail.reason 说明是怎么关的:close-button / escape / overlay 是用户发起的, api 是调用了 hide()。直接把 open 设为 false 不发 ptClose——那是你自己关的, 受控写法下不需要再被告知一次。同时打开多个弹窗时,Esc 与 Tab 只由最上层那个处理。
点遮罩默认不关闭
弹窗里多半是用户填到一半的东西,手滑点到旁边就整份丢掉,代价和收益完全不成比例。 关闭只留三条明路:右上角 ✕、底部按钮、Esc。
确实需要「点外面就关」的轻量场景,显式加 close-on-overlay。 必须做出选择的场景(如二次确认)可以用 no-escape 连 Esc 也禁掉。
确认框
删除确认这类「必须明确答复」的场景加 alert:面板的角色换成 alertdialog(读屏会连同说明一起念出来)、 遮罩加深、默认不显示右上角 ✕,并且点遮罩永远不关(close-on-overlay 对它无效)。 Esc 仍然可用,等同「取消」;要连它也禁掉再加 no-escape。
alert 形态:role="alertdialog"、遮罩加深、默认没有右上角关闭按钮,点遮罩不关。同样初始关闭,这里只展示结构。
确认框默认关闭,页面上看不到它;下面的代码就是它的全部结构。
<pt-modal alert>
<span slot="title">删除这份报表?</span>
<span slot="description">删除后不可恢复。</span>
<pt-button slot="footer" variant="secondary">取消</pt-button>
<pt-button slot="footer" variant="destructive">删除</pt-button>
</pt-modal>确认框确实需要 ✕ 时显式写 hide-close="false"(框架里 hideClose={false})—— hide-close 不设置时跟随形态,设了就以它为准。
拦截关闭
用户点 ✕、按 Esc、点遮罩(开了 close-on-overlay 时)的那一刻,弹窗先发一个可取消的 ptRequestClose,detail.reason 同上。监听方调用 event.preventDefault(),弹窗就保持打开,也不会发 ptClose —— 典型用法是表单有未保存的改动时先拦下来,换成一个二次确认:
<pt-modal id="edit">…</pt-modal>
<script>
document.querySelector('#edit').addEventListener('ptRequestClose', event => {
if (form.dirty) event.preventDefault();
});
</script><PtModal
open={open}
onPtRequestClose={event => {
if (dirty) event.preventDefault();
}}
onPtClose={() => setOpen(false)}
>
…
</PtModal><PtModal :open="open" @pt-request-close="e => dirty && e.preventDefault()" @pt-close="open = false">
…
</PtModal>hide() 与直接把 open 设为 false 是使用方自己的决定,不经过这个事件,也拦不住。
焦点
打开时焦点落在弹窗容器本身,不给第一个按钮 —— 否则按钮带着焦点环出场, 看上去像「已经默认选中了」。读屏照常宣布弹窗,Tab 从头遍历且不会跑出弹窗, 关闭后焦点还给打开它的那个元素。
需要把焦点交给某个特定元素(比如正文里的输入框)时,设 auto-focus="false"(框架里 autoFocus={false}), 在 ptOpen 里自己聚焦。这时弹窗不移动焦点,但焦点陷阱照样生效:焦点还在弹窗外面时按 Tab, 会被拉到弹窗里的第一个可聚焦元素(Shift+Tab 到最后一个)。
右上角 ✕ 的读屏名称默认是内置文案「关闭」(随 locale 切换),这一处要说别的话时用 close-label。
层级
遮罩打开时进浏览器的顶层(popover API):不会被祖先的 overflow: hidden 裁掉,也不受 transform 祖先和层叠上下文影响,不用再调 z-index。弹窗里再打开的下拉、提示同样进顶层,而且比弹窗后进, 所以始终盖在弹窗之上。不支持 popover API 的环境退回 position: fixed + --pt-z-overlay。
移动端
窄屏(≤640px)下自动切换为底部抽屉:贴着底边、铺满宽度、上方两角圆角。
API
属性
| 属性 | Attribute | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
alert | alert | boolean | false | 确认框形态:面板 `role="alertdialog"`、遮罩加深、默认不显示关闭按钮、点遮罩永远不关。 用于删除确认这类必须明确答复的场景。 |
ariaLabelText | aria-label | string | undefined | — | 无障碍名称:原生 HTML 里写 aria-label,React / Vue / Angular 包装层里属性名是 ariaLabelText。 没有用 title 插槽时必须给。 |
autoFocus | auto-focus | boolean | true | 打开时把焦点移进弹窗(落在弹窗容器本身)。设为 false 时焦点原地不动 —— 适合在 ptOpen 里自己聚焦某个输入框;焦点陷阱照样生效,第一次 Tab 就会被拉进弹窗。 默认为真:原生 HTML 里关掉它写 `auto-focus="false"`。 |
closeLabel | close-label | string | undefined | — | 右上角关闭按钮的无障碍名称。不设置时用内置文案(`t('modal.close')`,随 locale 切换)。 |
closeOnOverlay | close-on-overlay | boolean | false | 允许点遮罩关闭(默认关闭,理由见组件说明)。`alert` 形态下无效 |
hideClose | hide-close | boolean | undefined | — | 隐藏右上角关闭按钮。隐藏后必须自己在 footer 里提供关闭入口。 不设置时跟随形态:普通弹窗显示,`alert` 确认框隐藏;显式设为 false 可以让确认框也显示 ✕ (原生 HTML 里写 `hide-close="false"`,或用 JS 赋值)。 |
noEscape | no-escape | boolean | false | 按 Esc 不关闭。用于「必须做出选择」的场景 |
open | open | boolean | false | 是否打开 |
size | size | "lg" | "md" | "sm" | 'sm' | 宽度档位 |
事件
| 事件 | detail 类型 | 说明 |
|---|---|---|
ptClose | { reason: PtModalCloseReason; } | 关闭后触发,detail 说明是怎么关的:`close-button` / `escape` / `overlay` 是用户发起的 (之前的 ptRequestClose 没被取消),`api` 是调用了 hide()。 直接把 open 设为 false **不发**:那是使用方自己关的,受控写法下再回告一次只会让 onPtClose → setOpen(false) 多绕一圈(与 pt-tooltip 的约定一致) **不冒泡**:弹层常互相嵌套(弹窗里的菜单、气泡卡片里的下拉),冒泡的话外层上的监听会收到内层的开关。 |
ptOpen | void | 打开后触发 **不冒泡**:弹层常互相嵌套(弹窗里的菜单、气泡卡片里的下拉),冒泡的话外层上的监听会收到内层的开关。 |
ptRequestClose | { reason: PtModalRequestCloseReason; } | 用户要关闭弹窗时(点 ✕ / 按 Esc / 点遮罩)、真正关闭之前触发,可取消: `event.preventDefault()` 后弹窗保持打开,也不会发 ptClose。 `hide()` 与直接把 open 设为 false 不经过它。 **不冒泡**:弹层常互相嵌套(弹窗里的菜单、气泡卡片里的下拉),冒泡的话外层上的监听会收到内层的开关。 |
方法
| 方法 | 说明 |
|---|---|
hide() => Promise<void> | 关闭弹窗 |
show() => Promise<void> | 打开弹窗 |
插槽
| 名称 | 说明 |
|---|---|
(默认) | 正文 |
description | 标题下的说明 |
footer | 底部操作区 |
title | 标题 |
可定制的内部元素(::part())
| part | 说明 |
|---|---|
base | 最外层容器(与 overlay 同一元素) |
body | 正文 |
close | 关闭按钮(pt-button 宿主) |
close-button | 关闭按钮内部的 button 元素(pt-button 的 base part 转发) |
footer | 底部 |
header | 头部 |
overlay | 遮罩 |
panel | 弹窗本体 |
CSS 变量
| 变量 | 说明 |
|---|---|
--pt-modal-width | 弹窗最大宽度,默认按 size 档位(sm 480px / md 640px / lg 960px)。窄屏抽屉形态下铺满宽度,不受它影响 |