diff --git a/ui-react/apps/console/src/components/common/ClipboardProvider.tsx b/ui-react/apps/console/src/components/common/ClipboardProvider.tsx index a11756534d2..a5a6fee40e3 100644 --- a/ui-react/apps/console/src/components/common/ClipboardProvider.tsx +++ b/ui-react/apps/console/src/components/common/ClipboardProvider.tsx @@ -1,27 +1,8 @@ -import { - createContext, - ReactNode, - useCallback, - useContext, - useEffect, - useId, - useMemo, - useRef, - useState, -} from "react"; +import { ReactNode, useCallback, useId, useMemo, useState } from "react"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; +import { ClipboardContext } from "@/hooks/useCopy"; import BaseDialog from "./BaseDialog"; -// ─── Context ────────────────────────────────────────────────────────────────── - -interface ClipboardContextValue { - triggerWarning: () => void; -} - -const ClipboardContext = createContext(null); - -// ─── Provider ───────────────────────────────────────────────────────────────── - /** * Mounts a single clipboard-warning dialog for the whole app. * Wrap the app root with this once; every `useCopy()` call shares it. @@ -81,59 +62,3 @@ export function ClipboardProvider({ children }: { children: ReactNode }) { ); } - -// ─── Hook ───────────────────────────────────────────────────────────────────── - -export interface UseCopyResult { - /** Call with the text to copy. Shows the warning dialog when clipboard access - * is unavailable (insecure context or API error). */ - copy: (text: string) => void; - /** True for 1500 ms after a successful copy. Use for inline visual feedback. */ - copied: boolean; -} - -/** - * Safe clipboard copy with automatic insecure-context handling. - * - * Must be used within ``. - * - * ```tsx - * const { copy, copied } = useCopy(); - * - * ``` - */ -export function useCopy(): UseCopyResult { - const ctx = useContext(ClipboardContext); - if (!ctx) throw new Error("useCopy must be used within "); - - const { triggerWarning } = ctx; - const [copied, setCopied] = useState(false); - const timerRef = useRef | null>(null); - - const copy = useCallback( - (text: string) => { - if (!globalThis.isSecureContext) { - triggerWarning(); - return; - } - - navigator.clipboard.writeText(text).then( - () => { - if (timerRef.current) clearTimeout(timerRef.current); - setCopied(true); - timerRef.current = setTimeout(() => setCopied(false), 1500); - }, - () => triggerWarning(), - ); - }, - [triggerWarning], - ); - - useEffect(() => { - return () => { - if (timerRef.current) clearTimeout(timerRef.current); - }; - }, []); - - return { copy, copied }; -} diff --git a/ui-react/apps/console/src/components/common/CopyButton.tsx b/ui-react/apps/console/src/components/common/CopyButton.tsx index a854eb2b289..a1da6d10e9c 100644 --- a/ui-react/apps/console/src/components/common/CopyButton.tsx +++ b/ui-react/apps/console/src/components/common/CopyButton.tsx @@ -1,5 +1,5 @@ import { CheckIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline"; -import { useCopy } from "./ClipboardProvider"; +import { useCopy } from "@/hooks/useCopy"; const sizes = { sm: { button: "p-1 rounded", icon: "w-3.5 h-3.5" }, diff --git a/ui-react/apps/console/src/components/common/CopyWarning.tsx b/ui-react/apps/console/src/components/common/CopyWarning.tsx index 38d800f49ea..96fb11856c6 100644 --- a/ui-react/apps/console/src/components/common/CopyWarning.tsx +++ b/ui-react/apps/console/src/components/common/CopyWarning.tsx @@ -1,5 +1,5 @@ import { forwardRef, ReactNode, useCallback, useEffect, useImperativeHandle, useRef } from "react"; -import { useCopy } from "./ClipboardProvider"; +import { useCopy } from "@/hooks/useCopy"; export interface CopyWarningRenderProps { /** Triggers a clipboard copy. Shows the warning dialog if clipboard access diff --git a/ui-react/apps/console/src/components/common/__tests__/ClipboardProvider.test.tsx b/ui-react/apps/console/src/components/common/__tests__/ClipboardProvider.test.tsx index ad70310d6c0..8ccbed71290 100644 --- a/ui-react/apps/console/src/components/common/__tests__/ClipboardProvider.test.tsx +++ b/ui-react/apps/console/src/components/common/__tests__/ClipboardProvider.test.tsx @@ -7,7 +7,8 @@ vi.mock("@/hooks/useFocusTrap", () => ({ useFocusTrap: vi.fn(), })); -import { ClipboardProvider, useCopy } from "../ClipboardProvider"; +import { ClipboardProvider } from "../ClipboardProvider"; +import { useCopy } from "@/hooks/useCopy"; // ─── clipboard setup ────────────────────────────────────────────────────────── diff --git a/ui-react/apps/console/src/hooks/useCopy.ts b/ui-react/apps/console/src/hooks/useCopy.ts new file mode 100644 index 00000000000..4cdd9cfe3d3 --- /dev/null +++ b/ui-react/apps/console/src/hooks/useCopy.ts @@ -0,0 +1,72 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; + +// ─── Context ────────────────────────────────────────────────────────────────── + +export interface ClipboardContextValue { + triggerWarning: () => void; +} + +export const ClipboardContext = createContext(null); + +// ─── Hook ───────────────────────────────────────────────────────────────────── + +interface UseCopyResult { + /** Call with the text to copy. Shows the warning dialog when clipboard access + * is unavailable (insecure context or API error). */ + copy: (text: string) => void; + /** True for 1500 ms after a successful copy. Use for inline visual feedback. */ + copied: boolean; +} + +/** + * Safe clipboard copy with automatic insecure-context handling. + * + * Must be used within ``. + * + * ```tsx + * const { copy, copied } = useCopy(); + * + * ``` + */ +export function useCopy(): UseCopyResult { + const ctx = useContext(ClipboardContext); + if (!ctx) throw new Error("useCopy must be used within "); + + const { triggerWarning } = ctx; + const [copied, setCopied] = useState(false); + const timerRef = useRef | null>(null); + + const copy = useCallback( + (text: string) => { + if (!globalThis.isSecureContext) { + triggerWarning(); + return; + } + + navigator.clipboard.writeText(text).then( + () => { + if (timerRef.current) clearTimeout(timerRef.current); + setCopied(true); + timerRef.current = setTimeout(() => setCopied(false), 1500); + }, + () => triggerWarning(), + ); + }, + [triggerWarning], + ); + + useEffect(() => { + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, []); + + return { copy, copied }; +}