feat: add copy functionality for phone and Telegram username in ContactLinks component

- Implemented copy buttons for phone number and Telegram username in the ContactLinks component, enhancing user interaction.
- Integrated tooltip feedback to indicate successful copy actions.
- Updated tests to cover new copy functionality and ensure proper rendering of copy buttons based on props.
- Added localization support for new copy-related strings in the i18n messages.
This commit is contained in:
2026-03-06 18:55:56 +03:00
parent 34001d22d9
commit 24d6ecbedb
7 changed files with 388 additions and 20 deletions

View File

@@ -0,0 +1,64 @@
/**
* Unit tests for copyToClipboard: Clipboard API and execCommand fallback.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
describe("copyToClipboard", () => {
const writeTextMock = vi.fn();
beforeEach(async () => {
vi.resetModules();
writeTextMock.mockReset();
Object.defineProperty(navigator, "clipboard", {
value: { writeText: writeTextMock },
configurable: true,
writable: true,
});
});
afterEach(() => {
vi.restoreAllMocks();
});
it("returns true when navigator.clipboard.writeText succeeds", async () => {
writeTextMock.mockResolvedValue(undefined);
const { copyToClipboard } = await import("./copy-to-clipboard");
const result = await copyToClipboard("+79991234567");
expect(result).toBe(true);
expect(writeTextMock).toHaveBeenCalledWith("+79991234567");
});
it("returns false when text is empty", async () => {
const { copyToClipboard } = await import("./copy-to-clipboard");
expect(await copyToClipboard("")).toBe(false);
expect(await copyToClipboard(" ")).toBe(false);
expect(writeTextMock).not.toHaveBeenCalled();
});
it("returns false when clipboard.writeText rejects", async () => {
writeTextMock.mockRejectedValue(new Error("Permission denied"));
const { copyToClipboard } = await import("./copy-to-clipboard");
const result = await copyToClipboard("hello");
expect(result).toBe(false);
});
it("uses execCommand fallback when clipboard is missing", async () => {
Object.defineProperty(navigator, "clipboard", { value: undefined, configurable: true });
const execCommandMock = vi.fn().mockReturnValue(true);
(document as unknown as { execCommand?: ReturnType<typeof vi.fn> }).execCommand =
execCommandMock;
vi.resetModules();
const { copyToClipboard } = await import("./copy-to-clipboard");
const result = await copyToClipboard("fallback text");
expect(result).toBe(true);
expect(execCommandMock).toHaveBeenCalledWith("copy");
});
});

View File

@@ -0,0 +1,41 @@
/**
* Copy text to the clipboard. Uses Clipboard API with execCommand fallback.
* Used for copying phone/Telegram on the current duty view.
*/
/**
* Copies the given text to the clipboard.
*
* @param text - Raw text to copy (e.g. phone number or @username).
* @returns Promise resolving to true on success, false on failure. Errors are logged, not thrown.
*/
export async function copyToClipboard(text: string): Promise<boolean> {
if (typeof text !== "string" || text.trim() === "") {
return false;
}
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.warn("clipboard.writeText failed", err);
}
}
try {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "");
textarea.style.position = "absolute";
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.select();
const ok = document.execCommand("copy");
document.body.removeChild(textarea);
return ok;
} catch (err) {
console.warn("execCommand copy fallback failed", err);
return false;
}
}