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

Support Shadow DOM #17473

Closed
sravannerella opened this issue Sep 17, 2019 · 44 comments · Fixed by #33007
Closed

Support Shadow DOM #17473

sravannerella opened this issue Sep 17, 2019 · 44 comments · Fixed by #33007
Labels
docs Improvements or additions to the documentation new feature New feature or request waiting for 👍 Waiting for upvotes

Comments

@sravannerella
Copy link

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
Copy link
Author

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
Copy link
Member

oliviertassinari commented Sep 20, 2019

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:

  • Improve security: prefer iframe
  • CSS encapsulation/micro-frontend: hash/prefix your classes. emotion, styled-components, JSS, they all solve this problem natively, it's simpler.
  • Browser extension: maybe, unclear if it has a real benefit.
  • else?

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
Copy link
Contributor

Another workaroud. #16223 (comment)

@Jack-Works
Copy link
Contributor

This is partly an upstream bug of React. Fix is already drafted. facebook/react#15894

@sravannerella
Copy link
Author

@Jack-Works Thank you!

@sravannerella
Copy link
Author

@oliviertassinari I noticed one more issue:

Unable to focus on input boxes

Steps to reproduce:

  • Create an Input inside a Modal
  • Put that modal inside a shadow DOM
  • Now every time you click on the text field inside the modal, it closes the modal and never focuses.

@Jack-Works
Copy link
Contributor

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

And does hack on #16223 (comment) solves your problem?

@sravannerella
Copy link
Author

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

@Jack-Works
Copy link
Contributor

Jack-Works commented Nov 5, 2019

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

DimensionDev/Maskbook@0b0602f

DimensionDev/Maskbook@f18410b

DimensionDev/Maskbook@dc15ade

@oliviertassinari

This comment has been minimized.

@bologer
Copy link

bologer commented Jul 20, 2020

Please consider adding support for shadow DOM

@oliviertassinari
Copy link
Member

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
Copy link
Contributor

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
Copy link
Member

oliviertassinari commented Jul 21, 2020

@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
Copy link
Contributor

Jack-Works commented Jul 21, 2020

@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
Copy link
Member

@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
Copy link
Contributor

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
Copy link
Contributor

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

@oliviertassinari
Copy link
Member

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

@Jack-Works
Copy link
Contributor

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
Copy link
Member

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

@Jack-Works
Copy link
Contributor

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
Copy link
Member

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

@NMinhNguyen
Copy link
Contributor

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.

@rudfoss
Copy link

rudfoss commented Jun 28, 2021

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
Copy link
Contributor

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.

@hata6502
Copy link

hata6502 commented Sep 5, 2021

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
Copy link

hata6502 commented Sep 5, 2021

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
Copy link

leopardy commented Nov 3, 2021

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
Copy link

leopardy commented Nov 3, 2021

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
Copy link

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

@ris314
Copy link

ris314 commented Jun 2, 2022

@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
Copy link

ris314 commented Jun 2, 2022

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

@cherniavskii
Copy link
Member

I've found a better solution to address this - see mui/mui-x#5030 (comment)

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.

@cherniavskii cherniavskii added the docs Improvements or additions to the documentation label Jun 3, 2022
@jacobweberbowery
Copy link
Contributor

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
Copy link
Member

@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
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Improvements or additions to the documentation new feature New feature or request waiting for 👍 Waiting for upvotes
Projects
None yet
Development

Successfully merging a pull request may close this issue.