Skip to content

Commit a7295b4

Browse files
committed
fix(input): making validation reading work more consistently across browsers
1 parent e358cab commit a7295b4

File tree

6 files changed

+701
-68
lines changed

6 files changed

+701
-68
lines changed

core/src/components/input/input.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ export class Input implements ComponentInterface {
421421
);
422422

423423
// Watch for class changes to update validation state
424-
if (Build.isBrowser) {
424+
if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
425425
this.validationObserver = new MutationObserver(() => {
426426
const newIsInvalid = this.checkValidationState();
427427
if (this.isInvalid !== newIsInvalid) {
@@ -433,11 +433,11 @@ export class Input implements ComponentInterface {
433433
attributes: true,
434434
attributeFilter: ['class'],
435435
});
436-
437-
// Set initial state
438-
this.isInvalid = this.checkValidationState();
439436
}
440437

438+
// Always set initial state
439+
this.isInvalid = this.checkValidationState();
440+
441441
this.debounceChanged();
442442
if (Build.isBrowser) {
443443
document.dispatchEvent(
@@ -664,20 +664,14 @@ export class Input implements ComponentInterface {
664664
* Renders the helper text or error text values
665665
*/
666666
private renderHintText() {
667-
const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
667+
const { helperText, errorText, helperTextId, errorTextId } = this;
668668

669669
return [
670670
<div id={helperTextId} class="helper-text">
671671
{helperText}
672672
</div>,
673-
<div
674-
id={errorTextId}
675-
class="error-text"
676-
role={isInvalid && errorText ? 'alert' : undefined}
677-
aria-live={isInvalid && errorText ? 'polite' : 'off'}
678-
aria-atomic="true"
679-
>
680-
{isInvalid && errorText ? errorText : ''}
673+
<div id={errorTextId} class="error-text">
674+
{errorText}
681675
</div>,
682676
];
683677
}
@@ -908,7 +902,7 @@ export class Input implements ComponentInterface {
908902
onCompositionstart={this.onCompositionStart}
909903
onCompositionend={this.onCompositionEnd}
910904
aria-describedby={this.getHintTextID()}
911-
aria-invalid={this.getHintTextID() === this.errorTextId}
905+
aria-invalid={this.isInvalid ? 'true' : 'false'}
912906
{...this.inheritedAttributes}
913907
/>
914908
{this.clearInput && !readonly && !disabled && (

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

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -144,44 +144,49 @@ describe('input: error text accessibility', () => {
144144

145145
const errorTextEl = page.body.querySelector('ion-input .error-text');
146146
expect(errorTextEl).not.toBe(null);
147-
148-
// Error text element should always exist and have aria-atomic
149-
expect(errorTextEl!.getAttribute('aria-atomic')).toBe('true');
150147
expect(errorTextEl!.getAttribute('id')).toContain('error-text');
148+
expect(errorTextEl!.textContent).toBe('This field is required');
151149
});
152150

153-
it('should maintain error text structure when error text changes dynamically', async () => {
151+
it('should set aria-invalid when input is invalid', async () => {
154152
const page = await newSpecPage({
155153
components: [Input],
156-
html: `<ion-input label="Input"></ion-input>`,
154+
html: `<ion-input label="Input" error-text="Required field" class="ion-touched ion-invalid"></ion-input>`,
157155
});
158156

159-
const input = page.body.querySelector('ion-input')!;
160-
161-
// Add error text dynamically
162-
input.setAttribute('error-text', 'Invalid email format');
163-
await page.waitForChanges();
157+
const nativeInput = page.body.querySelector('ion-input input')!;
164158

165-
const errorTextEl = page.body.querySelector('ion-input .error-text');
166-
expect(errorTextEl).not.toBe(null);
167-
expect(errorTextEl!.getAttribute('aria-atomic')).toBe('true');
168-
expect(errorTextEl!.getAttribute('id')).toContain('error-text');
159+
// Should be invalid because of the classes
160+
expect(nativeInput.getAttribute('aria-invalid')).toBe('true');
169161
});
170162

171-
it('should have proper aria-describedby reference structure', async () => {
163+
it('should set aria-describedby to error text when invalid', async () => {
172164
const page = await newSpecPage({
173165
components: [Input],
174-
html: `<ion-input label="Input" error-text="Required field"></ion-input>`,
166+
html: `<ion-input label="Input" error-text="Required field" class="ion-touched ion-invalid"></ion-input>`,
175167
});
176168

169+
const nativeInput = page.body.querySelector('ion-input input')!;
177170
const errorTextEl = page.body.querySelector('ion-input .error-text')!;
178171

179-
// Verify the error text element has an ID
172+
// Verify aria-describedby points to error text
180173
const errorId = errorTextEl.getAttribute('id');
181-
expect(errorId).toContain('error-text');
174+
expect(nativeInput.getAttribute('aria-describedby')).toBe(errorId);
175+
});
176+
177+
it('should set aria-describedby to helper text when valid', async () => {
178+
const page = await newSpecPage({
179+
components: [Input],
180+
html: `<ion-input label="Input" helper-text="Enter a value" error-text="Required field"></ion-input>`,
181+
});
182+
183+
const nativeInput = page.body.querySelector('ion-input input')!;
184+
const helperTextEl = page.body.querySelector('ion-input .helper-text')!;
182185

183-
// Note: aria-describedby is dynamically set based on validation state
184-
// The actual connection happens when the input becomes invalid
186+
// When not invalid, should point to helper text
187+
const helperId = helperTextEl.getAttribute('id');
188+
expect(nativeInput.getAttribute('aria-describedby')).toBe(helperId);
189+
expect(nativeInput.getAttribute('aria-invalid')).toBe('false');
185190
});
186191

187192
it('should have helper text element with proper structure', async () => {
@@ -195,4 +200,22 @@ describe('input: error text accessibility', () => {
195200
expect(helperTextEl!.getAttribute('id')).toContain('helper-text');
196201
expect(helperTextEl!.textContent).toBe('Enter a valid value');
197202
});
203+
204+
it('should maintain error text content when error text changes dynamically', async () => {
205+
const page = await newSpecPage({
206+
components: [Input],
207+
html: `<ion-input label="Input"></ion-input>`,
208+
});
209+
210+
const input = page.body.querySelector('ion-input')!;
211+
212+
// Add error text dynamically
213+
input.setAttribute('error-text', 'Invalid email format');
214+
await page.waitForChanges();
215+
216+
const errorTextEl = page.body.querySelector('ion-input .error-text');
217+
expect(errorTextEl).not.toBe(null);
218+
expect(errorTextEl!.getAttribute('id')).toContain('error-text');
219+
expect(errorTextEl!.textContent).toBe('Invalid email format');
220+
});
198221
});

0 commit comments

Comments
 (0)