From 6d94ac7b9345143f7c995dfa82dd10e00dbed312 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 01:42:19 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Improve=20Button=20lo?= =?UTF-8?q?ading=20state=20accessibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `aria-busy` attribute to Button when loading. - Refactor Button rendering to keep children in DOM (visually hidden) instead of unmounting. - Fix layout shift regression by replicating flex properties in content wrapper. - Move inline styles to CSS modules. - Add tests for loading state accessibility. Co-authored-by: ragusa-it <196988693+ragusa-it@users.noreply.github.com> --- .Jules/palette.md | 4 ++++ src/components/ui/Button.module.css | 21 +++++++++++++++++++++ src/components/ui/Button.tsx | 12 ++++++++---- src/components/ui/__tests__/Button.test.tsx | 9 +++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/.Jules/palette.md b/.Jules/palette.md index 2b51482..5a5b459 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -1,3 +1,7 @@ ## 2024-05-24 - Accessible Input Validation **Learning:** React 19 renders `aria-invalid={false}` as `aria-invalid="false"`, unlike older versions which might have omitted it. Explicitly handling this in tests is crucial. Also, ensuring DOM cleanup (`cleanup()`) in `afterEach` is vital when testing similar components with same labels across tests to avoid "finding the wrong element" false positives/negatives. **Action:** Always include `afterEach(() => cleanup())` in `vitest` setup for DOM tests, and expect `aria-invalid="false"` (or explicitly handle `undefined` if omission is desired) when testing valid states in React 19. + +## 2024-05-24 - Accessible Loading Buttons +**Learning:** Replacing button text with a spinner destroys the accessible name. +**Action:** Use `aria-busy="true"`, keep children in DOM (visually hidden via opacity/class), and overlay spinner absolutely. Ensure wrapper element replicates flex layout (gap/alignment) to prevent layout shifts. diff --git a/src/components/ui/Button.module.css b/src/components/ui/Button.module.css index 151f0f9..19c0613 100644 --- a/src/components/ui/Button.module.css +++ b/src/components/ui/Button.module.css @@ -1,5 +1,6 @@ .button { display: inline-flex; + position: relative; align-items: center; justify-content: center; gap: var(--space-sm); @@ -80,3 +81,23 @@ transform: rotate(360deg); } } + +.content { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-sm); + transition: opacity 0.2s; +} + +.contentHidden { + opacity: 0; +} + +.loaderWrapper { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 43a5144..998208e 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -24,14 +24,18 @@ export function Button({ type={type} className={`${styles.button} ${styles[variant]} ${styles[size]} ${className || ''}`} disabled={disabled || isLoading} + aria-busy={isLoading} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} {...props} > - {isLoading ? ( - - ) : ( - children + + {children} + + {isLoading && ( +