程序员阿江(Relakkes) fd25e5732f fix: hide message actions after pointer clicks #642
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
2026-05-31 17:42:09 +08:00

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>
)
}