diff --git a/.jules/palette.md b/.jules/palette.md index 8c4d7fb..9b34d55 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -1,3 +1,6 @@ +## 2025-02-18 - Missing Alerts for Dynamic Status +**Learning:** The application uses `framer-motion` for dynamic feedback messages but consistently lacks `role="alert"` and `aria-live` attributes, causing screen readers to miss critical status updates. +**Action:** When auditing forms, check all `motion.div/p` elements used for feedback and add `role="alert"` and `aria-live="polite"` (or "assertive" for errors). ## 2024-05-22 - Semantic Required Fields with Custom Validation **Learning:** To combine custom validation UI with semantic `required` attributes (vital for a11y), add `noValidate` to the `
`. This prevents native browser bubbles while keeping the accessibility benefits. **Action:** Use `noValidate` on forms when implementing custom validation but keep `required` attributes on inputs. diff --git a/src/pages/Contact.tsx b/src/pages/Contact.tsx index ef2db68..3e47b1a 100644 --- a/src/pages/Contact.tsx +++ b/src/pages/Contact.tsx @@ -249,6 +249,8 @@ export function Contact() { className={styles.error} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} + role="alert" + aria-live="polite" > {t.contact.form.rateLimit} @@ -273,6 +275,7 @@ export function Contact() { fill="none" stroke="currentColor" strokeWidth="2" + aria-hidden="true" > @@ -288,7 +291,7 @@ export function Contact() {
- +
diff --git a/src/pages/__tests__/Contact.test.tsx b/src/pages/__tests__/Contact.test.tsx index 6e9a170..60b735f 100644 --- a/src/pages/__tests__/Contact.test.tsx +++ b/src/pages/__tests__/Contact.test.tsx @@ -116,6 +116,8 @@ describe('Contact Page', () => { // Verify success message const successMessage = await screen.findByText('Message sent successfully!'); expect(successMessage).toBeTruthy(); + expect(successMessage.getAttribute('role')).toBe('alert'); + expect(successMessage.getAttribute('aria-live')).toBe('polite'); }); it('sanitizes input before sending', async () => { @@ -193,4 +195,32 @@ describe('Contact Page', () => { // EmailJS should NOT be called expect(emailjs.send).not.toHaveBeenCalled(); }); + + it('shows error message with alert role when submission fails', async () => { + // Mock failure + const sendMock = vi.mocked(emailjs.send); + sendMock.mockRejectedValueOnce(new Error('Network error')); + + render(); + + // Fill out the form + fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'John Doe' } }); + fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'john@example.com' } }); + fireEvent.change(screen.getByLabelText('Subject'), { target: { value: 'Test Subject' } }); + fireEvent.change(screen.getByLabelText('Message'), { target: { value: 'Hello world' } }); + + // Submit + fireEvent.click(screen.getByRole('button', { name: 'Send Message' })); + + // Wait for submission attempt + await waitFor(() => { + expect(emailjs.send).toHaveBeenCalled(); + }); + + // Verify error message + const errorMessage = await screen.findByText('Failed to send message.'); + expect(errorMessage).toBeTruthy(); + expect(errorMessage.getAttribute('role')).toBe('alert'); + expect(errorMessage.getAttribute('aria-live')).toBe('polite'); + }); });