Skip to content

fix(modal): keep focus on dialog for cycle sheet modals#31293

Merged
ShaneK merged 1 commit into
mainfrom
fix/modal-focus-cycle
Jul 23, 2026
Merged

fix(modal): keep focus on dialog for cycle sheet modals#31293
ShaneK merged 1 commit into
mainfrom
fix/modal-focus-cycle

Conversation

@ShaneK

@ShaneK ShaneK commented Jul 23, 2026

Copy link
Copy Markdown
Member

Issue number: internal


What is the current behavior?

When a sheet modal uses handleBehavior="cycle", the host element is focusable (tabIndex=0) and onModalFocus redirects focus to the drag handle whenever the host is focused. present() moves focus to the .modal-wrapper (the role="dialog" element) inside the shadow DOM, but that focus event is retargeted to the host, so onModalFocus sees ev.target === el and treats it as a direct host focus. It then bounces focus onto the handle.

This is latent on the default handleBehavior="none" (the host isn't focusable, so the redirect never runs), but reproduces on any sheet modal that opts into cycle.

What is the new behavior?

onModalFocus now redirects to the handle only when the host itself was focused directly, detected by el.shadowRoot?.activeElement being null. When present() focuses the dialog wrapper, activeElement is the wrapper (not null), so the redirect is skipped and the dialog keeps focus. Tabbing into the modal from outside still lands on the handle, since the host is the focused element in that case.

Does this introduce a breaking change?

  • Yes
  • No

Other information

Related to the dialog focus work in #31260 . That change is correct under the default handleBehavior, but the cycle path was not covered until now. Adds an e2e test to utils/test/overlays/overlays.e2e.ts that presents a sheet modal with handle-behavior="cycle" and asserts focus stays on the wrapper.

The same fix ships on the major-9.0 sync (#31290), where cycle is the default and this bug is hit on every sheet modal.

Preview (sheet modal test page):

@ShaneK
ShaneK requested a review from a team as a code owner July 23, 2026 16:17
@ShaneK
ShaneK requested a review from BenOsodrac July 23, 2026 16:17
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ionic-framework Ready Ready Preview, Comment Jul 23, 2026 4:17pm

Request Review

@github-actions github-actions Bot added the package: core @ionic/core package label Jul 23, 2026

@thetaPC thetaPC left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment on lines +1178 to +1183
/**
* 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.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that this comment is different than the one on major-9.0. I assume that this was intentional.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, just a little more concise. I'll probably take this when I merge main into major-9.0

@ShaneK
ShaneK added this pull request to the merge queue Jul 23, 2026
Merged via the queue into main with commit b4b2603 Jul 23, 2026
51 checks passed
@ShaneK
ShaneK deleted the fix/modal-focus-cycle branch July 23, 2026 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: core @ionic/core package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants