Skip to content

Commit 6c6a375

Browse files
committed
fix(input): add aria-live attributes to error text
1 parent e5ed8a1 commit 6c6a375

File tree

2 files changed

+56
-1
lines changed

2 files changed

+56
-1
lines changed

core/src/components/input/input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,7 @@ export class Input implements ComponentInterface {
632632
<div id={helperTextId} class="helper-text">
633633
{helperText}
634634
</div>,
635-
<div id={errorTextId} class="error-text">
635+
<div id={errorTextId} class="error-text" aria-live="assertive" aria-atomic="true">
636636
{errorText}
637637
</div>,
638638
];

core/src/components/input/test/input.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,58 @@ describe('input: clear icon', () => {
133133
expect(icon.getAttribute('icon')).toBe('foo');
134134
});
135135
});
136+
137+
// Regression tests for screen reader accessibility of error messages
138+
describe('input: error text accessibility', () => {
139+
it('should have aria-live="assertive" and aria-atomic="true" on error text for screen reader announcements', async () => {
140+
const page = await newSpecPage({
141+
components: [Input],
142+
html: `<ion-input label="Input" error-text="This field is required"></ion-input>`,
143+
});
144+
145+
const errorTextEl = page.body.querySelector('ion-input .error-text');
146+
expect(errorTextEl).not.toBe(null);
147+
148+
// These attributes ensure screen readers announce errors immediately when they appear
149+
expect(errorTextEl!.getAttribute('aria-live')).toBe('assertive');
150+
expect(errorTextEl!.getAttribute('aria-atomic')).toBe('true');
151+
});
152+
153+
it('should maintain accessibility attributes when error text changes dynamically', async () => {
154+
const page = await newSpecPage({
155+
components: [Input],
156+
html: `<ion-input label="Input"></ion-input>`,
157+
});
158+
159+
const input = page.body.querySelector('ion-input')!;
160+
161+
// Simulate validation error appearing on blur
162+
input.setAttribute('error-text', 'Invalid email format');
163+
await page.waitForChanges();
164+
165+
const errorTextEl = page.body.querySelector('ion-input .error-text');
166+
expect(errorTextEl).not.toBe(null);
167+
expect(errorTextEl!.getAttribute('aria-live')).toBe('assertive');
168+
expect(errorTextEl!.getAttribute('aria-atomic')).toBe('true');
169+
expect(errorTextEl!.textContent).toBe('Invalid email format');
170+
});
171+
172+
it('should properly link error text to input via aria-describedby when invalid', async () => {
173+
const page = await newSpecPage({
174+
components: [Input],
175+
html: `<ion-input label="Input" error-text="Required field" class="ion-touched ion-invalid"></ion-input>`,
176+
});
177+
178+
const nativeInput = page.body.querySelector('ion-input input')!;
179+
const errorTextEl = page.body.querySelector('ion-input .error-text')!;
180+
181+
// Verify the input references the error text ID
182+
const describedBy = nativeInput.getAttribute('aria-describedby');
183+
const errorId = errorTextEl.getAttribute('id');
184+
185+
expect(describedBy).toBe(errorId);
186+
expect(describedBy).toContain('error-text');
187+
// When invalid, aria-invalid should be set
188+
expect(nativeInput.hasAttribute('aria-invalid')).toBe(true);
189+
});
190+
});

0 commit comments

Comments
 (0)