Clipboard
Copy a value to the clipboard, and say so, twice, because a label alone is not announced.
Copying is one action with a state that lasts two seconds, and the state is the whole of the
component: a value is written, the control says it worked, and then it goes back to being a
control. The framework ships it as two halves, CopyButton, a button that already says the
right thing, and Clipboard, the headless primitive that owns the writing and the timing and
renders whatever a consumer's render function returns.
What separates it from a Button with a clipboard call in its handler is that the result is
reported twice. The label changes to Copied and the same wording is written into a live region
beside it, because a label that changes under the reader's finger is not announced by a screen
reader; the framework's control supplies both so a consumer does not have to remember the second.
Usage
import { CopyButton } from "@asheeui/web";
<CopyButton
value="INV-1042"
label="Copy invoice number"
copiedAnnouncement="Invoice number copied"
/>
value is what is written, and it is the only required prop. label and copiedLabel are the
two names of the control, registered as Copy and Copied; copiedAnnouncement is the sentence
the live region receives, and without it the announcement is the copied label and the value
together, Copied: INV-1042, which is usually too much to hear but always true.
The copied state lasts timeout milliseconds, registered as 2000: long enough to read, short
enough that the control is a copy control again by the time a reader wants it. onCopy receives
the text that was written, so a screen can record the copy or offer to undo it, and onError
receives whatever the browser threw.
copyIcon and copiedIcon replace the drawing before the label. Everything else is a button's:
variant, color, size, radius, className, isDisabled and the rest reach the framework's
Button unchanged, so a copy control sitting in a table row is a small ghost button and one at
the foot of a dialog is whatever the dialog's actions are.
Your own control
Clipboard is the half that does the work. It renders nothing of its own: it takes the value,
the timing and a render function, and hands that function the state to draw with, copied,
error, copy and reset.
import { Clipboard } from "@asheeui/web";
<Clipboard value={invoice.id}>
{({ copied, copy }) => (
<button type="button" onClick={() => void copy()}>
{copied ? "Copied" : "Copy invoice number"}
</button>
)}
</Clipboard>
copy writes the component's value, or a text passed to it, copy(row.email) is how one
control copies a different cell of the row it sits in. It resolves to whether the write
succeeded, so a consumer can branch without reading the state:
const copied = await copy();
if (!copied) report("The clipboard refused us");
error is the other half of that answer, and it is a state a consumer usually wants to show,
because a browser may refuse clipboard access outside a user gesture or without permission. The
primitive does not give up silently: when the asynchronous clipboard API is missing it falls back
to a temporary selection and document.execCommand("copy"), restoring focus to whatever was
focused before, so a page over plain HTTP still copies. reset clears both states without
copying, for a control that is dismissed before its two seconds are up.
The announcement is the consumer's. Clipboard renders exactly what the render function
returns, so a bare Clipboard that draws a button and no status region copies silently. That is
why the preview on this page uses CopyButton: it is the same primitive with the live region
already in it. A consumer's own control needs its own role="status" element, as the demo above
has.
Props
Clipboard takes the value, the timing, the two callbacks and the render function, and resolves
timeout from the instance, then components.clipboard.timeout, then the fallback of 2000.
The registered values for the control are label Copy, copiedLabel Copied, variant
bordered, color primary, size sm and timeout 2000, a small bordered button with the
product's own accent, because a copy control is a secondary action beside the thing it copies.
radius resolves from the instance, then components.clipboard.radius, then the application's
defaultRadius, then the fallback sm, the same cascade the Button it renders follows, so a
product that rounds its buttons rounds its copy control with them unless it says otherwise.
value, label, copiedLabel, copiedAnnouncement, errorAnnouncement, the two icons and the
two callbacks are instance-only. What is being copied, what it is called and what is said about
it are statements about one control in one place; the treatment and the timing are statements
about the design.
Accessibility
- The result is reported twice: the label becomes
Copiedand a sentence is written into a visually hiddenrole="status"region witharia-live="polite". The region issr-only, so it stays in the accessibility tree without taking space, and it is the reason a copy is announced at all, a label change alone is not. - The control is the same
<button type="button">before and after a copy, so focus never moves: a reader hears the announcement where they are and can copy again immediately.type="button"means it never submits a form it happens to sit in. - The announcement names what was copied, by default the copied label and the value, or
copiedAnnouncementwhen a product wants a shorter sentence. Because it is a live region, the sentence replaces the previous one, so a value with a long URL in it is read out in full unless the consumer shortens it. - A failure is a state rather than silence, and it is announced as one: the write being refused,
or there being nothing to copy, sets
error, leaves the label onCopy(a control that could not copy never claims it did) and writes a sentence into the same live region,Copy failedunlesserrorAnnouncementsays otherwise. The consumer still receivesonErrorwhether or not it also states its own wording.
Native
@asheeui/native ships both halves with the same props, and copies through the platform's own
clipboard module rather than a selection. The package carries no icon set, so the affordances are
the characters ⧉ and ✓ unless copyIcon or copiedIcon replaces them, the control's name is
its wording rather than the wording plus a glyph, and the status region is a pixel-sized
transparent view that stays in the accessibility tree. See React Native.