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

Styles are wrong when used inside a shadow root #2612

Open
1 task done
chriscoomber opened this issue Nov 23, 2023 · 4 comments · May be fixed by #2615
Open
1 task done

Styles are wrong when used inside a shadow root #2612

chriscoomber opened this issue Nov 23, 2023 · 4 comments · May be fixed by #2615
Labels

Comments

@chriscoomber
Copy link

chriscoomber commented Nov 23, 2023

Is there an existing issue for this?

  • I have searched the existing issues

Describe the issue

I placed a react-native-web component in a shadow root, and the styles were wrong. Specifically, these components have the correct class names, but the style sheet seems to have not been loaded.

<Button color='red' title='Test'/>

looks like

Screenshot 2023-11-23 at 10 30 22

In the chrome debugger, its DOM element looks like:

<button role="button" class="css-view-175oi2r r-transitionProperty-1i6wzkk r-userSelect-lrvibr r-cursor-1loqt21 r-touchAction-1otgn73 r-borderRadius-1jkafct" type="button" style="background-color: rgb(255, 0, 0); transition-duration: 0s;"><div dir="auto" class="css-text-146c3p1 r-color-jwli3a r-fontWeight-majxgm r-padding-edyy15 r-textAlign-q4m81j r-textTransform-tsynxw">Test</div></button>

however Chrome can't resolve any of these classes - only the host's stylesheet is being applied.

Expected behavior

Should look like
Screenshot 2023-11-23 at 10 31 13

Steps to reproduce

  1. Quickstart with Create React App

  2. Obtain a ShadowDom react component (either react-shadow or roll your own).

I had the same issue with react-shadow (which was presumably written by someone who knows what they're doing!), but for the sake of this issue I'm using the following wrapper as it's a simpler repro - please let me know if I'm making a glaring mistake as I'm not an experienced web developer.

function ShadowDomWrapper({children}) {
  const shadowHost = useRef();
  const reactRoot = useRef();

  useEffect(() => {
    if (shadowHost.current) {
      if (!reactRoot.current) {
        const shadowRoot = shadowHost.current.attachShadow({mode: "open"});
        reactRoot.current = createRoot(shadowRoot);
      }
      reactRoot.current.render(children);
    }
  });

  return <div ref={shadowHost}></div>
}
  1. Render a react-native-web component in and outside the shadow root to see how they differ:
function App() {
  return (
    <div className="App">
      <header className="App-header">
        ...other stuff...
        <Button color='red' title='Test'/>
        <ShadowDomWrapper>
          <Button color='red' title='Test'/>
        </ShadowDomWrapper>
        ...other stuff...

Test case

https://codesandbox.io/p/sandbox/react-native-web-shadow-dom-repro-5gy7vw

Additional comments

I saw some related issues, e.g. #1517 and it seems support for the shadow DOM was added with 0.18. It's very possible that I'm just missing a step, but there's no documentation for how to get this working! How does react-native-web provide the stylesheets to the root node of the app? How do I do this for the shadow root?

Why am I trying to do this?

I have a component library written in react-native, for cross-platform compatibility between our react-native app and our next.js app. When rendering components from the library in next.js, they are suddenly affected by CSS stylesheets from the next.js app, which can break the styling. The components are already styled with the style prop - I do not need external styling so I want to isolate them from the styling added by the website, hence the use of shadow dom. However, I seem to be losing the RNW styles too!

@necolas
Copy link
Owner

necolas commented Nov 28, 2023

You can't use createRoot directly from react-dom/client, as that doesn't allow the library to insert the style sheet into the shadow DOM. You have to use the render function from RNW

https://codesandbox.io/p/sandbox/react-native-web-shadow-dom-repro-forked-xf696q

However, you shouldn't have to reach into the internals to do this, but the exported render wasn't updated to use React 18's createRoot. I also encountered a couple of other bugs related to this functionality that I'll fix in a patch. I'll post it here for you to try once it's up. Thanks!

necolas added a commit that referenced this issue Nov 29, 2023
AppRegistry.runApplication uses `ReactDOM.createRoot` by default, but
the `render` export uses the legacy `ReactDOM.render`. This patch makes
those 2 APIs consistent.

It also makes some adjustments to the `createSheet` internals to more
reliably implement and test style sheet replication within ShadowRoot's
and iframes.

Fix #2612
@necolas necolas linked a pull request Nov 29, 2023 that will close this issue
@necolas
Copy link
Owner

necolas commented Nov 29, 2023

The PR related to this issue is #2615

You can install 0.0.0-dee1467a3 to try it out - it's just the PR stacked on top of 0.19.9. Here's an updated code example:

https://codesandbox.io/p/sandbox/react-native-web-shadow-dom-repro-forked-86y3vd

Please try it out in your app, and me know if you encounter any issues, including any issues outside of the shadow DOM. If everything looks good after you've audited your stuff, I can merge the PR and cut a main-line release. Thanks!

@chriscoomber
Copy link
Author

Yep, that seems to work for me! Thanks a lot.

I have one question though - it looks like if I define react Contexts outside of the ShadowDomWrapper, they're not available within. Is that to be expected? Is there any way to work around that (aside from re-declaring them all manually in ShadowDomWrapper)?

Example: https://codesandbox.io/p/sandbox/react-native-web-shadow-dom-repro-forked-yn52hj

@necolas
Copy link
Owner

necolas commented Nov 30, 2023

Yeah that's expected because we're creating a completely new React root. If you want to avoid consuming and re-providing contexts, I think you could render the components into a portal:

function ShadowDomWrapper({ children }) {
  const shadowHost = useRef();
  const shadowRoot = useRef();
  const [portal, setPortal] = React.useState(null);
  useEffect(() => {
    if (shadowHost.current) {
      if (!shadowRoot.current) {
        shadowRoot.current = shadowHost.current.attachShadow({ mode: 'open' });
        // Render dummy content into the shadow DOM for style sheet insertion
        render(<div />, shadowRoot.current);
      }
      setPortal(createPortal(children, shadowRoot.current));
    }
  }, [children]);
  return (
    <div ref={shadowHost}>
      {portal}
    </div>
  );
}

https://codesandbox.io/p/sandbox/react-native-web-shadow-dom-repro-forked-cfddm3

There will be other differences between render and createPortal implementations, e.g., getting event targets propagating from within the shadow dom. React doesn't really support this use case very well, and events-from-within portals are a bit funky.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants
@necolas @chriscoomber and others