Dialog
Dialog draws a panel and a scrim. The behaviour comes from @hozo/patterns.
Open it, then press Escape or one of its buttons:
import { Heading, Paragraph } from '@hozo/core'import { Button, Dialog, Stack } from '@hozo/ui'import { useState } from 'react'
export function DialogDemo() { const [open, setOpen] = useState(false) return ( <> <Button tone="danger" onPress={() => setOpen(true)}> Delete project </Button> <Dialog open={open} onClose={() => setOpen(false)} accessibilityLabel="Delete the project"> <Stack gap="normal"> <Heading level={3} className="text-lg font-bold text-hozo-text"> Delete the project? </Heading> <Paragraph className="text-sm text-hozo-text-body"> Every build and report goes with it. This cannot be undone. </Paragraph> <Stack direction="row" gap="tight"> <Button tone="danger" onPress={() => setOpen(false)}> Delete </Button> <Button tone="quiet" onPress={() => setOpen(false)}> Keep it </Button> </Stack> </Stack> </Dialog> </> )}On the Web it is a real <dialog> opened with showModal(), so the browser
supplies what a modal needs:
- the rest of the page becomes inert
- focus stays inside
- Escape closes it, through the
cancelevent - focus goes back to the button that opened it
The scrim is the dialog’s ::backdrop, so there is no overlay element and no
z-index to manage.
opendrives it, andonCloseis called when the browser asks to close it (Escape). Keepopenin your state and set it tofalsethere.accessibilityLabelis the dialog’s accessible name. A screen reader announces it when the dialog opens.- There is no close button. A dismiss control needs a name and a place in the reading order, so it is yours to put in the children, as the example does. Escape works without one.
- On React Native it is a
Modal. PassrestoreFocusTowith a ref to the opener so TalkBack and VoiceOver land back on it, since React Native cannot report what had focus.