mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Message action controls stay visible through focus-within for keyboard navigation, but pointer clicks left the copy/fork buttons focused after the mouse moved away. Release pointer focus on message actions so hover controls return to their quiet state while keeping Tab focus behavior intact. Constraint: Message actions use focus-within for keyboard accessibility and should not remove that path. Rejected: Remove focus-within reveal | would regress keyboard users who tab to copy or fork. Confidence: high Scope-risk: narrow Directive: Keep pointer-only blur scoped to message action controls; do not remove focus-within without a replacement keyboard affordance. Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx --run Tested: bun run check:desktop Not-tested: Live Tauri manual hover smoke Related: #642
62 lines
1.3 KiB
TypeScript
62 lines
1.3 KiB
TypeScript
import { useEffect, useState, type PointerEventHandler, type ReactNode } from 'react'
|
|
import { copyTextToClipboard } from '../chat/clipboard'
|
|
|
|
type Props = {
|
|
text: string
|
|
label?: string
|
|
copiedLabel?: string
|
|
displayLabel?: ReactNode
|
|
displayCopiedLabel?: ReactNode
|
|
className?: string
|
|
onPointerUp?: PointerEventHandler<HTMLButtonElement>
|
|
}
|
|
|
|
export function CopyButton({
|
|
text,
|
|
label = 'Copy',
|
|
copiedLabel = 'Copied',
|
|
displayLabel,
|
|
displayCopiedLabel,
|
|
className = '',
|
|
onPointerUp,
|
|
}: Props) {
|
|
const [copied, setCopied] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (!copied) return
|
|
const timer = window.setTimeout(() => setCopied(false), 1500)
|
|
return () => window.clearTimeout(timer)
|
|
}, [copied])
|
|
|
|
const handleCopy = async () => {
|
|
try {
|
|
const ok = await copyTextToClipboard(text)
|
|
if (!ok) {
|
|
setCopied(false)
|
|
return
|
|
}
|
|
setCopied(true)
|
|
} catch {
|
|
setCopied(false)
|
|
}
|
|
}
|
|
|
|
const currentLabel = copied ? copiedLabel : label
|
|
const buttonText = copied
|
|
? (displayCopiedLabel ?? copiedLabel)
|
|
: (displayLabel ?? label)
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={handleCopy}
|
|
onPointerUp={onPointerUp}
|
|
className={className}
|
|
aria-label={currentLabel}
|
|
title={currentLabel}
|
|
>
|
|
{buttonText}
|
|
</button>
|
|
)
|
|
}
|