mirror of
https://github.com/vernu/textbee.git
synced 2026-07-31 01:17:52 -04:00
The bottom of the modal was a label/value table, and most of it repeated the header: Direction restated the To/From already shown with its arrow icon, and Number restated the number in the title. What remained was two facts wrapped in table furniture. Those facts are now inline chips: status, device, and the gateway ID in a mono chip. The redundant rows are gone. Copy moved from a single footer button to one button per field, next to the thing it copies: the number in the header, the message body, and the gateway ID. A lone "Copy text" button left the target to be inferred, and it could only ever copy one of the three. Reused the existing shared CopyButton rather than adding another, which also brought its copied-state feedback and toast. It had no accessible name though, so every icon-only copy button was announced identically. It now names itself after what it copies, which is what lets the new test address them individually. Also overrode the dialog primitive's mobile text centring in this header: it left the title on the left and the timestamp centred beneath it. E2e reads the clipboard after each button and asserts all three return their own value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
58 lines
1.5 KiB
TypeScript
58 lines
1.5 KiB
TypeScript
'use client'
|
|
|
|
import { Button } from '@/components/ui/button'
|
|
import { Check, Copy } from 'lucide-react'
|
|
import { useState } from 'react'
|
|
import { useToast } from '@/hooks/use-toast'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface CopyButtonProps {
|
|
value: string
|
|
/** What is being copied, e.g. "Message". Names the button for assistive tech. */
|
|
label: string
|
|
className?: string
|
|
}
|
|
|
|
export function CopyButton({ value, label, className }: CopyButtonProps) {
|
|
const [copied, setCopied] = useState(false)
|
|
const { toast } = useToast()
|
|
|
|
const copyToClipboard = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(value)
|
|
setCopied(true)
|
|
toast({
|
|
title: 'Copied!',
|
|
description: `${label} copied to clipboard`,
|
|
})
|
|
setTimeout(() => setCopied(false), 2000)
|
|
} catch (err) {
|
|
toast({
|
|
title: 'Failed to copy',
|
|
description: 'Please try again',
|
|
variant: 'destructive',
|
|
})
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
type='button'
|
|
variant='ghost'
|
|
size='icon'
|
|
// Icon-only buttons carried no accessible name, so several of them in
|
|
// one view were indistinguishable to a screen reader.
|
|
aria-label={`Copy ${label.toLowerCase()}`}
|
|
title={`Copy ${label.toLowerCase()}`}
|
|
className={cn('shrink-0', className)}
|
|
onClick={copyToClipboard}
|
|
>
|
|
{copied ? (
|
|
<Check className='h-4 w-4 text-green-600 dark:text-green-400' />
|
|
) : (
|
|
<Copy className='h-4 w-4' />
|
|
)}
|
|
</Button>
|
|
)
|
|
}
|