Merge pull request #19 from ragusa-it/palette-input-a11y-improvement-8429205258158315670
🎨 Palette: Improve Input accessibility with ARIA attributes
This commit was merged in pull request #19.
This commit is contained in:
3
.Jules/palette.md
Normal file
3
.Jules/palette.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## 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.
|
||||||
@@ -9,6 +9,7 @@ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|||||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ label, error, id, className, ...props }, ref) => {
|
({ label, error, id, className, ...props }, ref) => {
|
||||||
const inputId = id || label.toLowerCase().replace(/\s+/g, '-');
|
const inputId = id || label.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
const errorId = `${inputId}-error`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.field} ${error ? styles.hasError : ''} ${className || ''}`}>
|
<div className={`${styles.field} ${error ? styles.hasError : ''} ${className || ''}`}>
|
||||||
@@ -19,9 +20,11 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
id={inputId}
|
id={inputId}
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
aria-describedby={error ? errorId : undefined}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{error && <span className={styles.error}>{error}</span>}
|
{error && <span id={errorId} className={styles.error}>{error}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -37,6 +40,7 @@ interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|||||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
({ label, error, id, className, ...props }, ref) => {
|
({ label, error, id, className, ...props }, ref) => {
|
||||||
const inputId = id || label.toLowerCase().replace(/\s+/g, '-');
|
const inputId = id || label.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
const errorId = `${inputId}-error`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.field} ${error ? styles.hasError : ''} ${className || ''}`}>
|
<div className={`${styles.field} ${error ? styles.hasError : ''} ${className || ''}`}>
|
||||||
@@ -47,9 +51,11 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
id={inputId}
|
id={inputId}
|
||||||
className={`${styles.input} ${styles.textarea}`}
|
className={`${styles.input} ${styles.textarea}`}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
aria-describedby={error ? errorId : undefined}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{error && <span className={styles.error}>{error}</span>}
|
{error && <span id={errorId} className={styles.error}>{error}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
52
src/components/ui/__tests__/Input.test.tsx
Normal file
52
src/components/ui/__tests__/Input.test.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { render, screen, cleanup } from '@testing-library/react';
|
||||||
|
import { describe, it, expect, afterEach } from 'vitest';
|
||||||
|
import { Input, Textarea } from '../Input';
|
||||||
|
|
||||||
|
describe('Input', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with correct accessibility attributes when error is present', () => {
|
||||||
|
render(<Input label="Test Input" error="Invalid input" />);
|
||||||
|
|
||||||
|
const input = screen.getByLabelText('Test Input');
|
||||||
|
const error = screen.getByText('Invalid input');
|
||||||
|
|
||||||
|
expect(input.getAttribute('aria-invalid')).toBe('true');
|
||||||
|
expect(input.hasAttribute('aria-describedby')).toBe(true);
|
||||||
|
|
||||||
|
const describedBy = input.getAttribute('aria-describedby');
|
||||||
|
expect(describedBy).toBe(error.id);
|
||||||
|
expect(error.id).toBeDefined();
|
||||||
|
expect(error.id).not.toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders without accessibility error attributes when valid', () => {
|
||||||
|
render(<Input label="Test Input" />);
|
||||||
|
|
||||||
|
const input = screen.getByLabelText('Test Input');
|
||||||
|
|
||||||
|
// In React 19, aria-invalid={false} renders aria-invalid="false"
|
||||||
|
expect(input.getAttribute('aria-invalid')).toBe('false');
|
||||||
|
expect(input.hasAttribute('aria-describedby')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Textarea', () => {
|
||||||
|
it('renders with correct accessibility attributes when error is present', () => {
|
||||||
|
render(<Textarea label="Test Textarea" error="Invalid textarea" />);
|
||||||
|
|
||||||
|
const textarea = screen.getByLabelText('Test Textarea');
|
||||||
|
const error = screen.getByText('Invalid textarea');
|
||||||
|
|
||||||
|
expect(textarea.getAttribute('aria-invalid')).toBe('true');
|
||||||
|
expect(textarea.hasAttribute('aria-describedby')).toBe(true);
|
||||||
|
|
||||||
|
const describedBy = textarea.getAttribute('aria-describedby');
|
||||||
|
expect(describedBy).toBe(error.id);
|
||||||
|
expect(error.id).toBeDefined();
|
||||||
|
expect(error.id).not.toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user