recipes/accessible-page/page.tsx
One file of recipes/accessible-page, at commit 9a5bb24.
// A page everyone can use: an image that says what it shows, a form whose fields have names, a
// button that is only an icon and still has a name, a menu and a dialog the keyboard drives, and
// a status message a screen reader hears when it changes.
//
// Nothing here is written for the checks. It is the ordinary way to write these things with the
// stack, and the checks are what say so: the build reads the markup, the mount reads the page,
// and `main.ts` drives it with `audit` and `walk`.
import { mutable } from '@aweftjs/core';
import {
Button, Checkbox, Default, Icon, Menu, Modal, Stage, StageContext, TextField, Theme, h,
} from '@aweftjs/ui';
import type { StageValue } from '@aweftjs/ui';
Theme.define({
// A page's own sizes are named where they are given, the way the theme names its own.
main: { $measure: '40rem', display: 'flex', flexDirection: 'column', alignItems: 'start', gap: '$space4', maxWidth: '$measure' },
mark: { $markSize: '4rem', width: '$markSize', height: '$markSize' },
fields: { display: 'flex', flexDirection: 'column', gap: '$space3', width: '100%' },
actions: { display: 'flex', gap: '$space2', alignItems: 'center' },
});
// One drawing, inline, so the recipe carries no image file. The `alt` beside it is the point.
const MARK = 'data:image/svg+xml,' + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="28" fill="#4a7" /></svg>',
);
const Note = (): unknown => (
<div theme="column">
<p theme="text" id="dialog-body">The note goes out the moment you close this.</p>
</div>
);
export const Page = (): unknown => {
const name = mutable('');
const note = mutable('');
const copy = mutable(false);
// A live region announces what changes inside it, so the message lands in an element that is
// already on the page, and the cell fills it in (WCAG 4.1.3).
const status = mutable('');
const picked = mutable('');
const send = (): void => {
status.set(`Sent to ${name.get() === '' ? 'nobody' : name.get()}${copy.get() ? ', with a copy to you' : ''}.`);
};
const clear = (): void => {
name.set('');
note.set('');
copy.set(false);
status.set('Cleared.');
};
const Opener = StageContext.use((stage: StageValue | null) => (): unknown => (
<Button
label="Preview"
type="quiet"
id="preview"
onClick={() => { stage?.open({ name: 'preview', template: Modal, label: 'Your note' }); }}
/>
));
return (
<main theme="main" id="page">
<h1>Send a note</h1>
{/* WCAG 1.1.1: what the image shows, in words. Decoration would be alt="". */}
<img src={MARK} alt="A green circle, the mark of this recipe" theme="mark" />
{/* WCAG 1.3.1, 3.3.2: each field has a label the component pairs to it. */}
<div theme="fields">
<TextField label="To" value={name} id="to" />
<TextField label="Note" value={note} id="note" onEnter={send} />
<Checkbox label="Send me a copy" value={copy} id="copy" />
</div>
<div theme="actions">
<Button label="Send" id="send" onClick={send} />
{/* WCAG 4.1.2: an icon on its own is not a name, so the button carries one. */}
<Button size="icon" type="quiet" aria-label="Clear the form" id="clear" icon={<Icon name="lucide:x" />} onClick={clear} />
<Menu
id="more"
label="More"
items={[
{ label: 'Save as draft', onSelect: () => { picked.set('draft'); } },
{ label: 'Schedule', onSelect: () => { picked.set('schedule'); } },
]}
/>
<StageContext acts={{ '': () => null, preview: Note }} initial="" template={Default}>
<Opener />
<Stage />
</StageContext>
</div>
{/* WCAG 4.1.3: a status message, in the page before it has anything to say. */}
<p theme={['text', 'sm']} role="status" id="status">{status}</p>
<p theme={['text', 'sm', 'muted']} id="picked">{picked}</p>
</main>
);
};