From 4f6c8d2171f836aef7e57d5661cf498751caeddc Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 30 Jun 2026 10:30:11 +0000
Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20aria-controls?=
=?UTF-8?q?=20to=20expandable=20PPP=20savings=20panel?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
💡 What: Added `aria-controls` to the expand/collapse button and applied the corresponding `id` to both branch elements of the PPP savings panel conditionally rendered content.
🎯 Why: Fixes broken screen reader association. Without an explicitly associated ID present in all states, screen readers lose track of the content area being toggled when React swaps the DOM elements.
📸 Before/After: Visuals remain unchanged, but the accessibility tree now correctly maps the trigger to its content in both collapsed and expanded states.
♿ Accessibility: Ensures WCAG compliance for accordion-style expand/collapse interactions by explicitly associating the toggle trigger with the content container via `aria-controls`.
Co-authored-by: brycejohnson1417 <257422776+brycejohnson1417@users.noreply.github.com>
---
.Jules/palette.md | 3 +++
components/accounts/ppp-savings-panel.tsx | 8 +++++---
2 files changed, 8 insertions(+), 3 deletions(-)
create mode 100644 .Jules/palette.md
diff --git a/.Jules/palette.md b/.Jules/palette.md
new file mode 100644
index 0000000..826d1d4
--- /dev/null
+++ b/.Jules/palette.md
@@ -0,0 +1,3 @@
+## 2025-01-20 - Ensure unbroken screen reader association in React conditional rendering
+**Learning:** When using React conditional rendering to swap out entire blocks of content for an accordion or expand/collapse area (e.g., `` vs ``), the screen reader's `aria-controls` association can break if the target ID is only present on one branch or disappears during state transitions.
+**Action:** Assigned the same dynamically generated `id` (via `useId()`) to both conditional branch elements to ensure the `aria-controls` target is always present in the DOM when the section is expanded or collapsed.
\ No newline at end of file
diff --git a/components/accounts/ppp-savings-panel.tsx b/components/accounts/ppp-savings-panel.tsx
index e6d961b..75146ac 100644
--- a/components/accounts/ppp-savings-panel.tsx
+++ b/components/accounts/ppp-savings-panel.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useMemo, useState } from "react";
+import { useId, useMemo, useState } from "react";
import DOMPurify from "isomorphic-dompurify";
import { Calculator, ChevronDown, ChevronUp, Copy, Download, Loader2, Mail } from "lucide-react";
import type { PppSavingsReport } from "@/lib/application/runtime/ppp-savings-service";
@@ -34,6 +34,7 @@ export function PppSavingsPanel({ orgSlug, accountId }: PppSavingsPanelProps) {
const [error, setError] = useState(null);
const [copied, setCopied] = useState(false);
const [collapsed, setCollapsed] = useState(false);
+ const contentId = useId();
const mailtoHref = useMemo(() => {
if (!report?.recipientEmail || !draft) {
@@ -102,6 +103,7 @@ export function PppSavingsPanel({ orgSlug, accountId }: PppSavingsPanelProps) {
type="button"
onClick={() => setCollapsed((value) => !value)}
aria-expanded={!collapsed}
+ aria-controls={contentId}
className="inline-flex items-center justify-center gap-2 rounded-lg border border-[var(--border-subtle)] bg-[var(--surface-elevated)] px-4 py-3 text-sm font-semibold text-[var(--text-secondary)] transition hover:border-[var(--border-strong)] hover:text-[var(--text-primary)]"
>
{collapsed ? : }
@@ -123,7 +125,7 @@ export function PppSavingsPanel({ orgSlug, accountId }: PppSavingsPanelProps) {
{error ?