|
| 1 | +--- |
| 2 | +sidebar_position: 2 |
| 3 | +--- |
| 4 | + |
| 5 | +# Dialog |
| 6 | + |
| 7 | +Dialogs interrupt the user with an important prompt or a focused task. |
| 8 | +Follows the [Material Design 3 Dialog](https://m3.material.io/components/dialogs/overview) specification. |
| 9 | + |
| 10 | +Dialog renders through [Portal](./portal.mdx), so your app must be wrapped in a `PortalHost` once at the root. It claims the `PORTAL_LAYERS.dialog` layer, which puts it above bottom sheets and below snackbars, menus, and tooltips. |
| 11 | + |
| 12 | +## Usage |
| 13 | + |
| 14 | +<SnackExample |
| 15 | + name="RootNative Dialog — Usage" |
| 16 | + code={` |
| 17 | + import { Button, Dialog, PortalHost } from '@rootnative/components' |
| 18 | + import { ThemeProvider } from '@rootnative/core' |
| 19 | + import { SafeAreaProvider } from 'react-native-safe-area-context' |
| 20 | + import { useState } from 'react' |
| 21 | + import { View } from 'react-native' |
| 22 | +
|
| 23 | + function Screen() { |
| 24 | + const [open, setOpen] = useState(false) |
| 25 | + return ( |
| 26 | + <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> |
| 27 | + <Button onPress={() => setOpen(true)}>Delete file</Button> |
| 28 | + <Dialog visible={open} onDismiss={() => setOpen(false)}> |
| 29 | + <Dialog.Title>Delete file?</Dialog.Title> |
| 30 | + <Dialog.Content> |
| 31 | + This permanently removes report.pdf. You cannot undo this action. |
| 32 | + </Dialog.Content> |
| 33 | + <Dialog.Actions> |
| 34 | + <Button variant="text" onPress={() => setOpen(false)}>Cancel</Button> |
| 35 | + <Button variant="text" onPress={() => setOpen(false)}>Delete</Button> |
| 36 | + </Dialog.Actions> |
| 37 | + </Dialog> |
| 38 | + </View> |
| 39 | + ) |
| 40 | + } |
| 41 | +
|
| 42 | + export default function App() { |
| 43 | + return ( |
| 44 | + <SafeAreaProvider> |
| 45 | + <ThemeProvider> |
| 46 | + <PortalHost> |
| 47 | + <Screen /> |
| 48 | + </PortalHost> |
| 49 | + </ThemeProvider> |
| 50 | + </SafeAreaProvider> |
| 51 | + ) |
| 52 | + } |
| 53 | +`} |
| 54 | +/> |
| 55 | + |
| 56 | +## Slots |
| 57 | + |
| 58 | +Dialog composes from four slots. **Write them in any order** — Dialog places them |
| 59 | +in MD3 order (icon → headline → content → actions) itself, which is what lets the |
| 60 | +fullscreen variant move the title and actions into its header. |
| 61 | + |
| 62 | +| Slot | Renders | MD3 role | |
| 63 | +|------|---------|----------| |
| 64 | +| `Dialog.Icon` | An `IconSource` at 24dp, centered | Optional hero icon. Its presence centers the headline. | |
| 65 | +| `Dialog.Title` | `headlineSmall` / `onSurface` | Headline | |
| 66 | +| `Dialog.Content` | Strings get `bodyMedium` / `onSurfaceVariant`; anything else renders as-is | Supporting text or arbitrary content | |
| 67 | +| `Dialog.Actions` | An end-aligned row with an 8dp gap | Action buttons — text `Button`s per MD3 | |
| 68 | + |
| 69 | +Any child that isn't one of these is treated as content, so `<Dialog><MyForm /></Dialog>` works without ceremony. |
| 70 | + |
| 71 | +## With an icon |
| 72 | + |
| 73 | +An icon makes the dialog a hero dialog: the icon sits centered above the headline |
| 74 | +in `secondary`, and the headline centers with it. |
| 75 | + |
| 76 | +<SnackExample |
| 77 | + name="RootNative Dialog — Icon" |
| 78 | + code={` |
| 79 | + import { Button, Dialog, PortalHost } from '@rootnative/components' |
| 80 | + import { ThemeProvider } from '@rootnative/core' |
| 81 | + import { SafeAreaProvider } from 'react-native-safe-area-context' |
| 82 | + import { useState } from 'react' |
| 83 | + import { View } from 'react-native' |
| 84 | +
|
| 85 | + function Screen() { |
| 86 | + const [open, setOpen] = useState(false) |
| 87 | + return ( |
| 88 | + <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> |
| 89 | + <Button variant="tonal" onPress={() => setOpen(true)}>Reset settings</Button> |
| 90 | + <Dialog visible={open} onDismiss={() => setOpen(false)}> |
| 91 | + <Dialog.Icon icon="alert-circle-outline" /> |
| 92 | + <Dialog.Title>Reset settings?</Dialog.Title> |
| 93 | + <Dialog.Content> |
| 94 | + Every preference returns to its default. Your data is untouched. |
| 95 | + </Dialog.Content> |
| 96 | + <Dialog.Actions> |
| 97 | + <Button variant="text" onPress={() => setOpen(false)}>Cancel</Button> |
| 98 | + <Button variant="text" onPress={() => setOpen(false)}>Reset</Button> |
| 99 | + </Dialog.Actions> |
| 100 | + </Dialog> |
| 101 | + </View> |
| 102 | + ) |
| 103 | + } |
| 104 | +
|
| 105 | + export default function App() { |
| 106 | + return ( |
| 107 | + <SafeAreaProvider> |
| 108 | + <ThemeProvider> |
| 109 | + <PortalHost> |
| 110 | + <Screen /> |
| 111 | + </PortalHost> |
| 112 | + </ThemeProvider> |
| 113 | + </SafeAreaProvider> |
| 114 | + ) |
| 115 | + } |
| 116 | +`} |
| 117 | +/> |
| 118 | + |
| 119 | +## Full-screen |
| 120 | + |
| 121 | +`variant="fullscreen"` fills the screen instead of centering a card. Dialog builds |
| 122 | +the MD3 header for you: a leading close button wired to `onDismiss`, the |
| 123 | +`Dialog.Title` as a `titleLarge` headline, and `Dialog.Actions` as the trailing |
| 124 | +confirming action. `Dialog.Content` scrolls below it. There is no scrim. |
| 125 | + |
| 126 | +<SnackExample |
| 127 | + name="RootNative Dialog — Full-screen" |
| 128 | + code={` |
| 129 | + import { Button, Column, Dialog, PortalHost, TextField } from '@rootnative/components' |
| 130 | + import { ThemeProvider } from '@rootnative/core' |
| 131 | + import { SafeAreaProvider } from 'react-native-safe-area-context' |
| 132 | + import { useState } from 'react' |
| 133 | + import { View } from 'react-native' |
| 134 | +
|
| 135 | + function Screen() { |
| 136 | + const [open, setOpen] = useState(false) |
| 137 | + return ( |
| 138 | + <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> |
| 139 | + <Button variant="tonal" onPress={() => setOpen(true)}>Edit profile</Button> |
| 140 | + <Dialog visible={open} variant="fullscreen" onDismiss={() => setOpen(false)}> |
| 141 | + <Dialog.Title>Edit profile</Dialog.Title> |
| 142 | + <Dialog.Actions> |
| 143 | + <Button variant="text" onPress={() => setOpen(false)}>Save</Button> |
| 144 | + </Dialog.Actions> |
| 145 | + <Dialog.Content> |
| 146 | + <Column gap="lg"> |
| 147 | + <TextField label="Display name" value="Ada Lovelace" /> |
| 148 | + <TextField label="Email" value="ada@example.com" /> |
| 149 | + </Column> |
| 150 | + </Dialog.Content> |
| 151 | + </Dialog> |
| 152 | + </View> |
| 153 | + ) |
| 154 | + } |
| 155 | +
|
| 156 | + export default function App() { |
| 157 | + return ( |
| 158 | + <SafeAreaProvider> |
| 159 | + <ThemeProvider> |
| 160 | + <PortalHost> |
| 161 | + <Screen /> |
| 162 | + </PortalHost> |
| 163 | + </ThemeProvider> |
| 164 | + </SafeAreaProvider> |
| 165 | + ) |
| 166 | + } |
| 167 | +`} |
| 168 | +/> |
| 169 | + |
| 170 | +## Dismissal |
| 171 | + |
| 172 | +`onDismiss` fires for the *user-initiated* ways out — a scrim tap, the Android |
| 173 | +hardware back button, and the fullscreen close button. Buttons inside |
| 174 | +`Dialog.Actions` are yours to wire; Dialog never closes itself on an action press. |
| 175 | + |
| 176 | +Set `dismissable={false}` when the user has to resolve the dialog with one of its |
| 177 | +actions. The scrim stops responding and stops being announced as an accessibility |
| 178 | +affordance, and the back button falls through to navigation. |
| 179 | + |
| 180 | +## Motion |
| 181 | + |
| 182 | +Basic dialogs scale up from 80% and fade in on `springDefaultSpatial`; the scrim |
| 183 | +fades on `springFastEffects`. The fullscreen variant rises 48dp instead of scaling. |
| 184 | + |
| 185 | +Every one of these collapses to a hard cut under reduced motion — see [Motion](../motion.md). |
| 186 | + |
| 187 | +> **Intentional deviation.** MD3 slides a full-screen dialog up from the bottom |
| 188 | +> edge of the screen. That needs the measured surface height, which fights the |
| 189 | +> safe-area layout, so RootNative uses a 48dp rise. At spring speed the two read |
| 190 | +> the same. |
| 191 | +
|
| 192 | +## Tokens |
| 193 | + |
| 194 | +| Token | Basic | Full-screen | |
| 195 | +|-------|-------|-------------| |
| 196 | +| Container | `surfaceContainerHigh` | `surface` | |
| 197 | +| Corner | 28dp (`cornerExtraLarge`) | — | |
| 198 | +| Elevation | Level 3 | — | |
| 199 | +| Width | min 280dp, max 560dp | Fills the screen | |
| 200 | +| Padding | 24dp | 24dp body, 56dp header | |
| 201 | +| Icon | 24dp, `secondary` | 24dp, `secondary` | |
| 202 | +| Headline | `headlineSmall` / `onSurface` | `titleLarge` / `onSurface` | |
| 203 | +| Supporting text | `bodyMedium` / `onSurfaceVariant` | `bodyMedium` / `onSurfaceVariant` | |
| 204 | +| Actions | Text buttons, end-aligned, 8dp gap | Trailing text button in the header | |
| 205 | +| Scrim | `scrim` @ 32% | None | |
| 206 | + |
| 207 | +## Accessibility |
| 208 | + |
| 209 | +- The basic surface reports `role="alertdialog"`; the fullscreen surface reports `role="dialog"`. Both set `aria-modal` and `accessibilityViewIsModal`, so assistive tech treats the content beneath as inert. |
| 210 | +- `Dialog.Title` reports `role="heading"`. |
| 211 | +- The scrim is announced as a button labelled "Close dialog" — override with `scrimAccessibilityLabel`. It disappears from the accessibility tree when `dismissable={false}`. |
| 212 | +- The fullscreen close button is labelled "Close" — override with `closeAccessibilityLabel`. |
| 213 | + |
| 214 | +## Props |
| 215 | + |
| 216 | +<PropTable name="Dialog" /> |
| 217 | + |
| 218 | +### Dialog.Icon |
| 219 | + |
| 220 | +<PropTable name="DialogIcon" /> |
| 221 | + |
| 222 | +### Dialog.Title |
| 223 | + |
| 224 | +<PropTable name="DialogTitle" /> |
| 225 | + |
| 226 | +### Dialog.Content |
| 227 | + |
| 228 | +<PropTable name="DialogContent" /> |
| 229 | + |
| 230 | +### Dialog.Actions |
| 231 | + |
| 232 | +<PropTable name="DialogActions" /> |
0 commit comments