From bec6c7aadaddbe6e6bcf7a451e3742acf2381b2e Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 16 Sep 2026 10:30:49 -0700 Subject: [PATCH 1/6] fix(overlays): only draw the focus indicator when focus is redirected by the keyboard --- .../popover/test/focus-trap/index.html | 32 +++++++++++ .../popover/test/focus-trap/popover.e2e.ts | 44 +++++++++++++++ core/src/utils/focus-trap.ts | 17 +++++- core/src/utils/focus-visible.ts | 53 +++++++++++++++++++ core/src/utils/overlays.ts | 9 +++- 5 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 core/src/components/popover/test/focus-trap/index.html create mode 100644 core/src/components/popover/test/focus-trap/popover.e2e.ts diff --git a/core/src/components/popover/test/focus-trap/index.html b/core/src/components/popover/test/focus-trap/index.html new file mode 100644 index 00000000000..1889fef37b9 --- /dev/null +++ b/core/src/components/popover/test/focus-trap/index.html @@ -0,0 +1,32 @@ + + + + + Popover - Focus Trap + + + + + + + + + +
+ + + +
Focusable element outside of the popover
+
+
+ + + + + Notifications + + + +
+ + diff --git a/core/src/components/popover/test/focus-trap/popover.e2e.ts b/core/src/components/popover/test/focus-trap/popover.e2e.ts new file mode 100644 index 00000000000..eedc94148af --- /dev/null +++ b/core/src/components/popover/test/focus-trap/popover.e2e.ts @@ -0,0 +1,44 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +/** + * The focus trap redirects focus back into the overlay when focus lands outside + * of it. The indicator should only follow that redirect during keyboard + * navigation. + * + * It lands on the `ion-item` because a toggle inside an item has the item draw + * the indicator on its behalf. + */ +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('popover: focus trap'), () => { + test.beforeEach(async ({ page }) => { + await page.goto('/src/components/popover/test/focus-trap', config); + + const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); + + // Opening with a click leaves the focus utility in pointer mode. + await page.locator('#open-popover').click(); + await ionPopoverDidPresent.next(); + }); + + test('should not show a focus indicator when focus is redirected after a pointer interaction', async ({ page }) => { + const item = page.locator('ion-popover ion-item'); + + await page.locator('.ion-page div[tabindex="0"]').evaluate((el: HTMLElement) => el.focus()); + await page.waitForChanges(); + + await expect(item).not.toHaveClass(/ion-focused/); + }); + + test('should show a focus indicator when focus is redirected during keyboard navigation', async ({ page }) => { + const item = page.locator('ion-popover ion-item'); + + // Shift turns keyboard mode back on without moving focus. + await page.keyboard.press('Shift'); + await page.locator('.ion-page div[tabindex="0"]').evaluate((el: HTMLElement) => el.focus()); + await page.waitForChanges(); + + await expect(item).toHaveClass(/ion-focused/); + }); + }); +}); diff --git a/core/src/utils/focus-trap.ts b/core/src/utils/focus-trap.ts index 476e3726803..9db33591324 100644 --- a/core/src/utils/focus-trap.ts +++ b/core/src/utils/focus-trap.ts @@ -1,5 +1,20 @@ +import { isKeyboardMode } from '@utils/focus-visible'; import { focusVisibleElement } from '@utils/helpers'; +/** + * Focuses an element a focus trap is redirecting focus to. Only draws the + * keyboard focus indicator when the user is navigating with a keyboard, so a + * redirect caused by a tap or click does not leave the element looking as + * though it was tabbed to. + */ +export const focusRedirectedElement = (el: HTMLElement) => { + if (isKeyboardMode()) { + focusVisibleElement(el); + } else { + el.focus(); + } +}; + /** * This query string selects elements that * are eligible to receive focus. We select @@ -94,7 +109,7 @@ const focusElementInContext = ( if (radioGroup) { radioGroup.setFocus(); } else { - focusVisibleElement(elementToFocus); + focusRedirectedElement(elementToFocus); } } else { // Focus fallback element instead of letting focus escape diff --git a/core/src/utils/focus-visible.ts b/core/src/utils/focus-visible.ts index 3fba715defe..df2193521c6 100644 --- a/core/src/utils/focus-visible.ts +++ b/core/src/utils/focus-visible.ts @@ -18,6 +18,7 @@ const FOCUS_KEYS = [ export interface FocusVisibleUtility { destroy: () => void; setFocus: (elements: Element[]) => void; + isKeyboardMode: () => boolean; } let focusVisibleUtility: FocusVisibleUtility | null = null; @@ -46,10 +47,42 @@ export const focusElements = (elements: Element[]) => { focusVisible.setFocus(elements); }; +/** + * Whether the most recent interaction on the page was keyboard-driven. + * + * Check this before drawing the keyboard focus indicator programmatically. + * Reads the document-level utility, the only one that sees every interaction. + */ +export const isKeyboardMode = () => getOrInitFocusVisibleUtility().isKeyboardMode(); + +/** + * Watches how the user is interacting with the page and marks the focused + * element with `ion-focused`, so the keyboard focus indicator is only drawn + * while the user navigates with a keyboard. + * + * Returns `setFocus` to mark elements explicitly, for a focus move the + * listeners below cannot attribute to a keyboard event, and `destroy` to + * detach the listeners. + * + * @param rootEl Scopes the utility to this element's shadow root, so it only + * reacts to interactions inside that element. `ion-datetime` does this to draw + * indicators on its own buttons. Omit it to listen on the document, which is + * what `ion-app` does. Only the document-level instance publishes its mode + * through `isKeyboardMode`. + */ export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => { let currentFocus: Element[] = []; + + /* + * Starts as `true` so a focus move before the user has interacted at all + * still draws an indicator, such as an element focused on page load. + */ let keyboardMode = true; + /* + * `ref` is where the listeners go and `root` is the element focus falls back + * to once it leaves everything inside `ref`. + */ const ref = rootEl ? rootEl.shadowRoot! : document; const root = rootEl ? rootEl : document.body; @@ -63,12 +96,25 @@ export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => setFocus([]); }; + /* + * Only the keys that move focus keep the indicator on. Any other key means + * the user is typing into the focused element rather than navigating, so the + * indicator is dropped. + */ const onKeydown = (ev: Event) => { keyboardMode = FOCUS_KEYS.includes((ev as KeyboardEvent).key); if (!keyboardMode) { setFocus([]); } }; + + /* + * The indicator does not always belong to the element that took focus. The + * composed path is walked so every `ion-focusable` ancestor is marked too, + * which is how an `ion-item` draws the indicator for a checkbox slotted into + * it, since a checkbox in an item drops the class itself. The composed path + * is used because it reaches hosts across shadow boundaries. + */ const onFocusin = (ev: Event) => { if (keyboardMode && ev.composedPath !== undefined) { const toFocus = ev.composedPath().filter((el: any) => { @@ -81,6 +127,12 @@ export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => setFocus(toFocus); } }; + + /* + * Focus landing back on `root` means it left every focusable element, so + * nothing should stay marked. Focus moving between elements is left alone + * because `onFocusin` marks the new one. + */ const onFocusout = () => { if (ref.activeElement === root) { setFocus([]); @@ -104,5 +156,6 @@ export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => return { destroy, setFocus, + isKeyboardMode: () => keyboardMode, }; }; diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 2f28c0cafdb..7c054da6d46 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -1,5 +1,10 @@ import { doc } from '@utils/browser'; -import { focusFirstDescendant, focusLastDescendant, focusableQueryString } from '@utils/focus-trap'; +import { + focusFirstDescendant, + focusLastDescendant, + focusRedirectedElement, + focusableQueryString, +} from '@utils/focus-trap'; import type { BackButtonEvent } from '@utils/hardware-back-button'; import { shouldUseCloseWatcher } from '@utils/hardware-back-button'; import { printIonError, printIonWarning } from '@utils/logging'; @@ -294,7 +299,7 @@ const focusElementInOverlay = (hostToFocus: HTMLElement | null | undefined, over } if (elementToFocus) { - focusVisibleElement(elementToFocus); + focusRedirectedElement(elementToFocus); } else { // Focus overlay instead of letting focus escape overlay.focus(); From 1599c9df5a819802386831c293fa106b46bf801a Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 16 Sep 2026 11:40:03 -0700 Subject: [PATCH 2/6] docs(focus-visible): clarify the startFocusVisible and isKeyboardMode docs --- core/src/utils/focus-visible.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/core/src/utils/focus-visible.ts b/core/src/utils/focus-visible.ts index df2193521c6..e61a60c36eb 100644 --- a/core/src/utils/focus-visible.ts +++ b/core/src/utils/focus-visible.ts @@ -48,10 +48,12 @@ export const focusElements = (elements: Element[]) => { }; /** - * Whether the most recent interaction on the page was keyboard-driven. + * Reports whether the most recent interaction on the page was keyboard-driven. * * Check this before drawing the keyboard focus indicator programmatically. - * Reads the document-level utility, the only one that sees every interaction. + * + * @returns `true` while the user is navigating with a keyboard, and before the + * first interaction on the page. */ export const isKeyboardMode = () => getOrInitFocusVisibleUtility().isKeyboardMode(); @@ -60,22 +62,17 @@ export const isKeyboardMode = () => getOrInitFocusVisibleUtility().isKeyboardMod * element with `ion-focused`, so the keyboard focus indicator is only drawn * while the user navigates with a keyboard. * - * Returns `setFocus` to mark elements explicitly, for a focus move the - * listeners below cannot attribute to a keyboard event, and `destroy` to - * detach the listeners. - * * @param rootEl Scopes the utility to this element's shadow root, so it only - * reacts to interactions inside that element. `ion-datetime` does this to draw - * indicators on its own buttons. Omit it to listen on the document, which is - * what `ion-app` does. Only the document-level instance publishes its mode - * through `isKeyboardMode`. + * reacts to interactions inside it. Omit it to listen on the document. + * @returns `setFocus` to mark elements focused programmatically, and `destroy` + * to detach the listeners. */ export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => { let currentFocus: Element[] = []; /* - * Starts as `true` so a focus move before the user has interacted at all - * still draws an indicator, such as an element focused on page load. + * Starts as `true` so an element focused before the user has interacted, + * such as one focused on page load, still draws an indicator. */ let keyboardMode = true; From da37775a4b6b8295ea776ea9b165881227b59e37 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 16 Sep 2026 15:46:55 -0700 Subject: [PATCH 3/6] fix(overlays): only draw the focus indicator when focus is redirected by the keyboard --- .../popover/test/focus-trap/index.html | 32 ---------- .../popover/test/focus-trap/popover.e2e.ts | 44 -------------- core/src/utils/test/overlays/overlays.e2e.ts | 59 +++++++++++++++++++ 3 files changed, 59 insertions(+), 76 deletions(-) delete mode 100644 core/src/components/popover/test/focus-trap/index.html delete mode 100644 core/src/components/popover/test/focus-trap/popover.e2e.ts diff --git a/core/src/components/popover/test/focus-trap/index.html b/core/src/components/popover/test/focus-trap/index.html deleted file mode 100644 index 1889fef37b9..00000000000 --- a/core/src/components/popover/test/focus-trap/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - Popover - Focus Trap - - - - - - - - - -
- - - -
Focusable element outside of the popover
-
-
- - - - - Notifications - - - -
- - diff --git a/core/src/components/popover/test/focus-trap/popover.e2e.ts b/core/src/components/popover/test/focus-trap/popover.e2e.ts deleted file mode 100644 index eedc94148af..00000000000 --- a/core/src/components/popover/test/focus-trap/popover.e2e.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { expect } from '@playwright/test'; -import { configs, test } from '@utils/test/playwright'; - -/** - * The focus trap redirects focus back into the overlay when focus lands outside - * of it. The indicator should only follow that redirect during keyboard - * navigation. - * - * It lands on the `ion-item` because a toggle inside an item has the item draw - * the indicator on its behalf. - */ -configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { - test.describe(title('popover: focus trap'), () => { - test.beforeEach(async ({ page }) => { - await page.goto('/src/components/popover/test/focus-trap', config); - - const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); - - // Opening with a click leaves the focus utility in pointer mode. - await page.locator('#open-popover').click(); - await ionPopoverDidPresent.next(); - }); - - test('should not show a focus indicator when focus is redirected after a pointer interaction', async ({ page }) => { - const item = page.locator('ion-popover ion-item'); - - await page.locator('.ion-page div[tabindex="0"]').evaluate((el: HTMLElement) => el.focus()); - await page.waitForChanges(); - - await expect(item).not.toHaveClass(/ion-focused/); - }); - - test('should show a focus indicator when focus is redirected during keyboard navigation', async ({ page }) => { - const item = page.locator('ion-popover ion-item'); - - // Shift turns keyboard mode back on without moving focus. - await page.keyboard.press('Shift'); - await page.locator('.ion-page div[tabindex="0"]').evaluate((el: HTMLElement) => el.focus()); - await page.waitForChanges(); - - await expect(item).toHaveClass(/ion-focused/); - }); - }); -}); diff --git a/core/src/utils/test/overlays/overlays.e2e.ts b/core/src/utils/test/overlays/overlays.e2e.ts index b5cee3b7932..49d624c8924 100644 --- a/core/src/utils/test/overlays/overlays.e2e.ts +++ b/core/src/utils/test/overlays/overlays.e2e.ts @@ -529,5 +529,64 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => await expect(wrapper).toHaveAttribute('role', 'dialog'); await expect(wrapper).toBeFocused(); }); + + /* + * The focus trap redirects focus back into the overlay when focus lands + * outside of it. The indicator should only follow that redirect during + * keyboard navigation. + * + * It lands on the `ion-item` because a toggle inside an item has the item + * draw the indicator on its behalf. `ion-app` is required to apply the + * focused styles. + */ + const redirectContent = ` + + Show Modal +
Outside Element
+ + + + Notifications + + + +
+ `; + + test('should not show a focus indicator when focus is redirected after a pointer interaction', async ({ page }) => { + await page.setContent(redirectContent, config); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const item = page.locator('ion-modal ion-item'); + + // Opening with a click leaves the focus utility in pointer mode. + await page.locator('ion-button#open-modal').click(); + await ionModalDidPresent.next(); + + await page.locator('ion-app > div[tabindex="0"]').evaluate((el: HTMLElement) => el.focus()); + await page.waitForChanges(); + + await expect(item).not.toHaveClass(/ion-focused/); + }); + + test('should show a focus indicator when focus is redirected during keyboard navigation', async ({ + page, + pageUtils, + }) => { + await page.setContent(redirectContent, config); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const item = page.locator('ion-modal ion-item'); + + await page.locator('ion-button#open-modal').click(); + await ionModalDidPresent.next(); + + // Shift turns keyboard mode back on without moving focus. + await pageUtils.pressKeys('Shift'); + await page.locator('ion-app > div[tabindex="0"]').evaluate((el: HTMLElement) => el.focus()); + await page.waitForChanges(); + + await expect(item).toHaveClass(/ion-focused/); + }); }); }); From 2745248798153f0d2a9bedccfcd5b67fdf2ce2a7 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 16 Sep 2026 16:21:11 -0700 Subject: [PATCH 4/6] test(overlays): add a focus trap modal to the manual test page --- core/src/utils/focus-visible.ts | 8 +++- core/src/utils/test/overlays/index.html | 50 +++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/core/src/utils/focus-visible.ts b/core/src/utils/focus-visible.ts index e61a60c36eb..ceef6c43b83 100644 --- a/core/src/utils/focus-visible.ts +++ b/core/src/utils/focus-visible.ts @@ -136,14 +136,18 @@ export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => } }; - ref.addEventListener('keydown', onKeydown); + /* + * Capture phase, so the mode is current for the overlay focus trap, which + * intercepts Tab in its own capture listener. + */ + ref.addEventListener('keydown', onKeydown, true); ref.addEventListener('focusin', onFocusin); ref.addEventListener('focusout', onFocusout); ref.addEventListener('touchstart', pointerDown, { passive: true }); ref.addEventListener('mousedown', pointerDown); const destroy = () => { - ref.removeEventListener('keydown', onKeydown); + ref.removeEventListener('keydown', onKeydown, true); ref.removeEventListener('focusin', onFocusin); ref.removeEventListener('focusout', onFocusout); ref.removeEventListener('touchstart', pointerDown); diff --git a/core/src/utils/test/overlays/index.html b/core/src/utils/test/overlays/index.html index 60e0a693180..4c19b3b291a 100644 --- a/core/src/utils/test/overlays/index.html +++ b/core/src/utils/test/overlays/index.html @@ -45,6 +45,9 @@ Create and Present Toast + Create and Present Focus Trap Modal @@ -128,6 +131,53 @@ await toast.present(); }; + /* + Presents a modal for checking the focus trap's redirect by hand. An item + holding a single checkbox is focusable without being clickable, so the + trap resolves to the item itself and draws the indicator on it. + + Moving focus to an element outside the modal makes the trap redirect + focus back in. The indicator should only follow that redirect when the + interaction was keyboard driven. + */ + const createAndPresentFocusTrapModal = async () => { + const div = document.createElement('div'); + div.innerHTML = ` + + + + Dark Mode + + + + Move focus outside + +

+ Pointer case: click the button above. Keyboard case: press Tab twice to + reach it, then Enter. A third Tab escapes the modal, so the count + matters. +

+ +

+ Only the keyboard case should leave the indicator on the Dark Mode row. +

+
+ `; + + const moveFocusButton = div.querySelector('ion-button#modal-move-focus'); + moveFocusButton.onclick = () => { + document.querySelector('#root-input').setFocus(); + }; + + const modal = await modalController.create({ + component: div, + }); + + await modal.present(); + + return modal; + }; + const createNestedOverlayModal = async () => { const div = document.createElement('div'); div.innerHTML = ` From d734e69df27a2279f061778814212b4bd371d663 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 17 Sep 2026 16:11:50 -0700 Subject: [PATCH 5/6] test(overlays): update instructions --- core/src/utils/test/overlays/index.html | 42 ++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/core/src/utils/test/overlays/index.html b/core/src/utils/test/overlays/index.html index 4c19b3b291a..816020f3135 100644 --- a/core/src/utils/test/overlays/index.html +++ b/core/src/utils/test/overlays/index.html @@ -153,26 +153,58 @@ Move focus outside

- Pointer case: click the button above. Keyboard case: press Tab twice to - reach it, then Enter. A third Tab escapes the modal, so the count - matters. + Pointer case: click the button above. Keyboard case: press ArrowDown. + Both move focus to the same element outside the modal.

Only the keyboard case should leave the indicator on the Dark Mode row. + The indicator is a faint grey wash, so the reading below reports it + directly.

+ +

indicator on the Dark Mode row: not checked yet

`; - const moveFocusButton = div.querySelector('ion-button#modal-move-focus'); - moveFocusButton.onclick = () => { + const report = () => { + const item = div.querySelector('ion-item'); + const status = div.querySelector('#focus-trap-status'); + status.textContent = `indicator on the Dark Mode row: ${item.classList.contains('ion-focused')}`; + }; + + /* + The reading is deferred a frame because `setFocus` is async, so the + trap has not redirected focus back yet when it returns. + */ + const moveFocusOutside = () => { document.querySelector('#root-input').setFocus(); + requestAnimationFrame(report); }; + const moveFocusButton = div.querySelector('ion-button#modal-move-focus'); + moveFocusButton.onclick = moveFocusOutside; + + /* + ArrowDown gives the keyboard case a route that does not depend on tab + order, since Tab does not reach the button while the trap holds focus + on the wrapper. + */ + const onKeydown = (ev) => { + if (ev.key === 'ArrowDown') { + moveFocusOutside(); + } + }; + document.addEventListener('keydown', onKeydown); + const modal = await modalController.create({ component: div, }); + modal.onDidDismiss().then(() => { + document.removeEventListener('keydown', onKeydown); + }); + await modal.present(); return modal; From dabc361b1b65c559b142c25a5784e31bfd880404 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 17 Sep 2026 16:48:47 -0700 Subject: [PATCH 6/6] test(overlays): make the focus trap redirect testable by hand --- core/src/utils/test/overlays/index.html | 54 +++++++------------------ 1 file changed, 15 insertions(+), 39 deletions(-) diff --git a/core/src/utils/test/overlays/index.html b/core/src/utils/test/overlays/index.html index 816020f3135..f5d3f6695fe 100644 --- a/core/src/utils/test/overlays/index.html +++ b/core/src/utils/test/overlays/index.html @@ -132,45 +132,37 @@ }; /* - Presents a modal for checking the focus trap's redirect by hand. An item - holding a single checkbox is focusable without being clickable, so the - trap resolves to the item itself and draws the indicator on it. - - Moving focus to an element outside the modal makes the trap redirect - focus back in. The indicator should only follow that redirect when the - interaction was keyboard driven. + Presents a modal for checking the focus trap's redirect by hand. Both + routes move focus to the input behind the modal, which the trap + redirects back to the checkbox. Only the keyboard route should leave an + indicator there. */ const createAndPresentFocusTrapModal = async () => { const div = document.createElement('div'); div.innerHTML = ` - - - Dark Mode - - + Dark Mode Move focus outside -

- Pointer case: click the button above. Keyboard case: press ArrowDown. - Both move focus to the same element outside the modal. -

+

Pointer: click the button above. Expect false below.

+ +

Keyboard: reopen the modal, press Tab twice to reach the button, then Enter. Expect true below.

+ +

indicator on the checkbox: not checked yet

- Only the keyboard case should leave the indicator on the Dark Mode row. - The indicator is a faint grey wash, so the reading below reports it - directly. + The indicator is a faint wash, so the reading above is the reliable + check. Reopen the modal between the two routes, since a click leaves + the focus utility out of keyboard mode.

- -

indicator on the Dark Mode row: not checked yet

`; const report = () => { - const item = div.querySelector('ion-item'); + const checkbox = div.querySelector('ion-checkbox'); const status = div.querySelector('#focus-trap-status'); - status.textContent = `indicator on the Dark Mode row: ${item.classList.contains('ion-focused')}`; + status.textContent = `indicator on the checkbox: ${checkbox.classList.contains('ion-focused')}`; }; /* @@ -185,26 +177,10 @@ const moveFocusButton = div.querySelector('ion-button#modal-move-focus'); moveFocusButton.onclick = moveFocusOutside; - /* - ArrowDown gives the keyboard case a route that does not depend on tab - order, since Tab does not reach the button while the trap holds focus - on the wrapper. - */ - const onKeydown = (ev) => { - if (ev.key === 'ArrowDown') { - moveFocusOutside(); - } - }; - document.addEventListener('keydown', onKeydown); - const modal = await modalController.create({ component: div, }); - modal.onDidDismiss().then(() => { - document.removeEventListener('keydown', onKeydown); - }); - await modal.present(); return modal;