On this page
Import
import {
Button,
Checkbox,
Column,
ConfirmButton,
ConfirmDialog,
List,
Row,
Text,
TextField,
ToastProvider,
useToast,
} from '@envsemble/envui';
Usage
Act at once and offer Undo: useToast().show({ message, action: { label: 'Undo', onClick } }) returns the toast's id, and the action's onClick puts the item back and calls toast.dismiss(id).
- A toast with an
actionstays until dismissed, so keep what was archived reachable elsewhere too. - Move focus to the next row, or the list, with
tabIndex={-1}, when the action removes its own control.
import { useEffect, useRef, useState } from 'react';
import { Button, List, ToastProvider, useToast } from '@envsemble/envui';
import { IconArchive } from '@envsemble/envui/icons/Archive';
function Projects() {
const toast = useToast();
const [projects, setProjects] = useState(initialProjects);
const list = useRef<HTMLUListElement>(null);
// An archived row takes its button with it, so focus moves to the next row's button, or to the
// list once it is empty.
const focusNext = useRef<(() => void) | null>(null);
useEffect(() => {
focusNext.current?.();
focusNext.current = null;
});
const archive = (name: string) => {
const index = projects.indexOf(name);
focusNext.current = () => {
const buttons = list.current?.querySelectorAll('button') ?? [];
(buttons[Math.min(index, buttons.length - 1)] ?? list.current)?.focus();
};
// The project is archived at once, with no question first: Undo brings it back.
setProjects((current) => current.filter((project) => project !== name));
const id = toast.show({
message: `${name} archived.`,
action: {
label: 'Undo',
onClick: () => {
setProjects((current) =>
initialProjects.filter((project) => project === name || current.includes(project)),
);
toast.dismiss(id);
},
},
});
};
return (
<List.Root ref={list} aria-label="Projects" tabIndex={-1}>
{projects.map((project) => (
<List.Item
key={project}
label={project}
end={
<Button
variant="secondary"
size="sm"
startIcon={<IconArchive />}
aria-label={`Archive ${project}`}
onClick={() => archive(project)}
>
Archive
</Button>
}
/>
))}
{projects.length === 0 && <List.Item label="Every project is archived." />}
</List.Root>
);
}
export default function UndoInstead() {
// An application has one ToastProvider at its root; the example brings its own.
return (
<ToastProvider>
<Projects />
</ToastProvider>
);
}
const initialProjects = [
'Website refresh',
'Quarterly report',
'Onboarding guide',
'Mobile release',
];Confirm in place or in a dialog
A ConfirmButton runs onConfirm on its second press and shows confirmLabel, such as "Remove file", while armed. A ConfirmDialog with tone="danger" takes a title, a description and a confirmLabel, and puts focus on Cancel.
- Opened from a Menu.Item, pass the menu's trigger as
finalFocus, and put the item last, after a Menu.Separator, labelled with its consequence. - Return the action's promise from
onConfirm: the dialog waits, pending, and stays open witherrorset when it fails.
<ConfirmDialog
open={open}
onOpenChange={setOpen}
tone="danger"
title="Delete Website refresh?"
description="This deletes the project for everyone. You can’t undo this."
confirmLabel="Delete project"
onConfirm={() => deleteProject(project.id)}
/>
Type to confirm
Put a TextField for the name inside the ConfirmDialog, as the TypeToConfirm example does, and pass confirmDisabled while the name doesn't match: the confirm button is disabled, and Enter in the field doesn't confirm until it does.
<ConfirmDialog
open={open}
onOpenChange={setOpen}
tone="danger"
title="Delete Data migration?"
confirmLabel="Delete project"
confirmDisabled={typed.trim() !== 'Data migration'}
onConfirm={() => deleteProject(project.id)}
>
<TextField
label="Project name"
description="Enter Data migration to confirm."
value={typed}
onValueChange={setTyped}
/>
</ConfirmDialog>
Composition
Lay the items out as a List, each List.Item taking its trigger at its end: a Button, an IconButton named with aria-label, a ConfirmButton or a Menu. The danger tone, tone="danger", goes only on what deletes or ends something.
Bulk delete selects rows with Checkboxes and names the count in an outline danger Button, the dialog's title and its confirmLabel, as the BulkDelete example does. Its finalFocus returns focus to the button, or to the list, with tabIndex={-1}, once the button has gone with the deleted selection.
Accessibility
You provide
- A name for every icon-only trigger, such as "Remove annual-review.pdf".
finalFocus, and focus for when the action removes its own control.- Dismissing the undo toast once Undo has run, with
dismiss(id).
envUI provides
- ConfirmDialog as an
alertdialog, named and described, with focus on Cancel fortone="danger"; its pending state and anerrorannounced as an alert. - ConfirmButton's armed state in its label, disarmed on Esc, on blur and after
armedDuration. - Toasts announced politely and kept while they have an action.
Changelog
- 1.0.0
The page is on the figure template, with the confirmations of the content guidelines; confirming in place and from a menu are on Button and ConfirmDialog.