diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 0000000..2b51482 --- /dev/null +++ b/.Jules/palette.md @@ -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. diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index e44f1df..b3d0c24 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -9,6 +9,7 @@ interface InputProps extends InputHTMLAttributes { export const Input = forwardRef( ({ label, error, id, className, ...props }, ref) => { const inputId = id || label.toLowerCase().replace(/\s+/g, '-'); + const errorId = `${inputId}-error`; return (
@@ -19,9 +20,11 @@ export const Input = forwardRef( ref={ref} id={inputId} className={styles.input} + aria-invalid={!!error} + aria-describedby={error ? errorId : undefined} {...props} /> - {error && {error}} + {error && {error}}
); } @@ -37,6 +40,7 @@ interface TextareaProps extends TextareaHTMLAttributes { export const Textarea = forwardRef( ({ label, error, id, className, ...props }, ref) => { const inputId = id || label.toLowerCase().replace(/\s+/g, '-'); + const errorId = `${inputId}-error`; return (
@@ -47,9 +51,11 @@ export const Textarea = forwardRef( ref={ref} id={inputId} className={`${styles.input} ${styles.textarea}`} + aria-invalid={!!error} + aria-describedby={error ? errorId : undefined} {...props} /> - {error && {error}} + {error && {error}}
); } diff --git a/src/components/ui/__tests__/Input.test.tsx b/src/components/ui/__tests__/Input.test.tsx new file mode 100644 index 0000000..dd09cdd --- /dev/null +++ b/src/components/ui/__tests__/Input.test.tsx @@ -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(); + + 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(); + + 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(