diff --git a/.Jules/palette.md b/.Jules/palette.md
new file mode 100644
index 0000000..0b23f04
--- /dev/null
+++ b/.Jules/palette.md
@@ -0,0 +1,4 @@
+## 2025-03-05 - Missing aria-busy on loading buttons
+
+**Learning:** Found several buttons across the app (like refresh/sync/export buttons) that implement loading states with visual spinners or loading text, but fail to inform screen readers of their "busy" state via the \`aria-busy\` attribute.
+**Action:** Always add \`aria-busy={isLoading ? 'true' : undefined}\` to any interactive element that displays a loading state, ensuring the loading process is exposed to assistive technologies.
diff --git a/src/components/ExportButton.tsx b/src/components/ExportButton.tsx
index f368c65..e0ebff2 100644
--- a/src/components/ExportButton.tsx
+++ b/src/components/ExportButton.tsx
@@ -121,6 +121,7 @@ function ExportButton({ variant = 'default', className = '' }: ExportButtonProps
? 'Export quota reached for this week.'
: 'Export dashboard data (CSV, JSON, PDF)'
}
+ aria-busy={isExporting ? 'true' : undefined}
>
{isExporting ? (
@@ -152,7 +153,6 @@ function ExportButton({ variant = 'default', className = '' }: ExportButtonProps
stiffness: 300,
damping: 20,
}}
-
onClick={(e) => e.stopPropagation()}
className="w-full max-w-md rounded-lg border border-[#E5E7EB] bg-white p-6 shadow-lg"
role="dialog"
diff --git a/src/components/dashboard/ExportRoiRail.tsx b/src/components/dashboard/ExportRoiRail.tsx
index 374612e..1febaf8 100644
--- a/src/components/dashboard/ExportRoiRail.tsx
+++ b/src/components/dashboard/ExportRoiRail.tsx
@@ -166,6 +166,7 @@ export default function ExportRoiRail() {
onClick={() => exportData('csv')}
disabled={!!exporting || syncing || exportsDepleted}
className="inline-flex items-center gap-1.5 rounded-md border border-[#E5E7EB] bg-white px-3 py-1.5 text-[11px] font-semibold text-[#374151] transition hover:bg-[#F3F4F6] disabled:opacity-60"
+ aria-busy={exporting === 'csv' ? 'true' : undefined}
>
{exporting === 'csv' ? (
@@ -178,6 +179,7 @@ export default function ExportRoiRail() {
onClick={() => exportData('json')}
disabled={!!exporting || syncing || exportsDepleted}
className="inline-flex items-center gap-1.5 rounded-md border border-[#E5E7EB] bg-white px-3 py-1.5 text-[11px] font-semibold text-[#374151] transition hover:bg-[#F3F4F6] disabled:opacity-60"
+ aria-busy={exporting === 'json' ? 'true' : undefined}
>
{exporting === 'json' ? (
@@ -190,6 +192,7 @@ export default function ExportRoiRail() {
onClick={() => exportData('pdf')}
disabled={!!exporting || syncing || exportsDepleted}
className="inline-flex items-center gap-1.5 rounded-md border border-[#E5E7EB] bg-white px-3 py-1.5 text-[11px] font-semibold text-[#374151] transition hover:bg-[#F3F4F6] disabled:opacity-60"
+ aria-busy={exporting === 'pdf' ? 'true' : undefined}
>
{exporting === 'pdf' ? (
@@ -202,6 +205,7 @@ export default function ExportRoiRail() {
onClick={syncAll}
disabled={syncing || !!exporting}
className="inline-flex items-center gap-1.5 rounded-md border border-[#E5E7EB] bg-[#F8FAFC] px-3 py-1.5 text-[11px] font-semibold text-[#111827] transition hover:border-[#6C63FF] hover:bg-white disabled:opacity-60"
+ aria-busy={syncing ? 'true' : undefined}
>
{syncing ? (
diff --git a/src/components/dashboard/IntegrationHealthRail.tsx b/src/components/dashboard/IntegrationHealthRail.tsx
index 6edce6b..bc976ab 100644
--- a/src/components/dashboard/IntegrationHealthRail.tsx
+++ b/src/components/dashboard/IntegrationHealthRail.tsx
@@ -234,6 +234,7 @@ export default function IntegrationHealthRail() {
onClick={loadStatuses}
className="inline-flex items-center gap-1 rounded-md border border-[#E5E7EB] bg-white px-3 py-1.5 text-xs font-semibold text-[#374151] hover:bg-[#F3F4F6] transition-colors disabled:opacity-60"
disabled={loading}
+ aria-busy={loading ? 'true' : undefined}
>
{loading ? (
@@ -344,6 +345,7 @@ export default function IntegrationHealthRail() {
onClick={() => handleSync(integration)}
disabled={disabled || syncing[integration.type]}
className="flex-1 inline-flex items-center justify-center gap-1.5 rounded-md border border-[#E5E7EB] bg-white px-3 py-1.5 text-[11px] font-semibold text-[#374151] transition-colors hover:bg-[#F3F4F6] disabled:cursor-not-allowed disabled:opacity-60"
+ aria-busy={syncing[integration.type] ? 'true' : undefined}
>
{syncing[integration.type] ? (
diff --git a/src/components/dashboard/dashboard-header.tsx b/src/components/dashboard/dashboard-header.tsx
index fe86c11..e1e9b03 100644
--- a/src/components/dashboard/dashboard-header.tsx
+++ b/src/components/dashboard/dashboard-header.tsx
@@ -31,6 +31,7 @@ export function DashboardHeader({
disabled={isLoading}
className="p-1.5 text-[#9CA3AF] hover:text-[#333333] transition-colors rounded-md hover:bg-[#FAFAFA]"
aria-label="Refresh"
+ aria-busy={isLoading ? 'true' : undefined}
>