Skip to content

Commit dc55ac6

Browse files
committed
fix(input): announce helper and error text in screen readers
1 parent be7561d commit dc55ac6

File tree

2 files changed

+54
-54
lines changed

2 files changed

+54
-54
lines changed

core/src/components/input/input.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,28 @@ export class Input implements ComponentInterface {
575575
private renderHintText() {
576576
const { helperText, errorText } = this;
577577

578-
return [<div class="helper-text">{helperText}</div>, <div class="error-text">{errorText}</div>];
578+
return [
579+
<div id={HELPER_TEXT_ID} class="helper-text">
580+
{helperText}
581+
</div>,
582+
<div id={ERROR_TEXT_ID} class="error-text">
583+
{errorText}
584+
</div>,
585+
];
586+
}
587+
588+
private getHintTextID(): string | undefined {
589+
const { el, helperText, errorText } = this;
590+
591+
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
592+
return ERROR_TEXT_ID;
593+
}
594+
595+
if (helperText) {
596+
return HELPER_TEXT_ID;
597+
}
598+
599+
return undefined;
579600
}
580601

581602
private renderCounter() {
@@ -700,6 +721,8 @@ export class Input implements ComponentInterface {
700721
const hasValue = this.hasValue();
701722
const hasStartEndSlots = el.querySelector('[slot="start"], [slot="end"]') !== null;
702723

724+
console.log('el', this.el);
725+
console.log('id', this.getHintTextID());
703726
/**
704727
* If the label is stacked, it should always sit above the input.
705728
* For floating labels, the label should move above the input if
@@ -777,6 +800,8 @@ export class Input implements ComponentInterface {
777800
onKeyDown={this.onKeydown}
778801
onCompositionstart={this.onCompositionStart}
779802
onCompositionend={this.onCompositionEnd}
803+
aria-describedby={this.getHintTextID()}
804+
aria-invalid={this.getHintTextID() === ERROR_TEXT_ID}
780805
{...this.inheritedAttributes}
781806
/>
782807
{this.clearInput && !readonly && !disabled && (
@@ -817,3 +842,5 @@ export class Input implements ComponentInterface {
817842
}
818843

819844
let inputIds = 0;
845+
const HELPER_TEXT_ID = `${'helper-text-' + inputIds}`;
846+
const ERROR_TEXT_ID = `${'error-text-' + inputIds}`;

core/src/components/input/test/bottom-content/index.html

Lines changed: 26 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -49,67 +49,40 @@
4949
</ion-header>
5050

5151
<ion-content id="content" class="ion-padding">
52-
<div class="grid">
53-
<div class="grid-item">
54-
<h2>No Hint</h2>
55-
<ion-input label="Email"></ion-input>
56-
</div>
52+
<ion-input
53+
type="email"
54+
fill="solid"
55+
label="Email"
56+
label-placement="floating"
57+
helper-text="Enter a valid email"
58+
error-text="Invalid email"
59+
></ion-input>
5760

58-
<div class="grid-item">
59-
<h2>Helper Hint</h2>
60-
<ion-input label="Email" helper-text="Enter your email"></ion-input>
61-
</div>
61+
<script>
62+
const input = document.querySelector('ion-input');
6263

63-
<div class="grid-item">
64-
<h2>Error Hint</h2>
65-
<ion-input
66-
class="ion-touched ion-invalid"
67-
label="Email"
68-
error-text="Please enter a valid email"
69-
></ion-input>
70-
</div>
64+
input.addEventListener('ionInput', (ev) => validate(ev));
65+
input.addEventListener('ionBlur', () => markTouched());
7166

72-
<div class="grid-item">
73-
<h2>Custom Error Color</h2>
74-
<ion-input
75-
class="ion-touched ion-invalid custom-error-color"
76-
label="Email"
77-
error-text="Please enter a valid email"
78-
></ion-input>
79-
</div>
67+
const validateEmail = (email) => {
68+
return email.match(
69+
/^(?=.{1,254}$)(?=.{1,64}@)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
70+
);
71+
};
8072

81-
<div class="grid-item">
82-
<h2>Counter</h2>
83-
<ion-input label="Email" counter="true" maxlength="100"></ion-input>
84-
</div>
73+
const validate = (ev) => {
74+
const value = ev.target.value;
8575

86-
<div class="grid-item">
87-
<h2>Custom Counter</h2>
88-
<ion-input id="custom-counter" label="Email" counter="true" maxlength="100"></ion-input>
89-
</div>
76+
input.classList.remove('ion-valid');
77+
input.classList.remove('ion-invalid');
9078

91-
<div class="grid-item">
92-
<h2>Counter with Helper</h2>
93-
<ion-input label="Email" counter="true" maxlength="100" helper-text="Enter an email"></ion-input>
94-
</div>
79+
if (value === '') return;
9580

96-
<div class="grid-item">
97-
<h2>Counter with Error</h2>
98-
<ion-input
99-
class="ion-touched ion-invalid"
100-
label="Email"
101-
counter="true"
102-
maxlength="100"
103-
error-text="Please enter a valid email"
104-
></ion-input>
105-
</div>
106-
</div>
81+
validateEmail(value) ? input.classList.add('ion-valid') : input.classList.add('ion-invalid');
82+
};
10783

108-
<script>
109-
const customCounterInput = document.querySelector('ion-input#custom-counter');
110-
customCounterInput.counterFormatter = (inputLength, maxLength) => {
111-
const length = maxLength - inputLength;
112-
return `${maxLength - inputLength} characters left`;
84+
const markTouched = () => {
85+
input.classList.add('ion-touched');
11386
};
11487
</script>
11588
</ion-content>

0 commit comments

Comments
 (0)