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:
64
webapp-next/src/lib/copy-to-clipboard.test.ts
Normal file
64
webapp-next/src/lib/copy-to-clipboard.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
41
webapp-next/src/lib/copy-to-clipboard.ts
Normal file
41
webapp-next/src/lib/copy-to-clipboard.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user