@ui-sdk/vue 0.0.0-stage → 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +10 -2
  3. package/dist/accordion.d.ts +561 -0
  4. package/dist/accordion.js +24 -0
  5. package/dist/agent.d.ts +2 -0
  6. package/dist/agent.js +7 -0
  7. package/dist/alert-dialog.d.ts +231 -0
  8. package/dist/alert-dialog.js +33 -0
  9. package/dist/async-options.d.ts +18 -0
  10. package/dist/async-options.js +40 -0
  11. package/dist/autocomplete.d.ts +384 -0
  12. package/dist/autocomplete.js +344 -0
  13. package/dist/avatar.d.ts +40 -0
  14. package/dist/avatar.js +28 -0
  15. package/dist/button.d.ts +52 -0
  16. package/dist/button.js +43 -0
  17. package/dist/calendar-context.d.ts +10 -0
  18. package/dist/calendar-context.js +7 -0
  19. package/dist/calendar.d.ts +311 -0
  20. package/dist/calendar.js +349 -0
  21. package/dist/carousel.d.ts +236 -0
  22. package/dist/carousel.js +204 -0
  23. package/dist/checkbox.d.ts +211 -0
  24. package/dist/checkbox.js +151 -0
  25. package/dist/children.d.ts +41 -0
  26. package/dist/children.js +135 -0
  27. package/dist/collapsible.d.ts +54 -0
  28. package/dist/collapsible.js +18 -0
  29. package/dist/color-picker.d.ts +187 -0
  30. package/dist/color-picker.js +158 -0
  31. package/dist/color.d.ts +2 -0
  32. package/dist/color.js +21 -0
  33. package/dist/combobox.d.ts +517 -0
  34. package/dist/combobox.js +531 -0
  35. package/dist/command.d.ts +322 -0
  36. package/dist/command.js +345 -0
  37. package/dist/controller-target.d.ts +49 -0
  38. package/dist/controller-target.js +112 -0
  39. package/dist/create-parts.d.ts +126 -0
  40. package/dist/create-parts.js +206 -0
  41. package/dist/date-input.d.ts +133 -0
  42. package/dist/date-input.js +31 -0
  43. package/dist/date-picker.d.ts +328 -0
  44. package/dist/date-picker.js +354 -0
  45. package/dist/date.d.ts +2 -0
  46. package/dist/date.js +59 -0
  47. package/dist/dialog-parts.d.ts +259 -0
  48. package/dist/dialog-parts.js +300 -0
  49. package/dist/dialog.d.ts +251 -0
  50. package/dist/dialog.js +25 -0
  51. package/dist/drawer.d.ts +304 -0
  52. package/dist/drawer.js +373 -0
  53. package/dist/editor-context.d.ts +4 -0
  54. package/dist/editor-context.js +5 -0
  55. package/dist/field-context.d.ts +26 -0
  56. package/dist/field-context.js +44 -0
  57. package/dist/field.d.ts +154 -0
  58. package/dist/field.js +240 -0
  59. package/dist/file-input.d.ts +212 -0
  60. package/dist/file-input.js +255 -0
  61. package/dist/form-inputs.d.ts +16 -0
  62. package/dist/form-inputs.js +25 -0
  63. package/dist/gesture.d.ts +1 -0
  64. package/dist/gesture.js +15 -0
  65. package/dist/history.d.ts +2 -0
  66. package/dist/history.js +11 -0
  67. package/dist/hover-card.d.ts +137 -0
  68. package/dist/hover-card.js +153 -0
  69. package/dist/image-cropper.d.ts +74 -0
  70. package/dist/image-cropper.js +36 -0
  71. package/dist/image.d.ts +2 -0
  72. package/dist/image.js +5 -0
  73. package/dist/index.d.ts +59 -0
  74. package/dist/index.js +60 -0
  75. package/dist/input.d.ts +120 -0
  76. package/dist/input.js +73 -0
  77. package/dist/item-icon.d.ts +34 -0
  78. package/dist/item-icon.js +63 -0
  79. package/dist/label.d.ts +16 -0
  80. package/dist/label.js +8 -0
  81. package/dist/lifecycle.d.ts +11 -0
  82. package/dist/lifecycle.js +91 -0
  83. package/dist/list-items.d.ts +38 -0
  84. package/dist/list-items.js +59 -0
  85. package/dist/list-text.d.ts +7 -0
  86. package/dist/list-text.js +15 -0
  87. package/dist/listbox.d.ts +273 -0
  88. package/dist/listbox.js +126 -0
  89. package/dist/menu.d.ts +678 -0
  90. package/dist/menu.js +607 -0
  91. package/dist/menubar-context.d.ts +5 -0
  92. package/dist/menubar-context.js +5 -0
  93. package/dist/menubar.d.ts +61 -0
  94. package/dist/menubar.js +48 -0
  95. package/dist/meter.d.ts +41 -0
  96. package/dist/meter.js +37 -0
  97. package/dist/navigation-menu.d.ts +228 -0
  98. package/dist/navigation-menu.js +195 -0
  99. package/dist/number-input.d.ts +176 -0
  100. package/dist/number-input.js +231 -0
  101. package/dist/number.d.ts +2 -0
  102. package/dist/number.js +39 -0
  103. package/dist/nuxt/entries.json +977 -0
  104. package/dist/nuxt/module.d.ts +18 -0
  105. package/dist/nuxt/module.js +31 -0
  106. package/dist/nuxt/registrations.d.ts +22 -0
  107. package/dist/nuxt/registrations.js +22 -0
  108. package/dist/observe.d.ts +2 -0
  109. package/dist/observe.js +7 -0
  110. package/dist/otp-input.d.ts +142 -0
  111. package/dist/otp-input.js +130 -0
  112. package/dist/overlays.d.ts +40 -0
  113. package/dist/overlays.js +118 -0
  114. package/dist/pagination.d.ts +201 -0
  115. package/dist/pagination.js +146 -0
  116. package/dist/password-input.d.ts +144 -0
  117. package/dist/password-input.js +134 -0
  118. package/dist/popover.d.ts +211 -0
  119. package/dist/popover.js +258 -0
  120. package/dist/popup.d.ts +60 -0
  121. package/dist/popup.js +89 -0
  122. package/dist/preferences.d.ts +44 -0
  123. package/dist/preferences.js +102 -0
  124. package/dist/primitive.d.ts +2 -0
  125. package/dist/primitive.js +11 -0
  126. package/dist/progress.d.ts +41 -0
  127. package/dist/progress.js +37 -0
  128. package/dist/radio.d.ts +164 -0
  129. package/dist/radio.js +145 -0
  130. package/dist/rating.d.ts +149 -0
  131. package/dist/rating.js +146 -0
  132. package/dist/renderless.d.ts +4 -0
  133. package/dist/renderless.js +28 -0
  134. package/dist/resizable.d.ts +54 -0
  135. package/dist/resizable.js +22 -0
  136. package/dist/scroll-area.d.ts +72 -0
  137. package/dist/scroll-area.js +77 -0
  138. package/dist/segmented-control.d.ts +151 -0
  139. package/dist/segmented-control.js +110 -0
  140. package/dist/segmented-input.d.ts +76 -0
  141. package/dist/segmented-input.js +115 -0
  142. package/dist/select.d.ts +458 -0
  143. package/dist/select.js +468 -0
  144. package/dist/separator.d.ts +14 -0
  145. package/dist/separator.js +8 -0
  146. package/dist/shortcuts.d.ts +32 -0
  147. package/dist/shortcuts.js +56 -0
  148. package/dist/slider.d.ts +65 -0
  149. package/dist/slider.js +194 -0
  150. package/dist/switch.d.ts +103 -0
  151. package/dist/switch.js +71 -0
  152. package/dist/tabs.d.ts +338 -0
  153. package/dist/tabs.js +27 -0
  154. package/dist/text-control.d.ts +89 -0
  155. package/dist/text-control.js +51 -0
  156. package/dist/text-match.d.ts +2 -0
  157. package/dist/text-match.js +7 -0
  158. package/dist/textarea.d.ts +106 -0
  159. package/dist/textarea.js +68 -0
  160. package/dist/time-input.d.ts +131 -0
  161. package/dist/time-input.js +32 -0
  162. package/dist/timeline.d.ts +81 -0
  163. package/dist/timeline.js +30 -0
  164. package/dist/toast-parts.d.ts +203 -0
  165. package/dist/toast-parts.js +270 -0
  166. package/dist/toast.d.ts +2 -0
  167. package/dist/toast.js +21 -0
  168. package/dist/toggle-props.d.ts +22 -0
  169. package/dist/toggle-props.js +14 -0
  170. package/dist/toggle.d.ts +199 -0
  171. package/dist/toggle.js +129 -0
  172. package/dist/token-input.d.ts +136 -0
  173. package/dist/token-input.js +214 -0
  174. package/dist/toolbar-context.d.ts +7 -0
  175. package/dist/toolbar-context.js +20 -0
  176. package/dist/toolbar.d.ts +120 -0
  177. package/dist/toolbar.js +77 -0
  178. package/dist/tooltip.d.ts +160 -0
  179. package/dist/tooltip.js +224 -0
  180. package/dist/tree.d.ts +300 -0
  181. package/dist/tree.js +209 -0
  182. package/dist/trigger-menu.d.ts +452 -0
  183. package/dist/trigger-menu.js +254 -0
  184. package/dist/use-primitive.d.ts +132 -0
  185. package/dist/use-primitive.js +462 -0
  186. package/dist/waterfall.d.ts +201 -0
  187. package/dist/waterfall.js +171 -0
  188. package/docs/README.md +89 -0
  189. package/docs/agents.md +1523 -0
  190. package/docs/components/accordion.md +1369 -0
  191. package/docs/components/alert-dialog.md +799 -0
  192. package/docs/components/autocomplete.md +1857 -0
  193. package/docs/components/avatar.md +272 -0
  194. package/docs/components/button.md +435 -0
  195. package/docs/components/calendar.md +1589 -0
  196. package/docs/components/carousel.md +1171 -0
  197. package/docs/components/checkbox.md +1008 -0
  198. package/docs/components/collapsible.md +536 -0
  199. package/docs/components/color-picker.md +706 -0
  200. package/docs/components/combobox.md +3504 -0
  201. package/docs/components/command.md +1481 -0
  202. package/docs/components/date-input.md +536 -0
  203. package/docs/components/date-picker.md +1244 -0
  204. package/docs/components/dialog.md +1397 -0
  205. package/docs/components/drawer.md +1166 -0
  206. package/docs/components/field.md +538 -0
  207. package/docs/components/file-input.md +611 -0
  208. package/docs/components/hover-card.md +505 -0
  209. package/docs/components/image-cropper.md +556 -0
  210. package/docs/components/input.md +306 -0
  211. package/docs/components/label.md +203 -0
  212. package/docs/components/listbox.md +1004 -0
  213. package/docs/components/menu.md +1229 -0
  214. package/docs/components/menubar.md +443 -0
  215. package/docs/components/meter.md +315 -0
  216. package/docs/components/navigation-menu.md +1523 -0
  217. package/docs/components/number-input.md +1029 -0
  218. package/docs/components/otp-input.md +512 -0
  219. package/docs/components/pagination.md +613 -0
  220. package/docs/components/password-input.md +649 -0
  221. package/docs/components/popover.md +1448 -0
  222. package/docs/components/preferences.md +208 -0
  223. package/docs/components/progress.md +423 -0
  224. package/docs/components/radio.md +785 -0
  225. package/docs/components/rating.md +416 -0
  226. package/docs/components/resizable.md +910 -0
  227. package/docs/components/scroll-area.md +915 -0
  228. package/docs/components/segmented-control.md +435 -0
  229. package/docs/components/select.md +2015 -0
  230. package/docs/components/separator.md +229 -0
  231. package/docs/components/slider.md +1084 -0
  232. package/docs/components/switch.md +565 -0
  233. package/docs/components/tabs.md +944 -0
  234. package/docs/components/textarea.md +299 -0
  235. package/docs/components/time-input.md +224 -0
  236. package/docs/components/timeline.md +654 -0
  237. package/docs/components/toast.md +1224 -0
  238. package/docs/components/toggle.md +500 -0
  239. package/docs/components/token-input.md +953 -0
  240. package/docs/components/toolbar.md +739 -0
  241. package/docs/components/tooltip.md +818 -0
  242. package/docs/components/tree.md +1306 -0
  243. package/docs/components/trigger-menu.md +1223 -0
  244. package/docs/components/waterfall.md +717 -0
  245. package/docs/controllers.md +71 -0
  246. package/docs/core.md +8027 -0
  247. package/docs/custom-parts.md +89 -0
  248. package/docs/first-component.md +83 -0
  249. package/docs/forms.md +122 -0
  250. package/docs/installation.md +72 -0
  251. package/docs/introduction.md +32 -0
  252. package/docs/lists-and-menus.md +122 -0
  253. package/docs/llms.txt +1502 -0
  254. package/docs/parts-and-composition.md +85 -0
  255. package/docs/popups.md +108 -0
  256. package/docs/rendering.md +56 -0
  257. package/docs/server-rendering.md +180 -0
  258. package/docs/state-and-events.md +100 -0
  259. package/docs/styling-and-animation.md +148 -0
  260. package/docs/utilities/async-options.md +348 -0
  261. package/docs/utilities/shortcuts.md +159 -0
  262. package/package.json +352 -3
@@ -0,0 +1,1397 @@
1
+ # Dialog
2
+
3
+ Open a focused task over the page.
4
+
5
+ A dialog gives a task its own space: share a link, edit a record, or collect a few details. It manages focus, Escape, outside presses, and returning to where you started.
6
+
7
+ ```vue title="basic.vue"
8
+ <script setup lang="ts">
9
+ import {
10
+ Dialog,
11
+ DialogBackdrop,
12
+ DialogClose,
13
+ DialogContent,
14
+ DialogDescription,
15
+ DialogTitle,
16
+ DialogTrigger,
17
+ } from "@ui-sdk/vue/dialog"
18
+ import { Field, FieldLabel } from "@ui-sdk/vue/field"
19
+ import { Input } from "@ui-sdk/vue/input"
20
+ import { XIcon } from "@lucide/vue"
21
+ </script>
22
+
23
+ <template>
24
+ <Dialog>
25
+ <DialogTrigger>Share</DialogTrigger>
26
+ <DialogBackdrop />
27
+ <DialogContent>
28
+ <div data-slot="stack">
29
+ <DialogTitle>Share link</DialogTitle>
30
+ <DialogDescription
31
+ >Anyone who has this link will be able to view
32
+ this.</DialogDescription
33
+ >
34
+ </div>
35
+ <Field>
36
+ <FieldLabel>Link</FieldLabel>
37
+ <Input defaultValue="https://example.com/project" readonly />
38
+ </Field>
39
+ <div data-slot="row">
40
+ <DialogClose data-variant="solid">Done</DialogClose>
41
+ </div>
42
+ <DialogClose aria-label="Close">
43
+ <XIcon aria-hidden="true" />
44
+ </DialogClose>
45
+ </DialogContent>
46
+ </Dialog>
47
+ </template>
48
+ ```
49
+
50
+ Open Share, then close it with either Close button, Escape, or a press outside. Focus returns to Share.
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ npm install @ui-sdk/vue @ui-sdk/core
56
+ ```
57
+
58
+ ## Composition
59
+
60
+ Use the following composition to build a `Dialog`:
61
+
62
+ ```text
63
+ Dialog
64
+ ├── DialogTrigger
65
+ ├── DialogBackdrop
66
+ └── DialogContent
67
+ ├── DialogTitle
68
+ ├── DialogDescription
69
+ └── DialogClose
70
+ ```
71
+
72
+ The title names the dialog; the description gives it context. A dialog can have several Close parts. Give an icon-only close button an accessible name, as the first example does.
73
+
74
+ The backdrop is optional. Without one, style the content’s `::backdrop`. For a question that requires an explicit answer, use [Alert Dialog](./alert-dialog.md).
75
+
76
+ ## Usage
77
+
78
+ Import the parts and put them together:
79
+
80
+ ```vue
81
+ <script setup lang="ts">
82
+ import {
83
+ Dialog,
84
+ DialogBackdrop,
85
+ DialogClose,
86
+ DialogContent,
87
+ DialogDescription,
88
+ DialogTitle,
89
+ DialogTrigger,
90
+ } from "@ui-sdk/vue/dialog"
91
+ </script>
92
+
93
+ <template>
94
+ <Dialog>
95
+ <DialogTrigger>Open</DialogTrigger>
96
+ <DialogBackdrop />
97
+ <DialogContent>
98
+ <DialogTitle>Project details</DialogTitle>
99
+ <DialogDescription
100
+ >Review the details before continuing.</DialogDescription
101
+ >
102
+ <DialogClose>Close</DialogClose>
103
+ </DialogContent>
104
+ </Dialog>
105
+ </template>
106
+ ```
107
+
108
+ ## Build Your Own
109
+
110
+ `@ui-sdk/vue/dialog` comes with a ready-made Dialog. If you'd like to build your own, with your own names, start from the `dialog` primitive in `@ui-sdk/core`:
111
+
112
+ ```ts title="share.ts"
113
+ import { dialog } from "@ui-sdk/core/dialog"
114
+ import { createParts } from "@ui-sdk/vue/primitive"
115
+
116
+ const {
117
+ Root: Share,
118
+ Trigger: ShareTrigger,
119
+ Backdrop: ShareBackdrop,
120
+ Content: SharePanel,
121
+ Title: ShareTitle,
122
+ Description: ShareDescription,
123
+ Close: ShareClose,
124
+ } = createParts(dialog, {
125
+ trigger: "button",
126
+ backdrop: "div",
127
+ content: "div",
128
+ title: "h2",
129
+ description: "p",
130
+ close: "button",
131
+ })
132
+
133
+ export {
134
+ Share,
135
+ ShareBackdrop,
136
+ ShareClose,
137
+ ShareDescription,
138
+ SharePanel,
139
+ ShareTitle,
140
+ ShareTrigger,
141
+ }
142
+ ```
143
+
144
+ ```vue
145
+ <template>
146
+ <Share>
147
+ <ShareTrigger>Share</ShareTrigger>
148
+ <ShareBackdrop />
149
+ <SharePanel>
150
+ <ShareTitle>Share this page</ShareTitle>
151
+ <ShareDescription>Anyone with the link can view it.</ShareDescription>
152
+ <ShareClose>Done</ShareClose>
153
+ </SharePanel>
154
+ </Share>
155
+ </template>
156
+ ```
157
+
158
+ Each part keeps the behavior, state and data attributes of its ready-made counterpart: the content opens in the top layer, traps focus and makes the page inert. See [Rendering](../rendering.md) and [Custom Parts](../custom-parts.md) for more.
159
+
160
+ ## Scrollable Content
161
+
162
+ Keep the header and actions outside the scrolling area. A viewport height limit, a shrinking middle grid row, and `overflow-y: auto` keep long content usable. `initial-focus="content"` focuses the dialog itself so reading starts at the top.
163
+
164
+ ```vue title="scrollable.vue"
165
+ <script setup lang="ts">
166
+ import {
167
+ Dialog,
168
+ DialogBackdrop,
169
+ DialogClose,
170
+ DialogContent,
171
+ DialogDescription,
172
+ DialogTitle,
173
+ DialogTrigger,
174
+ } from "@ui-sdk/vue/dialog"
175
+
176
+ const paragraphs = [
177
+ "These terms govern your use of the service. By creating an account, you agree to them and to the privacy policy that explains how we handle your data.",
178
+ "You're responsible for the content you upload and for keeping your password safe. Tell us right away if you think someone else has used your account.",
179
+ "We may change the service, and these terms, as the product grows. When a change affects you, we'll let you know by email at least 30 days before it takes effect.",
180
+ "You can stop using the service at any time. We'll keep your data for 30 days after you close your account, so you can export it, and then delete it.",
181
+ "The service is provided as is. To the extent the law allows, we aren't liable for indirect damages, lost profits or lost data.",
182
+ "If you have questions about these terms, write to us. We read every message and reply within two working days.",
183
+ ]
184
+ </script>
185
+
186
+ <template>
187
+ <Dialog>
188
+ <DialogTrigger>Scrollable Content</DialogTrigger>
189
+ <DialogBackdrop />
190
+ <DialogContent initial-focus="content">
191
+ <div data-slot="stack">
192
+ <DialogTitle>Terms of service</DialogTitle>
193
+ <DialogDescription>Please read before you continue.</DialogDescription>
194
+ </div>
195
+ <div data-slot="scroll">
196
+ <p v-for="paragraph in paragraphs" :key="paragraph">
197
+ {{ paragraph }}
198
+ </p>
199
+ </div>
200
+ <div data-slot="row">
201
+ <DialogClose>Decline</DialogClose>
202
+ <DialogClose data-variant="solid" value="accept">Accept</DialogClose>
203
+ </div>
204
+ </DialogContent>
205
+ </Dialog>
206
+ </template>
207
+ ```
208
+
209
+ Open Scrollable Content in a short window. Scroll the terms while the title and Decline/Accept buttons stay in view.
210
+
211
+ ## Forms
212
+
213
+ A form with `method="dialog"` closes the dialog on a valid submission. The submit button’s `value` becomes the `returnValue`, just like a Close part’s `value`. An ordinary form stays open until you close the dialog yourself.
214
+
215
+ ```vue title="form.vue"
216
+ <script setup lang="ts">
217
+ import {
218
+ Dialog,
219
+ DialogBackdrop,
220
+ DialogContent,
221
+ DialogDescription,
222
+ DialogTitle,
223
+ DialogTrigger,
224
+ } from "@ui-sdk/vue/dialog"
225
+ import { Field, FieldLabel } from "@ui-sdk/vue/field"
226
+ import { Input } from "@ui-sdk/vue/input"
227
+ import { useLog } from "./log"
228
+
229
+ const log = useLog()
230
+
231
+ function onOpenChange(
232
+ open: boolean,
233
+ details: { reason: string; returnValue?: string }
234
+ ) {
235
+ if (!open) {
236
+ log(
237
+ details.returnValue
238
+ ? `Saved as ${details.returnValue}`
239
+ : `Closed (${details.reason})`
240
+ )
241
+ }
242
+ }
243
+ </script>
244
+
245
+ <template>
246
+ <Dialog @open-change="onOpenChange">
247
+ <DialogTrigger>New Article</DialogTrigger>
248
+ <DialogBackdrop />
249
+ <DialogContent>
250
+ <div data-slot="stack">
251
+ <DialogTitle>New article</DialogTitle>
252
+ <DialogDescription
253
+ >Save it as a draft or publish it now.</DialogDescription
254
+ >
255
+ </div>
256
+ <form data-slot="stack" method="dialog">
257
+ <Field>
258
+ <FieldLabel>Title</FieldLabel>
259
+ <Input required defaultValue="Shipping updates for spring" />
260
+ </Field>
261
+ <div data-slot="row">
262
+ <button type="submit" value="draft">Save Draft</button>
263
+ <button data-variant="solid" type="submit" value="published">
264
+ Publish
265
+ </button>
266
+ </div>
267
+ </form>
268
+ </DialogContent>
269
+ </Dialog>
270
+ </template>
271
+ ```
272
+
273
+ Open New Article and choose Save Draft or Publish. The console logs the submitted button's value. An empty title prevents submission.
274
+
275
+ ## Focus
276
+
277
+ Use `initial-focus` to pick where focus starts, and `final-focus` to choose where it goes after closing. Each takes a selector, an element or a function returning one. The default returns to the trigger, or to the element focused before a controller opened the dialog. See [popup focus](../popups.md#focus) for the shared focus rules.
278
+
279
+ ```vue
280
+ <DialogContent initial-focus="#invite-email" :final-focus="() => nextStep" />
281
+ ```
282
+
283
+ ## Controlled
284
+
285
+ Use `v-model:open` to keep the dialog's state in your component. Cancel a proposed change with `details.cancel()` in `@open-change`. `@open-change-complete` reports when opening or closing has finished; see [state and events](../state-and-events.md) for the common change contract.
286
+
287
+ ```vue title="controlled.vue"
288
+ <script setup lang="ts">
289
+ import { CheckIcon } from "@lucide/vue"
290
+ import { Checkbox, CheckboxIndicator } from "@ui-sdk/vue/checkbox"
291
+ import {
292
+ Dialog,
293
+ DialogBackdrop,
294
+ DialogClose,
295
+ DialogContent,
296
+ DialogDescription,
297
+ DialogTitle,
298
+ DialogTrigger,
299
+ } from "@ui-sdk/vue/dialog"
300
+ import { ref } from "vue"
301
+ import { useLog } from "./log"
302
+
303
+ const topics = [
304
+ { label: "Product updates", checked: true },
305
+ { label: "Security alerts", checked: true },
306
+ { label: "Weekly digest", checked: false },
307
+ ]
308
+
309
+ const log = useLog()
310
+ const open = ref(false)
311
+
312
+ function onOpenChange(next: boolean, details: { reason: string }) {
313
+ log(`${next ? "Opened" : "Closed"} (${details.reason})`)
314
+ }
315
+ </script>
316
+
317
+ <template>
318
+ <div data-slot="stack">
319
+ <div data-slot="controls">
320
+ <label data-slot="label">
321
+ <Checkbox
322
+ :checked="open"
323
+ @checked-change="(next) => (open = next === true)"
324
+ >
325
+ <CheckboxIndicator>
326
+ <CheckIcon aria-hidden="true" />
327
+ </CheckboxIndicator>
328
+ </Checkbox>
329
+ Open
330
+ </label>
331
+ </div>
332
+ <Dialog
333
+ v-model:open="open"
334
+ @open-change="onOpenChange"
335
+ @open-change-complete="
336
+ (next: boolean) => log(next ? 'Finished opening' : 'Finished closing')
337
+ "
338
+ >
339
+ <DialogTrigger>Notification Settings</DialogTrigger>
340
+ <DialogBackdrop />
341
+ <DialogContent>
342
+ <div data-slot="stack">
343
+ <DialogTitle>Notification settings</DialogTitle>
344
+ <DialogDescription>Choose what we email you about.</DialogDescription>
345
+ </div>
346
+ <div data-slot="stack">
347
+ <label v-for="topic in topics" :key="topic.label" data-slot="label">
348
+ <Checkbox :default-checked="topic.checked">
349
+ <CheckboxIndicator>
350
+ <CheckIcon aria-hidden="true" />
351
+ </CheckboxIndicator>
352
+ </Checkbox>
353
+ {{ topic.label }}
354
+ </label>
355
+ </div>
356
+ <div data-slot="row">
357
+ <DialogClose data-variant="solid">Done</DialogClose>
358
+ </div>
359
+ </DialogContent>
360
+ </Dialog>
361
+ </div>
362
+ </template>
363
+ ```
364
+
365
+ Check Open or press Notification Settings, then close the dialog. The console logs each change, its reason and when it finishes.
366
+
367
+ ## Keyboard Shortcut
368
+
369
+ A trigger’s `shortcut` opens it from the keyboard. `mod+j` means Command+J on Apple platforms and Control+J elsewhere. The Shortcut part displays the right keys and the trigger exposes `aria-keyshortcuts`.
370
+
371
+ ```vue title="shortcut.vue"
372
+ <script setup lang="ts">
373
+ import {
374
+ Dialog,
375
+ DialogBackdrop,
376
+ DialogClose,
377
+ DialogContent,
378
+ DialogDescription,
379
+ DialogShortcut,
380
+ DialogTitle,
381
+ DialogTrigger,
382
+ } from "@ui-sdk/vue/dialog"
383
+ import { Field, FieldLabel } from "@ui-sdk/vue/field"
384
+ import { Input } from "@ui-sdk/vue/input"
385
+ import { PlusIcon } from "@lucide/vue"
386
+ </script>
387
+
388
+ <template>
389
+ <Dialog>
390
+ <DialogTrigger shortcut="mod+j">
391
+ <PlusIcon aria-hidden="true" />
392
+ New Contact
393
+ <DialogShortcut />
394
+ </DialogTrigger>
395
+ <DialogBackdrop />
396
+ <DialogContent>
397
+ <div data-slot="stack">
398
+ <DialogTitle>New contact</DialogTitle>
399
+ <DialogDescription>Add someone to your address book.</DialogDescription>
400
+ </div>
401
+ <form data-slot="stack" method="dialog">
402
+ <Field>
403
+ <FieldLabel>Name</FieldLabel>
404
+ <Input name="name" required placeholder="Kofi Mensah" />
405
+ </Field>
406
+ <Field>
407
+ <FieldLabel>Email</FieldLabel>
408
+ <Input name="email" type="email" required placeholder="kofi@example.com" />
409
+ </Field>
410
+ <div data-slot="row">
411
+ <DialogClose>Cancel</DialogClose>
412
+ <button data-variant="solid" type="submit" value="save">
413
+ Save Contact
414
+ </button>
415
+ </div>
416
+ </form>
417
+ </DialogContent>
418
+ </Dialog>
419
+ </template>
420
+ ```
421
+
422
+ Press Command+J or Control+J to open New contact. The button shows the keys, and Escape returns focus to it.
423
+
424
+ ## Animation
425
+
426
+ The content and backdrop have `data-starting-style` while they open and `data-ending-style` while they close. Transition from those states, and the dialog stays mounted until the exit finishes. `@open-change-complete` runs after the transition, and `keep-mounted` keeps the content in the page while closed.
427
+
428
+ ```css
429
+ [data-slot="dialog-content"] {
430
+ transition:
431
+ opacity 0.2s,
432
+ translate 0.25s cubic-bezier(0.22, 1, 0.36, 1),
433
+ scale 0.25s cubic-bezier(0.22, 1, 0.36, 1);
434
+
435
+ &:is([data-starting-style], [data-ending-style]) {
436
+ opacity: 0;
437
+ translate: 0 16px;
438
+ scale: 0.96;
439
+ }
440
+
441
+ &[data-ending-style] {
442
+ transition-duration: 0.15s;
443
+ }
444
+ }
445
+ ```
446
+
447
+ ```vue title="animation.vue"
448
+ <script setup lang="ts">
449
+ import {
450
+ Dialog,
451
+ DialogBackdrop,
452
+ DialogClose,
453
+ DialogContent,
454
+ DialogDescription,
455
+ DialogTitle,
456
+ DialogTrigger,
457
+ } from "@ui-sdk/vue/dialog"
458
+ import { useLog } from "./log"
459
+
460
+ const log = useLog()
461
+ </script>
462
+
463
+ <template>
464
+ <Dialog
465
+ @open-change-complete="
466
+ (open: boolean) => log(open ? 'Finished opening' : 'Finished closing')
467
+ "
468
+ >
469
+ <DialogTrigger>Slide In</DialogTrigger>
470
+ <DialogBackdrop />
471
+ <DialogContent keep-mounted>
472
+ <div data-slot="stack">
473
+ <DialogTitle>Slide and fade</DialogTitle>
474
+ <DialogDescription
475
+ >300ms in and out. Focus returns as soon as it starts
476
+ closing.</DialogDescription
477
+ >
478
+ </div>
479
+ <div data-slot="row">
480
+ <DialogClose data-variant="solid">Close</DialogClose>
481
+ </div>
482
+ </DialogContent>
483
+ </Dialog>
484
+ </template>
485
+ ```
486
+
487
+ Open the dialog and close it. The console logs when each animation finishes; focus returns as closing starts.
488
+
489
+ ## Modal
490
+
491
+ `modal` controls how the dialog shares the page:
492
+
493
+ - `true`, the default, makes the page inert, locks scrolling, and keeps Tab inside.
494
+ - `"trap-focus"` keeps Tab inside while the page can still scroll and receive clicks. A click on the page also closes the dialog, unless `close-on-outside-press` is false.
495
+ - `false` puts the dialog beside the page and lets focus leave it. Escape closes it while focus is inside; outside presses close it only when `close-on-outside-press` is enabled.
496
+
497
+ To keep a small part of the page usable while a modal dialog is open, like a chat launcher, pass it to `persistent-elements`: an array of elements or selectors, or a function returning elements when the dialog opens. Persistent elements don't change stacking on their own.
498
+
499
+ Dialogs render where you put them and use the browser's top layer. Set `container` when their DOM needs to live elsewhere, like a shadow root. The [popup guide](../popups.md#mounting-and-containers) covers containers and iframes.
500
+
501
+ ```vue title="modal.vue"
502
+ <script setup lang="ts">
503
+ import { CheckIcon, ChevronDownIcon } from "@lucide/vue"
504
+ import { Checkbox, CheckboxIndicator } from "@ui-sdk/vue/checkbox"
505
+ import {
506
+ Dialog,
507
+ DialogBackdrop,
508
+ DialogClose,
509
+ DialogContent,
510
+ DialogDescription,
511
+ DialogTitle,
512
+ DialogTrigger,
513
+ } from "@ui-sdk/vue/dialog"
514
+ import {
515
+ Select,
516
+ SelectContent,
517
+ SelectItem,
518
+ SelectItemIndicator,
519
+ SelectLabel,
520
+ SelectTrigger,
521
+ SelectValue,
522
+ } from "@ui-sdk/vue/select"
523
+ import { computed, ref } from "vue"
524
+
525
+ const modes = [
526
+ { value: "true", label: "Modal" },
527
+ { value: "trap-focus", label: "Trap focus" },
528
+ { value: "false", label: "Non-modal" },
529
+ ]
530
+
531
+ type Mode = "true" | "trap-focus" | "false"
532
+
533
+ const notes = {
534
+ true: "The rest of the page is inert and doesn't scroll.",
535
+ "trap-focus":
536
+ "Tab stays inside, and the page still scrolls and takes clicks.",
537
+ false:
538
+ "The page stays usable. Tab leaves the notes, and Escape closes them while focus is inside.",
539
+ }
540
+
541
+ const mode = ref<Mode>("false")
542
+ const outside = ref(false)
543
+ const modal = computed(() =>
544
+ mode.value === "true" ? true : mode.value === "false" ? false : "trap-focus"
545
+ )
546
+ </script>
547
+
548
+ <template>
549
+ <div data-slot="stack">
550
+ <div data-slot="controls">
551
+ <Select
552
+ :items="modes"
553
+ :value="mode"
554
+ @value-change="(next) => next && (mode = next as Mode)"
555
+ >
556
+ <SelectLabel>Modal</SelectLabel>
557
+ <SelectTrigger>
558
+ <SelectValue />
559
+ <ChevronDownIcon />
560
+ </SelectTrigger>
561
+ <SelectContent>
562
+ <SelectItem v-for="option in modes" :key="option.value" :value="option.value">
563
+ {{ option.label }}
564
+ <SelectItemIndicator>
565
+ <CheckIcon />
566
+ </SelectItemIndicator>
567
+ </SelectItem>
568
+ </SelectContent>
569
+ </Select>
570
+ <label data-slot="label">
571
+ <Checkbox
572
+ :checked="outside"
573
+ @checked-change="(next) => (outside = next === true)"
574
+ >
575
+ <CheckboxIndicator>
576
+ <CheckIcon aria-hidden="true" />
577
+ </CheckboxIndicator>
578
+ </Checkbox>
579
+ Close on outside press
580
+ </label>
581
+ </div>
582
+ <Dialog :modal="modal" :close-on-outside-press="outside">
583
+ <DialogTrigger>Open Notes</DialogTrigger>
584
+ <DialogBackdrop v-if="modal === true" />
585
+ <DialogContent>
586
+ <div data-slot="stack">
587
+ <DialogTitle>Notes</DialogTitle>
588
+ <DialogDescription>{{ notes[mode] }}</DialogDescription>
589
+ </div>
590
+ <textarea aria-label="Notes">
591
+ Call the printer about the proofs.</textarea>
592
+ <div data-slot="row">
593
+ <DialogClose data-variant="solid">Done</DialogClose>
594
+ </div>
595
+ </DialogContent>
596
+ </Dialog>
597
+ </div>
598
+ </template>
599
+ ```
600
+
601
+ Choose a mode, open Notes, and try Tab or clicking the page. Check Close on outside press to compare dismissal.
602
+
603
+ ## Warn Before Closing a Dirty Form
604
+
605
+ Set `dismissible` to false while there are unsaved changes. Escape, an outside press, or a back gesture then keeps the dialog open and emits `@dismiss-attempt`. Explicit Close parts and controller calls still work, so you can offer a deliberate way out.
606
+
607
+ ```vue title="dirty-form.vue"
608
+ <script setup lang="ts">
609
+ import {
610
+ Dialog,
611
+ DialogBackdrop,
612
+ DialogClose,
613
+ DialogContent,
614
+ DialogDescription,
615
+ DialogTitle,
616
+ DialogTrigger,
617
+ } from "@ui-sdk/vue/dialog"
618
+ import { Field, FieldControl, FieldLabel } from "@ui-sdk/vue/field"
619
+ import { ref } from "vue"
620
+
621
+ const open = ref(false)
622
+ const dirty = ref(false)
623
+ const asking = ref(false)
624
+ let post: HTMLTextAreaElement | undefined
625
+
626
+ function onInput(event: Event) {
627
+ post = event.target as HTMLTextAreaElement
628
+ dirty.value = post.value !== ""
629
+ }
630
+
631
+ function close() {
632
+ open.value = false
633
+ dirty.value = false
634
+ asking.value = false
635
+ }
636
+
637
+ function keepEditing() {
638
+ asking.value = false
639
+ post?.focus()
640
+ }
641
+ </script>
642
+
643
+ <template>
644
+ <Dialog
645
+ :open="open"
646
+ :dismissible="!dirty"
647
+ @update:open="(next: boolean) => (next ? (open = true) : close())"
648
+ @dismiss-attempt="asking = true"
649
+ >
650
+ <DialogTrigger>Write a Post</DialogTrigger>
651
+ <DialogBackdrop />
652
+ <DialogContent>
653
+ <div data-slot="stack">
654
+ <DialogTitle>New post</DialogTitle>
655
+ <DialogDescription>Share an update with your team.</DialogDescription>
656
+ </div>
657
+ <Field>
658
+ <FieldLabel>Post</FieldLabel>
659
+ <FieldControl @input="onInput">
660
+ <template #render="{ props }">
661
+ <textarea v-bind="props" placeholder="What's new?" />
662
+ </template>
663
+ </FieldControl>
664
+ </Field>
665
+ <div v-if="asking" data-slot="row" role="alert">
666
+ Discard your changes?
667
+ <div data-slot="row">
668
+ <button type="button" @click="keepEditing">Keep Editing</button>
669
+ <button data-variant="solid" type="button" @click="close">
670
+ Discard
671
+ </button>
672
+ </div>
673
+ </div>
674
+ <div v-else data-slot="row">
675
+ <DialogClose data-variant="solid" value="post">Post</DialogClose>
676
+ </div>
677
+ </DialogContent>
678
+ </Dialog>
679
+ </template>
680
+ ```
681
+
682
+ Type a post, then press Escape or click outside. Keep editing returns to the field; Discard closes the dialog and clears the draft.
683
+
684
+ ## Nested Dialogs
685
+
686
+ A dialog opened inside another joins its stack. Escape closes the topmost dialog and returns focus to its trigger. Closing the parent also closes its children. `data-nested-open` and `--dialog-stack` let you show which layer is active.
687
+
688
+ ```vue title="nested.vue"
689
+ <script setup lang="ts">
690
+ import {
691
+ Dialog,
692
+ DialogBackdrop,
693
+ DialogClose,
694
+ DialogContent,
695
+ DialogDescription,
696
+ DialogTitle,
697
+ DialogTrigger,
698
+ } from "@ui-sdk/vue/dialog"
699
+ import { Field, FieldLabel } from "@ui-sdk/vue/field"
700
+ import { Input } from "@ui-sdk/vue/input"
701
+ </script>
702
+
703
+ <template>
704
+ <Dialog>
705
+ <DialogTrigger>Account</DialogTrigger>
706
+ <DialogBackdrop />
707
+ <DialogContent>
708
+ <div data-slot="stack">
709
+ <DialogTitle>Account</DialogTitle>
710
+ <DialogDescription>Manage how you sign in.</DialogDescription>
711
+ </div>
712
+ <div data-slot="card">
713
+ <div data-slot="stack">
714
+ <span>Password</span>
715
+ <span data-slot="hint">Last changed 3 months ago</span>
716
+ </div>
717
+ <Dialog>
718
+ <DialogTrigger>Change Password</DialogTrigger>
719
+ <DialogBackdrop />
720
+ <DialogContent>
721
+ <div data-slot="stack">
722
+ <DialogTitle>Change password</DialogTitle>
723
+ <DialogDescription>Use at least 12 characters.</DialogDescription>
724
+ </div>
725
+ <Field>
726
+ <FieldLabel>New password</FieldLabel>
727
+ <Input type="password" />
728
+ </Field>
729
+ <div data-slot="row">
730
+ <DialogClose>Cancel</DialogClose>
731
+ <DialogClose data-variant="solid">Update</DialogClose>
732
+ </div>
733
+ </DialogContent>
734
+ </Dialog>
735
+ </div>
736
+ <div data-slot="row">
737
+ <DialogClose data-variant="solid">Done</DialogClose>
738
+ </div>
739
+ </DialogContent>
740
+ </Dialog>
741
+ </template>
742
+ ```
743
+
744
+ Open Account, then Change Password. Press Escape once to return to Account, and again to close it.
745
+
746
+ ## Opening from a Menu Item
747
+
748
+ Render the dialog outside the menu and open it with the dialog’s controller. When the item disappears, the menu’s trigger becomes the place to return focus.
749
+
750
+ ```vue title="from-menu.vue"
751
+ <script setup lang="ts">
752
+ import {
753
+ createDialogController,
754
+ Dialog,
755
+ DialogBackdrop,
756
+ DialogClose,
757
+ DialogContent,
758
+ DialogDescription,
759
+ DialogTitle,
760
+ } from "@ui-sdk/vue/dialog"
761
+ import { Field, FieldLabel } from "@ui-sdk/vue/field"
762
+ import { Input } from "@ui-sdk/vue/input"
763
+ import { Menu, MenuContent, MenuItem, MenuTrigger } from "@ui-sdk/vue/menu"
764
+ import { CopyIcon, EllipsisIcon, PencilIcon } from "@lucide/vue"
765
+
766
+ const rename = createDialogController()
767
+ </script>
768
+
769
+ <template>
770
+ <Menu>
771
+ <MenuTrigger aria-label="File actions">
772
+ <EllipsisIcon aria-hidden="true" />
773
+ </MenuTrigger>
774
+ <MenuContent>
775
+ <MenuItem @select="rename.open()">
776
+ <PencilIcon aria-hidden="true" />
777
+ Rename…
778
+ </MenuItem>
779
+ <MenuItem>
780
+ <CopyIcon aria-hidden="true" />
781
+ Duplicate
782
+ </MenuItem>
783
+ </MenuContent>
784
+ </Menu>
785
+ <Dialog :controller="rename">
786
+ <DialogBackdrop />
787
+ <DialogContent>
788
+ <div data-slot="stack">
789
+ <DialogTitle>Rename file</DialogTitle>
790
+ <DialogDescription>Names must be unique in a folder.</DialogDescription>
791
+ </div>
792
+ <Field>
793
+ <FieldLabel>Name</FieldLabel>
794
+ <Input defaultValue="report.pdf" />
795
+ </Field>
796
+ <div data-slot="row">
797
+ <DialogClose>Cancel</DialogClose>
798
+ <DialogClose data-variant="solid" value="rename">Rename</DialogClose>
799
+ </div>
800
+ </DialogContent>
801
+ </Dialog>
802
+ </template>
803
+ ```
804
+
805
+ Open File Actions and choose Rename. Close the dialog to return focus to the File Actions button.
806
+
807
+ ## Opening from Anywhere
808
+
809
+ Connect several triggers to one `createDialogController<Payload, Result>()`. Each trigger passes a `payload`; a typed content alias, such as `DialogContent<Invoice>`, reads it from `state.payload` in its default slot. `controller.open({ payload })` opens it from code and returns a promise that resolves when it closes, with the reason and any `returnValue`. Each trigger remains the return-focus target for the dialog it opened. See [controllers](../controllers.md) for the shared controller pattern.
810
+
811
+ ```vue title="payload.vue"
812
+ <script setup lang="ts">
813
+ import {
814
+ createDialogController,
815
+ Dialog,
816
+ DialogBackdrop,
817
+ DialogClose,
818
+ DialogContent,
819
+ DialogDescription,
820
+ DialogTitle,
821
+ DialogTrigger,
822
+ } from "@ui-sdk/vue/dialog"
823
+ import { useLog } from "./log"
824
+
825
+ type Invoice = {
826
+ id: string
827
+ customer: string
828
+ amount: string
829
+ status: "Paid" | "Due"
830
+ }
831
+
832
+ const invoices: Invoice[] = [
833
+ { id: "INV-001", customer: "Acme", amount: "$250.00", status: "Paid" },
834
+ { id: "INV-002", customer: "Globex", amount: "$150.00", status: "Due" },
835
+ { id: "INV-003", customer: "Initech", amount: "$350.00", status: "Due" },
836
+ ]
837
+
838
+ const reminder = createDialogController<Invoice, "send">()
839
+ const InvoiceDialogContent = DialogContent<Invoice>
840
+ const log = useLog()
841
+
842
+ async function remindFromCode() {
843
+ const { reason, returnValue } = await reminder.open({ payload: invoices[2] })
844
+ log(returnValue === "send" ? "Reminder sent from code" : `Closed (${reason})`)
845
+ }
846
+
847
+ function onOpenChange(open: boolean, details: { returnValue?: string }) {
848
+ if (!open && details.returnValue === "send") {
849
+ log("Reminder sent")
850
+ }
851
+ }
852
+ </script>
853
+
854
+ <template>
855
+ <div data-slot="stack">
856
+ <div data-slot="controls">
857
+ <button type="button" @click="remindFromCode">Open from Code</button>
858
+ </div>
859
+ <ul data-slot="list">
860
+ <li v-for="invoice in invoices" :key="invoice.id" data-slot="row">
861
+ {{ invoice.customer }}
862
+ <span data-slot="hint"
863
+ >{{ invoice.amount }} · {{ invoice.status }}</span
864
+ >
865
+ <DialogTrigger
866
+ :controller="reminder"
867
+ :payload="invoice"
868
+ :disabled="invoice.status === 'Paid'"
869
+ :aria-label="`Remind ${invoice.customer}`"
870
+ >
871
+ Remind
872
+ </DialogTrigger>
873
+ </li>
874
+ </ul>
875
+ <Dialog :controller="reminder" @open-change="onOpenChange">
876
+ <DialogBackdrop />
877
+ <InvoiceDialogContent v-slot="{ state }">
878
+ <div data-slot="stack">
879
+ <DialogTitle>Remind {{ state.payload?.customer }}</DialogTitle>
880
+ <DialogDescription>
881
+ {{ state.payload?.id }} for {{ state.payload?.amount }} is due.
882
+ We'll email the billing contact.
883
+ </DialogDescription>
884
+ </div>
885
+ <div data-slot="row">
886
+ <DialogClose>Cancel</DialogClose>
887
+ <DialogClose data-variant="solid" value="send"
888
+ >Send reminder</DialogClose
889
+ >
890
+ </div>
891
+ </InvoiceDialogContent>
892
+ </Dialog>
893
+ </div>
894
+ </template>
895
+ ```
896
+
897
+ Open a reminder from either unpaid invoice, or press Open from Code. The same dialog shows that invoice, and the console logs how it closed.
898
+
899
+ ## Presentation
900
+
901
+ `presentation="sheet"` presents the same task as a sheet. Use `"auto"` to choose a sheet on small touch screens and a centered dialog elsewhere, decided each time it opens. A swipe down closes a dismissible sheet; a protected one springs back and reports a dismissal attempt.
902
+
903
+ Style `data-presentation` to support both layouts. The sheet manages its own position and movement, so do not apply the centered dialog’s translation to it. `--sheet-backdrop` styles the dimmed page when no Backdrop part is present.
904
+
905
+ ```vue title="presentation.vue"
906
+ <script setup lang="ts">
907
+ import {
908
+ Dialog,
909
+ DialogClose,
910
+ DialogContent,
911
+ DialogDescription,
912
+ DialogTitle,
913
+ DialogTrigger,
914
+ } from "@ui-sdk/vue/dialog"
915
+ import { Field, FieldLabel } from "@ui-sdk/vue/field"
916
+ import { Input } from "@ui-sdk/vue/input"
917
+ </script>
918
+
919
+ <template>
920
+ <Dialog presentation="sheet">
921
+ <DialogTrigger>Edit Profile</DialogTrigger>
922
+ <DialogContent>
923
+ <div data-slot="stack">
924
+ <DialogTitle>Edit profile</DialogTitle>
925
+ <DialogDescription
926
+ >Make changes to your profile here. Save when you're
927
+ done.</DialogDescription
928
+ >
929
+ </div>
930
+ <Field>
931
+ <FieldLabel>Name</FieldLabel>
932
+ <Input defaultValue="Hollis Grant" />
933
+ </Field>
934
+ <Field>
935
+ <FieldLabel>Username</FieldLabel>
936
+ <Input defaultValue="@hollis" />
937
+ </Field>
938
+ <div data-slot="row">
939
+ <DialogClose>Cancel</DialogClose>
940
+ <DialogClose data-variant="solid" value="save"
941
+ >Save Changes</DialogClose
942
+ >
943
+ </div>
944
+ </DialogContent>
945
+ </Dialog>
946
+ </template>
947
+ ```
948
+
949
+ Open Edit Profile as a sheet. On a touch screen, drag it down to dismiss it, or use Cancel or Save Changes.
950
+
951
+ ## Custom Rendering
952
+
953
+ Every part takes a `render` slot, which receives the props to bind and the part's state, and the default slot receives the state too.
954
+
955
+ ```vue title="custom-rendering.vue"
956
+ <script setup lang="ts">
957
+ import {
958
+ Dialog,
959
+ DialogBackdrop,
960
+ DialogClose,
961
+ DialogContent,
962
+ DialogDescription,
963
+ DialogShortcut,
964
+ DialogTitle,
965
+ DialogTrigger,
966
+ } from "@ui-sdk/vue/dialog"
967
+ </script>
968
+
969
+ <template>
970
+ <Dialog>
971
+ <DialogTrigger shortcut="mod+i">
972
+ <template #render="{ props, state }">
973
+ <button v-bind="props">
974
+ {{ state.open ? "Viewing" : "View Details" }}
975
+ <DialogShortcut v-slot="{ state: shortcut }">{{
976
+ shortcut.text
977
+ }}</DialogShortcut>
978
+ </button>
979
+ </template>
980
+ </DialogTrigger>
981
+ <DialogBackdrop />
982
+ <DialogContent v-slot="{ state }">
983
+ <div data-slot="stack">
984
+ <DialogTitle>
985
+ <template #render="{ props }">
986
+ <h3 v-bind="props">Details</h3>
987
+ </template>
988
+ </DialogTitle>
989
+ <DialogDescription>
990
+ {{
991
+ state.nested
992
+ ? "Opened from another dialog."
993
+ : "Quarterly report, updated today."
994
+ }}
995
+ </DialogDescription>
996
+ </div>
997
+ <div data-slot="row">
998
+ <DialogClose>Close</DialogClose>
999
+ <DialogClose data-variant="solid" href="https://example.com/details">
1000
+ <template #render="{ props }">
1001
+ <a v-bind="props">Open the Full Page</a>
1002
+ </template>
1003
+ </DialogClose>
1004
+ </div>
1005
+ </DialogContent>
1006
+ </Dialog>
1007
+ </template>
1008
+ ```
1009
+
1010
+ Open View Details. The trigger's label and the description come from their state, and Close renders as a link.
1011
+
1012
+ ## Styling
1013
+
1014
+ Each part has a `data-slot`, such as `dialog-content`, and shows its state through the attributes listed under [Data Attributes](#data-attributes). Use `data-open` and `data-closed` for each state, and `data-starting-style` and `data-ending-style` for transitions.
1015
+
1016
+ ### DialogContent
1017
+ - `--dialog-stack`: How many dialogs are open above this one, nested inside it. 0 for the topmost.
1018
+ - `--keyboard-inset`: Unless it presents as a sheet: how many pixels of the page the on-screen keyboard covers, 0 without one.
1019
+ - `--visual-viewport-width`: Unless it presents as a sheet: the width of the part of the page you can see.
1020
+ - `--visual-viewport-height`: Unless it presents as a sheet: the height of the part of the page you can see, above the keyboard.
1021
+ - `--visual-viewport-top`: Unless it presents as a sheet: how far the part of the page you can see is scrolled from the top of the layout, as when the keyboard pushes it up.
1022
+ - `--drawer-progress`: While it presents as a sheet: how far open the sheet is, from 0 closed to 1 at its largest snap point. Never past 1.
1023
+ - `--drawer-dim`: While it presents as a sheet: how dim the page behind is, from 0 to 1. The backdrop's opacity follows it.
1024
+ - `--drawer-expand`: While it presents as a sheet: 0 at the second-largest snap point and below, 1 at the largest. With one snap point, the whole travel.
1025
+ - `--drawer-stack`: While it presents as a sheet: how many sheets opened from inside this one are open above it, rising smoothly as each one opens. Push it back with it.
1026
+ - `--sheet-backdrop`: Set it yourself: the color the page dims to while it presents as a sheet. Black at 0.2 by default.
1027
+
1028
+ ### Viewport
1029
+ - `--keyboard-inset`: While it presents as a sheet: how many pixels of the page the on-screen keyboard covers, 0 without one.
1030
+ - `--visual-viewport-width`: While it presents as a sheet: the width of the part of the page you can see.
1031
+ - `--visual-viewport-height`: While it presents as a sheet: the height of the part of the page you can see, above the keyboard.
1032
+ - `--visual-viewport-top`: While it presents as a sheet: how far the part of the page you can see is scrolled from the top of the layout, as when the keyboard pushes it up.
1033
+
1034
+ ## Data Attributes
1035
+
1036
+ ### DialogTrigger
1037
+ `[data-slot="dialog-trigger"]`
1038
+
1039
+ - `data-closed`: Present while the dialog is closed, including while it animates out.
1040
+ - `data-disabled`: Present when the trigger is disabled.
1041
+ - `data-open`: Present while the dialog is open. On a trigger, while it's open from that trigger.
1042
+
1043
+ ### DialogBackdrop
1044
+ `[data-slot="dialog-backdrop"]`
1045
+
1046
+ - `data-closed`: Present while the dialog is closed, including while it animates out.
1047
+ - `data-open`: Present while the dialog is open. On a trigger, while it's open from that trigger.
1048
+ - `data-starting-style`: Present for the first frame. Style the state you animate from.
1049
+ - `data-ending-style`: Present while the dialog animates out. Style the state you animate to.
1050
+
1051
+ ### DialogContent
1052
+ `[data-slot="dialog-content"]`
1053
+
1054
+ - `data-closed`: Present while the dialog is closed, including while it animates out.
1055
+ - `data-presentation` (`dialog`, `sheet`): How the dialog presents this time it's open.
1056
+ - `data-open`: Present while the dialog is open. On a trigger, while it's open from that trigger.
1057
+ - `data-starting-style`: Present for the first frame. Style the state you animate from.
1058
+ - `data-ending-style`: Present while the dialog animates out. Style the state you animate to.
1059
+ - `data-keyboard`: Present while the on-screen keyboard covers part of the page.
1060
+
1061
+ ### DialogClose
1062
+ `[data-slot="dialog-close"]`
1063
+
1064
+ - `data-pending`: Present on a Close part while its action runs.
1065
+
1066
+ ## API Reference
1067
+
1068
+ Props are the same as below, and callbacks are events.
1069
+
1070
+ ### Dialog
1071
+
1072
+ - `container?` (`DocumentFragment | Element | null`): Renders the popup into this element instead of in place, like a container inside a shadow root or an iframe. On web components, a CSS selector. Nested popups stay inside their parent.
1073
+ - `open?` (`boolean`): Whether the dialog is open. Pass it to control the dialog.
1074
+ - `defaultOpen?` (`boolean`): Whether the dialog starts open, when uncontrolled. Focus moves into it once it's on the page.
1075
+ - `@open-change-complete` (`((open: boolean) => void)`): Runs once the dialog has finished opening or closing, after any animation.
1076
+ - `dismissible?` (`boolean`): Whether Escape, an outside press and Android's back gesture can close the dialog. Close parts, triggers and the controller always can. Pass `false` while a form has unsaved changes, and ask in `@dismiss-attempt`.
1077
+ - `@dismiss-attempt` (`((details: DialogDismissAttemptDetails) => void)`): Runs when a dismissal was refused, because `dismissible` is false, an outside press doesn't close it, or a Close part's action is running. Ask "Discard changes?" here.
1078
+ - `persistentElements?` (`(string | Element)[] | (() => Element[])`): Elements that stay usable while the dialog makes the page inert, like a chat launcher or a password manager's button. Elements, CSS selectors, or a function that returns elements, read when the dialog opens. Presses on them never close it.
1079
+ - `pendingAnnouncement?` (`string`): What screen readers hear once a Close part's action has been running for a second, so a slow action doesn't pass in silence. Set it in the page's language. An empty string says nothing.
1080
+ - `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.
1081
+ - `@open-change` (`((open: boolean, details: DialogOpenChangeDetails) => void)`): Runs when the dialog wants to open or close. `details.reason` says why, `details.cancel()` stops it, and `details.returnValue` carries what closed it.
1082
+ - `@payload-change` (`((payload: unknown, details: ChangeDetails<DialogPayloadChangeReason>) => void)`): Runs when the payload the content shows changes: a trigger with another payload opens the dialog, or takes a non-modal dialog over while it's open, which fires no `@open-change`, and so does a controller's `open({ payload })` on the open dialog. `details.cancel()` keeps the previous payload.
1083
+ - `modal?` (`"trap-focus" | boolean`): `true`: the rest of the page is inert and doesn't scroll, focus is trapped, and an outside press only closes the dialog. `"trap-focus"`: traps focus, and leaves the page scrollable and clickable. `false`: a dialog beside the page, which Tab can leave and outside presses don't close.
1084
+ - `closeOnOutsidePress?` (`boolean`): Whether pressing outside the dialog, like on its backdrop, closes it. The press counts once it's released outside too, so dragging a text selection out of a field doesn't close it.
1085
+ - `presentation?` (`PresentationSetting<"dialog">`): How the dialog presents. `"sheet"` slides it up from the bottom of the screen, where it can be swiped away, and `"auto"` does that on small touch screens. Decided each time it opens; the server renders `"dialog"` for `"auto"`.
1086
+ - `controller?` (`PopupController<any, DialogOpenChangeReason, {}, any, DialogPayloadChangeReason>`): Opens, closes and inspects the dialog from anywhere, and connects triggers outside it. Create one with `createDialogController()`.
1087
+
1088
+ ### DialogTrigger
1089
+
1090
+ A trigger outside the dialog also takes `controller`.
1091
+
1092
+ - `disabled?` (`boolean`): Stops the trigger from opening the dialog. A native button can't be focused or pressed, unless `focusableWhenDisabled` is set.
1093
+ - `focusableWhenDisabled?` (`boolean`): Keeps a disabled trigger in the tab order with `aria-disabled` instead of the native attribute, so it can host a tooltip that says why. Presses still do nothing.
1094
+ - `payload?` (`unknown`): Data for the content to render when this trigger opens the dialog, like the row it belongs to.
1095
+ - `shortcut?` (`string`): A keyboard shortcut that presses the trigger, like `"mod+k"`. `mod` is Command on Apple platforms and Control elsewhere. Sets `aria-keyshortcuts`, and a Shortcut part inside shows it. A modal dialog pauses the page's shortcuts while it's open.
1096
+ - `controller?` (`PopupController<any, DialogOpenChangeReason, {}, any, DialogPayloadChangeReason>`): Connects a trigger outside the dialog, like one per table row, to the dialog with this controller.
1097
+
1098
+ ### DialogBackdrop
1099
+
1100
+ Dims the page behind a modal dialog. Renders a `div`.
1101
+
1102
+ ### DialogContent
1103
+
1104
+ - `container?` (`DocumentFragment | Element | null`): Renders the popup into this element instead of in place, like a container inside a shadow root or an iframe. On web components, a CSS selector. Nested popups stay inside their parent.
1105
+ - `initialFocus?` (`DialogInitialFocus`): Where focus goes when the dialog opens. Opening with touch focuses the dialog itself, so no on-screen keyboard appears.
1106
+ - `finalFocus?` (`PopupFocusTarget`): Where focus goes when the dialog closes while focus is inside it.
1107
+ - `keepMounted?` (`boolean`): Whether the dialog stays in the page, hidden, while closed. For animation libraries that drive the exit from state.
1108
+
1109
+ ### DialogTitle
1110
+
1111
+ Names the dialog. Renders an `h2`.
1112
+
1113
+ ### DialogDescription
1114
+
1115
+ Describes the dialog while it's rendered. Renders a `p`.
1116
+
1117
+ ### DialogClose
1118
+
1119
+ Its `@action` handler receives the event and `{ source }`, how the part was pressed, as [Button](./button.md#changes-after-an-await) describes.
1120
+
1121
+ - `value?` (`string`): Reported as `returnValue` in `@open-change` and by the controller's `open()`. `createConfirm()` resolves `true` for `"confirm"`.
1122
+ - `action?` (`((event: Event | undefined, details: ActionDetails) => unknown)`): Runs when the part is pressed, before the dialog closes. A returned Promise keeps the dialog open while it runs: the part gets `data-pending`, `aria-busy` and `aria-disabled`, screen readers hear the dialog's `pendingAnnouncement` after a second, and nothing but the controller closes the dialog. It closes with reason `"action"` once the Promise resolves. A rejection keeps it open and puts the reason in the part's `error` state until the next press, or until the dialog opens again. `details.source` says how the part was pressed, for changes made after an `await`.
1123
+
1124
+ ### DialogShortcut
1125
+
1126
+ - `shortcut?` (`string`): The keys to show. Inside a trigger, its `shortcut`.
1127
+
1128
+ ### State
1129
+
1130
+ What render functions, state-based class names and slots receive, per part.
1131
+
1132
+ `DialogTrigger`:
1133
+
1134
+ - `open` (`boolean`): Whether the dialog is open from this trigger.
1135
+ - `disabled` (`boolean`): Set by the trigger's `disabled` prop, which stops it opening the dialog.
1136
+
1137
+ `DialogBackdrop`:
1138
+
1139
+ - `open` (`boolean`): Whether the dialog is open. False while it animates out.
1140
+ - `nested` (`boolean`): Whether the dialog was opened from inside another dialog.
1141
+ - `nestedOpen` (`boolean`): Whether a dialog opened from inside this one is open.
1142
+
1143
+ `DialogContent`:
1144
+
1145
+ - `open` (`boolean`): Whether the dialog is open. False while it animates out.
1146
+ - `modal` (`boolean | "trap-focus"`): The `modal` setting. Always true for an alert dialog.
1147
+ - `nested` (`boolean`): Whether the dialog was opened from inside another dialog.
1148
+ - `nestedOpen` (`boolean`): Whether a dialog opened from inside this one is open.
1149
+ - `stack` (`number`): How many dialogs are open above this one, nested inside it.
1150
+ - `payload` (`Payload | undefined`): The payload of the last open, from the trigger that opened it or a controller, typed as the controller's `Payload`.
1151
+ - `pending` (`boolean`): Whether a Close part's action is running.
1152
+ - `presentation` (`"dialog" | "sheet"`): How it presents this time it's open.
1153
+
1154
+ `DialogClose`:
1155
+
1156
+ - `pending` (`boolean`): Whether this part's action is running.
1157
+ - `error` (`unknown`): Why this part's last action failed, until it's pressed again or the dialog opens again.
1158
+
1159
+ `DialogShortcut`:
1160
+
1161
+ - `text` (`string`): The shortcut formatted for this platform: `⌘K` on Apple platforms and `Ctrl+K` elsewhere. Empty without one.
1162
+
1163
+ ### Reasons
1164
+
1165
+ `details.reason` says why the dialog opened or closed.
1166
+
1167
+ - `trigger-press` (`MouseEvent | KeyboardEvent | TouchEvent`): Click, tap, Enter or Space on the part that opens, closes or toggles. Arrow keys that open a menu or submenu count too. A tap on a combobox's input that opens a sheet reports its `touchend`.
1168
+ - `close-press` (`MouseEvent | KeyboardEvent | SubmitEvent`): A Close part was pressed, or a form with `method="dialog"` was submitted.
1169
+ - `outside-press` (`MouseEvent`): A press outside the layer.
1170
+ - `escape-key` (`KeyboardEvent`): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
1171
+ - `parent-close` (`none`): The parent layer closed.
1172
+ - `back` (`Event`): Going back: the Android back button or gesture, or Backspace in an empty command input, which returns to the previous page.
1173
+ - `action` (`MouseEvent | KeyboardEvent`): A Close part's `action` finished.
1174
+ - `shortcut` (`KeyboardEvent`): A keyboard shortcut, like a trigger's `shortcut`.
1175
+ - `swipe` (`TouchEvent | PointerEvent`): A swipe gesture: a finger, mouse or pen dragged it.
1176
+ - `imperative` (`none`): A controller or `api` call, or a property set on a web component.
1177
+
1178
+ For a payload change, it says what opened the dialog or took it over.
1179
+
1180
+ - `trigger-press` (`MouseEvent | KeyboardEvent | TouchEvent`): Click, tap, Enter or Space on the part that opens, closes or toggles. Arrow keys that open a menu or submenu count too. A tap on a combobox's input that opens a sheet reports its `touchend`.
1181
+ - `shortcut` (`KeyboardEvent`): A keyboard shortcut, like a trigger's `shortcut`.
1182
+ - `imperative` (`none`): A controller or `api` call, or a property set on a web component.
1183
+
1184
+ The dismissals `dismissible` can refuse:
1185
+
1186
+ - `outside-press` (`MouseEvent`): A press outside the layer.
1187
+ - `escape-key` (`KeyboardEvent`): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
1188
+ - `back` (`Event`): Going back: the Android back button or gesture, or Backspace in an empty command input, which returns to the previous page.
1189
+ - `swipe` (`TouchEvent | PointerEvent`): A swipe gesture: a finger, mouse or pen dragged it.
1190
+
1191
+ ### Controller
1192
+
1193
+ Create one with `createDialogController<Payload, Result>()` and pass it as `controller`, to the dialog and to any
1194
+ triggers outside it.
1195
+
1196
+ Every write method takes an optional `{ reason, event }`, passed to `@open-change`. Closing with a controller always works, even when the dialog isn't dismissible or an action is running.
1197
+
1198
+ - `isOpen()` (`() => boolean`): Whether it's open. False as soon as it starts closing, while it still animates out.
1199
+ - `setOpen(open, options?)` (`(open: boolean, options?: PopupChangeOptions<DialogOpenChangeReason> | undefined) => boolean`): Opens or closes it. Returns false when the change was canceled or it was already open or closed.
1200
+ - `open(options?)` (`(options?: (PopupChangeOptions<DialogPayloadChangeReason> & { payload?: Payload | undefined; anchor?: Anchor | undefined; signal?: AbortSignal | undefined; }) | undefined) => Promise<...>`): Opens it, and resolves once it closes, with why, how and any return value. Resolves exactly once and never rejects: on the next close from any source, when `signal` aborts, or with reason `"unmount"` when the component leaves the page.
1201
+ - `close(options?)` (`(options?: (PopupChangeOptions<DialogOpenChangeReason> & { returnValue?: Result | undefined; }) | undefined) => boolean`): Closes it. `returnValue` is passed to the promise `open()` returned. Returns false when the close was canceled or it was already closed.
1202
+ - `toggle(options?)` (`(options?: PopupChangeOptions<DialogOpenChangeReason> | undefined) => boolean`): Opens it when it's closed, and closes it when it's open. Returns false when the change was canceled.
1203
+ - `getPayload()` (`() => Payload | undefined`): The payload of the last open, from a controller or the trigger that opened it.
1204
+ - `focus()` (`() => void`): Moves focus to the trigger that opened it last, or the first trigger.
1205
+
1206
+ ### Events
1207
+
1208
+ Events get the new state first, then the details. `v-model` only updates when `@open-change` wasn't canceled.
1209
+
1210
+ | Binding | On | Receives |
1211
+ | ----------------------- | -------- | ------------------------------------------------------------------------------ |
1212
+ | `v-model:open` | `Dialog` | Whether the dialog is open |
1213
+ | `@open-change` | `Dialog` | `(open, details)`: the new state, the [reason](#reasons) and any `returnValue` |
1214
+ | `@open-change-complete` | `Dialog` | `(open)`, after the animation |
1215
+ | `@payload-change` | `Dialog` | `(payload, details)`: the content's new payload, and the [reason](#reasons) |
1216
+ | `@dismiss-attempt` | `Dialog` | `({ reason, event })`, when a dismissal was refused |
1217
+
1218
+ ## Accessibility
1219
+
1220
+ The trigger is a button that opens a dialog, and the dialog follows the [WAI-ARIA modal dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/): `aria-modal` while it's modal, labelled by its title, described by its description only while one is rendered. Every line below is an automated test.
1221
+
1222
+ ### Keyboard
1223
+
1224
+ | Key | Does |
1225
+ | -------------------- | -------------------------------------------------------------------- |
1226
+ | Enter, Space | On the trigger, opens the dialog |
1227
+ | Tab, Shift+Tab | Moves through the dialog's controls, staying inside while it's modal |
1228
+ | Escape | Closes the topmost dialog and returns focus to its trigger |
1229
+ | A trigger's shortcut | Opens its dialog from anywhere on the page |
1230
+
1231
+ ### Tested Behavior
1232
+
1233
+ **Trigger**
1234
+
1235
+ - Advertises a dialog.
1236
+ - Click opens the dialog.
1237
+ - Enter opens the dialog.
1238
+ - Space opens the dialog.
1239
+
1240
+ **Focus**
1241
+
1242
+ - Opening moves focus to the first focusable element.
1243
+ - Each time focus moves in, the dialog is named by its title and described by its description.
1244
+ - Tab moves through the dialog in order.
1245
+ - Tab from the last element wraps to the first.
1246
+ - Shift+Tab from the first element wraps to the last.
1247
+ - Shift+Tab from the first element wraps to the checked radio of a group outside a form.
1248
+ - Tab reaches radios whose group's checked radio is behind the dialog.
1249
+ - Tab visits a positive tabindex first, like the browser.
1250
+ - A positive tabindex keeps Tab inside a dialog that leaves the page usable.
1251
+ - A positive tabindex slotted into a custom element keeps its host's place.
1252
+ - With a positive tabindex in the dialog, Tab still moves through a shadow root and slots.
1253
+ - With a positive tabindex in the dialog, Tab from a focused element outside the tab order goes to the next stop.
1254
+ - Tab from a custom element with a positive tabindex enters its shadow root.
1255
+ - Opening focuses the first field even when a later one has a positive tabindex.
1256
+ - Escape closes the dialog and returns focus to the trigger.
1257
+ - Escape after a click returns focus to the trigger.
1258
+ - The close button closes the dialog and returns focus to the trigger.
1259
+ - A virtual click from a screen reader moves focus into the dialog.
1260
+ - A touch press focuses the dialog itself, so no on-screen keyboard appears.
1261
+ - Pressing a button inside keeps the dialog open.
1262
+
1263
+ **Dismissal**
1264
+
1265
+ - Pressing the backdrop closes the dialog.
1266
+ - Pressing the backdrop returns focus to the trigger.
1267
+ - Clicking inside the dialog keeps it open.
1268
+ - An outside press doesn't reach the page.
1269
+
1270
+ **Presses**
1271
+
1272
+ - Selecting text in a field and releasing over the backdrop keeps the dialog open.
1273
+ - Pressing the backdrop and releasing inside the dialog keeps it open.
1274
+ - A right click on the backdrop keeps the dialog open.
1275
+ - A dialog styled only with ::backdrop closes when the backdrop is pressed.
1276
+ - With closeOnOutsidePress false, pressing the backdrop keeps the dialog open.
1277
+ - Pressing a nested dialog's backdrop closes only the nested dialog.
1278
+ - While the dialog animates out, a press on its backdrop doesn't reach the page.
1279
+ - Escape right after opening a nested dialog closes only the nested one.
1280
+
1281
+ **Modal**
1282
+
1283
+ - Tab never leaves the dialog.
1284
+ - The page doesn't scroll while the dialog is open.
1285
+ - The rest of the page is hidden from assistive technology.
1286
+ - With modal false, the page stays usable.
1287
+
1288
+ **Nested dialogs**
1289
+
1290
+ - Escape closes only the nested dialog and returns focus to its trigger.
1291
+ - The nested dialog's close button closes only it.
1292
+ - The outer dialog's open change hears only the outer dialog.
1293
+ - Tab stays inside the nested dialog.
1294
+ - A second Escape closes the outer dialog.
1295
+
1296
+ Runs in React, Vue and web components.
1297
+
1298
+ Additional behaviors:
1299
+
1300
+ **Escape that closes exactly one thing**
1301
+
1302
+ - Escape while an input method is composing text keeps the dialog open.
1303
+ - Escape closes a non-modal dialog only while focus is inside it.
1304
+
1305
+ **Focus that always lands somewhere useful**
1306
+
1307
+ - Closing returns focus to the trigger before the exit animation finishes.
1308
+ - Closing after the trigger is gone moves focus to the element after where it was.
1309
+ - Closing after the trigger is gone skips past a later positive tabindex.
1310
+ - A dialog open from the start moves focus inside.
1311
+ - With initial focus on the content, the dialog itself takes focus.
1312
+
1313
+ **The outside is inert, except what must stay live**
1314
+
1315
+ - The page outside is inert while open and restored exactly after.
1316
+ - A live region outside the dialog keeps announcing while it's open.
1317
+
1318
+ **Non-modal dialogs that behave like the APG**
1319
+
1320
+ - Tab leaves a non-modal dialog and the dialog stays open.
1321
+ - A non-modal dialog stays open when the page is clicked.
1322
+
1323
+ **Nested and stacked**
1324
+
1325
+ - A dialog opened inside a dialog marks both, for styling.
1326
+ - Closing the parent closes the nested dialog with reason parent-close.
1327
+
1328
+ **Opening from anywhere**
1329
+
1330
+ - A controller opens the dialog, moves focus in, and resolves with how it closed.
1331
+ - Aborting the signal closes the dialog, and the promise resolves once.
1332
+ - A pending open() resolves with reason unmount when the dialog leaves the page. Web components only.
1333
+ - A form with method dialog closes the dialog and reports the submitter's value.
1334
+ - A confirm helper resolves true for the action and false otherwise.
1335
+ - Closing reports why, for every way a dialog closes.
1336
+
1337
+ **Shortcuts**
1338
+
1339
+ - A trigger connected through a controller shows its shortcut in a Shortcut part, and binds it.
1340
+
1341
+ **Web components**
1342
+
1343
+ - The open property and attribute open and close the dialog. Web components only.
1344
+
1345
+ Runs in React, Vue and web components.
1346
+
1347
+ **Opening a dialog from a menu item**
1348
+
1349
+ - Choosing the item closes the menu and moves focus into the dialog.
1350
+ - Choosing the item with the keyboard moves focus into the dialog.
1351
+
1352
+ **The page and focus around a dialog opened from a menu item**
1353
+
1354
+ - The page around the dialog is inert, the menu's trigger included.
1355
+ - Closing the dialog returns focus to the menu's trigger.
1356
+ - A dialog the item opens through its open prop returns focus to the menu's trigger. React and Vue only.
1357
+
1358
+ **Shortcuts**
1359
+
1360
+ - A trigger's shortcut opens the dialog.
1361
+ - The trigger advertises its shortcut, and shows it to sighted users only.
1362
+ - A modal dialog pauses the page's shortcuts and keeps its own.
1363
+ - A dialog's own shortcut doesn't run while the dialog is closed.
1364
+ - Menu and popover triggers take shortcuts too.
1365
+ - Plain keys typed in a text field are text, and modifier shortcuts still run.
1366
+ - A plain-key shortcut runs outside text fields.
1367
+
1368
+ **A dirty form**
1369
+
1370
+ - Refuses Escape and outside presses, and reports each attempt.
1371
+ - A clean form closes on Escape.
1372
+
1373
+ **Persistent elements**
1374
+
1375
+ - Stay usable while a modal dialog is open, and pressing them keeps it open.
1376
+
1377
+ **Async actions**
1378
+
1379
+ - Keep the alert dialog open and pending, then close with reason action.
1380
+ - A failed action keeps the alert dialog open and stops pending.
1381
+
1382
+ **Popups inside a portaled popover**
1383
+
1384
+ - A menu and a dialog inside it render into the popover's container.
1385
+
1386
+ **Android's back gesture**
1387
+
1388
+ - Closes the topmost dialog with reason back.
1389
+
1390
+ Runs in React, Vue and web components.
1391
+
1392
+ Screen readers hear the same thing in each tested framework, recorded by a virtual screen reader:
1393
+
1394
+ - Announcements while using the dialog with a keyboard.
1395
+ - Announcements while using the alert dialog with a keyboard.
1396
+
1397
+ Runs in React, Vue and web components.