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>(
|
||||
({ label, error, id, className, ...props }, ref) => {
|
||||
const inputId = id || label.toLowerCase().replace(/\s+/g, '-');
|
||||
const errorId = `${inputId}-error`;
|
||||
|
||||
return (
|
||||
<div className={`${styles.field} ${error ? styles.hasError : ''} ${className || ''}`}>
|
||||
@@ -19,9 +20,11 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
className={styles.input}
|
||||
aria-invalid={!!error}
|
||||
aria-describedby={error ? errorId : undefined}
|
||||
{...props}
|
||||
/>
|
||||
{error && <span className={styles.error}>{error}</span>}
|
||||
{error && <span id={errorId} className={styles.error}>{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -37,6 +40,7 @@ interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ label, error, id, className, ...props }, ref) => {
|
||||
const inputId = id || label.toLowerCase().replace(/\s+/g, '-');
|
||||
const errorId = `${inputId}-error`;
|
||||
|
||||
return (
|
||||
<div className={`${styles.field} ${error ? styles.hasError : ''} ${className || ''}`}>
|
||||
@@ -47,9 +51,11 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
className={`${styles.input} ${styles.textarea}`}
|
||||
aria-invalid={!!error}
|
||||
aria-describedby={error ? errorId : undefined}
|
||||
{...props}
|
||||
/>
|
||||
{error && <span className={styles.error}>{error}</span>}
|
||||
{error && <span id={errorId} className={styles.error}>{error}</span>}
|
||||
</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