diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx
index f2430eabf32..2a9b9ec34e7 100644
--- a/core/src/components/input/input.tsx
+++ b/core/src/components/input/input.tsx
@@ -33,6 +33,8 @@ import { getCounterText } from './input.utils';
export class Input implements ComponentInterface {
private nativeInput?: HTMLInputElement;
private inputId = `ion-input-${inputIds++}`;
+ private helperTextId = `${this.inputId}-helper-text`;
+ private errorTextId = `${this.inputId}-error-text`;
private inheritedAttributes: Attributes = {};
private isComposing = false;
private slotMutationController?: SlotMutationController;
@@ -573,9 +575,30 @@ export class Input implements ComponentInterface {
* Renders the helper text or error text values
*/
private renderHintText() {
- const { helperText, errorText } = this;
+ const { helperText, errorText, helperTextId, errorTextId } = this;
+
+ return [
+
+ {helperText}
+
,
+
+ {errorText}
+
,
+ ];
+ }
+
+ private getHintTextID(): string | undefined {
+ const { el, helperText, errorText, helperTextId, errorTextId } = this;
+
+ if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
+ return errorTextId;
+ }
+
+ if (helperText) {
+ return helperTextId;
+ }
- return [{helperText}
, {errorText}
];
+ return undefined;
}
private renderCounter() {
@@ -777,6 +800,8 @@ export class Input implements ComponentInterface {
onKeyDown={this.onKeydown}
onCompositionstart={this.onCompositionStart}
onCompositionend={this.onCompositionEnd}
+ aria-describedby={this.getHintTextID()}
+ aria-invalid={this.getHintTextID() === this.errorTextId}
{...this.inheritedAttributes}
/>
{this.clearInput && !readonly && !disabled && (
diff --git a/core/src/components/input/test/bottom-content/input.e2e.ts b/core/src/components/input/test/bottom-content/input.e2e.ts
index 9f09ffd78a4..5de0ca79e19 100644
--- a/core/src/components/input/test/bottom-content/input.e2e.ts
+++ b/core/src/components/input/test/bottom-content/input.e2e.ts
@@ -68,6 +68,19 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
await expect(helperText).toHaveText('my helper');
await expect(errorText).toBeHidden();
});
+ test('input should have an aria-describedby attribute when helper text is present', async ({ page }) => {
+ await page.setContent(
+ ``,
+ config
+ );
+
+ const input = page.locator('ion-input input');
+ const helperText = page.locator('ion-input .helper-text');
+ const helperTextId = await helperText.getAttribute('id');
+ const ariaDescribedBy = await input.getAttribute('aria-describedby');
+
+ expect(ariaDescribedBy).toBe(helperTextId);
+ });
test('error text should be visible when input is invalid', async ({ page }) => {
await page.setContent(
``,
@@ -96,6 +109,48 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
const errorText = page.locator('ion-input .error-text');
await expect(errorText).toHaveScreenshot(screenshot(`input-error-custom-color`));
});
+ test('input should have an aria-describedby attribute when error text is present', async ({ page }) => {
+ await page.setContent(
+ ``,
+ config
+ );
+
+ const input = page.locator('ion-input input');
+ const errorText = page.locator('ion-input .error-text');
+ const errorTextId = await errorText.getAttribute('id');
+ const ariaDescribedBy = await input.getAttribute('aria-describedby');
+
+ expect(ariaDescribedBy).toBe(errorTextId);
+ });
+ test('input should have aria-invalid attribute when input is invalid', async ({ page }) => {
+ await page.setContent(
+ ``,
+ config
+ );
+
+ const input = page.locator('ion-input input');
+
+ await expect(input).toHaveAttribute('aria-invalid');
+ });
+ test('input should not have aria-invalid attribute when input is valid', async ({ page }) => {
+ await page.setContent(
+ ``,
+ config
+ );
+
+ const input = page.locator('ion-input input');
+
+ await expect(input).not.toHaveAttribute('aria-invalid');
+ });
+ test('input should not have aria-describedby attribute when no hint or error text is present', async ({
+ page,
+ }) => {
+ await page.setContent(``, config);
+
+ const input = page.locator('ion-input input');
+
+ await expect(input).not.toHaveAttribute('aria-describedby');
+ });
});
test.describe('input: hint text rendering', () => {
test.describe('regular inputs', () => {
diff --git a/core/src/components/textarea/test/bottom-content/textarea.e2e.ts b/core/src/components/textarea/test/bottom-content/textarea.e2e.ts
index e6c60e1ce0d..79fcfc4cfac 100644
--- a/core/src/components/textarea/test/bottom-content/textarea.e2e.ts
+++ b/core/src/components/textarea/test/bottom-content/textarea.e2e.ts
@@ -27,6 +27,19 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
await expect(helperText).toHaveText('my helper');
await expect(errorText).toBeHidden();
});
+ test('textarea should have an aria-describedby attribute when helper text is present', async ({ page }) => {
+ await page.setContent(
+ ``,
+ config
+ );
+
+ const textarea = page.locator('ion-textarea textarea');
+ const helperText = page.locator('ion-textarea .helper-text');
+ const helperTextId = await helperText.getAttribute('id');
+ const ariaDescribedBy = await textarea.getAttribute('aria-describedby');
+
+ expect(ariaDescribedBy).toBe(helperTextId);
+ });
test('error text should be visible when textarea is invalid', async ({ page }) => {
await page.setContent(
``,
@@ -55,6 +68,48 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
const errorText = page.locator('ion-textarea .error-text');
await expect(errorText).toHaveScreenshot(screenshot(`textarea-error-custom-color`));
});
+ test('textarea should have an aria-describedby attribute when error text is present', async ({ page }) => {
+ await page.setContent(
+ ``,
+ config
+ );
+
+ const textarea = page.locator('ion-textarea textarea');
+ const errorText = page.locator('ion-textarea .error-text');
+ const errorTextId = await errorText.getAttribute('id');
+ const ariaDescribedBy = await textarea.getAttribute('aria-describedby');
+
+ expect(ariaDescribedBy).toBe(errorTextId);
+ });
+ test('textarea should have aria-invalid attribute when input is invalid', async ({ page }) => {
+ await page.setContent(
+ ``,
+ config
+ );
+
+ const textarea = page.locator('ion-textarea textarea');
+
+ await expect(textarea).toHaveAttribute('aria-invalid');
+ });
+ test('textarea should not have aria-invalid attribute when input is valid', async ({ page }) => {
+ await page.setContent(
+ ``,
+ config
+ );
+
+ const textarea = page.locator('ion-textarea textarea');
+
+ await expect(textarea).not.toHaveAttribute('aria-invalid');
+ });
+ test('textarea should not have aria-describedby attribute when no hint or error text is present', async ({
+ page,
+ }) => {
+ await page.setContent(``, config);
+
+ const textarea = page.locator('ion-textarea textarea');
+
+ await expect(textarea).not.toHaveAttribute('aria-describedby');
+ });
});
test.describe('textarea: hint text rendering', () => {
test.describe('regular textareas', () => {
diff --git a/core/src/components/textarea/textarea.tsx b/core/src/components/textarea/textarea.tsx
index 09d66928cfb..3349f0c1a85 100644
--- a/core/src/components/textarea/textarea.tsx
+++ b/core/src/components/textarea/textarea.tsx
@@ -45,6 +45,8 @@ import type { TextareaChangeEventDetail, TextareaInputEventDetail } from './text
export class Textarea implements ComponentInterface {
private nativeInput?: HTMLTextAreaElement;
private inputId = `ion-textarea-${textareaIds++}`;
+ private helperTextId = `${this.inputId}-helper-text`;
+ private errorTextId = `${this.inputId}-error-text`;
/**
* `true` if the textarea was cleared as a result of the user typing
* with `clearOnEdit` enabled.
@@ -576,9 +578,30 @@ export class Textarea implements ComponentInterface {
* Renders the helper text or error text values
*/
private renderHintText() {
- const { helperText, errorText } = this;
+ const { helperText, errorText, helperTextId, errorTextId } = this;
+
+ return [
+
+ {helperText}
+
,
+
+ {errorText}
+
,
+ ];
+ }
+
+ private getHintTextID(): string | undefined {
+ const { el, helperText, errorText, helperTextId, errorTextId } = this;
+
+ if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
+ return errorTextId;
+ }
+
+ if (helperText) {
+ return helperTextId;
+ }
- return [{helperText}
, {errorText}
];
+ return undefined;
}
private renderCounter() {
@@ -703,6 +726,8 @@ export class Textarea implements ComponentInterface {
onBlur={this.onBlur}
onFocus={this.onFocus}
onKeyDown={this.onKeyDown}
+ aria-describedby={this.getHintTextID()}
+ aria-invalid={this.getHintTextID() === this.errorTextId}
{...this.inheritedAttributes}
>
{value}