app/components/admin/copy-button.tsx
133 lines and 14 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| CopyButton | 8 to 68 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:32> | 32 to 51 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:35> | 35 to 42 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:39> | 39 to 41 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:43> | 43 to 50 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:47> | 47 to 49 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| CopyTextButton | 76 to 138 | 6 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:92> | 92 to 98 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:94> | 94 to 97 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| settle | 100 to 108 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:107> | 107 to 107 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:117> | 117 to 125 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:122> | 122 to 122 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:123> | 123 to 123 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
Imports
It imports app/components/admin/live-notice.tsx, app/components/admin/toast.tsx, app/lib/clipboard.ts. Imported by app/components/admin/media-inspector-sections.tsx, app/components/admin/media-inspector.tsx, app/components/admin/media-tile.tsx, app/components/admin/preview-links.tsx, app/components/admin/settings-drawer.tsx.
Source
import { useEffect, useRef, useState } from "react";
import { LiveNotice } from "~/components/admin/live-notice";
import { toast } from "~/components/admin/toast";
import { copyText } from "~/lib/clipboard";
// An icon, not the word: the word cost a third of the row and pushed the filename into truncation.
export function CopyButton({
value,
label,
name,
showLabel,
tabIndex,
}: {
value: string;
label: string;
name?: string;
showLabel?: boolean;
/** -1 on a grid tile that is not the grid's tab stop. */
tabIndex?: number;
}) {
return (
<button
type="button"
className="cap-btn"
data-variant="quiet"
data-size="sm"
data-icon-only={showLabel ? undefined : ""}
data-cap-copy=""
title={value}
tabIndex={tabIndex}
onClick={(event) => {
const button = event.currentTarget;
copyText(value)
.then(() => {
button.dataset.copied = "yes";
// Announced too: the data attribute drives a ::after that assistive technology cannot see.
toast(`Copied ${value}`);
window.setTimeout(() => {
delete button.dataset.copied;
}, 1500);
})
.catch(() => {
button.dataset.copied = "no";
toast(`Copy failed. The address is ${value}`);
// Cleared like success, or "failed" stays on the button after a later copy works.
window.setTimeout(() => {
delete button.dataset.copied;
}, 2400);
});
}}
>
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path
d="M5.5 5.5h7v8h-7zM3.5 10.5h-1v-8h7v1"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
{showLabel ? <span>{label}</span> : null}
{/* Always named: the visible label is absent on a tile and only a fragment elsewhere. */}
<span className="cap-sr-only">{name ?? `Copy the address for ${label}`}</span>
</button>
);
}
/**
* A text button whose own label reports the outcome, for rows where a toast has nowhere to show.
* The label is only what a sighted author sees; a screen reader hears the outcome from LiveNotice's
* regions, which are in the document before any copy, and both reset so a stale "Copied" never
* describes a later press.
*/
export function CopyTextButton({
value,
label,
name,
subject,
}: {
value: string;
label: string;
/** Read after the label and never shown, so a row of identical "Copy" buttons each says which. */
name?: string;
/** What was copied, for the announcement: "the link starting abc123". */
subject: string;
}) {
const [copied, setCopied] = useState<"" | "copied" | "failed">("");
const timer = useRef(0);
const mounted = useRef(false);
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
window.clearTimeout(timer.current);
};
}, []);
const settle = (outcome: "copied" | "failed") => {
// A copy that settles after the row is gone (a revoke, a closed drawer) must not start a timer
// the unmount cleanup already ran past.
if (!mounted.current) return;
setCopied(outcome);
window.clearTimeout(timer.current);
// A failure stays longer: it asks the author to do something.
timer.current = window.setTimeout(() => setCopied(""), outcome === "copied" ? 2000 : 5000);
};
return (
<>
<button
type="button"
className="cap-btn"
data-size="sm"
data-variant="quiet"
onClick={() => {
// Emptied first, so a second copy of the same thing is announced again.
window.clearTimeout(timer.current);
setCopied("");
copyText(value).then(
() => settle("copied"),
() => settle("failed"),
);
}}
>
{copied === "copied" ? "Copied" : copied === "failed" ? "Copy failed" : label}
{name ? <span className="cap-sr-only"> {name}</span> : null}
</button>
<div className="cap-sr-only">
<LiveNotice
status={copied === "copied" ? `Copied ${subject}.` : null}
alert={copied === "failed" ? `Could not copy ${subject}. The browser blocked the clipboard.` : null}
/>
</div>
</>
);
}