react-spectrum icon indicating copy to clipboard operation
react-spectrum copied to clipboard

wip: Use stacking contexts to determine non-inert elements outside modals

Open devongovett opened this issue 3 months ago • 1 comments

https://github.com/orgs/adobe/projects/19/views/4?pane=issue&itemId=126270389

Related issues:

  • https://github.com/adobe/react-spectrum/issues/8784
  • https://github.com/adobe/react-spectrum/issues/7397
  • https://github.com/adobe/react-spectrum/issues/5314
  • https://github.com/adobe/react-spectrum/issues/5799
  • https://github.com/adobe/react-spectrum/issues/6729
  • https://github.com/adobe/react-spectrum/issues/7954
  • https://github.com/adobe/react-spectrum/issues/6774
  • https://github.com/adobe/react-spectrum/discussions/8661
  • https://github.com/adobe/react-spectrum/discussions/6000
  • https://github.com/adobe/react-spectrum/discussions/6498
  • https://github.com/adobe/react-spectrum/discussions/7981
  • https://github.com/adobe/react-spectrum/discussions/5981
  • https://github.com/adobe/react-spectrum/discussions/4364

Currently, our dialogs mark all elements outside them as inert. This makes it difficult to use them with third party components that use portals, such as external dialogs, menus, browser extensions, embedded widgets like cookie consents, toasts, support chats, etc. Even before we switched to inert from aria-hidden, FocusScope would steal focus back to the dialog and screen readers could not access these elements. We have data-react-aria-top-layer as an opt out, but you must manually apply it which can be difficult.

This PR is an attempt to make this work automatically. Instead of marking all elements outside dialogs as inert (with a few exceptions), it compares their z-indexes with the modal. Any elements that are visually above the modal get preserved. This works by traversing down from the document body to find the root stacking contexts, and then comparing these with the root stacking context of the modal itself. When a new element such as a toast comes in after the dialog opens the same process occurs.

Since we use inert, we don't need FocusScope to contain focus for the most part. This will also mean the browser will handle tabbing, which should fix some issues with shadow DOM and native elements. Some exceptions to handle still:

  1. Inert does not prevent tabbing out of a dialog into the browser chrome like our current dialog does. Accessibility experts are divided on that issue. The native element does not prevent this, but the ARIA practices guide says it should. See https://github.com/w3c/aria-practices/issues/1772
  2. Inert does not prevent tabbing out of a dialog within an iframe to the parent page. This seems like a major issue especially for unified shell.

Perhaps we could do a more limited focus trap just within the window/iframe instead of only within the dialog. That would allow tabbing to other elements on top of the dialog, but not out of the window.

To do

  • [ ] Fix tests (JSDOM doesn't support inert so this will be interesting)
  • [ ] Do the same for popovers
  • [ ] Handle above cases

devongovett avatar Aug 29 '25 19:08 devongovett