On this page
Import
import {
Alert,
Badge,
Button,
Column,
DescriptionList,
Link,
List,
RelativeTime,
Row,
Select,
Text,
TextField,
ToastProvider,
useToast,
} from '@envsemble/envui';
Usage
useToast() returns show, dismiss and clear. One ToastProvider at the application's root holds the toasts.
show({ tone, message, action })shows a toast and returns its id;toneisneutral,success,infoordanger.- A success or neutral toast closes after its
duration, 5 seconds by default. - A danger toast, and one with an
action, stays until dismissed unlessdurationis set. - The provider's
limit, 3 by default, sets how many show at a time.
import { useEffect, useRef, useState, type FormEvent } from 'react';
import {
Button,
Select,
Column,
Row,
Text,
TextField,
ToastProvider,
useToast,
} from '@envsemble/envui';
import { IconSend } from '@envsemble/envui/icons/Send';
const isEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
function InviteForm() {
const toast = useToast();
const [email, setEmail] = useState('jordan.kim@example.com');
const [error, setError] = useState<string>();
// Says a refused submit's error where focus can't: the outcome itself goes in the toast.
const [status, setStatus] = useState('');
const [sending, setSending] = useState(false);
const field = useRef<HTMLInputElement>(null);
const timer = useRef<number>(undefined);
useEffect(() => () => window.clearTimeout(timer.current), []);
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!isEmail(email)) {
// A problem with a field shows on the field, never in a toast.
const message = 'Enter an email address like name@example.com';
setError(message);
// Focusing the field reads its error. After Enter in the field, focus is already there and
// focusing it again reads nothing, so only then does the status say the error.
if (document.activeElement === field.current) setStatus(message);
else {
setStatus('');
field.current?.focus();
}
return;
}
setStatus('');
setSending(true);
// The invitation goes to a server; a timer stands in for it here.
timer.current = window.setTimeout(() => {
setSending(false);
// People started this, so a short success notification is enough; it closes by itself.
toast.show({ tone: 'success', message: `Invitation sent to ${email.trim()}.` });
setEmail('');
}, 1000);
};
return (
<form noValidate onSubmit={submit}>
{/* envUI has no form column yet: one column, kept to a width that reads easily. */}
<Column gap={5} style={{ maxInlineSize: '32rem' }}>
<TextField
ref={field}
label="Email address"
type="email"
name="email"
autoComplete="off"
spellCheck={false}
value={email}
error={error}
onValueChange={(value) => {
setEmail(value);
if (isEmail(value)) {
setError(undefined);
setStatus('');
}
}}
/>
<Select label="Role" name="role" defaultValue="editor" items={roles} />
<Row wrap gap={3}>
{/* The button shows the work while it runs; the toast says how it ended. */}
<Button type="submit" startIcon={<IconSend />} pending={sending}>
Send invitation
</Button>
</Row>
<Text role="status" emphasis="muted">
{status}
</Text>
</Column>
</form>
);
}
export default function SaveConfirmation() {
// An application has one ToastProvider at its root; the example brings its own.
return (
<ToastProvider>
<InviteForm />
</ToastProvider>
);
}
const roles = [
{ value: 'viewer', label: 'Viewer' },
{ value: 'editor', label: 'Editor' },
{ value: 'owner', label: 'Owner' },
];Background work
A toast with an action, such as { label: 'Download', onClick }, stays until dismissed; call toast.dismiss(id) once it has run. Keep the result on the page too, such as a Link with download, since a toast can be dismissed, and say the progress in a role="status" message, as the BackgroundTask example does.
<Link href={`/exports/${file}`} download>
Download {file}
</Link>
Alerts
A field's error is its error prop, as on the forms page. An Alert on the page from the start takes a tone and actions, and no announce, so it is read with the page; announce gives a warning or danger Alert the alert role, for one that appears because something just happened. After a refused form or failed save, a danger Alert with tabIndex={-1} and a ref takes focus.
<Alert
tone="warning"
title="This project is read-only"
actions={
<Button variant="secondary" size="sm" onClick={requestAccess}>
Request edit access
</Button>
}
>
Ask Alex Morgan, its owner, for edit access to change it.
</Alert>
Status and counts
A Badge's tone adds to its words, one tone per status everywhere, such as success for On track and warning for At risk. A NavigationDrawer or NavigationRail destination's count shows its unread items and is said with its label; 0 shows nothing. A count Badge's aria-label says what it counts, such as "12 unread".
Composition
Status in rows is a List whose items take a Badge, with a RelativeTime in their description for when they changed, as the StatusInRows example does; a page's details beside an Alert are a DescriptionList. A notification centre is a Badge with count on its trigger, a Popover and a List of the notifications.
<List.Item label={project.name} description={<RelativeTime value={project.updated} />}>
<Badge tone="success">On track</Badge>
</List.Item>
Accessibility
You provide
- One ToastProvider at the root, and messages that make sense out of context.
announceonly on an Alert that appears because something just happened.- An
aria-labelon every count whose number alone doesn't say what it counts.
envUI provides
- Toast regions with a polite status, danger toasts announced at once, and F6 to reach them.
- Info and success Alerts as status messages; warning and danger ones take the alert role only with
announce. - Badges whose words carry the meaning, and a destination's count in its name, such as Inbox, 12 new.
Changelog
- 1.0.0
The page is on the figure template, with the feedback and status wording of the content guidelines.