Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React component's Material UI theme not scoped locally to Shadow DOM

Intro

I am building a Chrome Extension that renders a React component using the Content script. The React component is a toolbar that has sub-tools for users to use while browsing the page. I am using Material UI as my component library and styling solution for the toolbar and all of its other child components, popups, etc.

What Works

Injecting the root React component into the page works just fine using a div, "Extension" as a mountNode.

content.js (Chrome Content Script)

var app = $('<div id="Extension" style="position: fixed; display: block; bottom: 0px; left: 0px; width: 100vw; height: 48px; background: grey; z-index: 99999"></div>')
app.prependTo('body');
render(<App />, document.getElementById('Extension'))

app.js (Main React Component)

Material UI is also able to generate a new styles object and apply the css styling to the child components on the page. So this is all good

(source)

const styles = {
  root: {
    display: "block",
    color: "red",
  },
};

(generated)

.App-root-1 {
  color: red;
  display: block;
}

The Problem

Because I am using a content script in chrome, sites like Facebook with general css selectors try to override the styling in Material UI. It would also be possible for css attributes to leak from the the React toolbar into the main page.

Halfway Solution

My current solution is to use react-shadow to scope the styling around the React component and keep it isolated from the rest of the page.

import React from "react";
import PropTypes from 'prop-types';
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
import { withStyles } from '@material-ui/core/styles';

import ShadowDOM from 'react-shadow';
import ToolBar from './ToolBar'

const theme = createMuiTheme({
  palette: {
    primary: {
      main: '#ef5350',
    },
    type: 'light'
  },
  status: {
    danger: 'orange',
  },
});

const styles = {
  root: {
    display: "block",
    color: "red",
  },
};

class App extends React.Component {

  constructor(props) {
      super(props);
  
      this.state = { open: false }
  }

  render () {
    const { classes } = this.props;

    return (
      <ShadowDOM>
          <div id="Toolbox">
              <MuiThemeProvider theme={theme}>
                  <ToolBar />
              </MuiThemeProvider>
          </div>    
      </ShadowDOM>
    )
  }
};

App.propTypes = {
  classes: PropTypes.object,
};

export default withStyles(styles)(App)

When I did this, the generated theme from 'Material-UI' does not apply to the toolbar, and I am left with the default inline styling applied to the "Extension" div (defined above, content.js).

The theme is generated using createMuiTheme(options) from the material-ui package:

This function is successful, and the theme is applied using:

<MuiThemeProvider theme={theme}>

I can confirm that createMuiTheme(options) & <MuiThemeProvider/> are working because the theme's generated stylesheets are added as the last tags in the page's <head> tag, as seen here in the image:

<head> tag contains MUI generated styles

What I Need to Happen

Because my React toolbar element is inside #shadow-root, it is unable to recieve styling from the MUI generated classes, because they are contained in the main <head> tag, in the main DOM tree. I believe when <MuiThemeProvider/> is rendered, it appends its provided stylesheets to the top of the main DOM tree, NOT the #shadow-root (where they need to be so that the styles apply locally). See the image below:

MUI Stylesheets in </head>, but they need moved to #shadow-root

So I am looking for a solution to have the stylesheets generated by Material UI to be placed under the #shadow-root for them to apply correct styling on my React toolbar component.

1.) Is there any way to have <MuiThemeProvider/> scope to the shadow DOM, or prefix the classnames with something like :host?

2.) Is there a way to lock down the #shadow-root created with react-shadow, so that when <MuiThemeProvider/> appends the stylesheets, they are forcefully appended to the shadow root?

3.) I am still very inexperienced with React, Javascript, and creating Chrome Extensions. Perhaps I am missing a very simple solution already?

Any help is appreciated.

like image 351
Braden Preston Avatar asked Aug 14 '18 01:08

Braden Preston


1 Answers

I had the same issue packaging my web application for integration with another framework, which required my React app to be exported as a Web Component. After struggling for a while with libraries like react-jss and having no luck, I finally came across this Stack Overflow answer by @shawn-mclean this morning. I had seen your question while banging my head against my desk and wanted to pass on what worked for me.

In summary, I first had to create the Web Component

WCRoot.js

import React from 'react';
import ReactDOM from 'react-dom';
import { StylesProvider, jssPreset } from '@material-ui/styles';
import { create } from 'jss';
import App from './App';

class WCRoot extends HTMLElement {

    connectedCallback() {
        const shadowRoot = this.attachShadow({ mode: 'open' });
        const mountPoint = document.createElement('span');
        // first we need to store a reference to an element INSIDE of the shadow root        
        const reactRoot = shadowRoot.appendChild(mountPoint);

        // now we use that saved reference to create a JSS configuration
        const jss = create({
            ...jssPreset(),
            insertionPoint: reactRoot
        });

        // finally we need to wrap our application in a StylesProvider
        ReactDOM.render(
            <StylesProvider jss={jss}>
                <App />
            </StylesProvider>,
            mountPoint);
    }
}
customElements.define('wc-root', WCRoot);

Next, I set my index.js to render <wc-component></wc-component> instead of <App />:

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
// eslint-disable-next-line
import WCRoot from './WCRoot';

ReactDOM.render(<wc-root></wc-root>, document.getElementById('root'));

One thing which surprised me is that we don't need to modify the MuiThemeProvider at all...it just grabs the existing JSS context provided by the StylesProvider. I believe you should be able to modify content.js to look something like the following:

content.js

var app = $('<div id="Extension" style="position: fixed; display: block; bottom: 0px; left: 0px; width: 100vw; height: 48px; background: grey; z-index: 99999"></div>')
app.prependTo('body');

var shadowRoot = this.attachShadow({ mode: 'open' });
var mountPoint = document.getElementById('Extension');
var reactRoot = shadowRoot.appendChild(mountPoint);

// see code blocks above for the import statements for `create` and `jssPreset`
var jss = create({
    ...jssPreset(),
    insertionPoint: reactRoot
});

render(
    <StylesProvider jss={jss}>
        <App />
    </StylesProvider>,
    mountPoint);
like image 151
RemedialBear Avatar answered Oct 17 '22 19:10

RemedialBear