On this page
Import
import {
Badge,
Button,
Column,
ConfirmDialog,
DescriptionList,
List,
NavigationDrawer,
Page,
PageHeader,
Radio,
RadioGroup,
Row,
Select,
Surface,
Switch,
Text,
TextField,
ToastProvider,
useThemePreference,
useToast,
} from '@envsemble/envui';
Usage
A section of settings that apply at once is a section named by its heading, holding a List whose items each take a Switch at their end.
- Each Switch's
onCheckedChangestores the change at once; name it witharia-labelledbyandaria-describedbypointing at the row's title and detail. - Show a toast through
useToastonly for what the page can't show. - When storing fails, set the Switch back and show a danger toast with a Try again action.
import { useId, useState } from 'react';
import { List, Column, Switch, Text, ToastProvider, useToast } from '@envsemble/envui';
interface Setting {
id: string;
title: string;
detail: string;
}
/** A section of settings that each apply as they are switched: no Save button. */
function SettingsSection({
title,
settings,
values,
onChange,
}: {
title: string;
settings: readonly Setting[];
values: Record<string, boolean>;
onChange: (id: string, on: boolean) => void;
}) {
const id = useId();
return (
<Column render={<section aria-labelledby={`${id}-heading`} />} gap={3}>
<Text render={<h3 id={`${id}-heading`} />} variant="title">
{title}
</Text>
<List.Root aria-labelledby={`${id}-heading`}>
{settings.map((setting) => (
<List.Item
key={setting.id}
// A switch is narrow: it keeps to the row's end, and the text wraps beside it.
style={{ flexWrap: 'nowrap' }}
label={<span id={`${id}-${setting.id}-title`}>{setting.title}</span>}
description={<span id={`${id}-${setting.id}-detail`}>{setting.detail}</span>}
end={
// The row's title names the switch, and its detail describes it.
<Switch
aria-labelledby={`${id}-${setting.id}-title`}
aria-describedby={`${id}-${setting.id}-detail`}
checked={values[setting.id]}
onCheckedChange={(on) => onChange(setting.id, on)}
style={{ flexShrink: 0 }}
/>
}
/>
))}
</List.Root>
</Column>
);
}
function NotificationSettings() {
const toast = useToast();
const [values, setValues] = useState<Record<string, boolean>>({
comments: true,
mentions: true,
summary: true,
directory: false,
});
const change = (id: string, on: boolean) => {
// The change is saved as it is made; a request to the server would go here.
setValues((current) => ({ ...current, [id]: on }));
// Only a change that shows somewhere else gets a notification; the switch itself says the rest.
if (id === 'directory')
toast.show({
message: on
? 'Other people at Northwind can now find you in the directory.'
: 'Only your teams can now find you in the directory.',
});
};
return (
<Column gap={6}>
<SettingsSection
title="Email notifications"
settings={notifications}
values={values}
onChange={change}
/>
<SettingsSection title="Privacy" settings={privacy} values={values} onChange={change} />
</Column>
);
}
export default function ImmediateToggles() {
// An application has one ToastProvider at its root; the example brings its own.
return (
<ToastProvider>
<NotificationSettings />
</ToastProvider>
);
}
const notifications: readonly Setting[] = [
{ id: 'comments', title: 'Comments', detail: 'When someone comments on your documents.' },
{
id: 'mentions',
title: 'Mentions',
detail: 'When someone mentions you in a task or a comment.',
},
{ id: 'summary', title: 'Weekly summary', detail: 'Your projects’ progress, every Monday.' },
];
const privacy: readonly Setting[] = [
{
id: 'directory',
title: 'Show me in the directory',
detail: 'Everyone at Northwind can find your name, team and email address.',
},
];Save a section
A section that saves together is a form with noValidate and an onSubmit, on a Surface through render.
- A warning Badge, "Unsaved changes", shows while the draft differs from what was saved.
- Save is a
type="submit"Button withpendingwhile it saves, and Cancel, a secondary Button, puts the saved values back. - Say the outcome in a
role="status"message, and on a refused Save set the field'serrorand focus it.
<Surface render={<form noValidate aria-labelledby="profile-heading" onSubmit={submit} />}>
{changed && <Badge tone="warning">Unsaved changes</Badge>}
{fields}
<Button type="submit" pending={saving}>
Save
</Button>
<Button variant="secondary" onClick={cancel}>
Cancel
</Button>
</Surface>
Appearance
useThemePreference() returns the preference, system, light or dark, and setPreference, which applies it to the whole page at once through the root ThemePreferenceProvider. The scale, system, comfortable or compact, is stored by the application and passed to the root provider's scale, never to one page or section.
<RadioGroup label="Theme" name="theme" value={preference} onValueChange={setPreference}>
<Radio value="system" label="System" description="Follows your device’s setting." />
<Radio value="light" label="Light" />
<Radio value="dark" label="Dark" />
</RadioGroup>
Danger zone
The danger zone is the last section: a List item whose end is an outline Button with tone="danger", opening a ConfirmDialog with tone="danger". Where the action can't be undone and reaches everyone, the dialog holds a TextField for the name and takes confirmDisabled while the name doesn't match, as the DangerZone example does.
<ConfirmDialog
open={open}
onOpenChange={change}
tone="danger"
title="Delete the Northwind workspace?"
confirmLabel="Delete workspace"
onConfirm={confirm}
>
<TextField
ref={field}
label="Workspace name"
value={typed}
error={error}
onValueChange={setTyped}
/>
</ConfirmDialog>
Composition
Lay the page out as a Page with a PageHeader, its sections in a Column, each a section with a h2 or h3 Text heading and a List of settings. A long page lists its sections in a NavigationDrawer beside them. The anatomy's parts are the section heading, env-text, each setting, env-list-item, its Switch, env-switch, and the danger zone's button, env-button. Text fields, Selects and a RadioGroup sit in a section that saves together; a DescriptionList shows values that are only read.
Accessibility
You provide
- A heading for every section, and a name for every switch: its label, or
aria-labelledbyandaria-describedbyto the row's title and detail. - Storing each change, and a status when a section is saved.
- One ToastProvider and one ThemePreferenceProvider at the root.
envUI provides
- Switch as a native checkbox with the switch role, its label stable in both states.
- RadioGroup as a
fieldsetwith alegend, moved through with the arrow keys. - A ConfirmDialog with the alert dialog role, and toasts in a polite live region.
Changelog
- 1.0.0
The page is on the figure template.