mui / material-ui

Material UI: Comprehensive React component library that implements Google's Material Design. Free forever.
https://mui.com/material-ui/
MIT License
94.04k stars 32.31k forks source link

Support Shadow DOM #17473

Closed sravannerella closed 2 years ago

sravannerella commented 5 years ago

Issue:

When integrating modal or date picker or popover inside Shadow DOM, the click away listener is not working.

Expected Behavior:

On clicking outside, should close the modal, popover or the date picker

Current Behavior:

On clicking outside, the modal, popover or the date picker stays the same without closing.

A sample code: Code sandbox

I need this to be fixed as soon as possible for one of my projects. Please help.

sravannerella commented 5 years ago

Hello @oliviertassinari,

I found a workaround to fix this issue. Please let me know if you need code sandbox for a sample. https://stackoverflow.com/questions/57984666/reactjs-material-ui-clickawaylistener-is-not-working-properly-in-the-shadow

oliviertassinari commented 5 years ago

Thanks for sharing. We have currently no incentive in improving the support of shadow DOM. We have yet to see strong completing use cases that outweigh the cost. The current use cases we have heard about:

If we could use this issue to document how Shadow DOM can be done, that would be perfect.

I personally think that the current implementation of web components should have been abandoned and removed from the spec in 2015.

Jack-Works commented 5 years ago

Another workaroud. https://github.com/mui-org/material-ui/issues/16223#issuecomment-501998103

Jack-Works commented 5 years ago

This is partly an upstream bug of React. Fix is already drafted. https://github.com/facebook/react/pull/15894

sravannerella commented 5 years ago

@Jack-Works Thank you!

sravannerella commented 5 years ago

@oliviertassinari I noticed one more issue:

Unable to focus on input boxes

Steps to reproduce:

Jack-Works commented 5 years ago

Everyone have this issue please leave a comment for your usage on https://github.com/facebook/react/pull/15894 you can see Facebook doesn't have any action on that pr.

And does hack on https://github.com/mui-org/material-ui/issues/16223#issuecomment-501998103 solves your problem?

sravannerella commented 5 years ago

@Jack-Works Yes, it works but focusing on inputs is not functioning inside the shadow dom.

Jack-Works commented 5 years ago

@sravannerella yes we found that problem in our production. Our team found out why and here is the solution

https://github.com/DimensionDev/Maskbook/commit/0b0602f6661804a766915a4179d40b55397f3378

https://github.com/DimensionDev/Maskbook/commit/f18410b69cffaa9ea6f8b43ccbdd4393231bbbef

https://github.com/DimensionDev/Maskbook/commit/dc15ade31fec9f95b77f9b98a1d8ca4867ad6c28

oliviertassinari commented 4 years ago

I have added the waiting for users upvotes tag. I'm closing the issue as we are not sure enough people are looking for such support. So please upvote this issue (the issue main description, not my comment) if you are. We will prioritize our effort based on the number of upvotes.

bologer commented 4 years ago

Please consider adding support for shadow DOM

oliviertassinari commented 4 years ago

Do you guys have more details on why shadow DOM? It will help better understand the need. What's the objective of it? For instance, if it's isolation, how is it solving more problems than creating?

Jack-Works commented 4 years ago

Our project is a browser extension, we need to inject UI into other web pages so we choose ShadowDOM.

Currently, React, JSS and @material-ui don't support Shadow DOM well so we did a lot of hacks to make it work.

oliviertassinari commented 4 years ago

@Jack-Works Have you considered to increase the CSS specificity of the styles (can be done with a JSS pluggin, say to 10) over using shadow DOM? What's that limitations of iframe?

Jack-Works commented 4 years ago

@Jack-Works Have you considered to increase the CSS specificity of the styles (can be done with a JSS pluggin, say to 10) over using shadow DOM? What's that limitations of iframe?

It's not the problem of css priority. We need to hide the Dom we added to the webpage so the webpage cannot see any text we added to the page.

oliviertassinari commented 4 years ago

@Jack-Works Did you notice improvement opportunities from our side? I'm especially interested in the diff between the work that is required between having to React work in shadow DOM (this is outside of our scope) and the work required to make Material-UI work in shadow DOM. Basically, what can we do here to help the process? I have never looked at it in details, my distant perception on the matter is that React doesn't do any effort for the use case.

Jack-Works commented 4 years ago

Yes, IIRC in @material-ui there's some code assuming it is running in the main dom (maybe related to Modal) then it becomes buggy.

Anyway, the most important part is I want to let JSS support ShadowDOM but I think it's not the duty of @material-ui.

I also tried to contribute to React to improve the support for ShadowDOM but they don't accept it until now.

Jack-Works commented 4 years ago

We're using disableAutoFocus of Modal in our project, cause it's buggy in the ShadowDOM

oliviertassinari commented 4 years ago

disableAutoFocus might have been solved with https://github.com/mui-org/material-ui/pull/20694/files#diff-0e78f960bb89a643a4ed56411c35db66R71

Jack-Works commented 4 years ago

What about JSS? It would be nice to let JSS support inject style tags into ShadowRoot, even https://developers.google.com/web/updates/2019/02/constructable-stylesheets this modern feature!

oliviertassinari commented 4 years ago

You can inject the styles where ever you want with JSS (insertionPoint).

Jack-Works commented 4 years ago

Our code are multiple root (all of those root are in the ShadowRoot) React application so I have no idea if insertionPoint will work for us, but thanks I'll try it later.

oliviertassinari commented 4 years ago

cc @NMinhNguyen in case you have any insight on this one. I recall your team tried Shadow DOM at some point.

NMinhNguyen commented 4 years ago

Our code are multiple root (all of those root are in the ShadowRoot) React application so I have no idea if insertionPoint will work for us, but thanks I'll try it later.

You'd need an insertionPoint per shadow DOM root.

Jack-Works commented 4 years ago

Our code are multiple root (all of those root are in the ShadowRoot) React application so I have no idea if insertionPoint will work for us, but thanks I'll try it later.

You'd need an insertionPoint per shadow DOM root.

Hmm maybe I'm not clear. We have multiple root. Each root is in a separate ShadowDOM so it's required to have insertionPoint for every root.

NMinhNguyen commented 4 years ago

Our code are multiple root (all of those root are in the ShadowRoot) React application so I have no idea if insertionPoint will work for us, but thanks I'll try it later.

You'd need an insertionPoint per shadow DOM root.

Hmm maybe I'm not clear. We have multiple root. Each root is in a separate ShadowDOM so it's required to have insertionPoint for every root.

Sounds like we're saying the same thing? You're supposed to use a StylesProvider inside each root which would have its own insertion point https://material-ui.com/styles/advanced/#insertionpoint

bologer commented 4 years ago

@oliviertassinari

Do you guys have more details on why shadow DOM? It will help better understand the need. What's the objective of it? For instance, if it's isolation, how is it solving more problems than creating?

Sure. I love material ui and I build almost all of my projects with it. Recently, I was building an embeddable widget. So if I make it in shadow DOM, none of the styles would apply to MUI components inside of it. Without the shadow DOM, there can be a few conflicts with webpage styles.

oliviertassinari commented 4 years ago

Regarding the part about avoiding CSS conflicts when injecting in a hostile environment: #19455.

Jack-Works commented 4 years ago

Unluckily we got this: Warning: [JSS] Insertion point is not in the DOM. 

So we have to hack the ShadowRoot:

new Proxy(this.props.shadow as any, {
        get(target, property: keyof ShadowRoot) {
            if (property === 'parentNode') {
                const host = target.getRootNode({ composed: true })
                if (host !== document) {
                    return null
                }
                return target
            }
            return target[property]
        },
    })
Jack-Works commented 4 years ago

And without the hack I mentioned above, JSS won't render stylesheets

Jack-Works commented 4 years ago

Another problem: when we want to render Dialogs in another shadow root, it seems not possible to have a simple way to clone all the style tags we need into another Shadow Root.

rudfoss commented 3 years ago

I'd also love to see support for shadow DOM. Our specific use case is the need to embed an app in a legacy project where we have no control over styling. This legacy project has global styles on stuff like button which collides with MUI styling. We'd also like to isolate dialogs and popovers to the container element our app is given and not the entire window (kind of like a frame). Specifically we need to embed our app as a Web Part in an on-prem SharePoint environment.

It seems to me that a closed shadow DOM subtree is the solution that requires the least amount of work here, but I may be mistaken. We already have styles applied to the shadow tree using injection. If we could control where the portal appears that would basically solve all our remaining problems. For now we are stuck reimplementing dialogs and tooltips becaues they are not compatible.

Another use case for this might be micro-frontends though I have no experience with that personally.

Not sure how common this case is for others, but for me it has cropped up multiple times already.

Jack-Works commented 3 years ago

https://codesandbox.io/s/nostalgic-cloud-d2ny7?file=/src/Demo.tsx

All UI in the code sandbox above is rendered in the ShadowRoot. You can verify that by the devtools.

const picker = usePortalShadowRoot((container) => (
     <DatePicker
         DialogProps={{ container }}
         PopperProps={{ container }}
         value={new Date()}
         onChange={() => {}}
         renderInput={(props) => <TextField {...props} />}
     />
))

Note: Our application is licensed in AGPLv3, you may not be able to reuse this code otherwise the license will spread into your project. This solution is quite hacky and might have a potential performance problem.

hata6502 commented 3 years ago

I resolved this problem currently by using insertionPoint.

constructor() {
  super();

  const shadow = this.attachShadow({ mode: "open" });

  shadow.innerHTML = `
    <noscript id="jss-insertion-point"></noscript>
    <div id="app"></div>
  `;

  const appElement = shadow.querySelector("#app");
  const jssInsertionPointElement = shadow.querySelector("#jss-insertion-point");

  const createdJSS = jss.create({
    ...jssPreset(),
    insertionPoint: jssInsertionPointElement,
  });

  ReactDOM.render(
    <StylesProvider jss={createdJSS}>
      <KanvasThemeProvider>
        <CssBaseline />
        <Button variant="contained" color="primary">
          Primary
        </Button>
      </KanvasThemeProvider>
    </StylesProvider>,
    appElement
  );
}

Before Image from Gyazo

After Image from Gyazo

hata6502 commented 3 years ago

To use <Dialog /> component, it needs container property. https://material-ui.com/api/modal/#props

constructor() {
  super();

  const shadow = this.attachShadow({ mode: "open" });

  shadow.innerHTML = `
    <noscript id="jss-insertion-point"></noscript>

    <div>
      <div id="container">
        <div id="app"></div>
      </div>
    </div>
  `;

  const appElement = shadow.querySelector("#app");
  const containerElement = shadow.querySelector("#container");

  const jssInsertionPointElement = shadow.querySelector(
    "#jss-insertion-point"
  );

  if (!(jssInsertionPointElement instanceof HTMLElement)) {
    throw new Error("Could not find JSS insertion point.");
  }

  const createdJSS = jss.create({
    ...jssPreset(),
    insertionPoint: jssInsertionPointElement,
  });

  ReactDOM.render(
    <StylesProvider jss={createdJSS}>
      <KanvasThemeProvider>
        <CssBaseline />

        <Dialog container={containerElement} open={true}>
          <DialogTitle>Dialog title</DialogTitle>

          <DialogContent>
            <DialogContentText>
              Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
              eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
              enim ad minim veniam, quis nostrud exercitation ullamco laboris
              nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
              in reprehenderit in voluptate velit esse cillum dolore eu fugiat
              nulla pariatur. Excepteur sint occaecat cupidatat non proident,
              sunt in culpa qui officia deserunt mollit anim id est laborum.
            </DialogContentText>
          </DialogContent>

          <DialogActions>
            <Button variant="contained" color="primary">
              Primary
            </Button>
          </DialogActions>
        </Dialog>
      </KanvasThemeProvider>
    </StylesProvider>,
    appElement
  );
}

Image from Gyazo

leopardy commented 3 years ago

Does anyone know why the Select drop down breaks (loses styling-see screenshot, shows as bullet points- and some click abilities)? By the way, I have the exact same problem in v5 @mui/material Select also. https://codesandbox.io/s/small-cherry-3dr3s?file=/src/Demo.tsx

image

https://codesandbox.io/s/nostalgic-cloud-d2ny7?file=/src/Demo.tsx

All UI in the code sandbox above is rendered in the ShadowRoot. You can verify that by the devtools.

  • It can handle all emotion-based components (all MUI components in v5, and styled API).
  • It can also handle all JSS based components (all MUI components in v4 and makeStyles API).
  • It contains a usePortalShadowRoot hooks to handle Modals (which needs to render across different Shadow Roots).
const picker = usePortalShadowRoot((container) => (
     <DatePicker
         DialogProps={{ container }}
         PopperProps={{ container }}
         value={new Date()}
         onChange={() => {}}
         renderInput={(props) => <TextField {...props} />}
     />
))

Note: Our application is licensed in AGPLv3, you may not be able to reuse this code otherwise the license will spread into your project. This solution is quite hacky and might have a potential performance problem.

leopardy commented 3 years ago

Got the answer through stackoverflow. Apparently, the fix is to just do this. <Select MenuProps={{ disablePortal: true }}>

I don't have portal directly imported into my app so I'm scratching my head. https://mui.com/api/portal/#props

jacobweber commented 2 years ago

@leopardy: Use <Select MenuProps={{ disablePortal: true }}> or <Select MenuProps={{ container: yourContainerEl }}>

ris314 commented 2 years ago

@cherniavskii provided extremely helpful content here

This works great:

// fragment taken from @cherniavskii's code snippet
const theme = createTheme({
  components: {
    MuiMenu: {
      defaultProps: {
        container: shadowRootElement
      }
    },
    MuiModal: {
      defaultProps: {
        container: shadowRootElement
      }
    }
  }

One inconvenience is that if several MUI components, which rely on portal functionality, are rendered within the Shadow DOM, then you'd have to hunt them all down and supply default containers for all of them explicitly - seems like a lot of error prone work.

Perhaps there could be a way to supply the default container for any portal rendering, in this case it would be shadowRootElement vs the default document.body? Maybe this could be exposed as portalRootElement prop of ThemeProvider?

ris314 commented 2 years ago

@cherniavskii, if I wanted to override the default container for MuiTooltip and MuiAutocomplete poppers, how would I go about doing that?

cherniavskii commented 2 years ago

I've found a better solution to address this - see https://github.com/mui/mui-x/issues/5030#issuecomment-1145815537

I think this can be addressed by the section in docs. I'm not 100% sure this is complete solution, but it looks like it addresses most of cases, and we can always update the docs if there's something to add here.

jacobweberbowery commented 2 years ago

One thing that it doesn't address is the overflow: hidden that's applied when something like a menu is visible. With the suggested approach, it will lock the given shadow root element, when you may want it to lock the actual document.body, if that's the part that actually scrolls.

cherniavskii commented 2 years ago

@jacobweberbowery Right, I can reproduce it with this demo: https://codesandbox.io/s/shadow-dom-forked-t3d7cl?file=/demo.tsx

I think we can use document.body as scroll container if container is inside of the shadow dom. I haven't tested it, but something like this should do the job:

index d1993e039ec..afefc953844 100644
--- a/packages/mui-base/src/ModalUnstyled/ModalManager.ts
+++ b/packages/mui-base/src/ModalUnstyled/ModalManager.ts
@@ -122,14 +122,21 @@ function handleContainer(containerInfo: Container, props: ManagedModalProps) {
       });
     }

-    // Improve Gatsby support
-    // https://css-tricks.com/snippets/css/force-vertical-scrollbar/
-    const parent = container.parentElement;
-    const containerWindow = ownerWindow(container);
-    const scrollContainer =
-      parent?.nodeName === 'HTML' && containerWindow.getComputedStyle(parent).overflowY === 'scroll'
-        ? parent
-        : container;
+    let scrollContainer: HTMLElement;
+
+    if (container.parentNode instanceof DocumentFragment) {
+      scrollContainer = ownerDocument(container).body;
+    } else {
+      // Improve Gatsby support
+      // https://css-tricks.com/snippets/css/force-vertical-scrollbar/
+      const parent = container.parentElement;
+      const containerWindow = ownerWindow(container);
+      scrollContainer =
+        parent?.nodeName === 'HTML' &&
+        containerWindow.getComputedStyle(parent).overflowY === 'scroll'
+          ? parent
+          : container;
+    }

     // Block the scroll even if no scrollbar is visible to account for mobile keyboard
     // screensize shrink.

@jacobweberbowery Would you like to submit a pull request?

jacobweberbowery commented 2 years ago

@cherniavskii Thanks for the quick response! Here's a PR based on your solution. It seems to work in my local testing.