@jobber/components 8.24.1 → 8.25.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.
- package/ProgressIndicator.d.ts +1 -0
- package/ProgressIndicator.js +17 -0
- package/dist/Dialog/Dialog.d.ts +47 -3
- package/dist/Dialog/Dialog.types.d.ts +1 -15
- package/dist/Dialog/DialogBottomSheet.d.ts +12 -13
- package/dist/Dialog/DialogModal.d.ts +11 -11
- package/dist/Dialog/dialogComposableShared.d.ts +13 -3
- package/dist/ProgressBar/ProgressBar.d.ts +7 -0
- package/dist/ProgressBar-cjs.js +4 -0
- package/dist/ProgressBar-es.js +4 -0
- package/dist/ProgressIndicator/ProgressIndicator.d.ts +60 -0
- package/dist/ProgressIndicator/index.cjs +9 -0
- package/dist/ProgressIndicator/index.d.ts +1 -0
- package/dist/ProgressIndicator/index.mjs +3 -0
- package/dist/ProgressIndicator-cjs.js +43 -0
- package/dist/ProgressIndicator-es.js +41 -0
- package/dist/dialogReturnFocus-cjs.js +57 -31
- package/dist/dialogReturnFocus-es.js +59 -33
- package/dist/docs/Dialog/Dialog.md +953 -0
- package/dist/docs/ProgressBar/ProgressBar.md +9 -0
- package/dist/docs/ProgressIndicator/ProgressIndicator.md +105 -0
- package/dist/docs/index.md +2 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/styles.css +101 -0
- package/dist/utils/meta/meta.json +1 -0
- package/package.json +7 -2
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
# Dialog
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
The dialog displays content in a layer above the page, focusing the user on a
|
|
6
|
+
single task or decision. It is activated by an interactive element such as a
|
|
7
|
+
button or icon button. On large screens it appears as a centered modal; on small
|
|
8
|
+
screens it appears as a sheet anchored to the bottom of the screen.
|
|
9
|
+
|
|
10
|
+
## Anatomy
|
|
11
|
+
|
|
12
|
+
| Part | Description |
|
|
13
|
+
| ---------- | ------------------------------------------------------------------- |
|
|
14
|
+
| Trigger | An interactive element that opens the dialog |
|
|
15
|
+
| Content | The floating panel (modal) or sheet that contains the dialog |
|
|
16
|
+
| Header | Layout slot at the top, commonly holding the title and close button |
|
|
17
|
+
| Title | Primary heading of the dialog, wired to the accessible name |
|
|
18
|
+
| Close | The dismiss control that closes the dialog |
|
|
19
|
+
| Body | The main content region, and the default scroll area |
|
|
20
|
+
| Footer | Layout slot at the bottom, commonly holding the actions |
|
|
21
|
+
| Actions | Optional: the primary, secondary, and tertiary action button layout |
|
|
22
|
+
| ScrollArea | Optional: advanced override for which region scrolls versus pins |
|
|
23
|
+
|
|
24
|
+
## Behavior
|
|
25
|
+
|
|
26
|
+
#### Opening and closing the dialog
|
|
27
|
+
|
|
28
|
+
The dialog opens when:
|
|
29
|
+
|
|
30
|
+
* The trigger is clicked or tapped
|
|
31
|
+
* The trigger is activated by keyboard (e.g. via the Enter or Space keys)
|
|
32
|
+
|
|
33
|
+
The dialog closes when:
|
|
34
|
+
|
|
35
|
+
* The close button is selected
|
|
36
|
+
* An action that resolves the task is selected
|
|
37
|
+
* A click or tap occurs outside the dialog (on the backdrop)
|
|
38
|
+
* The ESC key is pressed
|
|
39
|
+
|
|
40
|
+
An `alertdialog` is the exception: it ignores outside and backdrop dismissal, so
|
|
41
|
+
the user must choose an action to resolve it. ESC still closes it.
|
|
42
|
+
|
|
43
|
+
#### Width
|
|
44
|
+
|
|
45
|
+
Dialog width defaults to `base` (720px). It can be set to `small` (440px),
|
|
46
|
+
`large` (1032px), or `fullScreen`. These are max widths — the dialog narrows to
|
|
47
|
+
fit smaller viewports. `fullScreen` fills the viewport on large screens (squared
|
|
48
|
+
corners) and becomes a full-height sheet on small screens. See [Size](#size) for
|
|
49
|
+
when to use each.
|
|
50
|
+
|
|
51
|
+
#### Scrolling
|
|
52
|
+
|
|
53
|
+
The `Dialog.Body` is the default scroll region, while the header and footer stay
|
|
54
|
+
pinned as the content scrolls. Which region scrolls is decided by composition,
|
|
55
|
+
not a prop — wrap parts in a `Dialog.ScrollArea` when a header or footer should
|
|
56
|
+
scroll with the content instead of pinning.
|
|
57
|
+
|
|
58
|
+
#### Focus
|
|
59
|
+
|
|
60
|
+
While the dialog is open, focus is trapped inside it. When it closes, focus
|
|
61
|
+
returns to the trigger. Focus can be directed elsewhere when the trigger is
|
|
62
|
+
removed while the dialog is open (for example, a "Create…" action whose trigger
|
|
63
|
+
is replaced by a card on save).
|
|
64
|
+
|
|
65
|
+
#### Small screens
|
|
66
|
+
|
|
67
|
+
When a user is on a small screen such as a mobile device, the dialog opens as a
|
|
68
|
+
sheet anchored to the bottom of the screen. The parts and accessibility wiring
|
|
69
|
+
are identical to the desktop modal.
|
|
70
|
+
|
|
71
|
+
#### Native mobile
|
|
72
|
+
|
|
73
|
+
Atlantis does not provide a native `Dialog` equivalent in
|
|
74
|
+
`@jobber/components-native`. For a comparable mobile-native experience, use
|
|
75
|
+
[`ContentOverlay`](/components/ContentOverlay) from `@jobber/components-native`
|
|
76
|
+
to present content or actions in a sheet.
|
|
77
|
+
|
|
78
|
+
## Variants
|
|
79
|
+
|
|
80
|
+
#### Type
|
|
81
|
+
|
|
82
|
+
**1. Dialog:** The default. Used for most tasks and content. Can be dismissed by
|
|
83
|
+
clicking outside, pressing ESC, or the close button.
|
|
84
|
+
|
|
85
|
+
**2. Alert dialog:** Used to interrupt the user and require a decision, such as
|
|
86
|
+
confirming a destructive action. It is always modal, cannot be dismissed by
|
|
87
|
+
clicking outside, and has no close button — the user must choose an action.
|
|
88
|
+
|
|
89
|
+
#### Size
|
|
90
|
+
|
|
91
|
+
| Size | Max width | Use for |
|
|
92
|
+
| ----------- | --------- | ------------------------------------------------------------------------- |
|
|
93
|
+
| Small | 440px | Short confirmations or simple choices |
|
|
94
|
+
| Base | 720px | The default, suitable for most content |
|
|
95
|
+
| Large | 1032px | Denser content or forms |
|
|
96
|
+
| Full screen | Viewport | Fills the viewport on large screens; a full-height sheet on small screens |
|
|
97
|
+
|
|
98
|
+
Widths are max widths — the dialog narrows to fit smaller viewports.
|
|
99
|
+
|
|
100
|
+
#### Actions
|
|
101
|
+
|
|
102
|
+
`Dialog.Actions` arranges the action buttons: a tertiary/destructive action on
|
|
103
|
+
the left, and primary and secondary actions grouped on the right. The footer is
|
|
104
|
+
a generic region, so it can also hold a note, a checkbox, or a custom layout
|
|
105
|
+
instead.
|
|
106
|
+
|
|
107
|
+
#### Custom content
|
|
108
|
+
|
|
109
|
+
* The body supports flexible content layouts
|
|
110
|
+
* `fullBleed` removes body padding when content should reach the edges
|
|
111
|
+
* Header and footer are layout-only slots; they own arrangement, not typography
|
|
112
|
+
or actions
|
|
113
|
+
|
|
114
|
+
## Content Guidelines
|
|
115
|
+
|
|
116
|
+
These build on the general [Voice & tone](../voice-and-tone/voice-and-tone.md) and
|
|
117
|
+
[Formatting](../formatting/formatting.md) guidelines. A dialog interrupts the user, so
|
|
118
|
+
every word should earn its place: name the task, state the outcome, and give a
|
|
119
|
+
clear way out.
|
|
120
|
+
|
|
121
|
+
## Accessibility
|
|
122
|
+
|
|
123
|
+
Users need to be able to open the dialog, operate its content, and close it with
|
|
124
|
+
assistive technology.
|
|
125
|
+
|
|
126
|
+
#### Semantics
|
|
127
|
+
|
|
128
|
+
* The surface `role` is `dialog`, or `alertdialog` for the alert type
|
|
129
|
+
* `Dialog.Title` is auto-wired to `aria-labelledby`
|
|
130
|
+
* `Dialog.Body` is auto-wired to `aria-describedby`
|
|
131
|
+
|
|
132
|
+
#### Focus
|
|
133
|
+
|
|
134
|
+
* Focus is trapped within the dialog while it is open
|
|
135
|
+
* On close, focus returns to the trigger
|
|
136
|
+
* When the trigger is removed while the dialog is open, focus can be directed to
|
|
137
|
+
the closest sensible element instead
|
|
138
|
+
|
|
139
|
+
#### Keyboard navigation
|
|
140
|
+
|
|
141
|
+
| Key | Behavior |
|
|
142
|
+
| -------------- | ---------------------------------------------------- |
|
|
143
|
+
| Tab | Moves focus between focusable elements in the dialog |
|
|
144
|
+
| Enter or Space | Activates the focused control |
|
|
145
|
+
| Esc | Closes the dialog |
|
|
146
|
+
|
|
147
|
+
## Related components
|
|
148
|
+
|
|
149
|
+
* To trigger a single action rather than presenting a task or decision, use a
|
|
150
|
+
[Button](../Button/Button.md) or [IconButton](/components/IconButton)
|
|
151
|
+
* For a comparable overlay experience in native mobile, use
|
|
152
|
+
[BottomSheet](/components/BottomSheet) from `@jobber/components-native`
|
|
153
|
+
* To present a list of options or actions, use a [Menu](../Menu/Menu.md)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
## Component customization
|
|
157
|
+
|
|
158
|
+
### Composable API
|
|
159
|
+
|
|
160
|
+
`Dialog` is composed from its building-block subcomponents rather than driven by
|
|
161
|
+
a single set of props: `Dialog.Trigger`, `Dialog.Content`, `Dialog.Header`,
|
|
162
|
+
`Dialog.Title`, `Dialog.Close`, `Dialog.Body`, `Dialog.Footer`,
|
|
163
|
+
`Dialog.Actions`, and `Dialog.ScrollArea`. This gives you control over the
|
|
164
|
+
dialog's layout while keeping the accessibility wiring automatic.
|
|
165
|
+
|
|
166
|
+
Here is a basic example:
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
<Dialog>
|
|
170
|
+
<Dialog.Trigger>
|
|
171
|
+
<Button label="Open" />
|
|
172
|
+
</Dialog.Trigger>
|
|
173
|
+
<Dialog.Content>
|
|
174
|
+
<Dialog.Header>
|
|
175
|
+
<Dialog.Title>Dialog title</Dialog.Title>
|
|
176
|
+
<Dialog.Close />
|
|
177
|
+
</Dialog.Header>
|
|
178
|
+
<Dialog.Body>
|
|
179
|
+
<Text>Body content goes here.</Text>
|
|
180
|
+
</Dialog.Body>
|
|
181
|
+
<Dialog.Footer>
|
|
182
|
+
<Dialog.Actions
|
|
183
|
+
primary={{ label: "Save", onClick: handleSave }}
|
|
184
|
+
secondary={{ label: "Cancel", onClick: handleCancel }}
|
|
185
|
+
/>
|
|
186
|
+
</Dialog.Footer>
|
|
187
|
+
</Dialog.Content>
|
|
188
|
+
</Dialog>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`Dialog.Header` and `Dialog.Footer` are layout-only slots — they own
|
|
192
|
+
arrangement, not typography or actions. `Dialog.Actions` is the action-button
|
|
193
|
+
layout (tertiary on the left; primary/secondary grouped on the right) and is
|
|
194
|
+
dropped inside a `Dialog.Footer`.
|
|
195
|
+
|
|
196
|
+
### Controlled open state
|
|
197
|
+
|
|
198
|
+
By default the dialog manages its own open state — the trigger opens it and the
|
|
199
|
+
close/actions dismiss it, with no state to wire up. To drive it yourself (for
|
|
200
|
+
example, to open it from elsewhere or to gate closing on a save), pass `open`
|
|
201
|
+
and handle `onRequestClose`. `onRequestClose` fires whenever the user asks to
|
|
202
|
+
close — Escape, a backdrop click, or the dismiss button — so a controlled dialog
|
|
203
|
+
should update its state there. The trigger's own control still opens the dialog
|
|
204
|
+
via its `onClick`.
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
function ControlledDialog() {
|
|
208
|
+
const [open, setOpen] = useState(false);
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<Dialog open={open} onRequestClose={() => setOpen(false)}>
|
|
212
|
+
<Dialog.Trigger>
|
|
213
|
+
<Button label="Open" onClick={() => setOpen(true)} />
|
|
214
|
+
</Dialog.Trigger>
|
|
215
|
+
<Dialog.Content>
|
|
216
|
+
<Dialog.Header>
|
|
217
|
+
<Dialog.Title>Edit details</Dialog.Title>
|
|
218
|
+
<Dialog.Close />
|
|
219
|
+
</Dialog.Header>
|
|
220
|
+
<Dialog.Body>
|
|
221
|
+
<Text>Body content goes here.</Text>
|
|
222
|
+
</Dialog.Body>
|
|
223
|
+
<Dialog.Footer>
|
|
224
|
+
<Dialog.Actions
|
|
225
|
+
primary={{ label: "Save", onClick: () => setOpen(false) }}
|
|
226
|
+
secondary={{ label: "Cancel", onClick: () => setOpen(false) }}
|
|
227
|
+
/>
|
|
228
|
+
</Dialog.Footer>
|
|
229
|
+
</Dialog.Content>
|
|
230
|
+
</Dialog>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Because the state is yours, you can keep the dialog open when a save fails —
|
|
236
|
+
only call `setOpen(false)` once the action succeeds. When you don't need that
|
|
237
|
+
control, omit `open`/`onRequestClose` and let the dialog manage itself.
|
|
238
|
+
|
|
239
|
+
### Trigger and close customization
|
|
240
|
+
|
|
241
|
+
`Dialog.Trigger` and `Dialog.Close` accept Atlantis-style content and can render
|
|
242
|
+
a default control, composed Atlantis `Button` content, or a custom element via
|
|
243
|
+
`render`. An Atlantis `<Button>`/`<ButtonDismiss>` is adapted automatically; any
|
|
244
|
+
other valid element is used as-is. When `render` is a function, it receives the
|
|
245
|
+
Base UI props so the element stays wired to open/close the dialog.
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
{/* default primary Button trigger / default ButtonDismiss close */}
|
|
249
|
+
<Dialog.Trigger>Open</Dialog.Trigger>
|
|
250
|
+
<Dialog.Close />
|
|
251
|
+
|
|
252
|
+
{/* custom element */}
|
|
253
|
+
<Dialog.Trigger render={<Button type="tertiary" label="Open" />} />
|
|
254
|
+
<Dialog.Close render={<Button type="tertiary" label="Done" />} />
|
|
255
|
+
|
|
256
|
+
{/* render function */}
|
|
257
|
+
<Dialog.Close render={closeProps => <MyClose {...closeProps} />} />
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Detached triggers
|
|
261
|
+
|
|
262
|
+
When the trigger can't be nested inside the `Dialog` — for example an action
|
|
263
|
+
inside a `Combobox`, `Autocomplete`, or `Menu` overlay — connect it through a
|
|
264
|
+
`handle` from `Dialog.createHandle()` instead of nesting. The trigger can pass
|
|
265
|
+
`payload` data that is available to `Dialog`'s render-function `children`.
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
const handle = Dialog.createHandle();
|
|
269
|
+
|
|
270
|
+
<Dialog handle={handle}>
|
|
271
|
+
{({ payload }) => <Dialog.Content>{/* read payload here */}</Dialog.Content>}
|
|
272
|
+
</Dialog>;
|
|
273
|
+
|
|
274
|
+
<Dialog.Trigger handle={handle} payload={selectedClient}>
|
|
275
|
+
Create client
|
|
276
|
+
</Dialog.Trigger>;
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Scroll and sticky model
|
|
280
|
+
|
|
281
|
+
Composition — not a `sticky` prop — decides what scrolls. The scroll region is a
|
|
282
|
+
real element and owns the scrollbar:
|
|
283
|
+
|
|
284
|
+
* Content **inside** the scroll region scrolls (the scrollbar lives here).
|
|
285
|
+
* Content **outside** the scroll region (a direct child of `Dialog.Content`)
|
|
286
|
+
stays pinned.
|
|
287
|
+
|
|
288
|
+
`Dialog.Body` is the default scroll region, so a flat `Header`/`Body`/`Footer`
|
|
289
|
+
pins the header and footer and scrolls the body — the common case needs nothing
|
|
290
|
+
extra.
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
<Dialog.Content>
|
|
294
|
+
<Dialog.Header /> {/* pinned */}
|
|
295
|
+
<Dialog.Body>…</Dialog.Body> {/* scrolls */}
|
|
296
|
+
<Dialog.Footer /> {/* pinned */}
|
|
297
|
+
</Dialog.Content>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
To make a header or footer scroll with the content instead of pinning, wrap the
|
|
301
|
+
parts that should scroll together in `Dialog.ScrollArea`. Anything inside it
|
|
302
|
+
scrolls; anything left as a sibling in `Dialog.Content` stays pinned.
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
{
|
|
306
|
+
/* sticky Header, scrolling Footer */
|
|
307
|
+
}
|
|
308
|
+
<Dialog.Content>
|
|
309
|
+
<Dialog.Header /> {/* pinned */}
|
|
310
|
+
<Dialog.ScrollArea>
|
|
311
|
+
<Dialog.Body>…</Dialog.Body>
|
|
312
|
+
<Dialog.Footer /> {/* inside → scrolls */}
|
|
313
|
+
</Dialog.ScrollArea>
|
|
314
|
+
</Dialog.Content>;
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The four sticky combinations map to composition:
|
|
318
|
+
|
|
319
|
+
| Sticky combination | Composition |
|
|
320
|
+
| ------------------------------- | ---------------------------------------------------- |
|
|
321
|
+
| Both sticky | `Header`, `Body`, `Footer` flat in `Content` (basic) |
|
|
322
|
+
| Sticky header, scrolling footer | `Header`, then `ScrollArea{ Body, Footer }` |
|
|
323
|
+
| Scrolling header, sticky footer | `ScrollArea{ Header, Body }`, then `Footer` |
|
|
324
|
+
| Nothing sticky | `ScrollArea{ Header, Body, Footer }` |
|
|
325
|
+
|
|
326
|
+
### Rendering the surface as a form
|
|
327
|
+
|
|
328
|
+
`Dialog.Content` accepts Base UI's `render` escape hatch. Pass
|
|
329
|
+
`render={<form onSubmit={…} />}` to make the surface itself a `<form>`, so a
|
|
330
|
+
submit button in `Dialog.Footer` submits it while the header and footer stay
|
|
331
|
+
pinned. For two or more independent forms in one dialog, give each
|
|
332
|
+
`<form id="…">` its own id in the body and point each submit control at it with
|
|
333
|
+
the native `form` attribute instead.
|
|
334
|
+
|
|
335
|
+
```tsx
|
|
336
|
+
<Dialog.Content render={<form onSubmit={handleSubmit} />}>
|
|
337
|
+
<Dialog.Header>…</Dialog.Header>
|
|
338
|
+
<Dialog.Body>{/* form fields */}</Dialog.Body>
|
|
339
|
+
<Dialog.Footer>
|
|
340
|
+
<Dialog.Actions primary={{ label: "Save", type: "submit" }} />
|
|
341
|
+
</Dialog.Footer>
|
|
342
|
+
</Dialog.Content>
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Focus return when the trigger unmounts
|
|
346
|
+
|
|
347
|
+
Base UI returns focus on close to the trigger, then the element focused before
|
|
348
|
+
open — each only if it is still connected to the DOM. It never walks to a
|
|
349
|
+
neighbor. A common Jobber flow breaks both at once: a "Create…" action opens the
|
|
350
|
+
dialog, and on save the trigger is replaced by a card. By the time the dialog
|
|
351
|
+
closes, the trigger is gone and focus falls to `document.body`.
|
|
352
|
+
|
|
353
|
+
For that case, `finalFocus` accepts a function. Build it with
|
|
354
|
+
`getDialogReturnFocus({ trigger, boundary })`:
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
358
|
+
const boundaryRef = useRef<HTMLDivElement>(null);
|
|
359
|
+
|
|
360
|
+
<div ref={boundaryRef}>
|
|
361
|
+
{created ? <Card>…</Card> : <Trigger ref={triggerRef} />}
|
|
362
|
+
</div>;
|
|
363
|
+
|
|
364
|
+
<Dialog
|
|
365
|
+
handle={handle}
|
|
366
|
+
finalFocus={getDialogReturnFocus({
|
|
367
|
+
trigger: triggerRef,
|
|
368
|
+
boundary: boundaryRef,
|
|
369
|
+
})}
|
|
370
|
+
>
|
|
371
|
+
…
|
|
372
|
+
</Dialog>;
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Resolution order on close:
|
|
376
|
+
|
|
377
|
+
1. `trigger`, if it is still connected (the normal case);
|
|
378
|
+
2. otherwise the first focusable element inside `boundary` (typically the card
|
|
379
|
+
that replaced the trigger);
|
|
380
|
+
3. otherwise `null`, letting Base UI fall back to its default behavior.
|
|
381
|
+
|
|
382
|
+
Keep `boundary` scoped tightly around the region that swaps. When the trigger
|
|
383
|
+
never unmounts, a plain `finalFocus={triggerRef}` is enough and this helper is
|
|
384
|
+
unnecessary.
|
|
385
|
+
|
|
386
|
+
### Alert dialog
|
|
387
|
+
|
|
388
|
+
`type="alertdialog"` is always modal, ignores outside/backdrop dismissal, and
|
|
389
|
+
omits the default `Dialog.Close` button — the user must choose an action. Escape
|
|
390
|
+
still closes it. Always include a cancel action so there is a genuine way out.
|
|
391
|
+
|
|
392
|
+
```tsx
|
|
393
|
+
<Dialog type="alertdialog">
|
|
394
|
+
<Dialog.Trigger>Delete client</Dialog.Trigger>
|
|
395
|
+
<Dialog.Content>
|
|
396
|
+
<Dialog.Header>
|
|
397
|
+
<Dialog.Title>Delete client</Dialog.Title>
|
|
398
|
+
</Dialog.Header>
|
|
399
|
+
<Dialog.Body>
|
|
400
|
+
<Text>
|
|
401
|
+
Sarah Johnson and all her history will be removed. This can't be undone.
|
|
402
|
+
</Text>
|
|
403
|
+
</Dialog.Body>
|
|
404
|
+
<Dialog.Footer>
|
|
405
|
+
<Dialog.Actions
|
|
406
|
+
primary={{ label: "Delete Client", onClick: handleDelete }}
|
|
407
|
+
secondary={{ label: "Cancel", onClick: handleCancel }}
|
|
408
|
+
/>
|
|
409
|
+
</Dialog.Footer>
|
|
410
|
+
</Dialog.Content>
|
|
411
|
+
</Dialog>
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Sizing
|
|
415
|
+
|
|
416
|
+
`size` accepts `"small"` (440px), `"base"` (720px, the default), `"large"`
|
|
417
|
+
(1032px), and `"fullScreen"`. The pixel values are max widths; the dialog
|
|
418
|
+
narrows to fit smaller viewports.
|
|
419
|
+
|
|
420
|
+
Use `size="fullScreen"` for complex workflows or dense layouts that need more
|
|
421
|
+
space without turning the task into a new route. On desktop it fills the
|
|
422
|
+
viewport with squared corners; on mobile the bottom sheet becomes a full-width,
|
|
423
|
+
full-height surface instead of the default peek.
|
|
424
|
+
|
|
425
|
+
Use `fullBleed` on `Dialog.Body` to remove its padding when content should reach
|
|
426
|
+
the edges — for example a full-width image, table, or map.
|
|
427
|
+
|
|
428
|
+
## Accessibility
|
|
429
|
+
|
|
430
|
+
**Role and modality**: `Dialog.Content` renders with `role="dialog"`, or
|
|
431
|
+
`role="alertdialog"` when `type="alertdialog"`. Focus is trapped within the
|
|
432
|
+
dialog while it is open.
|
|
433
|
+
|
|
434
|
+
**Naming**: the accessible name and description are wired automatically from the
|
|
435
|
+
composed parts:
|
|
436
|
+
|
|
437
|
+
* `Dialog.Title` is wired to `aria-labelledby`.
|
|
438
|
+
* `Dialog.Body` is wired to `aria-describedby`.
|
|
439
|
+
|
|
440
|
+
**Responsive parity**: on small screens the dialog is presented as a bottom
|
|
441
|
+
sheet built on the `BottomSheet`/`Drawer` primitives, but the public parts and
|
|
442
|
+
accessibility wiring match the desktop modal.
|
|
443
|
+
|
|
444
|
+
### Testing tip
|
|
445
|
+
|
|
446
|
+
Query by the accessible name derived from `Dialog.Title`:
|
|
447
|
+
|
|
448
|
+
```tsx
|
|
449
|
+
screen.getByRole("dialog", { name: /Dialog title/i });
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## Migrating to Dialog
|
|
453
|
+
|
|
454
|
+
`Dialog` is the successor to both `Modal` (in its prop-driven and composable
|
|
455
|
+
`Modal.Provider` forms) and `ConfirmationModal`. This guide maps each old API
|
|
456
|
+
onto the composable Dialog parts.
|
|
457
|
+
|
|
458
|
+
The same target shape covers every source: a `Dialog` root that owns state and
|
|
459
|
+
behavior, a `Dialog.Content` surface, and the layout parts (`Header`, `Title`,
|
|
460
|
+
`Close`, `Body`, `Footer`, `Actions`) inside it.
|
|
461
|
+
|
|
462
|
+
```tsx
|
|
463
|
+
<Dialog open={open} onRequestClose={() => setOpen(false)}>
|
|
464
|
+
<Dialog.Content>
|
|
465
|
+
<Dialog.Header>
|
|
466
|
+
<Dialog.Title>Title</Dialog.Title>
|
|
467
|
+
<Dialog.Close />
|
|
468
|
+
</Dialog.Header>
|
|
469
|
+
<Dialog.Body>…</Dialog.Body>
|
|
470
|
+
<Dialog.Footer>
|
|
471
|
+
<Dialog.Actions primary={…} secondary={…} />
|
|
472
|
+
</Dialog.Footer>
|
|
473
|
+
</Dialog.Content>
|
|
474
|
+
</Dialog>
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Behavior differences to know first
|
|
478
|
+
|
|
479
|
+
A few defaults changed, so a mechanical find-and-replace won't be correct on its
|
|
480
|
+
own:
|
|
481
|
+
|
|
482
|
+
* **Sticky is inverted.** `Modal.Header`/`Modal.Actions` were `inline` by
|
|
483
|
+
default and opted into `variant="sticky"`. In Dialog the header and footer are
|
|
484
|
+
**pinned by default**, and you opt *out* by wrapping the parts that should
|
|
485
|
+
scroll in `Dialog.ScrollArea`. Most `variant="sticky"` usages become the plain
|
|
486
|
+
default; the rare non-sticky header/footer is what now needs extra markup.
|
|
487
|
+
* **`dismissible={false}` becomes `type="alertdialog"`.** Blocking
|
|
488
|
+
outside/backdrop dismissal is no longer a boolean — it's the alert dialog
|
|
489
|
+
type, which also drops the default close button. Escape still closes.
|
|
490
|
+
* **Naming is automatic.** `ariaLabel` / `modalLabelledBy` are gone. A
|
|
491
|
+
`Dialog.Title` is auto-wired to `aria-labelledby` and `Dialog.Body` to
|
|
492
|
+
`aria-describedby`. If you have no visible title, provide one (visually hidden
|
|
493
|
+
if needed) rather than an `ariaLabel` prop.
|
|
494
|
+
* **Sizes shifted.** See the size table at the end.
|
|
495
|
+
|
|
496
|
+
***
|
|
497
|
+
|
|
498
|
+
## Prop-driven Modal → Dialog
|
|
499
|
+
|
|
500
|
+
The legacy `<Modal>` is controlled by `open` and renders its title and actions
|
|
501
|
+
from props. Move the title into `Dialog.Header`/`Dialog.Title`, the children
|
|
502
|
+
into `Dialog.Body`, and the `*Action` props into `Dialog.Actions`. Keep
|
|
503
|
+
controlling `open`/`onRequestClose` exactly as before — a trigger is optional.
|
|
504
|
+
|
|
505
|
+
```tsx
|
|
506
|
+
// Before
|
|
507
|
+
<Modal
|
|
508
|
+
open={open}
|
|
509
|
+
onRequestClose={() => setOpen(false)}
|
|
510
|
+
title="Edit client"
|
|
511
|
+
size="large"
|
|
512
|
+
primaryAction={{ label: "Save", onClick: handleSave }}
|
|
513
|
+
secondaryAction={{ label: "Cancel", onClick: () => setOpen(false) }}
|
|
514
|
+
>
|
|
515
|
+
<Content>{/* form */}</Content>
|
|
516
|
+
</Modal>
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
```tsx
|
|
520
|
+
// After
|
|
521
|
+
<Dialog open={open} onRequestClose={() => setOpen(false)} size="large">
|
|
522
|
+
<Dialog.Content>
|
|
523
|
+
<Dialog.Header>
|
|
524
|
+
<Dialog.Title>Edit client</Dialog.Title>
|
|
525
|
+
<Dialog.Close />
|
|
526
|
+
</Dialog.Header>
|
|
527
|
+
<Dialog.Body>{/* form */}</Dialog.Body>
|
|
528
|
+
<Dialog.Footer>
|
|
529
|
+
<Dialog.Actions
|
|
530
|
+
primary={{ label: "Save", onClick: handleSave }}
|
|
531
|
+
secondary={{ label: "Cancel", onClick: () => setOpen(false) }}
|
|
532
|
+
/>
|
|
533
|
+
</Dialog.Footer>
|
|
534
|
+
</Dialog.Content>
|
|
535
|
+
</Dialog>
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
| Modal prop | Dialog equivalent |
|
|
539
|
+
| --------------------- | ------------------------------------------------------------ |
|
|
540
|
+
| `open` | `open` (unchanged) |
|
|
541
|
+
| `onRequestClose` | `onRequestClose` (now also receives Base UI event details) |
|
|
542
|
+
| `title` | `<Dialog.Title>` inside `<Dialog.Header>` |
|
|
543
|
+
| `children` | `<Dialog.Body>` |
|
|
544
|
+
| `primaryAction` | `<Dialog.Actions primary={…} />` |
|
|
545
|
+
| `secondaryAction` | `<Dialog.Actions secondary={…} />` |
|
|
546
|
+
| `tertiaryAction` | `<Dialog.Actions tertiary={…} />` (rendered left) |
|
|
547
|
+
| `size` | `size` (remap values — see table) |
|
|
548
|
+
| `dismissible={false}` | `type="alertdialog"` (also removes the default close button) |
|
|
549
|
+
| `ariaLabel` | Use a `Dialog.Title`; naming is auto-wired |
|
|
550
|
+
| `version` | Removed |
|
|
551
|
+
|
|
552
|
+
The default close button lives in the header as `<Dialog.Close />`, so add one
|
|
553
|
+
there instead of relying on the modal's built-in dismiss. If you were opening
|
|
554
|
+
the modal from a button, you can keep that button external (controlled `open`)
|
|
555
|
+
or wrap it in `Dialog.Trigger` and drop the manual `setOpen(true)`.
|
|
556
|
+
|
|
557
|
+
***
|
|
558
|
+
|
|
559
|
+
## Composable Modal (`Modal.Provider`) → Dialog
|
|
560
|
+
|
|
561
|
+
The Provider API already composes parts, so the migration is close to a rename —
|
|
562
|
+
with the sticky inversion being the one real change.
|
|
563
|
+
|
|
564
|
+
```tsx
|
|
565
|
+
// Before
|
|
566
|
+
<Modal.Provider open={open} onRequestClose={() => setOpen(false)}>
|
|
567
|
+
<Modal.Activator>
|
|
568
|
+
<Button label="Open" onClick={() => setOpen(true)} />
|
|
569
|
+
</Modal.Activator>
|
|
570
|
+
<Modal.Content>
|
|
571
|
+
<Modal.Header title="Billing settings" variant="sticky" />
|
|
572
|
+
<Content>{/* body */}</Content>
|
|
573
|
+
<Modal.Actions
|
|
574
|
+
variant="sticky"
|
|
575
|
+
primary={{ label: "Save", onClick: handleSave }}
|
|
576
|
+
secondary={{ label: "Cancel", onClick: () => setOpen(false) }}
|
|
577
|
+
/>
|
|
578
|
+
</Modal.Content>
|
|
579
|
+
</Modal.Provider>
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
```tsx
|
|
583
|
+
// After
|
|
584
|
+
<Dialog open={open} onRequestClose={() => setOpen(false)}>
|
|
585
|
+
<Dialog.Trigger>
|
|
586
|
+
<Button label="Open" onClick={() => setOpen(true)} />
|
|
587
|
+
</Dialog.Trigger>
|
|
588
|
+
<Dialog.Content>
|
|
589
|
+
<Dialog.Header>
|
|
590
|
+
<Dialog.Title>Billing settings</Dialog.Title>
|
|
591
|
+
<Dialog.Close />
|
|
592
|
+
</Dialog.Header>
|
|
593
|
+
<Dialog.Body>{/* body */}</Dialog.Body>
|
|
594
|
+
<Dialog.Footer>
|
|
595
|
+
<Dialog.Actions
|
|
596
|
+
primary={{ label: "Save", onClick: handleSave }}
|
|
597
|
+
secondary={{ label: "Cancel", onClick: () => setOpen(false) }}
|
|
598
|
+
/>
|
|
599
|
+
</Dialog.Footer>
|
|
600
|
+
</Dialog.Content>
|
|
601
|
+
</Dialog>
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
| `Modal.Provider` part | Dialog equivalent |
|
|
605
|
+
| ------------------------------------- | --------------------------------------------------- |
|
|
606
|
+
| `Modal.Provider` | `Dialog` (`open`, `onRequestClose`) |
|
|
607
|
+
| `Modal.Activator` | `Dialog.Trigger` |
|
|
608
|
+
| `Modal.Content` (outer) | `Dialog.Content` |
|
|
609
|
+
| `Modal.Header title="…"` | `Dialog.Header` + `Dialog.Title` (+ `Dialog.Close`) |
|
|
610
|
+
| `Modal.Header` with children | `Dialog.Header` with children |
|
|
611
|
+
| inner `Content` / body children | `Dialog.Body` |
|
|
612
|
+
| `Modal.Actions` | `Dialog.Footer` + `Dialog.Actions` |
|
|
613
|
+
| `variant="sticky"` (header/actions) | Default — flat parts pin automatically |
|
|
614
|
+
| `variant="inline"` (scroll with body) | Wrap the part in `Dialog.ScrollArea` |
|
|
615
|
+
| `dismissible={false}` | `type="alertdialog"` |
|
|
616
|
+
| `ariaLabel` / `modalLabelledBy` | `Dialog.Title` auto-wires `aria-labelledby` |
|
|
617
|
+
|
|
618
|
+
`Modal.Activator` also controlled focus return. That's its own migration — see
|
|
619
|
+
[Focus return](#focus-return-modalactivator--finalfocus) below.
|
|
620
|
+
|
|
621
|
+
***
|
|
622
|
+
|
|
623
|
+
## Focus return (`Modal.Activator`) → `finalFocus`
|
|
624
|
+
|
|
625
|
+
`Modal.Activator` wrapped the element that focus should return to after close.
|
|
626
|
+
Under the hood floating-ui also gave you a **fallback for free**: it anchored a
|
|
627
|
+
hidden `<span>` next to the activator, so if the element that opened the modal
|
|
628
|
+
unmounted while it was open — the classic "Create…" action whose trigger is
|
|
629
|
+
replaced by a card on save — focus still landed at a sensible nearby spot
|
|
630
|
+
instead of falling to `document.body`.
|
|
631
|
+
|
|
632
|
+
Base UI's dialog dropped that anchored-span mechanism, so Dialog reintroduces
|
|
633
|
+
the fallback explicitly through the root's `finalFocus`. There are two cases:
|
|
634
|
+
|
|
635
|
+
**The trigger stays mounted** (most dialogs). Point `finalFocus` at a ref and
|
|
636
|
+
you're done — this covers the plain `Modal.Activator` redirect:
|
|
637
|
+
|
|
638
|
+
```tsx
|
|
639
|
+
// Before
|
|
640
|
+
<Modal.Provider open={open} onRequestClose={() => setOpen(false)}>
|
|
641
|
+
<Modal.Content>…</Modal.Content>
|
|
642
|
+
<Modal.Activator>
|
|
643
|
+
<InputText
|
|
644
|
+
placeholder="Focus returns here"
|
|
645
|
+
value={value}
|
|
646
|
+
onChange={setValue}
|
|
647
|
+
/>
|
|
648
|
+
</Modal.Activator>
|
|
649
|
+
</Modal.Provider>
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
```tsx
|
|
653
|
+
// After
|
|
654
|
+
const returnRef = useRef<HTMLInputElement>(null);
|
|
655
|
+
|
|
656
|
+
<Dialog
|
|
657
|
+
open={open}
|
|
658
|
+
onRequestClose={() => setOpen(false)}
|
|
659
|
+
finalFocus={returnRef}
|
|
660
|
+
>
|
|
661
|
+
<Dialog.Content>…</Dialog.Content>
|
|
662
|
+
</Dialog>;
|
|
663
|
+
|
|
664
|
+
<InputText
|
|
665
|
+
ref={returnRef}
|
|
666
|
+
placeholder="Focus returns here"
|
|
667
|
+
value={value}
|
|
668
|
+
onChange={setValue}
|
|
669
|
+
/>;
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**The trigger can unmount** (the fallback floating-ui used to handle). Build
|
|
673
|
+
`finalFocus` with `getDialogReturnFocus({ trigger, boundary })`. The `boundary`
|
|
674
|
+
ref is the equivalent of the old anchored span: if the trigger is gone on close,
|
|
675
|
+
focus moves to the first focusable element inside `boundary` instead:
|
|
676
|
+
|
|
677
|
+
```tsx
|
|
678
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
679
|
+
const boundaryRef = useRef<HTMLDivElement>(null);
|
|
680
|
+
|
|
681
|
+
<div ref={boundaryRef}>
|
|
682
|
+
{created ? <Card>…</Card> : <Trigger ref={triggerRef} />}
|
|
683
|
+
</div>;
|
|
684
|
+
|
|
685
|
+
<Dialog
|
|
686
|
+
handle={handle}
|
|
687
|
+
finalFocus={getDialogReturnFocus({
|
|
688
|
+
trigger: triggerRef,
|
|
689
|
+
boundary: boundaryRef,
|
|
690
|
+
})}
|
|
691
|
+
>
|
|
692
|
+
…
|
|
693
|
+
</Dialog>;
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
Resolution order on close: the `trigger` if still connected, otherwise the first
|
|
697
|
+
focusable inside `boundary`, otherwise `null` (Base UI's default). Keep
|
|
698
|
+
`boundary` scoped tightly around the region that swaps.
|
|
699
|
+
|
|
700
|
+
| Before (`Modal.Activator`) | After (`finalFocus`) |
|
|
701
|
+
| ------------------------------------------- | ----------------------------------------------------------- |
|
|
702
|
+
| Wrap the return target in `Modal.Activator` | `finalFocus={ref}` pointing at the return target |
|
|
703
|
+
| Floating-ui's automatic unmount fallback | `finalFocus={getDialogReturnFocus({ trigger, boundary })}` |
|
|
704
|
+
| (no equivalent — was implicit) | `boundary` names where focus lands when the trigger is gone |
|
|
705
|
+
|
|
706
|
+
See the focus-return section of the component notes for the full behavior.
|
|
707
|
+
|
|
708
|
+
***
|
|
709
|
+
|
|
710
|
+
## ConfirmationModal → Dialog
|
|
711
|
+
|
|
712
|
+
A confirmation is an alert dialog: `type="alertdialog"` blocks outside dismissal
|
|
713
|
+
and omits the close button, so the user must choose confirm or cancel. Map
|
|
714
|
+
`confirmLabel`/`onConfirm` to the primary action and `cancelLabel`/`onCancel` to
|
|
715
|
+
the secondary.
|
|
716
|
+
|
|
717
|
+
```tsx
|
|
718
|
+
// Before
|
|
719
|
+
<ConfirmationModal
|
|
720
|
+
open={open}
|
|
721
|
+
title="Delete job #2121"
|
|
722
|
+
message="Deleting this job will remove all associated visits and invoices."
|
|
723
|
+
confirmLabel="Delete Job"
|
|
724
|
+
cancelLabel="Cancel"
|
|
725
|
+
variation="destructive"
|
|
726
|
+
onConfirm={handleDelete}
|
|
727
|
+
onCancel={() => setOpen(false)}
|
|
728
|
+
/>
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
```tsx
|
|
732
|
+
// After
|
|
733
|
+
<Dialog
|
|
734
|
+
type="alertdialog"
|
|
735
|
+
size="small"
|
|
736
|
+
open={open}
|
|
737
|
+
onRequestClose={() => setOpen(false)}
|
|
738
|
+
>
|
|
739
|
+
<Dialog.Content>
|
|
740
|
+
<Dialog.Header>
|
|
741
|
+
<Dialog.Title>Delete job #2121</Dialog.Title>
|
|
742
|
+
</Dialog.Header>
|
|
743
|
+
<Dialog.Body style={{ paddingTop: "var(--space-base)" }}>
|
|
744
|
+
<Text>
|
|
745
|
+
Deleting this job will remove all associated visits and invoices.
|
|
746
|
+
</Text>
|
|
747
|
+
</Dialog.Body>
|
|
748
|
+
<Dialog.Footer>
|
|
749
|
+
<Dialog.Actions
|
|
750
|
+
primary={{
|
|
751
|
+
label: "Delete Job",
|
|
752
|
+
onClick: handleDelete,
|
|
753
|
+
variation: "destructive",
|
|
754
|
+
}}
|
|
755
|
+
secondary={{ label: "Cancel", onClick: () => setOpen(false) }}
|
|
756
|
+
/>
|
|
757
|
+
</Dialog.Footer>
|
|
758
|
+
</Dialog.Content>
|
|
759
|
+
</Dialog>
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
**Match the body spacing.** The old `ConfirmationModal` rendered its message
|
|
763
|
+
with `16px` (`--space-base`) of space below the title. `Dialog.Body` doesn't add
|
|
764
|
+
that gap by default, so add `16px` of top padding to the body to keep the same
|
|
765
|
+
spacing:
|
|
766
|
+
|
|
767
|
+
```tsx
|
|
768
|
+
<Dialog.Body style={{ paddingTop: "var(--space-base)" }}>…</Dialog.Body>
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
| ConfirmationModal prop | Dialog equivalent |
|
|
772
|
+
| ---------------------------------- | ------------------------------------------------ |
|
|
773
|
+
| `open` | `open` |
|
|
774
|
+
| `onCancel` / dismiss | `onRequestClose` (+ the secondary/cancel action) |
|
|
775
|
+
| `title` | `<Dialog.Title>` |
|
|
776
|
+
| `message` | `<Dialog.Body><Text>…</Text></Dialog.Body>` |
|
|
777
|
+
| `children` (in place of `message`) | `<Dialog.Body>` |
|
|
778
|
+
| `confirmLabel` + `onConfirm` | `Dialog.Actions primary={{ label, onClick }}` |
|
|
779
|
+
| `cancelLabel` + `onCancel` | `Dialog.Actions secondary={{ label, onClick }}` |
|
|
780
|
+
| `variation="destructive"` | `primary={{ …, variation: "destructive" }}` |
|
|
781
|
+
| `variation="work"` | Default primary button |
|
|
782
|
+
| `size="small" \| "large"` | `size="small"` / `size="base"` |
|
|
783
|
+
| (implicit alert behavior) | `type="alertdialog"` |
|
|
784
|
+
|
|
785
|
+
### The imperative `ref.show()` pattern
|
|
786
|
+
|
|
787
|
+
`ConfirmationModal` exposed a `confirmationModalRef` with a `show({ … })` method
|
|
788
|
+
so one modal could be reused across a list of items. Dialog replaces that with a
|
|
789
|
+
detached trigger connected through a handle, passing the per-item data as
|
|
790
|
+
`payload`:
|
|
791
|
+
|
|
792
|
+
```tsx
|
|
793
|
+
const handle = Dialog.createHandle();
|
|
794
|
+
|
|
795
|
+
// One Dialog, rendered once
|
|
796
|
+
<Dialog handle={handle} type="alertdialog" size="small">
|
|
797
|
+
{({ payload }) => (
|
|
798
|
+
<Dialog.Content>
|
|
799
|
+
<Dialog.Header>
|
|
800
|
+
<Dialog.Title>Remove {payload?.name}</Dialog.Title>
|
|
801
|
+
</Dialog.Header>
|
|
802
|
+
<Dialog.Body>
|
|
803
|
+
<Text>{payload?.name} will be removed from your client list.</Text>
|
|
804
|
+
</Dialog.Body>
|
|
805
|
+
<Dialog.Footer>
|
|
806
|
+
<Dialog.Actions
|
|
807
|
+
primary={{
|
|
808
|
+
label: "Remove Client",
|
|
809
|
+
variation: "destructive",
|
|
810
|
+
onClick: () => payload?.onConfirm(),
|
|
811
|
+
}}
|
|
812
|
+
secondary={{ label: "Cancel" }}
|
|
813
|
+
/>
|
|
814
|
+
</Dialog.Footer>
|
|
815
|
+
</Dialog.Content>
|
|
816
|
+
)}
|
|
817
|
+
</Dialog>;
|
|
818
|
+
|
|
819
|
+
// A trigger per row, each passing its own payload
|
|
820
|
+
{
|
|
821
|
+
clients.map(client => (
|
|
822
|
+
<Dialog.Trigger key={client.id} handle={handle} payload={client}>
|
|
823
|
+
<Button label="Remove" />
|
|
824
|
+
</Dialog.Trigger>
|
|
825
|
+
));
|
|
826
|
+
}
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
If you'd rather not use a handle, hold the active item in state and drive a
|
|
830
|
+
single controlled `Dialog` — open it when a row's button is clicked and read the
|
|
831
|
+
stored item in the body.
|
|
832
|
+
|
|
833
|
+
***
|
|
834
|
+
|
|
835
|
+
## Size mapping
|
|
836
|
+
|
|
837
|
+
Dialog widths are max widths; the dialog narrows to fit smaller viewports.
|
|
838
|
+
|
|
839
|
+
| Source | Old width | Use in Dialog |
|
|
840
|
+
| -------------------------------- | --------- | ----------------------- |
|
|
841
|
+
| Modal default | 600px | `size="base"` (720px) |
|
|
842
|
+
| Modal `size="small"` | 400px | `size="small"` (440px) |
|
|
843
|
+
| Modal `size="large"` | 940px | `size="large"` (1032px) |
|
|
844
|
+
| Modal `size="fullScreen"` | 100dvw | `size="fullScreen"` |
|
|
845
|
+
| ConfirmationModal `size="small"` | — | `size="small"` |
|
|
846
|
+
| ConfirmationModal `size="large"` | — | `size="base"` |
|
|
847
|
+
|
|
848
|
+
Widths aren't pixel-identical to the old components — pick the closest size and
|
|
849
|
+
verify the result rather than expecting an exact match.
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
## Props
|
|
853
|
+
|
|
854
|
+
### Web
|
|
855
|
+
|
|
856
|
+
#### Dialog
|
|
857
|
+
|
|
858
|
+
| Prop | Type | Required | Default | Description |
|
|
859
|
+
|------|------|----------|---------|-------------|
|
|
860
|
+
| `children` | `ReactNode | ((state: { payload: Payload; }) => ReactNode)` | No | — | The content of the dialog. This can be a regular React node or a render function that receives the payload of the act... |
|
|
861
|
+
| `defaultTriggerId` | `string` | No | — | ID of the trigger that the dialog is associated with. This is useful in conjunction with the `defaultOpen` prop to cr... |
|
|
862
|
+
| `finalFocus` | `boolean | RefObject<HTMLElement> | ((closeType: InteractionType) => boolean | void | HTMLElement)` | No | — | @see {@link https://base-ui.com/react/components/dialog#DialogPopup-finalFocus} |
|
|
863
|
+
| `handle` | `DialogHandle<Payload>` | No | — | A handle to associate the dialog with a trigger. If specified, allows external triggers to control the dialog's open ... |
|
|
864
|
+
| `onOpenChange` | `(open: boolean, eventDetails: DialogRootChangeEventDetails) => void` | No | — | Event handler called when the dialog is opened or closed. |
|
|
865
|
+
| `onOpenChangeComplete` | `(open: boolean) => void` | No | — | Event handler called after any animations complete when the dialog is opened or closed. |
|
|
866
|
+
| `onRequestClose` | `(eventDetails: DialogRootChangeEventDetails) => void` | No | — | Called when the user asks to close (escape, backdrop, dismiss button). Receives BaseUI's change-event details (reason... |
|
|
867
|
+
| `open` | `boolean` | No | — | Whether the dialog is currently open. |
|
|
868
|
+
| `size` | `DialogSize` | No | `base` | Width of the dialog. On mobile when this is set to "fullScreen" the bottom sheet will be full screen (100% height). |
|
|
869
|
+
| `triggerId` | `string` | No | — | ID of the trigger that the dialog is associated with. This is useful in conjunction with the `open` prop to create a ... |
|
|
870
|
+
| `type` | `DialogType` | No | `dialog` | Type of the dialog. An `alertdialog` requires the user to choose an action before it can be closed (no outside/backdr... |
|
|
871
|
+
|
|
872
|
+
#### Dialog.Actions
|
|
873
|
+
|
|
874
|
+
| Prop | Type | Required | Default | Description |
|
|
875
|
+
|------|------|----------|---------|-------------|
|
|
876
|
+
| `align` | `DialogActionsAlign` | No | `right` | Layout for the right-hand action group. |
|
|
877
|
+
| `className` | `string` | No | — | Class name applied to the part's root element. |
|
|
878
|
+
| `primary` | `{ onClick?: never; external?: never; readonly name?: string; submit: never; readonly type?: ButtonType; readonly value?: string; readonly disabled?: boolean; readonly loading?: boolean; ... 17 more ...; readonly children?: never; } | ... 35 more ... | (({ ...; } | ... 35 more ... | ReactElement<...>) & ({ ...; } | ....` | No | — | Primary action (right). |
|
|
879
|
+
| `secondary` | `{ onClick?: never; external?: never; readonly name?: string; submit: never; readonly type?: ButtonType; readonly value?: string; readonly disabled?: boolean; readonly loading?: boolean; ... 17 more ...; readonly children?: never; } | ... 35 more ... | (({ ...; } | ... 35 more ... | ReactElement<...>) & ({ ...; } | ....` | No | — | Secondary action (right, subtle). |
|
|
880
|
+
| `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
|
|
881
|
+
| `tertiary` | `{ onClick?: never; external?: never; readonly name?: string; submit: never; readonly type?: ButtonType; readonly value?: string; readonly disabled?: boolean; readonly loading?: boolean; ... 17 more ...; readonly children?: never; } | ... 35 more ... | (({ ...; } | ... 35 more ... | ReactElement<...>) & ({ ...; } | ....` | No | — | Tertiary/destructive action (left). |
|
|
882
|
+
|
|
883
|
+
#### Dialog.Body
|
|
884
|
+
|
|
885
|
+
| Prop | Type | Required | Default | Description |
|
|
886
|
+
|------|------|----------|---------|-------------|
|
|
887
|
+
| `children` | `ReactNode` | Yes | — | Body content; describes the dialog (wired to `aria-describedby`). |
|
|
888
|
+
| `className` | `string` | No | — | Class name applied to the part's root element. |
|
|
889
|
+
| `fullBleed` | `boolean` | No | `false` | Removes body padding |
|
|
890
|
+
| `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
|
|
891
|
+
|
|
892
|
+
#### Dialog.Close
|
|
893
|
+
|
|
894
|
+
| Prop | Type | Required | Default | Description |
|
|
895
|
+
|------|------|----------|---------|-------------|
|
|
896
|
+
| `children` | `ReactNode` | No | — | Optional content for the dismiss control. An Atlantis `<Button>` is adapted automatically; any other valid element is... |
|
|
897
|
+
| `className` | `string` | No | — | Class name applied to the part's root element. |
|
|
898
|
+
| `nativeButton` | `boolean` | No | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if ... |
|
|
899
|
+
| `render` | `DialogCloseRender` | No | — | Renders a custom dismiss element in place of the default `ButtonDismiss` Pass an element or a function that receives ... |
|
|
900
|
+
| `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
|
|
901
|
+
|
|
902
|
+
#### Dialog.Content
|
|
903
|
+
|
|
904
|
+
| Prop | Type | Required | Default | Description |
|
|
905
|
+
|------|------|----------|---------|-------------|
|
|
906
|
+
| `className` | `string` | No | — | Class name applied to the part's root element. |
|
|
907
|
+
| `render` | `ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogPopupState>` | No | — | @see {@link https://base-ui.com/react/components/dialog#DialogPopup-render} |
|
|
908
|
+
| `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
|
|
909
|
+
|
|
910
|
+
#### Dialog.Footer
|
|
911
|
+
|
|
912
|
+
| Prop | Type | Required | Default | Description |
|
|
913
|
+
|------|------|----------|---------|-------------|
|
|
914
|
+
| `children` | `ReactNode` | Yes | — | Footer content. Commonly a `Dialog.Actions`, but the footer is a generic region — it can hold anything (a note, a che... |
|
|
915
|
+
| `className` | `string` | No | — | Class name applied to the part's root element. |
|
|
916
|
+
| `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
|
|
917
|
+
|
|
918
|
+
#### Dialog.Header
|
|
919
|
+
|
|
920
|
+
| Prop | Type | Required | Default | Description |
|
|
921
|
+
|------|------|----------|---------|-------------|
|
|
922
|
+
| `children` | `ReactNode` | Yes | — | Header content — commonly a `Dialog.Title` and a `Dialog.Close`. The header only owns layout; it does not impose typo... |
|
|
923
|
+
| `className` | `string` | No | — | Class name applied to the part's root element. |
|
|
924
|
+
| `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
|
|
925
|
+
|
|
926
|
+
#### Dialog.ScrollArea
|
|
927
|
+
|
|
928
|
+
| Prop | Type | Required | Default | Description |
|
|
929
|
+
|------|------|----------|---------|-------------|
|
|
930
|
+
| `children` | `ReactNode` | Yes | — | Content that scrolls together — typically a `Dialog.Body` and a non-pinned `Dialog.Header`/`Dialog.Footer`. |
|
|
931
|
+
| `className` | `string` | No | — | Class name applied to the part's root element. |
|
|
932
|
+
| `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
|
|
933
|
+
|
|
934
|
+
#### Dialog.Title
|
|
935
|
+
|
|
936
|
+
| Prop | Type | Required | Default | Description |
|
|
937
|
+
|------|------|----------|---------|-------------|
|
|
938
|
+
| `children` | `ReactNode` | Yes | — | Title text. Rendered as a `Heading` and auto-wired to the dialog's `aria-labelledby`. |
|
|
939
|
+
| `className` | `string` | No | — | Class name applied to the part's root element. |
|
|
940
|
+
| `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
|
|
941
|
+
|
|
942
|
+
#### Dialog.Trigger
|
|
943
|
+
|
|
944
|
+
| Prop | Type | Required | Default | Description |
|
|
945
|
+
|------|------|----------|---------|-------------|
|
|
946
|
+
| `children` | `ReactNode` | No | — | Trigger content; an Atlantis `<Button>` or raw text. Optional content forwarded to the rendered element. |
|
|
947
|
+
| `className` | `string` | No | — | Class name applied to the part's root element. |
|
|
948
|
+
| `handle` | `DialogHandle<Payload>` | No | — | Links a detached trigger to a `Dialog` with the matching `handle`, when the trigger can't be nested inside the `Dialog`. |
|
|
949
|
+
| `id` | `string` | No | — | ID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trig... |
|
|
950
|
+
| `nativeButton` | `boolean` | No | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if ... |
|
|
951
|
+
| `payload` | `Payload` | No | — | Data passed to the dialog when this trigger opens it (used with `handle`). |
|
|
952
|
+
| `render` | `DialogTriggerRender` | No | — | Renders a custom trigger element in place of the default `<Button>`. |
|
|
953
|
+
| `style` | `CSSProperties` | No | — | Inline styles applied to the part's root element. |
|