Merge pull request #27 from ragusa-it/palette/improve-button-loading-a11y-7318493501161328143
🎨 Palette: Improve Button loading state accessibility
This commit was merged in pull request #27.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 ? (
|
||||
<span className={`${styles.content} ${isLoading ? styles.contentHidden : ''}`}>
|
||||
{children}
|
||||
</span>
|
||||
{isLoading && (
|
||||
<span className={styles.loaderWrapper} aria-hidden="true">
|
||||
<span className={styles.loader} />
|
||||
) : (
|
||||
children
|
||||
</span>
|
||||
)}
|
||||
</motion.button>
|
||||
);
|
||||
|
||||
@@ -19,4 +19,13 @@ describe('Button', () => {
|
||||
const button = screen.getByTestId('custom-button');
|
||||
expect(button).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders loading state correctly', () => {
|
||||
render(<Button isLoading>Submit</Button>);
|
||||
const button = screen.getByRole('button', { name: /submit/i }) as HTMLButtonElement;
|
||||
expect(button.getAttribute('aria-busy')).toBe('true');
|
||||
expect(button.disabled).toBe(true);
|
||||
// Verify text is present (opacity: 0 doesn't remove from DOM)
|
||||
expect(screen.getByText('Submit')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user