Components
Dialog
A modal window with a focus trap and scroll lock.
A modal window: portalled into body, with a scrim, a focus trap, Escape and a scroll lock on the page beneath it.
Example
import { Dialog, Button, Field, Input } from "@toimetdev/pathlogs-core";
const [open, setOpen] = useState(false);
<Dialog
open={open}
onClose={() => setOpen(false)}
title="New project"
footer={
<>
<Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="primary" onClick={submit}>Create</Button>
</>
}
>
<Field label="Name" required>
{(props) => <Input {...props} />}
</Field>
</Dialog>The state lives outside. That way one and the same window serves both creating and editing without growing an internal mode of its own.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open* | boolean | — | Whether the window is shown. |
onClose* | () => void | — | Called on Escape, a click on the scrim, and the close button. |
title | string | — | The visible heading of the window. |
header | ReactNode | — | A custom heading instead of the title string — with an icon and a counter, say. |
label | string | — | The accessible name when there is no visible heading: the window draws its own, but a screen reader still needs a name. |
footer | ReactNode | — | The action bar at the bottom. |
size | "sm" | "md" | "lg" | "xl" | "md" | Maximum width: 24 / 32 / 42 / 56 rem. |
align | "center" | "top" | "center" | Pin the window to the top — this is how the command palette sits. |
busy | boolean | false | Saving in progress: closing is blocked so the result is not lost. |
dismissOnBackdrop | boolean | true | Close on a click on the scrim. |
dismissOnEscape | boolean | true | Close on Escape. |
open*booleanWhether the window is shown.
onClose*() => voidCalled on Escape, a click on the scrim, and the close button.
titlestringThe visible heading of the window.
headerReactNodeA custom heading instead of the title string — with an icon and a counter, say.
labelstringThe accessible name when there is no visible heading: the window draws its own, but a screen reader still needs a name.
footerReactNodeThe action bar at the bottom.
size"sm" | "md" | "lg" | "xl"defaults to: "md"
Maximum width: 24 / 32 / 42 / 56 rem.
align"center" | "top"defaults to: "center"
Pin the window to the top — this is how the command palette sits.
busybooleandefaults to: false
Saving in progress: closing is blocked so the result is not lost.
dismissOnBackdropbooleandefaults to: true
Close on a click on the scrim.
dismissOnEscapebooleandefaults to: true
Close on Escape.
Focus and keyboard
- On opening, focus moves to the first interactive element, or to the panel itself if there is none. Without this the keyboard would be left on the page beneath the scrim.
- Tab from the last element wraps back to the first: focus never leaves the window.
- On closing, focus returns to wherever the window was opened from — otherwise after Escape the keyboard would end up at the top of the page.
- Page scrolling beneath the window is locked: otherwise the wheel over the scrim scrolls the content away, and on closing the user is no longer where they were.
A window without a heading
If the window draws its own heading (a confirmation, a palette), pass label instead of title: no header bar appears, but the accessible name remains.
<Dialog open={open} onClose={close} label="Delete project?" size="sm">
{/* your own markup with an icon and a heading */}
</Dialog>