How to mount styles inside shadow root using cssinjs/jss
Asked Answered
A

4

11

I'm trying to use https://material-ui.com/ components inside shadow dom, and need a way to inject those styles inside shadow dom. by default material-ui, which uses jss under the hood injects styles in the head of the page.

Is that even possible? Can anyone come with an example?

Always answered 29/1, 2019 at 21:48 Comment(4)
I don't think injecting React and the whole shebang into the page is a good idea, it certainly doesn't seem so to me. The usual approach is to insert just one iframe that points to an html file in your extension (listed in web_accessible_resources) that is a standard page which can do whatever it wants and load any material UI components inside. I've seen at least one full example/tutorial for that on the web.Lax
Do you want the styles of a component into it's shadow DOM or would you be ok to put all styles into a single shadow DOM?Phillisphilly
@wOxxOm I completely agree, but the iframe approach it's a bit tricky in my experience because of few things like focus (when you click inside an iframe is like being in another page) and resizing of the iframe (i've had issues with that when for example I put a dropdown inside the iframe I need to tell the outside page to resize the iframe so the content is visible). a web component will definitely be more native. "epsilon" what I would like to do is to make a single web component using few material-ui widgets, and their style should be injected in the shadow root.Always
Even if you able to archive your goal you might get a problem with displaying popup material components from inside your shadow-root. Because seems material library insert popups dom elements directly into html page body which will be styleless in your case.Petrography
D
20

This is what my web component looks like, it is a web component that renders a react app that contains material-ui styles.

import * as React from 'react';
import { render } from 'react-dom';
import { StylesProvider, jssPreset } from '@material-ui/styles';
import { create } from 'jss';

import { App } from '@myApp/core';

class MyWebComponent extends HTMLElement {
  connectedCallback() {
    const shadowRoot = this.attachShadow({ mode: 'open' });
    const mountPoint = document.createElement('span');
    const reactRoot = shadowRoot.appendChild(mountPoint);
    const jss = create({
      ...jssPreset(),
      insertionPoint: reactRoot
    });

    render(
      <StylesProvider jss={jss}>
        <App />
      </StylesProvider>,
      mountPoint
    );
  }
}
customElements.define('my-web-commponent', MyWebComponent);

Setting the insertionPoint on jss to the actual react root inside the shadow root will tell jss to insert those styles inside that shadow root.

Difficulty answered 9/6, 2019 at 17:15 Comment(2)
This works with many components, however if you use the Dialog, its tags are placed in the body and this trick doesn't work.Melar
@agpx for dialogs, you will have to use react portalsPickled
L
13

Using https://github.com/Wildhoney/ReactShadow to create shadow dom (you could also do it by hand as shown in previous answer), I created a small WrapperComponent that encapsulates the logic.

import root from 'react-shadow';
import {jssPreset, StylesProvider} from "@material-ui/styles";
import {create} from 'jss';
import React, {useState} from "react"

const WrappedJssComponent = ({children}) => {
  const [jss, setJss] = useState(null);
  
  function setRefAndCreateJss(headRef) {
    if (headRef && !jss) {
      const createdJssWithRef = create({...jssPreset(), insertionPoint: headRef})
      setJss(createdJssWithRef)
    }
  }
  
  return (
    <root.div>
      <head>
        <style ref={setRefAndCreateJss}></style>
      </head>
      {jss &&
      <StylesProvider jss={jss}>
        {children}
      </StylesProvider>
      }
    
    </root.div>
  )
}

export default WrappedJssComponent

Then you just need to Wrap your app, or the part of your app you want to shadow inside <WrappedJssComponenent><YourComponent></YourComponent></WrappedJssComponenent>.

Be careful, some of the material-UI component won't work as usual (I had some trouble with

  • ClickAwayListener, maybe because it uses the parent dom, did not investigate more than that to be honest.
  • Popper, and everything that will try to use document.body as container will not have access to jss defined in shadow node. You should give an element inside the shadow dom as container.
Labourer answered 26/7, 2019 at 13:41 Comment(3)
This is awesome! You totally saved me! Thanks!Equilibrant
"You should give an element inside the shadow dom as container." - How can this be done?Heterogenous
@Heterogenous You can pass container prop to Popper (and a few other elements) with reference to the container element - that element could be inside shadow root as well.Bustup
S
1

There is also a whole page in the docs now (MaterialUI 5) that covers how to integrate MUI with a shadow-dom. You also might have to set Portal defaults not to target the dom. https://mui.com/material-ui/guides/shadow-dom/

Static answered 16/1, 2023 at 11:12 Comment(1)
While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From ReviewPercolation
U
0

When using @material-ui/core/CssBaseline with MUI, also emotion styles are being used. In order to support both legacy jss and emotion you can extend the accepted answer above with a CacheProvider like this:

import ReactDOM from 'react-dom/client'
import App from './App'
import createCache from '@emotion/cache'
import { CacheProvider } from '@emotion/react';
import { StylesProvider, jssPreset } from '@material-ui/styles';
import { create } from 'jss';

class ReportComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    const mountPoint = document.createElement('div');
    const emotionPoint = this.shadowRoot!.appendChild(document.createElement('div'));
    const emotionCache = createCache({
      key: 'report-component',
      container: emotionPoint
    });

    const reactRoot = this.shadowRoot!.appendChild(mountPoint);
    const root = ReactDOM.createRoot(reactRoot);
    const jss = create({
      ...jssPreset(),
      insertionPoint: reactRoot
    });

    root.render(
      <StylesProvider jss={jss}>
        <CacheProvider value={emotionCache}>
          <App />
        </CacheProvider>
      </StylesProvider>
    );
  }
}
customElements.define('report-component', ReportComponent);
Urissa answered 20/5, 2022 at 3:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.