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

Issues with DataGrid shadowDOM #3319

Closed
2 tasks done
jhswedeveloper opened this issue Dec 2, 2021 · 17 comments
Closed
2 tasks done

Issues with DataGrid shadowDOM #3319

jhswedeveloper opened this issue Dec 2, 2021 · 17 comments
Labels
component: data grid This is the name of the generic UI component, not the React module! feature: Rendering layout Related to the data grid Rendering engine

Comments

@jhswedeveloper
Copy link

Duplicates

  • I have searched the existing issues

Latest version

  • I have tested the latest version

Current behavior 😯

image

Expected behavior 🤔

should render menu buttons correctly

Steps to reproduce 🕹

import './reset.less';
import './styles.less';
import React from 'react';
import ReactDOM from 'react-dom';
import retargetEvents from 'react-shadow-dom-retarget-events';
import App from './src/app/App';
import {create} from 'jss';
// import {StylesProvider, jssPreset} from "@mui/material";
import {StylesProvider, jssPreset, createGenerateClassName} from '@mui/styles';
// import {StyledEngineProvider} from '@mui/material/styles';
import {CssBaseline} from "@mui/material";
// import {createGenerateClassName, StylesProvider, jssPreset} from '@material-ui/styles';
import createCache from '@emotion/cache';
import {CacheProvider} from '@emotion/react';
import {ThemeProvider, createTheme} from '@mui/material/styles';

class BroadbandComponent extends HTMLElement {

  // An instance of the element is created or upgraded. Useful for initializing state,
  // setting up event listeners, or creating a shadow dom.
  // See the spec for restrictions on what you can do in the constructor.
  constructor(props) {
    super();
    this.props = props;
    // Create a ShadowDOM
    this.attachShadow({mode: 'open'});
    this.isMounted = false;
  }

  propsChangedCallback(props) {
    this.props = props;
    this.render();
  }

  // Called every time the element is inserted into the DOM.
  // Useful for running setup code, such as fetching resources or rendering.
  // Generally, you should try to delay work until this time.
  connectedCallback() {
    this.mount();
    this.render();
  }

  // Called every time the element is removed from the DOM.
  // Useful for running clean up code.
  disconnectedCallback() {
    ReactDOM.unmountComponentAtNode(this.mountPoint);
    this.shadowRoot.removeChild(this.mountPoint);
    this.renderTo = null;
    this.removeListeners();
    this.isMounted = false;
  }

  mount() {
    if (!this.isMounted) {
      // this.id = "shadow-home";
      // Inject stylesheet
      const styles = document.createElement("link");
      styles.href = `/alo/proxy/one-ui-broadband/broadband-component-styles.css`;
      styles.rel = "stylesheet";
      this.shadowRoot.appendChild(styles);

      // Mount point
      this.mountPoint = document.createElement('div');
      // this.mountPoint.style.height = '100%';

      // This will be entry point to emotion (material ui 5 styling solution) to insert scoped shadow DOM styles.
      this.emotionRoot = document.createElement('style');
      this.shadowRoot.appendChild(this.emotionRoot);
      this.reactRoot = this.shadowRoot.appendChild(this.mountPoint);

      this.jss = create({
        ...jssPreset(),
        insertionPoint: this.reactRoot
      });

      this.cache = createCache({
        key: 'css',
        prepend: true,
        container: this.emotionRoot,
      });

      this.removeListeners = retargetEvents(this.shadowRoot);
      this.isMounted = true;

    }
  }

  render() {
    if (this.isMounted) {
      // this.id = "shadow-home";
      const generateClassName = createGenerateClassName({
        // By enabling this option, if you have non-MUI elements (e.g. `<div />`)
        // using MUI classes (e.g. `.MuiButton`) they will lose styles.
        // Make sure to convert them to use `styled()` or `<Box />` first.
        // disableGlobal: true,
        // Class names will receive this seed to avoid name collisions.
        seed: 'mui-jss',
      });

      const portalContainer = this.shadowRoot.appendChild(document.createElement("div"));

      const theme = createTheme({
        components: {
          MuiMenu: {
            defaultProps: {
              container: () => {
                return portalContainer;
              }
            }
          },
          MuiDataGrid: {
            defaultProps: {
              container: () => {
                return portalContainer;
              }
            }
          },
          MuiSelect: {
            defaultProps: {
              container: () => {
                return portalContainer;
              }
            }
          },
          MuiPopover: {
            defaultProps: {
              container: () => {
                return portalContainer;
              }
            }
          }
        }
      });

      ReactDOM.render(
        <StylesProvider jss={this.jss} generateClassName={generateClassName}>
          <CacheProvider value={this.cache}>
            <ThemeProvider theme={theme}>
              <CssBaseline/>
              <App {...this.props} />
            </ThemeProvider>
          </CacheProvider>
        </StylesProvider>,
        this.mountPoint
      );
    }

  }
}

export default BroadbandComponent;

Context 🔦

Hello, I am struggling seems like styling for the menu buttons doesn't work.

Could anyone point me into the right direction?

Your environment 🌎

`npx @mui/envinfo`
  Don't forget to mention which browser you used.
  Output from `npx @mui/envinfo` goes here.
@mnajdova mnajdova transferred this issue from mui/material-ui Dec 2, 2021
@jhswedeveloper
Copy link
Author

My suspicion is that this doesn't seem to kick in

          MuiDataGrid: {
            defaultProps: {
              container: () => {
                return portalContainer;
              }
            }
          },

since it works for other component that mounts on document.body for example Dialog

@hafley66
Copy link

hafley66 commented Dec 3, 2021

@junhuhdev I am integrating a new application within a legacy application, and have an internal library extending/wrapping mui theme. I have actually just solved this by using the following config, it will get you 99% of the way there:

const forShadowDomPortal = {
    // disablePortal: true,
    container: insert,
  };
  const defaults = React.useMemo(
    () =>
      insert
        ? ({
            components: {
              MuiPopover: {
                defaultProps: forShadowDomPortal,
              },
              MuiPopper: {
                defaultProps: forShadowDomPortal,
              },
              MuiPortal: {
                defaultProps: forShadowDomPortal,
              },
              MuiModal: {
                defaultProps: forShadowDomPortal,
              },
              MuiDialog: {
                defaultProps: forShadowDomPortal,
              },
              MuiTooltip: {
                defaultProps: {
                  PopperProps: forShadowDomPortal,
                },
              },
              // @ts-ignore
              MuiDataGrid: {
                defaultProps: {
                  componentsProps: {
                    columnMenu: forShadowDomPortal,
                    // This does not exist...yet
                    menu: forShadowDomPortal,
                    // 
                    columnsPanel: forShadowDomPortal,
                    filterPanel: forShadowDomPortal,
                    panel: forShadowDomPortal,
                    preferencesPanel: forShadowDomPortal,
                  },
                },
              },
            },
          })
        : {},
    [insert],
  );

Unfortunately, I have dug into the code, and there is no way to pass props to the GridMenu component, which is responsible for rendering the Tooltip in the column menu component.

@oliviertassinari I have been keeping watch every day on mui and mui-x, and am migrating my company's current 5 year frontend plan to this stack. All is good, and I am very aware of where you stand on shadow-dom support (and all the issues raised), but would it be possible to raise issue to add the GridMenu to the componentsProps as Menu? All other Portaled elements are alterable by this prop injection, so I feel it would be appropriate to add.

My company is currently not releasing column menu support yet but they will need it soon, so I may even learn how to add this myself to the repo and contribute, but I cannot remove shadow dom.

Portal was moved to mui/base, so I am going to raise separate issue on what is plan if at all for theming/defaultProps'ing those components from there

@m4theushw m4theushw added the status: waiting for maintainer These issues haven't been looked at yet by a maintainer label Dec 3, 2021
@jhswedeveloper
Copy link
Author

@hafley66 Thank you for ur help..

Do u mean like this? because it didnt work

import './reset.less';
import './styles.less';
import React from 'react';
import ReactDOM from 'react-dom';
import retargetEvents from 'react-shadow-dom-retarget-events';
import App from './src/app/App';
import {create} from 'jss';
import {StylesProvider, jssPreset, createGenerateClassName} from '@mui/styles';
import {CssBaseline} from "@mui/material";
import createCache from '@emotion/cache';
import {CacheProvider} from '@emotion/react';
import {ThemeProvider, createTheme, StyledEngineProvider} from '@mui/material/styles';
import "@mui/x-data-grid";
import {DataGridProps} from "@mui/x-data-grid";

class BroadbandComponent extends HTMLElement {

  // An instance of the element is created or upgraded. Useful for initializing state,
  // setting up event listeners, or creating a shadow dom.
  // See the spec for restrictions on what you can do in the constructor.
  constructor(props) {
    super();
    this.props = props;
    // Create a ShadowDOM
    this.attachShadow({mode: 'open'});
    this.isMounted = false;
  }

  propsChangedCallback(props) {
    this.props = props;
    this.render();
  }

  // Called every time the element is inserted into the DOM.
  // Useful for running setup code, such as fetching resources or rendering.
  // Generally, you should try to delay work until this time.
  connectedCallback() {
    this.mount();
    this.render();
  }

  // Called every time the element is removed from the DOM.
  // Useful for running clean up code.
  disconnectedCallback() {
    ReactDOM.unmountComponentAtNode(this.mountPoint);
    this.shadowRoot.removeChild(this.mountPoint);
    this.renderTo = null;
    this.removeListeners();
    this.isMounted = false;
  }

  mount() {
    if (!this.isMounted) {
      // this.id = "shadow-home";
      // Inject stylesheet
      const styles = document.createElement("link");
      styles.href = `/alo/proxy/one-ui-broadband/broadband-component-styles.css`;
      styles.rel = "stylesheet";
      this.shadowRoot.appendChild(styles);

      // Mount point
      this.mountPoint = document.createElement('div');
      this.mountPoint.style.height = '100%';

      // This will be entry point to emotion (material ui 5 styling solution) to insert scoped shadow DOM styles.
      this.emotionRoot = document.createElement('style');
      this.shadowRoot.appendChild(this.emotionRoot);
      this.reactRoot = this.shadowRoot.appendChild(this.mountPoint);

      this.jss = create({
        ...jssPreset(),
        insertionPoint: this.reactRoot
      });

      this.cache = createCache({
        key: 'css',
        prepend: true,
        container: this.emotionRoot,
      });

      this.removeListeners = retargetEvents(this.shadowRoot);
      this.isMounted = true;

    }
  }

  render() {
    if (this.isMounted) {
      // this.id = "shadow-home";
      const generateClassName = createGenerateClassName({
        // By enabling this option, if you have non-MUI elements (e.g. `<div />`)
        // using MUI classes (e.g. `.MuiButton`) they will lose styles.
        // Make sure to convert them to use `styled()` or `<Box />` first.
        // disableGlobal: true,
        // Class names will receive this seed to avoid name collisions.
        seed: 'mui-jss',
      });

      const portalContainer = this.shadowRoot.appendChild(document.createElement("div"));

      const theme = createTheme({
        components: {
          MuiContainer: {
            defaultProps: {
              container: () => {
                return portalContainer;
              }
            }
          },
          MuiTooltip: {
            defaultProps: {
              container: () => {
                return portalContainer;
              }
            }
          },
          MuiDialog: {
            defaultProps: {
              container: () => {
                return portalContainer;
              }
            }
          },
          MuiModal: {
            defaultProps: {
              container: () => {
                return portalContainer;
              }
            }
          },
          MuiMenu: {
            defaultProps: {
              container: () => {
                return portalContainer;
              }
            }
          },
          MuiMenuItem: {
            defaultProps: {
              container: () => {
                return portalContainer;
              }
            }
          },
          MuiToolbar: {
            defaultProps: {
              container: () => {
                return portalContainer;
              }
            }
          },
          MuiDataGrid: {
            defaultProps: {
              componentsProps: {
                columnMenu: portalContainer,
                // This does not exist...yet
                menu: portalContainer,
                //
                columnsPanel: portalContainer,
                filterPanel: portalContainer,
                panel: portalContainer,
                preferencesPanel: portalContainer,
              },
            },
          },
          MuiSelect: {
            defaultProps: {
              container: () => {
                return portalContainer;
              }
            }
          },
          MuiPopover: {
            defaultProps: {
              container: () => {
                return portalContainer;
              }
            }
          }
        }
      });

      ReactDOM.render(
        <StyledEngineProvider injectFirst>
          <StylesProvider jss={this.jss}>
            <CacheProvider value={this.cache}>
              <ThemeProvider theme={theme}>
                <CssBaseline/>
                <App {...this.props} />
              </ThemeProvider>
            </CacheProvider>
          </StylesProvider>
        </StyledEngineProvider>,
        this.mountPoint
      );
    }

  }
}

export default BroadbandComponent;

@m4theushw
Copy link
Member

Could anybody provide a reproduction? This codesandbox.io template may be a good starting point. We can't see the DataGrid component in #3319 (comment) since it's in another file.


@hafley66 did you try wrapping the component in a custom theme with a default container prop value for all Portal elements? Currently there's no way to customize the tooltip, however, we may need to add a new slot for it to allow to use another tooltip component instead of the default one provided by MUI Core.

@hafley66
Copy link

hafley66 commented Dec 3, 2021

you can customize tooltip -> popper -> portal, its super roundabout:

check here and search for PopperProps: https://mui.com/api/tooltip/#props

You can defaultProps this, so change MuiTooltip to the following.

I do not know why you column selector is not rendering correctly, I will try to repro in codesandbox as i also want to figure this out/contribute solution as I will be needing it soon.

@junhuhdev You are using both jss and emotion, I dont know what effect this has but I do not use jss FYI.
We also dont see you're invocation of data grid, so uncertain if you are using componentSlots directly.

I created a wrapper for my company, that uses merge from lodash with its passed props that come out of useTheme.
Otherwise they will get overridden i believe or not present.

@m4theushw You can workaround tooltip with this:

const themeSlice = {
  components: {
    MuiTooltip: {
      defaultProps: {
        PopperProps: { // do not forget to nest it here, Tooltip does not have portal props because its composed with Popper
          container: element
        },
      },
    },
    MuiPortal: {
      defaultProps: {
        // this would solve every shadow dom issue outside of the emotion setup, which is already solved by react-shadow-dom library
        // this would remove need for all the overrides we are discussing here, is it possible to wrap the re-export of mui/material/Portal to plug into the theme?
        container: element 
      }
    }
  }
}

@hafley66
Copy link

hafley66 commented Dec 3, 2021

@m4theushw @junhuhdev
Here is repro, has type errors left and right, but it demonstrates current issues.
The column menu has no way of overriding its tooltip component for some reason, but the Filter panel and Column select panel are able to attach to shadow dom with theme overrides.

Again, i removed jss from equation, but as of right now, there is no way to override GridMenu component's tooltip or portal, or maybe there is and I haven't dug deep enough. I also removed some entries you had, typescript was throwing on them, but maybe they do exist idk. Also you override the grid props wrong, its slot: { container: portalContainer}, not slot: portalContainer

https://codesandbox.io/s/https-github-com-mui-org-material-ui-x-issues-3319-frydd

@hafley66
Copy link

hafley66 commented Dec 3, 2021

The demo will also break with hot reload, gotta manual reload right screen, HMR for react doens't work for web components

@jhswedeveloper
Copy link
Author

Amazing thank you @hafley66

But this is the tooltip that is still broken that u are referring to right?

image

@hafley66
Copy link

hafley66 commented Dec 3, 2021

Amazing thank you @hafley66

But this is the tooltip that is still broken that u are referring to right?

image

Correct, this is powered by the GridMenu component, which is not slotted/over rideable (sorry on mobile)

@jhswedeveloper
Copy link
Author

@hafley66 Ok thank you for solving my other issues... I've been searching the whole web past 48 hours with no success.

Have a great evening, and I hope the support for GridMenu will come soon.

@hafley66
Copy link

hafley66 commented Dec 3, 2021

@junhuhdev no problem, glad to help.

@m4theushw I was gonna raise an issue near same to this one, but to discuss the column menu getting added to slots. Would it be preferred to keep this issue open or start new one?

@DanailH
Copy link
Member

DanailH commented Dec 7, 2021

@hafley66 Can you create a separate issue for the column menu. It will help us track the effort more easily. Also as this one seems to be answered we can close it.

@hafley66
Copy link

hafley66 commented Dec 7, 2021

@DanailH no problem, will do soon, thanks for advising next steps

@jhswedeveloper
Copy link
Author

@hafley66 have you had time to create new issue? If not I can do it.

@DanailH DanailH closed this as completed Dec 8, 2021
@DanailH
Copy link
Member

DanailH commented Dec 8, 2021

I'm closing this one as its answer. We can continue the discussion on the new one once it's created.

@hafley66
Copy link

hafley66 commented Dec 8, 2021

@junhuhdev on it, will link to this issue as well

@hafley66
Copy link

hafley66 commented Dec 8, 2021

Please see

  1. [DataGrid][ShadowDOM] Add GridMenu to GridSlotsComponent to override Popper.container -> Portal.container #3381
  2. [Portal] Add ThemeOptions.components.MuiPortal.defaultProps material-ui#30116

1 is a direct result of this card, but I dont know what else is using Portal that we will have to override. In terms of sheer code size, 1 will cost the most I think.

2 is the most comprehensive solution, allowing users to default the container prop, so we dont have to figure out how to override this prop on each of its dependent components. It also has least amount of code to add in theory, and should 100% cover all shadow dom use cases for portal since the library has made such good effort at keeping portal usage by same abstraction everywhere

@joserodolfofreitas joserodolfofreitas added component: data grid This is the name of the generic UI component, not the React module! feature: Rendering layout Related to the data grid Rendering engine and removed status: waiting for maintainer These issues haven't been looked at yet by a maintainer labels Feb 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: data grid This is the name of the generic UI component, not the React module! feature: Rendering layout Related to the data grid Rendering engine
Projects
None yet
Development

No branches or pull requests

5 participants