From 8a1503ef749bc6002e285022020211962217488a Mon Sep 17 00:00:00 2001
From: Usman Abeeb <136495186+therealbibson@users.noreply.github.com>
Date: Sun, 27 Sep 2026 04:34:19 +0100
Subject: [PATCH] fix(components): add copy success animation and aria-live
feedback to CopyLinkButton
Closes #663
---
src/__tests__/CopyLinkButton.test.tsx | 65 +++++++++++++++++++++++++++
src/components/CopyLinkButton.tsx | 56 ++++++++++++++++++++---
2 files changed, 114 insertions(+), 7 deletions(-)
create mode 100644 src/__tests__/CopyLinkButton.test.tsx
diff --git a/src/__tests__/CopyLinkButton.test.tsx b/src/__tests__/CopyLinkButton.test.tsx
new file mode 100644
index 00000000..4e310e15
--- /dev/null
+++ b/src/__tests__/CopyLinkButton.test.tsx
@@ -0,0 +1,65 @@
+import React from "react";
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import CopyLinkButton from "@/components/CopyLinkButton";
+
+const URL = "https://stellarsplit.app/i/42";
+
+function mockClipboard() {
+ const writeText = vi.fn().mockResolvedValue(undefined);
+ Object.defineProperty(navigator, "clipboard", {
+ value: { writeText },
+ configurable: true,
+ writable: true,
+ });
+ return writeText;
+}
+
+describe("CopyLinkButton", () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it("starts in the idle state", () => {
+ mockClipboard();
+ render();
+
+ expect(
+ screen.getByRole("button", { name: /copy verification link/i })
+ ).toBeInTheDocument();
+ expect(screen.getByText("Copy Link")).toBeInTheDocument();
+ });
+
+ it("switches to the success state and announces after a successful copy", async () => {
+ const writeText = mockClipboard();
+ render();
+
+ fireEvent.click(
+ screen.getByRole("button", { name: /copy verification link/i })
+ );
+
+ await waitFor(() =>
+ expect(screen.getByText("Copied!")).toBeInTheDocument()
+ );
+
+ expect(writeText).toHaveBeenCalledWith(URL);
+
+ // Icon swap: the checkmark replaces the link icon.
+ const button = screen.getByRole("button", { name: /copied/i });
+ expect(button.querySelector("svg")).toBeTruthy();
+
+ // Green success styling for the 2s window.
+ expect(button.className).toContain("bg-green-600");
+
+ // Screen-reader announcement.
+ expect(screen.getByText("Link copied")).toBeInTheDocument();
+ });
+
+ it("exposes a polite live region for the announcement", () => {
+ mockClipboard();
+ const { container } = render();
+
+ const live = container.querySelector('[aria-live="polite"]');
+ expect(live).not.toBeNull();
+ expect(live).toHaveTextContent("");
+ });
+});
diff --git a/src/components/CopyLinkButton.tsx b/src/components/CopyLinkButton.tsx
index e6c2c684..a7f3c82e 100644
--- a/src/components/CopyLinkButton.tsx
+++ b/src/components/CopyLinkButton.tsx
@@ -6,12 +6,54 @@ export default function CopyLinkButton({ url }: { url: string }) {
const { copy, copied } = useCopyToClipboard();
return (
-
+ <>
+
+
+
+ {copied ? "Link copied" : ""}
+
+ >
);
}