跳到主要内容

抽屉

Drawer 从屏幕边缘滑出可拖拽的面板:吸附点、键盘手柄、模态阈值与页面联动,共享 Dialog 的焦点与 dismiss 行为。

Drawer(抽屉)给任务一张贴着屏幕边缘、可拖拽调整的面板,不必离开页面。它共享对话框的焦点与 dismiss 行为,再加上拖拽、吸附点(snap points)和可键盘操作的手柄。居中呈现的聚焦任务用 Dialog;需要明确回答的确认用警告对话框。

组合

Drawer
├── DrawerTrigger
├── DrawerBackdrop
└── DrawerContent
    ├── DrawerTitle
    ├── DrawerDescription
    └── DrawerClose

DrawerContent 就是面板:抽屉把它放进全屏视口并用 translate 移动。你负责尺寸、表面与圆角,定位与位移交给抽屉;背板给个颜色(如 bg-black/30),透明度由抽屉管理。多吸附点时加 DrawerHandle,单吸附点用装饰性横条即可。

用法

import {
  Drawer,
  DrawerBackdrop,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerTitle,
  DrawerTrigger,
} from "@ui-sdk/react/drawer";

function Example() {
  return (
    <Drawer>
      <DrawerTrigger>筛选</DrawerTrigger>
      <DrawerBackdrop />
      <DrawerContent>
        <DrawerTitle>筛选</DrawerTitle>
        <DrawerDescription>缩小结果范围。</DrawerDescription>
        <DrawerClose>完成</DrawerClose>
      </DrawerContent>
    </Drawer>
  );
}

长内容放进内部 min-h-0 flex-1 overflow-y-auto 区域:向下拖先滚动内容,滚到顶后同一手势接着移动抽屉,关一张长面板不需要拖两次。

吸附点与手柄

snapPoints 是面板可以停靠的位置:0.5 是视口的一半,大于 1 的数字与 "320px" 是像素,还支持 "20rem"、"50%" 与 "content"(自身尺寸,默认)。打开时停在第一个点或 defaultSnapPoint,拖拽后就近停靠,快速轻扫可以跳过中间点。面板的 CSS 尺寸是上限:想让最大点接近全屏,给内容设 h-[calc(100dvh-3rem)] 之类的高度。

<Drawer snapPoints={[0.25, 0.5, 1]} defaultSnapPoint={0.5} expandOnScroll>
  <DrawerTrigger>查找地点</DrawerTrigger>
  <DrawerBackdrop />
  <DrawerContent>
    <DrawerHandle aria-label="调整大小" />
    <DrawerTitle>附近的地点</DrawerTitle>
    {/* 列表 */}
  </DrawerContent>
</Drawer>

DrawerHandle 是有无障碍名称的按钮:按一下移到下一个更大的吸附点并循环,沿抽屉轴的方向键逐级移动,Home / End 到两端。snapPoint 与 onSnapPointChange 可独立于 open 受控;取消变更时被拒的滑动会弹回。

方向与模态阈值

side 选择 bottom / top / left / right,逻辑值 start / end 跟随书写方向,内容的 data-side 给出解析后的物理边。侧边抽屉可设 preventEdgeSwipe 阻止 iOS Safari 的边缘返回手势(代价是贴边的点击也失效)。

modalAbove 让地图式交互成为可能:停在该吸附点及以下时页面可用、不压暗、不锁滚动、无 aria-modal(data-interactive 标记此态);高于它才应用 modal(true / "trap-focus" / false,同 Dialog)。切换发生在面板停稳时,不移动焦点。dismissible={false} 时 Esc、外部按压、返回手势与滑动都关不掉,onDismissAttempt 报告尝试,面板会弹回。

动作面板与控制器

给每个 Close 部件一个 value,抽屉就成了动作面板:createDrawerController<Payload, Result>() 的 open() 返回的 Promise 带着所选 returnValue resolve,Esc、外部按压或滑走则没有值。把风险最小的动作放最前,因为键盘打开从那里开始。

const photoActions = createDrawerController<undefined, "share" | "delete">();

const { returnValue = "cancel" } = await photoActions.open();

<Drawer controller={photoActions}>
  <DrawerBackdrop />
  <DrawerContent>
    <DrawerTitle>海滩.jpg</DrawerTitle>
    <DrawerClose value="share">分享</DrawerClose>
    <DrawerClose value="delete">删除</DrawerClose>
  </DrawerContent>
</Drawer>;

外部触发器传 payload,内容从 state.payload 读取,一个面板即可服务每一行;getSnapPoint() / setSnapPoint() 还能用代码移动面板。参见控制器。

驱动与页面联动

driver 决定面板如何移动:"scroll-snap" 用原生滚动与 CSS 吸附(触摸屏上的底部面板),"pointer" 用指针拖拽加弹簧运动,默认 "auto" 按输入方式与配置选择,data-driver 报告结果。physics 可覆盖 sheetPhysics.ios 的 response、dampingRatio 等参数。

DrawerOutlet 让页面元素跟随面板进度:keyframes 从收起到完全展开插值,如 { scale: [1, 0.94], borderRadius: ["0px", "24px"] },变换与透明度在合成器上运行。themeColor 让 <meta name="theme-color"> 随背板压暗,浏览器工具栏一起变暗。

样式与状态

各部件的 data-slot 为 drawer-trigger、drawer-backdrop、drawer-viewport、drawer-content、drawer-handle、drawer-close、drawer-outlet:

  • 内容与视口:data-open / data-closed、data-side、data-starting-style / data-ending-style;视口在软键盘弹出时带 data-keyboard。
  • 内容与手柄:data-snap-point(当前停靠点,如 0.5 或 content)、data-expanded(停在最大点);内容另有 data-dragging、data-driver、data-interactive。
  • Close 部件:action 运行中带 data-pending。

CSS 变量:内容与 Outlet 上有 --drawer-progress(开合进度 0 到 1)、--drawer-dim(压暗程度)、--drawer-expand(接近最大点的程度)、--drawer-stack(上方嵌套面板数,平滑上升);视口上有 --keyboard-inset 与 --visual-viewport-*。位移与背板透明度留给抽屉,其余随你:

[data-slot="drawer-content"] {
  border-radius: 16px 16px 0 0;
  background: var(--color-surface);
  scale: calc(1 - var(--drawer-stack) * 0.04);
}

嵌套时 Esc 只关最上层,父面板可用 --drawer-stack 后退。减少动态效果时抽屉改为淡入淡出。详见弹出层。

API

Prop 类型 说明
open / defaultOpen boolean 受控 / 非受控的开合状态。
onOpenChange (open, details) => void 开合变更;details.reason 含 "swipe"。
snapPoints SnapPoint[] 可停靠的位置:分数、像素、rem、% 或 "content"。
snapPoint / defaultSnapPoint SnapPoint 受控 / 初始吸附点。
onSnapPointChange (snapPoint, details) => void 停靠点变化及原因。
side Side 来源边,start / end 跟随书写方向。
modal boolean | "trap-focus" 模态级别,同 Dialog。
modalAbove SnapPoint 该点及以下页面保持可用、不压暗。
dismissible / onDismissAttempt boolean / (details) => void 拦截关闭并处理被拒的尝试。
driver / physics SheetDriverName / Partial<SheetPhysics> 移动驱动与弹簧参数。
expandOnScroll boolean 在可滚动内容上向上拖时先展开抽屉。
themeColor / preventEdgeSwipe boolean 工具栏随背板变暗 / 阻止边缘返回手势。
controller DrawerController 用 createDrawerController() 创建。

DrawerContent 支持 initialFocus、finalFocus、keepMounted、container;DrawerClose 有 value 与 action(Promise 期间面板保持打开)。

键盘与无障碍

按键 行为
Enter / Space 触发器上打开;手柄上移到下一个吸附点
沿轴方向键 手柄上逐级移动,不循环
Home / End 手柄上移到最小 / 最大吸附点
Tab / Shift+Tab 在面板控件间移动,模态时锁在内部
Escape 可 dismiss 时关闭最上层抽屉

模态时遵循 WAI-ARIA 模态对话框模式:aria-modal、焦点锁在内部、页面 inert,由标题命名、描述补充。手机上的屏幕阅读器把手柄当滑杆调节。触摸打开聚焦内容本身,避免立刻弹出软键盘;键盘弹出时视口收缩,聚焦字段在面板内滚入视野。

相关组件

  • 对话框:居中呈现的聚焦任务,也可用 presentation="sheet" 做简单底部面板。
  • 警告对话框:动作需要再次确认时使用。
  • 弹出层:顶层渲染、模态与 dismiss 的共享模型。
  • 控制器:从任意位置打开面板并等待所选动作。

输入关键词搜索…

↑↓ 移动↵ 选择Esc 关闭