Drawer 抽屉
从视口一边滑出、可以用手指拖动关闭的模态面板,移动端「从底部弹出」的首选。 桌面上从侧边滑出、不需要拖动的用 Sheet;打断流程、要求明确答复的用 Modal。
把手、标题、说明、正文、底部操作。抽屉初始是关闭的:把 open 设为 true 或调用 show() 才会从 direction 指定的一边滑出,这里只展示结构。
抽屉默认关闭,页面上看不到它;下面的代码就是它的全部结构。
<pt-drawer direction="bottom">
<span slot="title">调整目标</span>
<span slot="description">设置每天的活动目标。</span>
<p>正文内容。</p>
<pt-button slot="footer" variant="brand">提交</pt-button>
<pt-button slot="footer" variant="secondary" data-drawer-close="">取消</pt-button>
</pt-drawer>打开与关闭都走 open 属性(show() / hide() 只是它的糖)。关闭后会发 ptClose, detail.reason 说明是怎么关的:escape / overlay / drag / close 是用户发起的,api 是调用了 hide()。 直接把 open 设为 false 不发 ptClose。ptOpen / ptRequestClose / ptClose / ptSnap 都不冒泡 (弹层天然互相嵌套),外层统一监听要走捕获阶段。
方向
direction 取 bottom(默认)/ top / left / right:
- 上下:宽度铺满,高度随内容,离对边至少留 96px;带一个 100×8 的把手(
handle="false"去掉),top时把手在底部。 - 左右:高度铺满,宽 75%,≥640px 时最多 384px(
--pt-drawer-size可改);没有把手。
方向是物理方向,RTL 下不翻转,与源包依赖的 vaul 一致:它说的是面板贴着屏幕哪条边、往哪边拖能关, 和文字方向无关。
开合 500ms、cubic-bezier(0.32, 0.72, 0, 1)(vaul 的曲线),系统开了「减少动态效果」时不播。 源包的 shouldScaleBackground(打开时把页面缩小)不做。
拖动关闭
按住把手或面板上不可滚动的地方往关闭方向拖,松手时:
- 往关闭方向的速度超过 0.4 px/ms(快速一甩),或拖过面板露出部分的 25%:关闭;
- 否则回弹到原位。拖动中遮罩跟着淡出。
速度取的是松手前最近约 100ms 的末段速度、带方向,不是按下到松手的平均:按住片刻再快甩照样算甩; 往下拖了一段再往上快甩,是留下(有吸附点时开大一档),不会按净位移关掉。
面板里可滚动的区域(正文本身就是)没滚到顶之前,往下拉是滚动内容,滚到顶了再往下拉才是拖抽屉。 文本框、下拉框(包括 Input、Textarea、InputGroup、Combobox、Select、NativeSelect 整个控件,前后缀与箭头也算)、 带 data-drawer-no-drag 的元素上按下不起拖;在 pointerdown 里自己 preventDefault() 的控件(如 Slider)也不会被拖走。
吸附点
snapPoints 是一组停靠位置(只能用 JS 赋值),按从小到大排列:
0–1的数字:露出视口高度(左右方向时为宽度)的比例;'240px'这样的字符串:露出多少像素。
超过面板自身尺寸的按全开算;每个点至少露出 44px,0、'0px' 或换算下来不足 44px 的点夹到 44px, 并在控制台警告一次 —— 露出为零时面板整块在视口外,遮罩、滚动锁和焦点陷阱却还在,dismissible="false" 时页面就被锁死了。 要「收起」就关闭抽屉。打开时停在 snap-index(默认 0);拖动松手吸附到最近的点, 较快地甩一下逐档切换,甩得很快(> 2 px/ms)直接关掉或开到最后一个点。用户拖到新的点时组件改写 snap-index 并发 ptSnap(detail 为 { index, point });外部改 snap-index 会动画移过去,不发 ptSnap。
const drawer = document.querySelector('pt-drawer');
drawer.snapPoints = ['160px', 0.5, 1];
drawer.addEventListener('ptSnap', event => console.log(event.detail.index));与 vaul 的两处差异:遮罩在任何吸附点上都完全显示(vaul 只在最后一个点上显示);从第一个点慢慢往下拖过它露出部分的 25% 同样会关闭(vaul 只认速度)。
关闭按钮与拦截关闭
正文或页脚里带 data-drawer-close 属性的元素(对应源包的 DrawerClose),点击后走一次 reason 为 close 的关闭。
用户按 Esc、点遮罩、拖动关闭、点 data-drawer-close 元素的那一刻,抽屉先发一个可取消的 ptRequestClose, detail.reason 同上。调用 event.preventDefault(),抽屉就保持打开(拖动的回弹到原来的吸附点),也不发 ptClose:
<PtDrawer
open={open}
onPtRequestClose={event => {
if (dirty) event.preventDefault();
}}
onPtClose={() => setOpen(false)}
>
…
</PtDrawer>dismissible="false" 时点遮罩、按 Esc 不关、拖动只回弹,只剩 data-drawer-close 与应用自己改 open。
焦点与层级
与 Modal、Sheet 共用同一套模态层:打开时焦点落在面板容器本身,Tab 困在面板里,关闭后还给打开它的元素; auto-focus="false" 时焦点不动。打开期间锁住页面滚动,面板进浏览器的顶层,不被祖先的 overflow 裁切。 抽屉里再打开的下拉、气泡卡片、弹窗盖在它之上,Esc 每次只关最上面那一层。
API
属性
| 属性 | Attribute | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
ariaLabelText | aria-label | string | undefined | — | 无障碍名称:原生 HTML 里写 aria-label,React / Vue / Angular 包装层里属性名是 ariaLabelText。 没有用 title 插槽时必须给。 |
autoFocus | auto-focus | boolean | true | 打开时把焦点移进面板(落在面板容器本身)。设为 false 时焦点原地不动;焦点陷阱照样生效。 默认为真:原生 HTML 里关掉它写 `auto-focus="false"`。 |
direction | direction | "bottom" | "left" | "right" | "top" | 'bottom' | 从哪条边滑出。物理方向,RTL 下不翻转 |
dismissible | dismissible | boolean | true | 能否由用户关闭:点遮罩、按 Esc、拖动。设为 false 时这三种都不关(拖动只回弹), `data-drawer-close` 元素与应用自己改 `open` 照常生效。原生 HTML 里写 `dismissible="false"`。 |
handle | handle | boolean | true | 上下方向时显示顶部(top 方向时在底部)的把手。原生 HTML 里去掉写 `handle="false"` |
open | open | boolean | false | 是否打开。用户交互时组件自己改写;外部直接赋值立即生效,不发 ptRequestClose / ptClose |
snapIndex | snap-index | number | 0 | 当前停在哪个吸附点(snapPoints 的下标)。打开时停在这里;用户拖到别的点时组件改写它并发 ptSnap。 外部改它会动画移到对应位置。没有 snapPoints 时不起作用 |
snapPoints | — | DrawerSnapPoint[] | undefined | — | 吸附点,按从小到大排列:0–1 的比例(露出视口高 / 宽的多少)或 `'240px'` 这样的像素字符串。 缺省时只有全开一个位置。**只能用 JS 赋值**(数组)。认不出的值按全开处理; 露出不足 44px 的(包括 `0`、`'0px'`)夹到 44px 并 console.warn —— 面板不能整块藏在视口外还锁着页面 |
事件
| 事件 | detail 类型 | 说明 |
|---|---|---|
ptClose | { reason: PtDrawerCloseReason; } | 关闭后触发(退场动画开始时,不等它播完),detail 说明是怎么关的:`escape` / `overlay` / `drag` / `close` 是用户发起的(之前的 ptRequestClose 没被取消),`api` 是调用了 hide()。 直接把 open 设为 false **不发**。 **不冒泡**:同 ptOpen。 |
ptOpen | void | 打开并渲染出来之后触发(交互或属性打开都发) **不冒泡**:弹层常互相嵌套(抽屉里的下拉、抽屉里再开抽屉),冒泡的话外层上的监听会收到内层的开关。 |
ptRequestClose | { reason: PtDrawerRequestCloseReason; } | 用户要关闭抽屉时(按 Esc / 点遮罩 / 拖动关闭 / 点 `data-drawer-close` 元素)、真正关闭之前触发,可取消: `event.preventDefault()` 后抽屉保持打开(拖动的回弹到原来的吸附点),也不会发 ptClose。 `hide()` 与直接把 open 设为 false 不经过它。 **不冒泡**:同 ptOpen。 |
ptSnap | PtDrawerSnapDetail | 用户拖动后吸附到了另一个吸附点时触发(`snap-index` 已改写)。外部改 `snap-index` 不发。 **不冒泡**:抽屉可以嵌套,外层的监听不该收到内层的吸附。 |
方法
| 方法 | 说明 |
|---|---|
hide() => Promise<void> | 关闭抽屉(ptClose 的 reason 为 `api`) |
show() => Promise<void> | 打开抽屉 |
插槽
| 名称 | 说明 |
|---|---|
(默认) | 正文(在面板里滚动) |
description | 标题下的说明 |
footer | 底部操作区,按钮纵向排列 |
title | 标题 |
可定制的内部元素(::part())
| part | 说明 |
|---|---|
base | 进顶层的最外层容器(铺满视口,包着遮罩与面板) |
body | 正文 |
footer | 底部 |
handle | 把手(上下方向时) |
header | 头部(标题与说明) |
overlay | 遮罩 |
panel | 面板本体 |
CSS 变量
| 变量 | 说明 |
|---|---|
--pt-drawer-size | 左右两个方向时的面板宽度。默认 75%、≥640px 时最多 384px;设了之后宽度与上限都按它。上下方向时不起作用 |