On this page
Import
import {
Accordion,
Badge,
Button,
Checkbox,
Column,
DescriptionList,
Dialog,
Disclosure,
IconButton,
Link,
List,
Menu,
Popover,
Row,
Select,
Sheet,
Text,
TextField,
Tooltip,
} from '@envsemble/envui';
Usage
A Disclosure takes its summary as title and holds the optional settings.
- Control it with
openandonOpenChangewhere the application opens it, or start it open withdefaultOpen. - Open it from the application when it holds an invalid field, as the Disclosure page describes.
import { useEffect, useRef, useState, type FormEvent } from 'react';
import {
Button,
Checkbox,
Disclosure,
Select,
Column,
Row,
Text,
TextField,
} from '@envsemble/envui';
export default function AdvancedOptions() {
const [format, setFormat] = useState<string | null>('csv');
const [advanced, setAdvanced] = useState(false);
const [fileName, setFileName] = useState('');
const [fileNameError, setFileNameError] = useState<string>();
// The status under the action: the outcome, or the error when an export is refused.
const [status, setStatus] = useState('');
const fileNameField = useRef<HTMLInputElement>(null);
// Counts the refused exports; each focuses the file name once its disclosure has opened.
const [refused, setRefused] = useState(0);
useEffect(() => {
if (refused) fileNameField.current?.focus();
}, [refused]);
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (fileName && !/^[\w-]+$/.test(fileName)) {
const message = 'Use only letters, numbers, hyphens and underscores';
setFileNameError(message);
// An error inside the disclosure opens it, so the field and its message show.
setAdvanced(true);
// 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 === fileNameField.current) setStatus(message);
else {
setStatus('');
setRefused((count) => count + 1);
}
return;
}
setStatus(`Exported ${fileName || 'quarterly-report'}.${format}.`);
};
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' }}>
<Select
label="Format"
name="format"
items={formats}
value={format}
onValueChange={setFormat}
/>
{/* Settings most people leave alone wait behind a disclosure; nothing required is in it. */}
<Disclosure title="Advanced options" open={advanced} onOpenChange={setAdvanced}>
<Column gap={4}>
<Checkbox name="archived" label="Include archived projects" />
<TextField
ref={fileNameField}
label="File name (optional)"
name="fileName"
description="Without one, the file takes the report’s name."
suffix={`.${format}`}
autoComplete="off"
spellCheck={false}
value={fileName}
error={fileNameError}
onValueChange={(value) => {
setFileName(value);
if (!value || /^[\w-]+$/.test(value)) {
setFileNameError(undefined);
setStatus('');
}
}}
/>
</Column>
</Disclosure>
<Row wrap gap={3}>
<Button type="submit">Export report</Button>
</Row>
<Text role="status" emphasis="muted">
{status}
</Text>
</Column>
</form>
);
}
const formats = [
{ value: 'csv', label: 'CSV' },
{ value: 'pdf', label: 'PDF' },
{ value: 'xlsx', label: 'Spreadsheet (.xlsx)' },
];Show all
A text Button after the list, with aria-expanded and aria-controls naming the list, toggles between the first few items and all of them, and keeps focus as the list grows.
<Button
variant="text"
aria-expanded={expanded}
aria-controls={listId}
onClick={() => setExpanded((current) => !current)}
>
{expanded ? 'Show fewer' : `Show all ${activity.length}`}
</Button>
Details in a sheet
A row's text Button, with aria-haspopup="dialog", opens a Sheet controlled through open and onOpenChange, with the item's details, such as a stacked DescriptionList, and an actions link to its own page. Keep the shown item while the sheet closes, so its content doesn't empty as it slides out.
<Sheet
open={open}
onOpenChange={setOpen}
title={project.name}
closeLabel="Close details"
actions={<Button href={`/projects/${project.id}`}>Open project</Button>}
>
<DescriptionList.Root layout="stacked">{details}</DescriptionList.Root>
</Sheet>
Overflow actions
A Menu named by aria-label takes a ghost IconButton with IconMoreVert and aria-label="More actions" as its trigger, wrapped in a Tooltip that names it. A Menu.Separator sets a destructive item apart, last, as on Menu.
<Menu.Root
aria-label="Launch brief actions"
trigger={
<Tooltip content="More actions">
<IconButton variant="ghost" icon={<IconMoreVert />} aria-label="More actions" />
</Tooltip>
}
>
<Menu.Item icon={<IconFileCopy />}>Duplicate</Menu.Item>
<Menu.Separator />
<Menu.Item icon={<IconArchive />}>Archive</Menu.Item>
</Menu.Root>
Explain a term
A Popover with side="bottom" opens from an IconButton named for the term, such as aria-label="About billable hours", and holds a short definition and a Link.
Composition
The anatomy's parts are the visible layer, here a TextField's env-text-field, the Disclosure's summary, env-disclosure summary, its panel, env-disclosure-panel, and the More actions trigger, env-button ~ env-button.
Accessibility
You provide
- A name for every icon-only trigger, and
aria-expandedandaria-controlson a Show all button.
envUI provides
- Disclosure on native
detailsandsummary, whose closed content is hidden from assistive technology. - Focus moved in, trapped while modal and returned by Sheet and Dialog; the menu button pattern for Menu.
Changelog
- 1.0.0
The page is on the figure template, with Accordion among the depths.