From 30005f8b1075e310c084554e915e7e25662a3bc7 Mon Sep 17 00:00:00 2001 From: CAESAR MCMXCVII Date: Sat, 30 May 2026 13:20:54 +0100 Subject: [PATCH] Add-helper-for-auto-selecting-all-text-in-copy-input-fields-on-focus --- src/components/common/CopyField.tsx | 72 +++++++++++++++++++ src/components/common/TransactionHashRow.tsx | 70 +++++------------- src/components/common/TruncatedAddress.tsx | 68 +++++------------- .../__tests__/TransactionHashRow.test.tsx | 6 +- src/hooks/useAutoSelectOnFocus.ts | 21 ++++++ 5 files changed, 132 insertions(+), 105 deletions(-) create mode 100644 src/components/common/CopyField.tsx create mode 100644 src/hooks/useAutoSelectOnFocus.ts diff --git a/src/components/common/CopyField.tsx b/src/components/common/CopyField.tsx new file mode 100644 index 0000000..6ecfffc --- /dev/null +++ b/src/components/common/CopyField.tsx @@ -0,0 +1,72 @@ +import { useState } from 'react'; +import { Copy, Check } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { useAutoSelectOnFocus } from '@/hooks/useAutoSelectOnFocus'; +import CopySuccessAnnouncement from '@/components/common/CopySuccessAnnouncement'; +import { useCopySuccessAnnouncement } from '@/hooks/useCopySuccessAnnouncement'; + +interface CopyFieldProps { + value: string; + label: string; + className?: string; + inputClassName?: string; + buttonClassName?: string; +} + +const CopyField: React.FC = ({ + value, + label, + className, + inputClassName, + buttonClassName, +}) => { + const [copied, setCopied] = useState(false); + const inputRef = useAutoSelectOnFocus(); + const { announcement, announceCopySuccess } = useCopySuccessAnnouncement(); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(value); + announceCopySuccess(`${label} copied.`); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + setCopied(false); + } + }; + + return ( +
+ + + +
+ ); +}; + +export default CopyField; diff --git a/src/components/common/TransactionHashRow.tsx b/src/components/common/TransactionHashRow.tsx index 275dad7..71ff1bd 100644 --- a/src/components/common/TransactionHashRow.tsx +++ b/src/components/common/TransactionHashRow.tsx @@ -1,9 +1,7 @@ -import React, { useState } from 'react'; -import { Copy, Check, ExternalLink } from 'lucide-react'; +import type { FC } from 'react'; +import { ExternalLink } from 'lucide-react'; import { cn } from '@/lib/utils'; -import { shortenAddress } from '@/lib/web3/format'; -import CopySuccessAnnouncement from '@/components/common/CopySuccessAnnouncement'; -import { useCopySuccessAnnouncement } from '@/hooks/useCopySuccessAnnouncement'; +import CopyField from '@/components/common/CopyField'; interface TransactionHashRowProps { hash: string; @@ -12,23 +10,12 @@ interface TransactionHashRowProps { className?: string; } -const TransactionHashRow: React.FC = ({ +const TransactionHashRow: FC = ({ hash, label = 'Transaction Hash', explorerUrl, className, }) => { - const [copied, setCopied] = useState(false); - const { announcement, announceCopySuccess } = useCopySuccessAnnouncement(); - - const handleCopy = async (e: React.MouseEvent) => { - e.stopPropagation(); - await navigator.clipboard.writeText(hash); - announceCopySuccess('Transaction hash copied.'); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - return (
= ({ {label}
- - {shortenAddress(hash, 8, 8)} - - + + + )}
); diff --git a/src/components/common/TruncatedAddress.tsx b/src/components/common/TruncatedAddress.tsx index 896d7e6..a143953 100644 --- a/src/components/common/TruncatedAddress.tsx +++ b/src/components/common/TruncatedAddress.tsx @@ -1,67 +1,37 @@ -import { useState } from 'react'; -import { Copy, Check } from 'lucide-react'; import { cn } from '@/lib/utils'; -import CopySuccessAnnouncement from '@/components/common/CopySuccessAnnouncement'; -import { useCopySuccessAnnouncement } from '@/hooks/useCopySuccessAnnouncement'; +import CopyField from '@/components/common/CopyField'; interface TruncatedAddressProps { address: string; - prefixChars?: number; - suffixChars?: number; copyable?: boolean; className?: string; } -function truncate(address: string, prefix: number, suffix: number): string { - if (address.length <= prefix + suffix + 3) { - return address; - } - return `${address.slice(0, prefix)}...${address.slice(-suffix)}`; -} - const TruncatedAddress: React.FC = ({ address, - prefixChars = 6, - suffixChars = 4, copyable = false, className, }) => { - const [copied, setCopied] = useState(false); - const { announcement, announceCopySuccess } = useCopySuccessAnnouncement(); - - const handleCopy = async () => { - await navigator.clipboard.writeText(address); - announceCopySuccess('Address copied.'); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; + if (!copyable) { + return ( + + {address} + + ); + } return ( - - {truncate(address, prefixChars, suffixChars)} - {copyable && ( - <> - - - - )} - + ); }; diff --git a/src/components/common/__tests__/TransactionHashRow.test.tsx b/src/components/common/__tests__/TransactionHashRow.test.tsx index b55b1aa..416170b 100644 --- a/src/components/common/__tests__/TransactionHashRow.test.tsx +++ b/src/components/common/__tests__/TransactionHashRow.test.tsx @@ -22,7 +22,7 @@ describe('TransactionHashRow', () => { expect(writeText).toHaveBeenCalledWith('0xabcdef1234567890'); expect( - screen.getByRole('button', { name: 'Transaction hash copied' }) + screen.getByRole('button', { name: /transaction hash copied/i }) ).toBeInTheDocument(); act(() => { @@ -30,10 +30,10 @@ describe('TransactionHashRow', () => { }); const status = screen.getByRole('status'); - expect(status).toHaveTextContent('Transaction hash copied.'); + expect(status).toHaveTextContent(/transaction hash copied\./i); expect(status).toHaveClass('sr-only'); expect(status).not.toHaveTextContent( - 'Transaction hash copied to clipboard' + 'transaction hash copied to clipboard' ); vi.useRealTimers(); diff --git a/src/hooks/useAutoSelectOnFocus.ts b/src/hooks/useAutoSelectOnFocus.ts new file mode 100644 index 0000000..a2ba904 --- /dev/null +++ b/src/hooks/useAutoSelectOnFocus.ts @@ -0,0 +1,21 @@ +import { useRef, useEffect, type RefObject } from 'react'; + +export function useAutoSelectOnFocus(): RefObject { + const ref = useRef(null); + + useEffect(() => { + const node = ref.current; + if (!node) return; + + const handleFocus = () => { + node.select(); + }; + + node.addEventListener('focus', handleFocus); + return () => { + node.removeEventListener('focus', handleFocus); + }; + }, []); + + return ref; +}