Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP]: Shadow dom rfc #7687

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions rfcs/2025-shadow-dom-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<!-- Copyright 2025 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License. -->

- Start Date: 2025/1/28
- RFC PR: exploration PRs: https://github.com/adobe/react-spectrum/pull/6046
- Authors: Rob Snow

# Improving React Aria Shadow DOM Support

## Summary

This RFC outlines a plan for progressive enhancement of Shadow DOM support in React Aria, React Aria Components, and React Spectrum S2.
Shadow DOM support can mean many things; open root vs closed root, single containing shadow root, or multitude of individual components.
We can improve across all of these.


## Motivation

As Web Components have become more prominent and more libraries and applications use them, we have encountered friction for people trying to adopt or use our libraries alongside others.
Copy link
Contributor

Choose a reason for hiding this comment

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

The use case is beyond web components. For example, 3rd party widgets or SDKs who do not want their styles to be affected by user's website code may want to use shadow DOM without web components. This is what we do. The other option for such apps is dynamic iframes. The good thing is that with the changes we make for shadow DOM, we automatically take care of dynamic iframes too by using the correct document.

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, I'll reframe a bit, I was using Web Components as a bit of a stand-in term for shadow dom, but this is more accurate

I think we are potentially over simplifying by stating that solving shadow dom will solve iframes. They are two fundamentally different technologies even if they have overlap in terms of problems they solve.

See for example the need for


This is not needed for Shadow DOM. If we only worked on supporting Shadow DOM, this would be missed. That is because events inside an iframe will not appear outside the frame. While for Shadow DOM, events still have a capture phase and bubble phase outside of the Shadow root.

There will be other cases where behaviour differs between the two technologies.


Some examples of these are:
- [Dialog's focus management and work with 3rd party dialogs](https://github.com/adobe/react-spectrum/issues/5314)
- [Video Controls are not respected when using FocusScope](https://github.com/adobe/react-spectrum/issues/6729)
- [FocusScope not working when used inside shadowRoot](https://github.com/adobe/react-spectrum/issues/1472)
- [ariaHideOutside incorrect behavior inside shadow DOM.](https://github.com/adobe/react-spectrum/issues/6133)
- [usePress is not work in shadowRoot](https://github.com/adobe/react-spectrum/issues/2040)
- [useOverlay Click Outside in Shadow-DOM context](https://github.com/adobe/react-spectrum/issues/3970)

We have also had a contribution to solve some of the issues. While this is useful, we would like it to feel less hacky and more importantly, we'd like to incorporate the support into our daily lives in as easy as way as possible.


## Detailed Design

As mentioned earlier, there are proposed parts to this initiative:

1. Custom React Testing Library Renderer
Copy link
Contributor

Choose a reason for hiding this comment

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

We do not write special tests for shadow DOM. All our component testing is done using playwright's component testing. We run the same tests both with and without shadow DOM.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure what this comment has to do with us? The RFC is for what we need to do to support shadow dom in our library.

I'm also suggesting that we run the same tests both with and without shadow DOM

we should create a custom React Testing Library renderer for our unit tests which can wrap each test's rendered dom in a shadow root.
... without needing to write many specific tests.

There will need to be specific tests as well, but they can be added as edge cases are discovered. For now I'd like to get all of our current tests running both ways.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure what this comment has to do with us?

Not much. This was just me sharing how we handle this situation.


Much like our custom renderer to test React.StrictMode, we should create a custom React Testing Library renderer for our unit tests which can wrap each test's rendered dom in a shadow root.

This will give us a baseline to develop against and it will also hold us accountable in any future changes without needing to write many specific tests. In the worst case, should we pull the plug on this, it will also make it easy to remove the tests.

I expect many tests will fail in the beginning. We make use of a lot of DOM API's and have not generally thought of the ShadowDOM while developing.

2. Avoid DOM Traversal/Manipulation

This is most prominent in Focus Scope where we traverse the DOM in order to assign focus, such as in Collections, and contain focus such as in Dialogs.

One proposal to avoid this traversal is to create focusable sentinels in order to trap focus.

This will have the side benefit of theoretically working with closed root shadow doms as well, such as the aforementioned native video players inside a Dialog.

3. In-team Education

Writing code that can handle the prescence of Shadow DOM can be tricky. As a result, there will be a learning curve for the team.

Some work has already been done to write utilities to hide away some of this complexity. We will also want to write lint rules to help us automatically catch as many common situations as possible. Some examples of these are:

```jsx
document.activeElement
<->
getActiveElement();

e.target
<->
getTargetElement(e);

let walker = document.createTreeWalker(...)
<->
let walker = createShadowTreeWalker(...)

e.currentTarget.contains(e.target)
<->
nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))
```

4. Communication

We have our current Shadow DOM support gated behind a feature flag to both signal that it is experimental as well as to protect users who are not using any Shadow DOM from possible performance hits.

We will want to expand this to also shield those users from code size increases. This could be accomplished with a global registry for the utility functions we provide that would replace the feature flag system we have for it right now.

We also need to communicate, through documentation, what the specifics are of our Shadow DOM support since we likely will not be able to support everything, and certainly not much for a while.

For example, some of our support may require open root Shadow DOMs. Or someone using our FocusScope's focus manager may need to know about different traversal methods to avoid the same issues we are trying to avoid.

## Drawbacks

One concern is on-going support. Our current use cases do not call for Shadow DOM support, so knowledge on the team is currently thin. It will take time for people to ramp up on skills. Not only that, but it isn't part of our weekly testing and we have no plans for it to be at this time, which means we must rely on unit tests as much as possible.

In addition, contributions may occur for a little while until those teams have their needs met. However, it should be assumed that there will be further work to complete the goals as outlined here.

Another concern is that the current approach is, for lack of a better word, hacky. This is because we are accessing and manipulating the Shadow DOM in ways that it wasn't really intended. If we were to rewrite our library today, there are other ways we'd solve these issues which would respect more of the concept of the Shadow DOM and its purpose. What we do here and now may complicate a future where we have different APIs to support this vision of what support would ideally look like.
Copy link
Contributor

@ritz078 ritz078 Feb 8, 2025

Choose a reason for hiding this comment

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

I think one way to think about this is supporting different root elements instead of just think about it as shadow DOM support. See #7743 for example. Components like modal and popover always treat document.body as their root element. Instead react-aria should provide options to treat any other element as a container element.

Copy link
Member Author

Choose a reason for hiding this comment

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

Possibly, what happens in the case of nested shadow doms though? Would we expect people to addWindowFocusTracking equivalent for each root and each hook/function that needs it? Would components need to accept a root element to pass down to hooks? context?

This wouldn't work for closed roots. This is why we want to re-think some of the ways parts of our library work. For example, FocusScope moves focus around for you when it's contain=true. This can't move into closed shadow roots, which is why the video tag breaks our FocusScope at the moment in certain cases. Instead, if we moved to a sentinel-based approach, we could let the browser handle the tab movement.

Copy link
Contributor

Choose a reason for hiding this comment

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

Initially I thought that supporting closed shadow root might not be a high priority at the beginning but you raised a good point about the video element. In that case, we do need to support both modes of shadow root.

Would we expect people to addWindowFocusTracking equivalent for each root and each hook/function that needs it? Would components need to accept a root element to pass down to hooks? context?

This might not be a good solution in the long run. But I can't answer much right now before going into the details of closed shadow root.



## Backwards Compatibility Analysis

This is a backwards compatible change, we should just be extending functionality, not breaking any of it.

## Alternatives

Unknown, haven't done research here yet.

## Open Questions

* How to actually define the limitations of our support? See Introduction, it's missing a final sentence with this information.
Copy link
Member Author

Choose a reason for hiding this comment

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

A possible classification:
Container components that can have anything vs create name components such as RadioGroup that don't allow shadowdom's inside them

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the best way to approach it is to come up with a setup where all new code is compatible and the existing code can be changed when someone needs it. I think in a while, it will be just about using a set of utilities.

We can start with the getOwnerWindow and getOwnerDocument eslint rule in more packages.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this is ambiguous

all new code is compatible

Does that mean it works in an application that has a single shadow dom at the root? that it can use video anywhere? that it can have 3rd party web components anywhere? that it can have shadow roots anywhere?

What if the new code is a new feature in an existing component or hook that has no support? what if that hook depends on lower hooks that don't have support and changing those hooks would also change how other components work.

It's a lofty goal and I think we should aim for support of everything. But it has to be moderated with the cost of implementation and ongoing support.


I don't believe this is true

it will be just about using a set of utilities

as I mentioned earlier, in order to support closed root (as an example), we have to rethink how many components and hooks are implemented.


I agree with this

We can start with the getOwnerWindow and getOwnerDocument eslint rule in more packages.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree. If the aim here is to add full support for shadow DOM (which will be great), this isn't as simple as adding utilities. When I say full support, I mean everything just works out of the box irrespective of the where a shadow DOM is located. And that will include much more than some utilities and will involve refactoring of certain sections.


## Help Needed

The biggest help we can receive is tests, either in the form of unit tests or in the form of examples of real life applications/setups that we can turn into unit tests. The more tests we have, the less likely we will break anything moving forward after the initial effort is complete.
Copy link
Contributor

Choose a reason for hiding this comment

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

We want to avoid maintaining a fork so we will try to do our best here. Right now we use patches which are a nightmare during upgrades.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure what this has to do with the "help needed" section? Are you just saying it will be hard to create tests and examples?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry for not being clear. I meant we will like to help here as much as possible so that we do not have to maintain a fork. At the end of the day, we want shadow DOM support to land up here.


## Frequently Asked Questions

* How much can we count on contributor support? will the code just rot after the initial push?
* We should expect that we'll take ownership of any code that comes in, we should not count on external support.
* Is there any benefit to our selves or other people not using Shadow DOM?
* Yes, see some of the issues listed above, specifically native video players breaking FocusScopes in Dialogs.
* Users may be unaware that they are using Shadow DOM as it may be an implementation detail of a 3rd party component.


## Related Discussions

Original issue: [feat: Focus Management within ShadowDOM](https://github.com/adobe/react-spectrum/pull/6046). Many of the ideas discussed in this RFC are from conversations around this PR.
Loading