From ffe37fad37608aecfa93e44baec0bb8d2bf06f25 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:08:34 +0000 Subject: [PATCH] feat: Improve Input accessibility with ARIA attributes Add `aria-invalid` and `aria-describedby` attributes to Input and Textarea components when an error is present. This ensures screen readers announce the validation error when the input is focused. Also added unit tests for these accessibility attributes. --- .Jules/palette.md | 3 ++ src/components/ui/Input.tsx | 10 ++++- src/components/ui/__tests__/Input.test.tsx | 52 ++++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 .Jules/palette.md create mode 100644 src/components/ui/__tests__/Input.test.tsx 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(