Skip to content

Commit 123514e

Browse files
committed
fix(ion-alert): fix a11y focus and keyboard navigation.
1 parent cd5c27a commit 123514e

File tree

3 files changed

+30
-11
lines changed

3 files changed

+30
-11
lines changed

core/src/components/alert/alert.tsx

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,18 @@ export class Alert implements ComponentInterface, OverlayInterface {
237237
return;
238238
}
239239

240+
/**
241+
* Ensure when alert container is being focused, and the user presses the tab + shift keys, the focus will be set to the last alert button.
242+
*/
243+
if(ev.target.classList.contains('alert-wrapper')) {
244+
if (ev.key === 'Tab' && ev.shiftKey) {
245+
ev.preventDefault();
246+
const lastChildBtn = this.wrapperEl?.querySelector('.alert-button:last-child') as HTMLButtonElement;
247+
lastChildBtn.focus();
248+
return;
249+
}
250+
}
251+
240252
// The only inputs we want to navigate between using arrow keys are the radios
241253
// ignore the keydown event if it is not on a radio button
242254
if (
@@ -400,9 +412,19 @@ export class Alert implements ComponentInterface, OverlayInterface {
400412

401413
await this.delegateController.attachViewToDom();
402414

403-
await present(this, 'alertEnter', iosEnterAnimation, mdEnterAnimation);
415+
await present(this, 'alertEnter', iosEnterAnimation, mdEnterAnimation).then(() => {
416+
if(this.buttons.length === 1 && this.inputs.length === 0) {
417+
const queryBtn = this.wrapperEl?.querySelector('.alert-button') as HTMLButtonElement;
418+
419+
queryBtn.focus();
420+
}
421+
else {
422+
this.wrapperEl?.focus();
423+
}
424+
});
404425

405426
unlock();
427+
406428
}
407429

408430
/**
@@ -725,8 +747,8 @@ export class Alert implements ComponentInterface, OverlayInterface {
725747
const { overlayIndex, header, subHeader, message, htmlAttributes } = this;
726748
const mode = getIonMode(this);
727749
const hdrId = `alert-${overlayIndex}-hdr`;
728-
const subHdrId = `alert-${overlayIndex}-sub-hdr`;
729750
const msgId = `alert-${overlayIndex}-msg`;
751+
const subHdrId = `alert-${overlayIndex}-sub-hdr`;
730752
const role = this.inputs.length > 0 || this.buttons.length > 0 ? 'alertdialog' : 'alert';
731753

732754
/**
@@ -739,12 +761,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
739761

740762
return (
741763
<Host
742-
role={role}
743-
aria-modal="true"
744-
aria-labelledby={ariaLabelledBy}
745-
aria-describedby={message !== undefined ? msgId : null}
746764
tabindex="-1"
747-
{...(htmlAttributes as any)}
748765
style={{
749766
zIndex: `${20000 + overlayIndex}`,
750767
}}
@@ -761,7 +778,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
761778

762779
<div tabindex="0" aria-hidden="true"></div>
763780

764-
<div class="alert-wrapper ion-overlay-wrapper" ref={(el) => (this.wrapperEl = el)}>
781+
<div class="alert-wrapper ion-overlay-wrapper" role={role} aria-modal="true" aria-labelledby={ariaLabelledBy} aria-describedby={message !== undefined ? msgId : null} tabindex="0" ref={(el) => (this.wrapperEl = el)} {...(htmlAttributes as any)}>
765782
<div class="alert-head">
766783
{header && (
767784
<h2 id={hdrId} class="alert-title">

core/src/components/alert/test/a11y/alert.e2e.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const testAria = async (
1616
await didPresent.next();
1717

1818
const alert = page.locator('ion-alert');
19+
const alertwrapper = alert.locator('.alert-wrapper');
1920

2021
const header = alert.locator('.alert-title');
2122
const subHeader = alert.locator('.alert-sub-title');
@@ -42,8 +43,8 @@ const testAria = async (
4243
* expect().toHaveAttribute() can't check for a null value, so grab and check
4344
* the values manually instead.
4445
*/
45-
const ariaLabelledBy = await alert.getAttribute('aria-labelledby');
46-
const ariaDescribedBy = await alert.getAttribute('aria-describedby');
46+
const ariaLabelledBy = await alertwrapper.getAttribute('aria-labelledby');
47+
const ariaDescribedBy = await alertwrapper.getAttribute('aria-describedby');
4748

4849
expect(ariaLabelledBy).toBe(expectedAriaLabelledBy);
4950
expect(ariaDescribedBy).toBe(expectedAriaDescribedBy);

core/src/components/alert/test/basic/alert.e2e.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ configs({ directions: ['ltr'] }).forEach(({ config, screenshot, title }) => {
1919
await page.keyboard.press(tabKey);
2020
await expect(alertBtns.nth(0)).toBeFocused();
2121

22+
await page.keyboard.press(`Shift+${tabKey}`); // this will focus the alert-wrapper
2223
await page.keyboard.press(`Shift+${tabKey}`);
2324
await expect(alertBtns.nth(2)).toBeFocused();
2425

@@ -30,7 +31,7 @@ configs({ directions: ['ltr'] }).forEach(({ config, screenshot, title }) => {
3031
const alertFixture = new AlertFixture(page, screenshot);
3132

3233
const alert = await alertFixture.open('#basic');
33-
await expect(alert).toHaveAttribute('data-testid', 'basic-alert');
34+
await expect(alert.locator(".alert-wrapper")).toHaveAttribute('data-testid', 'basic-alert');
3435
});
3536

3637
test('should dismiss when async handler resolves', async ({ page }) => {

0 commit comments

Comments
 (0)