From 3e6c7075d0795504c55d442a8d6dde689fbfab0a Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Sun, 3 May 2026 18:21:42 -0700 Subject: [PATCH 1/2] canvas/TermsGate: stop hiding the dialog from screen readers + a11y polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five fixes for the terms-acceptance modal: 1. CRITICAL: aria-hidden="true" on the modal's wrapper hid the dialog AND its descendants from screen readers. The entire ToS-acceptance flow was invisible to AT users. Removed the false aria-hidden — the wrapper is just a backdrop, the dialog inside still has role=dialog aria-modal=true so AT recognises it correctly. 2. Added focus management: when the modal opens, focus moves to the "I agree" button (WCAG 2.4.3). Hard gate so no focus-trap loop or Esc-dismiss — the user must accept or close the page. 3. "I agree" button hovered LIGHTER (bg-emerald-500 over bg-emerald-600). On white text that drops below AA — same trap fixed in ApprovalBanner and ConfirmDialog. Flipped to bg-emerald-700. 4. Added focus-visible ring on the "I agree" button. Was relying on browser default outline only. 5. Privacy/Terms links: hardcoded text-sky-400 → text-accent (theme- aware) + hover:text-accent-strong (was hover:text-sky-400, no-op same color) + focus-visible ring. Added aria-describedby pointing to the body div so SR can read the description with the title. Co-Authored-By: Claude Opus 4.7 (1M context) --- canvas/src/components/TermsGate.tsx | 67 +++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/canvas/src/components/TermsGate.tsx b/canvas/src/components/TermsGate.tsx index cc32b9a6..e165ba3d 100644 --- a/canvas/src/components/TermsGate.tsx +++ b/canvas/src/components/TermsGate.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { PLATFORM_URL } from "@/lib/api"; // TermsGate blocks the page it wraps until the user has accepted the @@ -73,39 +73,72 @@ export function TermsGate({ children }: { children: React.ReactNode }) { } }; + // Move focus to the "I agree" button when the modal opens (WCAG 2.4.3). + // The dialog is a hard gate — no Esc dismiss — so we don't need a focus + // trap loop, just a one-shot focus move into the dialog. + const agreeButtonRef = useRef(null); + useEffect(() => { + if (status !== "pending") return; + const raf = requestAnimationFrame(() => agreeButtonRef.current?.focus()); + return () => cancelAnimationFrame(raf); + }, [status]); + return ( <> {children} {status === "pending" && ( -