From 12c889da9d7b3b8c49fc1f30c56e11b9815e86c0 Mon Sep 17 00:00:00 2001 From: ShaneK Date: Thu, 23 Jul 2026 09:02:47 -0700 Subject: [PATCH] fix(modal): keep focus on dialog for cycle sheet modals --- core/src/components/modal/modal.tsx | 9 +++++-- core/src/utils/test/overlays/overlays.e2e.ts | 26 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index e5c5f1301e2..b3a728b0ccf 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -1175,8 +1175,13 @@ export class Modal implements ComponentInterface, OverlayInterface { */ private onModalFocus = (ev: FocusEvent) => { const { dragHandleEl, el } = this; - // Only handle focus if the modal itself was focused (not a child element) - if (ev.target === el && dragHandleEl && dragHandleEl.tabIndex !== -1) { + /** + * Shadow DOM focus is retargeted to the host, so `ev.target === el` is also + * true when a shadow child (the dialog wrapper present() focuses) is focused. + * shadowRoot's activeElement is null only when the host was focused directly, + * so redirect to the handle only then and leave present()'s wrapper focus intact. + */ + if (ev.target === el && el.shadowRoot?.activeElement == null && dragHandleEl && dragHandleEl.tabIndex !== -1) { dragHandleEl.focus(); } }; diff --git a/core/src/utils/test/overlays/overlays.e2e.ts b/core/src/utils/test/overlays/overlays.e2e.ts index de73fd2f011..b5cee3b7932 100644 --- a/core/src/utils/test/overlays/overlays.e2e.ts +++ b/core/src/utils/test/overlays/overlays.e2e.ts @@ -473,6 +473,32 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => await expect(wrapper).toBeFocused(); }); + test('should focus the sheet modal wrapper on present when handleBehavior is cycle', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'FW-7611', + }); + // present()'s wrapper focus must survive the cycle-handle focus redirect. + await page.setContent( + ` + + Sheet Modal Content + + `, + config + ); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const modal = page.locator('ion-modal'); + const wrapper = page.locator('ion-modal .modal-wrapper'); + + await modal.evaluate((el: HTMLIonModalElement) => el.present()); + await ionModalDidPresent.next(); + + await expect(wrapper).toHaveAttribute('role', 'dialog'); + await expect(wrapper).toBeFocused(); + }); + test('should focus the card modal wrapper on present', async ({ page }, testInfo) => { testInfo.annotations.push({ type: 'issue',