From 42b3888a76a6723d85d3009c746208a56c0a3844 Mon Sep 17 00:00:00 2001 From: HangerThem Date: Mon, 19 Oct 2026 14:23:09 +0200 Subject: [PATCH] fix(a11y): accessible names for icon links and the copy button - aria-labels on share links, sidebar social links and mobile nav icons - aria-current on active nav links - CopyButton: React state instead of innerHTML mutation, clipboard error handling, accessible label and a polite live region for feedback (Related code-block and image-caption a11y fixes landed with the ImageWrapper/CodeWrapper rewrite in the previous perf commit.) Co-Authored-By: Claude Opus 5.5 --- components/layout/MobileNavbar.tsx | 2 + components/layout/NavLinks.tsx | 6 ++- components/layout/Sidebar.tsx | 3 +- components/post/ShareLinks.tsx | 3 +- components/ui/CopyButton.tsx | 77 ++++++++++++++++++++---------- 5 files changed, 62 insertions(+), 29 deletions(-) diff --git a/components/layout/MobileNavbar.tsx b/components/layout/MobileNavbar.tsx index 97d6408..abd3c5e 100644 --- a/components/layout/MobileNavbar.tsx +++ b/components/layout/MobileNavbar.tsx @@ -24,6 +24,8 @@ export function MobileNavbar() { ) : ( - + {item.icon} {item.label} diff --git a/components/layout/Sidebar.tsx b/components/layout/Sidebar.tsx index 42b7ba7..37c268c 100644 --- a/components/layout/Sidebar.tsx +++ b/components/layout/Sidebar.tsx @@ -47,8 +47,9 @@ export async function Sidebar() {
{socialLinks.map(({ icon, url }) => ( - + diff --git a/components/post/ShareLinks.tsx b/components/post/ShareLinks.tsx index 426c4b0..efee949 100644 --- a/components/post/ShareLinks.tsx +++ b/components/post/ShareLinks.tsx @@ -38,8 +38,9 @@ export function ShareLinks({ url, title, description }: ShareLinksProps) { return (
{networks.map(({ name, icon, href }) => ( - + diff --git a/components/ui/CopyButton.tsx b/components/ui/CopyButton.tsx index 4a65596..b22fa1b 100644 --- a/components/ui/CopyButton.tsx +++ b/components/ui/CopyButton.tsx @@ -1,41 +1,66 @@ 'use client' -import { ButtonHTMLAttributes, useRef } from 'react' -import { Copy } from 'lucide-react' +import { ButtonHTMLAttributes, useEffect, useState } from 'react' +import { Check, Copy } from 'lucide-react' import { cn } from '@/utils/cn' type CopyButtonProps = ButtonHTMLAttributes & { textToCopy: string } -export function CopyButton({ textToCopy, className, ...props }: CopyButtonProps) { - const buttonRef = useRef(null) +type CopyState = 'idle' | 'copied' | 'failed' - const handleCopy = () => { - navigator.clipboard.writeText(textToCopy) - const button = buttonRef.current - if (button) { - const original = button.innerHTML - button.innerHTML = 'Copied!' - button.disabled = true - setTimeout(() => { - button.innerHTML = original - button.disabled = false - }, 2000) +const labels: Record = { + idle: 'Copy code', + copied: 'Copied!', + failed: 'Copy failed', +} + +export function CopyButton({ textToCopy, className, ...props }: CopyButtonProps) { + const [state, setState] = useState('idle') + + useEffect(() => { + if (state === 'idle') return + const timeout = setTimeout(() => setState('idle'), 2000) + return () => clearTimeout(timeout) + }, [state]) + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(textToCopy) + setState('copied') + } catch { + setState('failed') } } return ( - + <> + + {/* Outside the button: a button's children are presentational, so they aren't announced */} + + {state === 'idle' ? '' : labels[state]} + + ) }