PlConfirmProvider
One dialog, asked for from anywhere under it. await confirm(…) returns the answer, so the branch after a question stays in the handler that asked it.
import { PlConfirmProvider, usePlConfirm } from 'plass-ui';
// once, near the root
<PlConfirmProvider>
<App />
</PlConfirmProvider>;
// anywhere under it
const { confirm } = usePlConfirm();
if (await confirm({ title: 'Delete this project?', color: 'danger' })) {
await remove(project);
}import 'package:plass_ui/plass_ui.dart';
// once, near the root
PlConfirmProvider(child: MyApp());
// anywhere under it
if (await PlConfirmProvider.of(context).confirm(
const PlConfirmOptions(title: Text('Delete this project?'), color: PlassColor.danger),
)) {
await remove(project);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The default size for every question. A single call can override it |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | The family the confirming button takes by default |
| confirmLabel | ReactNode | 'Confirm' | The default word on the button that answers yes |
| cancelLabel | ReactNode | 'Cancel' | The default word on the button that answers no |
| acknowledgeLabel | ReactNode | 'OK' | The default word on an alert's single button |
| width | number | string | — | How wide the sheet may get |
| children | ReactNode | — | The tree that can ask questions |
| Prop | Type | Default | Description |
|---|---|---|---|
| child * | Widget | — | The application |
| confirmLabel | Widget | Text('Confirm') | The default word on the button that answers yes |
| cancelLabel | Widget | Text('Cancel') | The default word on the button that answers no |
| acknowledgeLabel | Widget | Text('OK') | The default word on an alert's single button |
| width | double? | — | How wide the sheet may get |
| sizeshared | PlassSize | PlassSize.md | The default size for every question. A single call can override it |
| colorshared | PlassColor | PlassColor.primary | The family the confirming button takes by default |
The provider's props are defaults for every question asked under it. A single call can override any of them, see PlConfirmOptions below.
PlConfirmOptions
| Prop | Type | Default | Description |
|---|---|---|---|
| title | ReactNode | — | The question, as the <h2> that names the dialog |
| description | ReactNode | — | A line under it, and the dialog's accessible description. Say what happens |
| children | ReactNode | — | Anything more that belongs in the body |
| confirmLabel | ReactNode | — | The word on the button that answers yes |
| cancelLabel | ReactNode | — | The word on the button that answers no. Not drawn by alert |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | — | The family the confirming button takes. danger for anything that removes something |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | — | The size of this one question |
| initialFocus | 'confirm' | 'cancel' | 'cancel' | Which button holds the focus when it opens. cancel by default, because an Enter key landing on the destructive action defeats the dialog |
| dismissible | boolean | true | Whether Escape and a click outside answer no. A question that cannot be escaped is a trap |
| width | number | string | — | How wide the sheet may get |
| Prop | Type | Default | Description |
|---|---|---|---|
| title | Widget? | — | The question, as the <h2> that names the dialog |
| description | Widget? | — | A line under it, and the dialog's accessible description. Say what happens |
| child | Widget? | — | Anything more that belongs in the body |
| confirmLabel | Widget? | — | The word on the button that answers yes |
| cancelLabel | Widget? | — | The word on the button that answers no. Not drawn by alert |
| colorshared | PlassColor | — | The family the confirming button takes. danger for anything that removes something |
| sizeshared | PlassSize | — | The size of this one question |
| initialFocus | PlConfirmFocus | PlConfirmFocus.cancel | Which button holds the focus when it opens. cancel by default, because an Enter key landing on the destructive action defeats the dialog |
| dismissible | bool | true | Whether Escape and a click outside answer no. A question that cannot be escaped is a trap |
| width | double? | — | How wide the sheet may get |
PlConfirmProvider.of(context) rather than a hook, the same lookup PlToastProvider offers, and the framework's own shape for this. It asserts outside a provider rather than returning null, for the reason the React build throws.
initialFocus takes a PlConfirmFocus rather than a string. There is no dismissible: false equivalent to worry about: a press outside and the × both report through the same path.
The hook form
The thing a caller has at the moment a question is warranted is a click handler, not a place in the tree. Without this, the same delete button needs a piece of state, a <PlModal> kept mounted beside it, and the work after the answer torn in half across a callback, three edits to add a confirmation to one button, repeated at every button that needs one.
It is PlToastProvider's arrangement for the same reason and with the same trade: one component near the root, and a hook everywhere else.
Examples
alert
One button and no answer. It resolves when the message has been acknowledged, which is what makes it awaitable in the middle of a sequence.
initialFocus
Cancel holds the focus by default, and that is the decision worth stating: a confirm dialog exists to make somebody stop, and an Enter key that lands on the destructive action defeats the whole thing.
Move it for a question whose yes is the harmless answer, "Save before closing?", where making somebody reach for the mouse to agree is its own kind of rude.
One vocabulary for the application
<PlConfirmProvider confirmLabel="확인" cancelLabel="취소" acknowledgeLabel="확인">
<App />
</PlConfirmProvider>A question that has to be answered
await confirm({
title: 'Your changes have not been saved.',
confirmLabel: 'Discard',
cancelLabel: 'Go back',
dismissible: false
});dismissible is on by default, because Escape is the universal "no" and a question that cannot be escaped is a trap. Turn it off for the one that genuinely has to be answered, and mean it.
Notes
- Questions asked while one is open are queued, in the order they were asked, and the dialog's content changes rather than the sheet closing and reopening. The alternative is a promise nobody ever resolves, which is a hung button rather than a visible bug.
- A provider that unmounts with questions outstanding resolves them all with
false. A promise that is never settled is a handler that never runs itsfinally, so a route change would otherwise leave a button spinning for the rest of the session. usePlConfirmthrows outside a provider rather than resolvingfalse. A silentfalseis a delete button that quietly does nothing, which is worse than a missing provider, since that fails on the first press.- Escape and a click outside answer no, never yes.
Accessibility
- It is a real modal dialog: the focus is trapped inside it, the page behind is inert, and the focus returns to whatever opened it.
titleis the<h2>that names the dialog anddescriptionis its accessible description, so a screen reader reads the question and the consequence before either button.- The two buttons are named by their labels. Name them for what they do ("Delete", "Discard", "Save") rather than "Yes" and "No", which are unreadable out of context and are exactly what a screen reader reads out of context.