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 && ( +