From beb4defc36a89ad40691779c40f622f47929a755 Mon Sep 17 00:00:00 2001 From: ranjeet150 Date: Mon, 28 Sep 2026 09:18:02 +0545 Subject: [PATCH] fix(components): clean up share button copy timeout on unmount and dialog close Closes #455 --- src/components/shared/share-button.test.tsx | 54 +++++++++++++++++++++ src/components/shared/share-button.tsx | 36 ++++++++++++-- 2 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 src/components/shared/share-button.test.tsx diff --git a/src/components/shared/share-button.test.tsx b/src/components/shared/share-button.test.tsx new file mode 100644 index 0000000..9fdbe82 --- /dev/null +++ b/src/components/shared/share-button.test.tsx @@ -0,0 +1,54 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, fireEvent, act } from "@testing-library/react"; +import { ShareButton } from "@/components/shared/share-button"; + +describe("ShareButton", () => { + let writeTextMock: ReturnType; + + beforeEach(() => { + writeTextMock = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { + value: { + writeText: writeTextMock, + }, + writable: true, + configurable: true, + }); + }); + + it("renders the share button trigger", () => { + render(); + expect(screen.getByRole("button", { name: /share/i })).toBeInTheDocument(); + }); + + it("opens modal and handles copy and timeout cleanup", async () => { + render(); + + const shareBtn = screen.getByRole("button", { name: /share/i }); + fireEvent.click(shareBtn); + + const copyBtn = screen.getByRole("button", { name: /copy/i }); + expect(copyBtn).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(copyBtn); + }); + + expect(writeTextMock).toHaveBeenCalledWith("https://example.com/share-test"); + }); + + it("clears timeout on component unmount", async () => { + const clearTimeoutSpy = vi.spyOn(global, "clearTimeout"); + const { unmount } = render(); + + fireEvent.click(screen.getByRole("button", { name: /share/i })); + const copyBtn = screen.getByRole("button", { name: /copy/i }); + + await act(async () => { + fireEvent.click(copyBtn); + }); + + unmount(); + expect(clearTimeoutSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/components/shared/share-button.tsx b/src/components/shared/share-button.tsx index b469ebf..0416380 100644 --- a/src/components/shared/share-button.tsx +++ b/src/components/shared/share-button.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Share2, Copy, Twitter, Linkedin, Check } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { @@ -31,6 +31,7 @@ export function ShareButton({ const [open, setOpen] = useState(false); const [copied, setCopied] = useState(false); const [canShare, setCanShare] = useState(false); + const copyTimeoutRef = useRef | null>(null); useEffect(() => { if (typeof navigator !== 'undefined' && navigator.share) { @@ -38,6 +39,25 @@ export function ShareButton({ } }, []); + useEffect(() => { + return () => { + if (copyTimeoutRef.current) { + clearTimeout(copyTimeoutRef.current); + } + }; + }, []); + + const handleOpenChange = (isOpen: boolean) => { + setOpen(isOpen); + if (!isOpen) { + if (copyTimeoutRef.current) { + clearTimeout(copyTimeoutRef.current); + copyTimeoutRef.current = null; + } + setCopied(false); + } + }; + const handleShareClick = async () => { if (canShare) { try { @@ -50,19 +70,25 @@ export function ShareButton({ } catch (err) { if ((err as Error).name !== 'AbortError') { console.error('Error sharing', err); - setOpen(true); + handleOpenChange(true); } } } else { - setOpen(true); + handleOpenChange(true); } }; const handleCopy = async () => { try { await navigator.clipboard.writeText(url); + if (copyTimeoutRef.current) { + clearTimeout(copyTimeoutRef.current); + } setCopied(true); - setTimeout(() => setCopied(false), 2000); + copyTimeoutRef.current = setTimeout(() => { + setCopied(false); + copyTimeoutRef.current = null; + }, 2000); } catch (err) { console.error('Failed to copy', err); } @@ -85,7 +111,7 @@ export function ShareButton({ Share - + Share link