Drawer 抽屉面板

用于抽屉面板场景的 MoriUI 组件。

导入

import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHandle, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger } from 'moriui'

示例

概览

从底部滑出的编辑面板。

<script setup lang="ts">
  import {
    Button,
    Drawer,
    DrawerClose,
    DrawerContent,
    DrawerDescription,
    DrawerFooter,
    DrawerHeader,
    DrawerTitle,
    DrawerTrigger,
    Input,
    Label
  } from 'moriui'
</script>

<template>
  <Drawer>
    <DrawerTrigger as-child>
      <Button variant="outline">
        编辑资料
      </Button>
    </DrawerTrigger>
    <DrawerContent>
      <DrawerHeader>
        <DrawerTitle>编辑资料</DrawerTitle>
        <DrawerDescription>在此修改你的资料,完成后点击保存。</DrawerDescription>
      </DrawerHeader>
      <div class="grid gap-4 p-4">
        <div class="grid gap-2">
          <Label for="drawer-name">姓名</Label>
          <Input id="drawer-name" default-value="张三" />
        </div>
        <div class="grid gap-2">
          <Label for="drawer-username">用户名</Label>
          <Input id="drawer-username" default-value="@zhangsan" />
        </div>
      </div>
      <DrawerFooter>
        <Button type="submit">
          保存
        </Button>
        <DrawerClose as-child>
          <Button variant="outline">
            取消
          </Button>
        </DrawerClose>
      </DrawerFooter>
    </DrawerContent>
  </Drawer>
</template>

方向

从各侧滑入。

<script setup lang="ts">
  import { ref } from 'vue'
  import {
    Button,
    Drawer,
    DrawerContent,
    DrawerHeader,
    DrawerTitle,
    DrawerTrigger
  } from 'moriui'

  const openBottom = ref(false)
  const openTop = ref(false)
  const openLeft = ref(false)
  const openRight = ref(false)
</script>

<template>
  <div class="flex flex-wrap gap-2">
    <Drawer v-model:open="openBottom" swipe-direction="down">
      <DrawerTrigger as-child>
        <Button variant="outline">
          底部
        </Button>
      </DrawerTrigger>
      <DrawerContent show-handle>
        <DrawerHeader>
          <DrawerTitle>底部抽屉</DrawerTitle>
        </DrawerHeader>
        <div class="p-4 text-sm text-muted-foreground">
          从底部滑入的抽屉,适合移动端操作。
        </div>
      </DrawerContent>
    </Drawer>

    <Drawer v-model:open="openTop" swipe-direction="up">
      <DrawerTrigger as-child>
        <Button variant="outline">
          顶部
        </Button>
      </DrawerTrigger>
      <DrawerContent show-handle>
        <DrawerHeader>
          <DrawerTitle>顶部抽屉</DrawerTitle>
        </DrawerHeader>
        <div class="p-4 text-sm text-muted-foreground">
          从顶部滑入的抽屉,适合通知或横幅。
        </div>
      </DrawerContent>
    </Drawer>

    <Drawer v-model:open="openLeft" swipe-direction="left">
      <DrawerTrigger as-child>
        <Button variant="outline">
          左侧
        </Button>
      </DrawerTrigger>
      <DrawerContent show-handle>
        <DrawerHeader>
          <DrawerTitle>左侧抽屉</DrawerTitle>
        </DrawerHeader>
        <div class="p-4 text-sm text-muted-foreground">
          从左侧滑入的抽屉,适合导航菜单。
        </div>
      </DrawerContent>
    </Drawer>

    <Drawer v-model:open="openRight" swipe-direction="right">
      <DrawerTrigger as-child>
        <Button variant="outline">
          右侧
        </Button>
      </DrawerTrigger>
      <DrawerContent show-handle>
        <DrawerHeader>
          <DrawerTitle>右侧抽屉</DrawerTitle>
        </DrawerHeader>
        <div class="p-4 text-sm text-muted-foreground">
          从右侧滑入的抽屉,适合详情面板。
        </div>
      </DrawerContent>
    </Drawer>
  </div>
</template>

吸附点

snapPoints 控制多级高度位置。

<script setup lang="ts">
  import { ref } from 'vue'
  import {
    Button,
    Drawer,
    DrawerContent,
    DrawerHeader,
    DrawerTitle,
    DrawerTrigger
  } from 'moriui'

  const snap = ref(0.5)
</script>

<template>
  <Drawer v-model:snap-point="snap" :snap-points="[0.25, 0.5, 0.85]">
    <DrawerTrigger as-child>
      <Button variant="outline">
        吸附点抽屉
      </Button>
    </DrawerTrigger>
    <DrawerContent show-handle>
      <DrawerHeader>
        <DrawerTitle>吸附点</DrawerTitle>
      </DrawerHeader>
      <div class="p-4 text-sm text-muted-foreground">
        当前吸附位置:{{ Math.round(snap * 100) }}%
      </div>
      <div class="px-4 pb-4 text-sm text-muted-foreground">
        拖拽抽屉到不同位置会吸附到预设的点位(25%、50%、85%)。
      </div>
    </DrawerContent>
  </Drawer>
</template>

拖拽手柄

手柄区域触发拖拽交互。

<script setup lang="ts">
  import { ref } from 'vue'
  import {
    Button,
    Drawer,
    DrawerContent,
    DrawerHeader,
    DrawerTitle,
    DrawerTrigger
  } from 'moriui'

  const showHandle = ref(true)
</script>

<template>
  <div class="flex items-center gap-4">
    <Drawer>
      <DrawerTrigger as-child>
        <Button variant="outline">
          有拖拽手柄
        </Button>
      </DrawerTrigger>
      <DrawerContent show-handle>
        <DrawerHeader>
          <DrawerTitle>拖拽手柄</DrawerTitle>
        </DrawerHeader>
        <div class="p-4 text-sm text-muted-foreground">
          顶部拖拽手柄可以让你通过拖拽关闭抽屉。
        </div>
      </DrawerContent>
    </Drawer>

    <Drawer>
      <DrawerTrigger as-child>
        <Button variant="outline">
          无拖拽手柄
        </Button>
      </DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>无手柄</DrawerTitle>
        </DrawerHeader>
        <div class="p-4 text-sm text-muted-foreground">
          通过传递 <code>show-handle</code> prop 控制手柄显隐。
        </div>
      </DrawerContent>
    </Drawer>
  </div>
</template>

非模态

非模态时背景可交互。

抽屉已关闭,背景按钮仍可点击

<script setup lang="ts">
  import { ref } from 'vue'
  import {
    Button,
    Drawer,
    DrawerContent,
    DrawerHeader,
    DrawerTitle,
    DrawerTrigger
  } from 'moriui'

  const open = ref(false)
</script>

<template>
  <div class="flex flex-col items-center gap-4">
    <Drawer v-model:open="open" :modal="false">
      <DrawerTrigger as-child>
        <Button variant="outline">
          非模态抽屉
        </Button>
      </DrawerTrigger>
      <DrawerContent show-handle>
        <DrawerHeader>
          <DrawerTitle>非模态抽屉</DrawerTitle>
        </DrawerHeader>
        <div class="p-4 text-sm text-muted-foreground">
          非模态抽屉不阻挡背景交互,你可以继续与页面上的其他元素进行互动。
        </div>
      </DrawerContent>
    </Drawer>
    <p class="text-xs text-muted-foreground">
      抽屉已{{ open ? '打开' : '关闭' }},背景按钮仍可点击
    </p>
  </div>
</template>

嵌套

在 Drawer 内打开另一个 Drawer。

<script setup lang="ts">
  import {
    Button,
    Drawer,
    DrawerClose,
    DrawerContent,
    DrawerDescription,
    DrawerFooter,
    DrawerHeader,
    DrawerTitle,
    DrawerTrigger
  } from 'moriui'
</script>

<template>
  <Drawer>
    <DrawerTrigger as-child>
      <Button variant="outline">
        打开嵌套抽屉
      </Button>
    </DrawerTrigger>
    <DrawerContent show-handle>
      <DrawerHeader>
        <DrawerTitle>主抽屉</DrawerTitle>
        <DrawerDescription>这是第一层抽屉,你可以在其中打开第二个抽屉。</DrawerDescription>
      </DrawerHeader>
      <div class="p-4">
        <Drawer>
          <DrawerTrigger as-child>
            <Button>打开二级抽屉</Button>
          </DrawerTrigger>
          <DrawerContent show-handle>
            <DrawerHeader>
              <DrawerTitle>二级抽屉</DrawerTitle>
              <DrawerDescription>这是嵌套在第一个抽屉内部的第二个抽屉。</DrawerDescription>
            </DrawerHeader>
            <DrawerFooter>
              <DrawerClose as-child>
                <Button variant="outline">
                  关闭
                </Button>
              </DrawerClose>
            </DrawerFooter>
          </DrawerContent>
        </Drawer>
      </div>
    </DrawerContent>
  </Drawer>
</template>

对话框

结合 Dialog 与 Drawer 的响应式模式。

<script setup lang="ts">
  import { ref } from 'vue'
  import {
    Button,
    Dialog,
    DialogContent,
    DialogDescription,
    DialogFooter,
    DialogHeader,
    DialogTitle,
    DialogTrigger,
    Drawer,
    DrawerClose,
    DrawerContent,
    DrawerDescription,
    DrawerFooter,
    DrawerHeader,
    DrawerTitle,
    DrawerTrigger
  } from 'moriui'

  const isDesktop = ref(false)
</script>

<template>
  <div class="flex items-center gap-4">
    <Drawer v-if="!isDesktop">
      <DrawerTrigger as-child>
        <Button variant="outline">
          响应式抽屉
        </Button>
      </DrawerTrigger>
      <DrawerContent show-handle>
        <DrawerHeader>
          <DrawerTitle>编辑资料</DrawerTitle>
          <DrawerDescription>在移动端使用抽屉模式。</DrawerDescription>
        </DrawerHeader>
        <DrawerFooter>
          <Button>保存</Button>
          <DrawerClose as-child>
            <Button variant="outline">
              取消
            </Button>
          </DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>

    <Dialog v-else>
      <DialogTrigger as-child>
        <Button variant="outline">
          响应式对话框
        </Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>编辑资料</DialogTitle>
          <DialogDescription>在桌面端使用对话框模式。</DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <Button>保存</Button>
          <Button variant="outline">
            取消
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>

    <label class="flex items-center gap-2 text-sm">
      <input v-model="isDesktop" type="checkbox" class="toggle">
      桌面端模式
    </label>
  </div>
</template>

RTL

在 RTL 页面中保持自然的方向逻辑。

<script setup lang="ts">
  import {
    Button,
    DirectionProvider,
    Drawer,
    DrawerClose,
    DrawerContent,
    DrawerDescription,
    DrawerFooter,
    DrawerHeader,
    DrawerTitle,
    DrawerTrigger
  } from 'moriui'
</script>

<template>
  <DirectionProvider direction="rtl">
    <Drawer dir="rtl">
      <DrawerTrigger as-child>
        <Button variant="outline">
          فتح الدرج
        </Button>
      </DrawerTrigger>
      <DrawerContent show-handle>
        <DrawerHeader>
          <DrawerTitle>درج من اليمين</DrawerTitle>
          <DrawerDescription>اتجاه المحتوى من اليمين إلى اليسار.</DrawerDescription>
        </DrawerHeader>
        <div class="p-4 text-sm text-muted-foreground">
          يدعم هذا الدرج الاتجاه من اليمين إلى اليسار للغات مثل العربية والعبرية.
        </div>
        <DrawerFooter>
          <DrawerClose as-child>
            <Button variant="outline">
              إغلاق
            </Button>
          </DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  </DirectionProvider>
</template>

使用说明

组合方式

Drawer 由 Trigger、Portal、Overlay、Content 与 Handle 组成;Content 内可使用 Header、Title、Description 与 Footer 组织结构。

吸附点

snapPoints 设置内容停靠位置;activeSnapPoint 控制当前吸附点,用户拖拽时自动捕捉。

拖拽手柄

showHandle 控制交互手柄的显示;DrawerHandle 也可独立使用 Reka Drawer 原语的拖拽行为。

从各侧滑出

direction 控制抽屉从屏幕的哪一侧滑入,支持 top、right、bottom、left。

非模态

modal 控制是否阻止背景交互;非模态时用户可与背景内容交互。

RTL

direction 的左右逻辑在 RTL 上下文中自然翻转。

变体

Drawer 的 direction 支持 top、right、bottom、left;snapPoints 通过 number 定义吸附位置百分比。

可访问性

Drawer 复用 Reka UI Dialog 的焦点管理、Escape 关闭、遮罩交互和焦点归还。Content 内应提供 DrawerTitle,需要时提供 DrawerDescription。拖拽交互在触摸设备上需确保有键盘替代方式。

API 参考

属性类型默认值说明
Drawer v-model:openbooleanundefined抽屉的受控打开状态。
Drawer defaultOpenbooleanfalse非受控初始打开状态。
Drawer direction'top' | 'right' | 'bottom' | 'left'undefined抽屉从屏幕边缘滑出的方向。
Drawer modalbooleantrue是否阻断背景区域交互。
Drawer dismissiblebooleantrue是否点击遮罩或拖拽关闭。
Drawer snapPointsnumber[]undefined可吸附的高度百分比位置。
DrawerContent showHandlebooleanfalse是否显示拖拽手柄。
DrawerHandle classHTMLAttributes["class"]undefined合并到手柄的调用方类。
DrawerHeader as / asChildstring | Component / boolean'div' / false页眉的元素组合能力。
DrawerFooter as / asChildstring | Component / boolean'div' / false页脚的元素组合能力。
DrawerClose as / asChildstring | Component / booleanundefined关闭按钮的元素组合能力。