对话框 (模态)
一个完全管理的无渲染对话框组件,包含辅助功能和键盘功能,非常适合为您的下一个应用程序构建完全自定义的模态和对话框窗口。
要开始,请通过 npm 安装无头 UI。
请注意,**此库仅支持 Vue 3**。
npm install @headlessui/vue
对话框使用 Dialog
、DialogPanel
、DialogTitle
和 DialogDescription
组件构建。
当对话框的 open
属性为 true
时,对话框的内容将呈现。焦点将移动到对话框内部并被捕获,因为用户在可聚焦元素之间循环。滚动被锁定,您应用程序 UI 的其余部分对屏幕阅读器隐藏,单击 DialogPanel
外部或按 Escape 键将触发 close
事件并关闭对话框。
<template> <Dialog :open="isOpen" @close="setIsOpen"> <DialogPanel> <DialogTitle>Deactivate account</DialogTitle> <DialogDescription> This will permanently deactivate your account </DialogDescription> <p> Are you sure you want to deactivate your account? All of your data will be permanently removed. This action cannot be undone. </p> <button @click="setIsOpen(false)">Deactivate</button> <button @click="setIsOpen(false)">Cancel</button> </DialogPanel> </Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogPanel, DialogTitle, DialogDescription, } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>
如果您的对话框有标题和描述,请使用 DialogTitle
和 DialogDescription
组件提供最易于访问的体验。这将通过 aria-labelledby
和 aria-describedby
属性将您的标题和描述链接到根对话框组件,确保在您的对话框打开时,其内容会向使用屏幕阅读器的用户宣布。
对话框没有自动管理其打开/关闭状态。要显示和隐藏您的对话框,请将 ref 传递到 open
属性中。当 open
为 true 时,对话框将呈现,当它为 false 时,对话框将卸载。
当打开的对话框被关闭时,close
事件会触发,这发生在用户单击 DialogPanel
外部或按 Escape 键时。您可以使用此事件将 open
设置回 false 并关闭您的对话框。
<template> <!-- Pass the `isOpen` ref to the `open` prop, and use the `close` event to set the ref back to `false` when the user clicks outside of the dialog or presses the escape key. -->
<Dialog :open="isOpen" @close="setIsOpen"><DialogPanel> <DialogTitle>Deactivate account</DialogTitle> <DialogDescription> This will permanently deactivate your account </DialogDescription> <p> Are you sure you want to deactivate your account? All of your data will be permanently removed. This action cannot be undone. </p> <!-- You can render additional buttons to dismiss your dialog by setting your `isOpen` state to `false`. --> <button @click="setIsOpen(false)">Cancel</button> <button @click="handleDeactivate">Deactivate</button></DialogPanel></Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogPanel, DialogTitle, DialogDescription, } from '@headlessui/vue' // The open/closed state lives outside of the Dialog and // is managed by you. const isOpen = ref(true) function setIsOpen(value) {isOpen.value = value} function handleDeactivate() { // ... } </script>
使用 class
或 style
属性像对待任何其他元素一样,对 Dialog
和 DialogPanel
组件进行样式化。如果需要,您也可以引入额外的元素来实现特定设计。
<template> <Dialog :open="isOpen" @close="setIsOpen" class="relative z-50"> <div class="fixed inset-0 flex w-screen items-center justify-center p-4"> <DialogPanel class="w-full max-w-sm rounded bg-white"> <DialogTitle>Complete your order</DialogTitle> <!-- ... --> </DialogPanel> </div> </Dialog> </template> <script setup> import { ref } from 'vue' import { DialogPanel, DialogTitle, DialogDescription } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>
单击 DialogPanel
组件外部将关闭对话框,所以在决定哪个元素应该接收给定样式时请记住这一点。
如果您想在 DialogPanel
后面添加一个覆盖层或遮罩以引起对面板本身的注意,我们建议您使用一个专门用于遮罩的元素,并使其成为您面板容器的同级元素。
<template> <Dialog :open="isOpen" @close="setIsOpen" class="relative z-50"> <!-- The backdrop, rendered as a fixed sibling to the panel container -->
<div class="fixed inset-0 bg-black/30" aria-hidden="true" /><!-- Full-screen container to center the panel --> <div class="fixed inset-0 flex w-screen items-center justify-center p-4"> <!-- The actual dialog panel --> <DialogPanel class="w-full max-w-sm rounded bg-white"> <DialogTitle>Complete your order</DialogTitle> <!-- ... --> </DialogPanel> </div> </Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogTitle, DialogDescription } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>
这使您可以 过渡 遮罩和面板独立使用自己的动画,并将它们作为同级元素呈现可确保它不会干扰您滚动长对话框的能力。
使对话框可滚动完全由 CSS 处理,具体实现取决于您要实现的设计。
以下是一个示例,其中整个面板容器是可滚动的,面板本身会在您滚动时移动。
<template> <Dialog :open="isOpen" @close="setIsOpen" class="relative z-50"> <!-- The backdrop, rendered as a fixed sibling to the panel container --> <div class="fixed inset-0 bg-black/30" aria-hidden="true" /> <!-- Full-screen scrollable container -->
<div class="fixed inset-0 w-screen overflow-y-auto"><!-- Container to center the panel --><div class="flex min-h-full items-center justify-center p-4"><!-- The actual dialog panel --> <DialogPanel class="w-full max-w-sm rounded bg-white"> <DialogTitle>Complete your order</DialogTitle> <!-- ... --> </DialogPanel> </div> </div> </Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogTitle, DialogDescription } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>
在使用遮罩创建可滚动对话框时,请确保遮罩渲染在可滚动容器后面,否则滚动轮在悬停在遮罩上时将无法工作,遮罩可能会遮挡滚动条并阻止用户用鼠标单击它。
出于辅助功能原因,您的对话框应该至少包含一个可聚焦元素。默认情况下,Dialog
组件将在呈现后聚焦第一个可聚焦元素(按 DOM 顺序),按 Tab 键将循环遍历内容中的所有其他可聚焦元素。
只要对话框被渲染,焦点就会被捕获在对话框内,因此,制表到末尾将开始再次从开头循环。对话框外部的所有其他应用程序元素将被标记为惰性,因此无法聚焦。
如果您希望在您的对话框最初呈现时,除第一个可聚焦元素以外的元素接收初始焦点,您可以使用 initialFocus
ref。
<template>
<Dialog :initialFocus="completeButtonRef" :open="isOpen" @close="setIsOpen"><DialogPanel> <DialogTitle>Complete your order</DialogTitle> <p>Your order is all ready!</p> <button @click="setIsOpen(false)">Deactivate</button> <!-- Use `initialFocus` to force initial focus to a specific ref. --><button ref="completeButtonRef" @click="completeOrder">Complete order </button> </DialogPanel> </Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogPanel, DialogTitle, DialogDescription, } from '@headlessui/vue'const completeButtonRef = ref(null)const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } function completeOrder() { // ... } </script>
如果您之前曾经实现过 Dialog,那么您可能已经遇到了门户的概念。门户允许您从 DOM 中的一个位置(例如,在您的应用程序 UI 深处)调用组件,但实际上完全渲染到 DOM 的另一个位置。
由于对话框及其遮罩占用了整个页面,因此您通常希望将它们作为您应用程序的根节点的同级元素呈现。这样,您可以依赖自然的 DOM 顺序来确保其内容在您现有的应用程序 UI 之上呈现。这也有助于轻松地将滚动锁定应用于您应用程序的其余部分,并确保您的对话框的内容和遮罩不受阻碍地接收焦点和点击事件。
由于这些辅助功能问题,无头 UI 的 Dialog
组件实际上在幕后使用了门户。这样,我们可以提供诸如无阻碍的事件处理和使您的应用程序的其余部分处于惰性状态之类的功能。因此,在使用我们的 Dialog 时,您无需自己使用门户!我们已经处理了它。
要为对话框的打开/关闭添加动画,请将它包裹在无头 UI 的 TransitionRoot
组件中,并从您的 Dialog
中删除 open
属性,而是将您的打开/关闭状态传递到 TransitionRoot
上的 show
属性中。
<template> <!-- Wrap your dialog in a `TransitionRoot` to add transitions. -->
<TransitionRoot:show="isOpen"as="template"enter="duration-300 ease-out"enter-from="opacity-0"enter-to="opacity-100"leave="duration-200 ease-in"leave-from="opacity-100"leave-to="opacity-0"><Dialog @close="setIsOpen"> <DialogPanel> <DialogTitle>Deactivate account</DialogTitle> <!-- ... --> <button @click="isOpen = false">Close</button> </DialogPanel> </Dialog></TransitionRoot></template> <script setup> import { ref } from 'vue' import {TransitionRoot,Dialog, DialogPanel, DialogTitle, } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>
要分别为您的遮罩和面板添加动画,请用 TransitionRoot
包裹您的 Dialog
,并用各自的 TransitionChild
包裹您的遮罩和面板。
<template> <!-- Wrap your dialog in a `TransitionRoot`. -->
<TransitionRoot :show="isOpen" as="template"><Dialog @close="setIsOpen"> <!-- Wrap your backdrop in a `TransitionChild`. --><TransitionChildenter="duration-300 ease-out"enter-from="opacity-0"enter-to="opacity-100"leave="duration-200 ease-in"leave-from="opacity-100"leave-to="opacity-0"><div class="fixed inset-0 bg-black/30" /></TransitionChild><!-- Wrap your panel in a `TransitionChild`. --><TransitionChildenter="duration-300 ease-out"enter-from="opacity-0 scale-95"enter-to="opacity-100 scale-100"leave="duration-200 ease-in"leave-from="opacity-100 scale-100"leave-to="opacity-0 scale-95"><DialogPanel> <DialogTitle>Deactivate account</DialogTitle> <!-- ... --> </DialogPanel></TransitionChild></Dialog> </TransitionRoot> </template> <script setup> import { ref } from 'vue' import {TransitionRoot,TransitionChild,Dialog, DialogPanel, DialogTitle, } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>
要了解有关无头 UI 中过渡的更多信息,请阅读专门的 过渡文档。
当 Dialog 的 open
属性为 true
时,Dialog 的内容将呈现,焦点将移动到 Dialog 内部并被捕获。第一个可聚焦元素(根据 DOM 顺序)将接收焦点,尽管您可以使用 initialFocus
ref 来控制哪个元素接收初始焦点。在打开的 Dialog 上按 Tab 键会循环遍历所有可聚焦元素。
当渲染 Dialog
时,单击 DialogPanel
外部将关闭 Dialog
。
默认情况下,没有用于打开 Dialog
的鼠标交互,尽管通常您会将 <button />
元素连接到一个 click
处理程序,该处理程序将 Dialog 的 open
属性切换为 true
。
命令 | 描述 |
Esc | 关闭任何打开的对话框 |
Tab | 循环遍历打开的对话框的内容 |
Shift + Tab | 向后循环遍历打开的对话框的内容 |
属性 | 默认值 | 描述 |
open | — | Boolean
|
initialFocus | — | HTMLElement 对应该首先接收焦点的元素的 ref。 |
as | div | String | Component
|
static | false | Boolean 元素是否应该忽略内部管理的打开/关闭状态。 |
unmount | true | Boolean 元素是否应该根据打开/关闭状态卸载或隐藏。 |
事件 | 描述 |
close | 当 |
插槽属性 | 描述 |
open |
对话框是否打开。 |
属性 | 默认值 | 描述 |
as | div | String | Component
|
渲染属性 | 描述 |
open |
对话框是否打开。 |
属性 | 默认值 | 描述 |
as | h2 | String | Component
|
插槽属性 | 描述 |
open |
对话框是否打开。 |
属性 | 默认值 | 描述 |
as | p | String | Component
|
插槽属性 | 描述 |
open |
对话框是否打开。 |
从 Headless UI v1.6 版本开始,DialogOverlay
已被弃用,请查看 发行说明 获取迁移说明。
属性 | 默认值 | 描述 |
as | div | String | Component
|
插槽属性 | 描述 |
open |
对话框是否打开。 |