From 1060a3d6ac1cc0f1283cd478abf516ddf0255548 Mon Sep 17 00:00:00 2001 From: georgeefaith Date: Fri, 26 Jun 2026 22:56:29 +0000 Subject: [PATCH] feat(a11y): improve retire-modal accessibility (#487) - aria-live='polite' region announces success and error messages to screen readers without interrupting the user mid-flow - role='alert' on inline error banner for immediate screen reader announcement on failure - role='status' + CheckCircle success state replaces the form on completion so screen readers detect the outcome - aria-busy on submit button signals loading state to assistive tech - Close and Cancel buttons disabled while submitting; textarea also disabled during in-flight request - Error caught from onConfirm() and surfaced visually and via aria-live Closes #487 --- apps/web/src/components/retire-modal.tsx | 160 ++++++++++++++--------- 1 file changed, 95 insertions(+), 65 deletions(-) diff --git a/apps/web/src/components/retire-modal.tsx b/apps/web/src/components/retire-modal.tsx index b20571d..70a0b50 100644 --- a/apps/web/src/components/retire-modal.tsx +++ b/apps/web/src/components/retire-modal.tsx @@ -1,7 +1,7 @@ 'use client' import { useRef, useState } from 'react' -import { X, Leaf } from 'lucide-react' +import { X, Leaf, CheckCircle } from 'lucide-react' import { CopyableText } from './copy-button' interface Props { @@ -14,13 +14,19 @@ interface Props { export function RetireModal({ certificateId, kwh, onConfirm, onClose }: Props) { const [reason, setReason] = useState('') const [submitting, setSubmitting] = useState(false) + const [succeeded, setSucceeded] = useState(false) + const [error, setError] = useState(null) const inputRef = useRef(null) async function handleSubmit(e: React.FormEvent) { e.preventDefault() setSubmitting(true) + setError(null) try { await onConfirm(reason) + setSucceeded(true) + } catch (err) { + setError(err instanceof Error ? err.message : 'Retirement failed. Please try again.') } finally { setSubmitting(false) } @@ -32,9 +38,15 @@ export function RetireModal({ certificateId, kwh, onConfirm, onClose }: Props) { aria-modal="true" aria-labelledby="retire-title" className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" - onClick={(e) => e.target === e.currentTarget && onClose()} + onClick={(e) => e.target === e.currentTarget && !submitting && onClose()} >
+ {/* aria-live region announces success/error to screen readers */} +
+ {succeeded && 'Certificate retired successfully.'} + {error && `Error: ${error}`} +
+
-

- You are about to permanently retire certificate{' '} - - {' '}({kwh.toFixed(3)} kWh). - This action cannot be undone. -

- -
-
- -