mui / material-ui

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

[Drawer] - How to Initialize Drawer Inside Container Element #11749

Closed umar-khan closed 6 years ago

umar-khan commented 6 years ago

I'm trying to use a "temporary" right Drawer component, and I want the drawer and it's backdrop to be contained within the parent div.

I can't find any working examples of this, and I'm not sure how to target the Backdrop component CSS from my Drawer component.

Expected Behavior

When specifying a container prop for my Drawer, I expect that the drawer, and it's backdrop would be enclosed inside that container. Since this is not the case, it's unclear how to override the desired styles.

It should function similar to this sidebar example

Current Behavior

The Drawer, Modal, and Backdrop components use fixed for their positioning, resulting in them always displaying at the top-right of the viewport.

Context

I created a CodeSandbox with an example scenario.

If you click on the "Open Drawer" button, <DrawerRight> opens to the right of the viewport as expected. It overlays all other elements, including the <Navbar>.

The goal is to have the drawer open, but only overlay elements in it's containing element.

I attempted to override the styling with classes as shown in the docs

Here is an updated version of the above CodeSandbox with my changes. The main differences are:

  1. Pass reference to <MainContent> as container prop to <Drawer>
  2. Use withStyles to add styles based on <Drawer> CSS API

Note that the drawer now seemed to be sized correctly, but the backdrop is still covering the entire viewport. Also, the animation has become jittery, with the other elements being shifted while the drawer is opening.

Your Environment

Tech Version
Material-UI v1.2.0
React v16.3.2
browser Firefox 60.0.1
adeelibr commented 6 years ago

Does this help?

import React from "react";

const styles = {
  position: "relative",
  zIndex: 1301,
  backgroundColor: "red"
};

const Navbar = () => (
  <div style={styles}>Navbar: this should not be covered</div>
);

export default Navbar;

The default zIndex provided to Drawer component is 1200, you can over ride it like that. :point_up:

umar-khan commented 6 years ago

@adeelibr Thanks for the suggestion!

That should work for the simplified example I provided, but in my actual use-case, there are several more components (eg. a sidebar) that I don't want to be covered.

I don't think it's a maintainable solution to hard-code component z-index values whenever I want a contained drawer.

I was hoping there was a way I can use the CSS API or classes to pass down styles Drawer -> Modal -> Backdrop

adeelibr commented 6 years ago

Can you provide the use case example via a code pen & i'll look into it.

umar-khan commented 6 years ago

Here's a sandbox which more accurately represents my use-case: CodeSandbox

I want the drawer and backdrop to only overlay the orange section. Preferably without having to hard-code z-index values for the other elements.

adeelibr commented 6 years ago

I don't see any other way other then provide a zIndex with a relative position. https://codesandbox.io/s/9jw7y3q00p

umar-khan commented 6 years ago

Hmm not the solution I was hoping for, but I don't see a straight-forward alternative either.

Thanks!

rooch84 commented 6 years ago

I have the same requirement. Can we reopen this as a feature request?

rooch84 commented 6 years ago

For anyone who stumbles on this page looking for an answer. I managed to do it by setting the modal container, then specifying the css positioning on the modal root, backdrop and docked drawer to absolute:

<Drawer 
            variant="temporary"
            open={this.props.showDrawer}
            anchor="left"
            onClose={this.props.toggleDrawer}
            ModalProps={this.ref.current ? {container: this.ref.current} : {}}
            classes={{
              paperAnchorLeft: "class1",
              modal: "class2"
            }}
            BackdropProps={{
              className:  "class3"
            }}
          >
LeroyDoornebal commented 5 years ago

For anyone that still wonders about the entire solution, the following worked for me.

Please note that I used document.getElementById here and that requires that you set the ID property to that value if you want the drawer to be contained in that element. It should also be possible to use a ref in this case, and that is preferred in my opinion.

Step 1: Set an ID attribute for the element that you want to contain the drawer elements

<div id="drawer-container" style="position: relative">
  <span>Some elements</span>
</div>

Make sure that you add position: relative to this element.

Step 2: Set correct styling and reference to container element on Drawer element.

<Drawer
  open={true}
  onClose={() => {}}
  PaperProps={{ style: { position: 'absolute' } }}
  BackdropProps={{ style: { position: 'absolute' } }}
  ModalProps={{
    container: document.getElementById('drawer-container'),
    style: { position: 'absolute' }
  }}
  variant="temporary"
>
  <span>Some elements</span>
</Drawer>

The things to take away from the example above are the PaperProps, BackdropProps and the ModalProps props. For them to be contained within the container element that we created in step one, these elements have to be absolutely positioned.

Also note the document.getElementById to get a reference to the container element.

DorineLam commented 5 years ago

Hi ! I tried the example above, but there is this error : "document is not defined". Do you have any information about this. How should I define it ? Thanks in advance

sclavijo93 commented 5 years ago

I have the same requirement. Can we reopen this as a feature request?

neomib commented 5 years ago

Hi thanks, it worked! But when I try to set the anchor to "right" (I need to open the drawer from the right side) it opens weirdly (shaking). Any solution for that?

kelseyleftwich commented 4 years ago

Hi thanks, it worked! But when I try to set the anchor to "right" (I need to open the drawer from the right side) it opens weirdly (shaking). Any solution for that?

variant="persistent" on Drawer worked for me.

blshort43 commented 4 years ago

variant="persistent" on Drawer worked for me.

Me too! Thanks @kelseyleftwich !!!

sandeepgahlawat commented 4 years ago

@LeroyDoornebal
thank you so much it worked

sandeepgahlawat commented 4 years ago

@LeroyDoornebal Although it's working fine for the most part. The only problem is with slide exit animation, the drawer slides out of the container it is contained in and goes off-screen. Whereas the expected behavior should be that the drawer should end slide exit animation inside the container it is in. Can you help me fix it?

mnemanja commented 4 years ago

@kelseyleftwich While this fixed the weird behavior, it also removed the backdrop which I need.

giovanniantonaccio commented 4 years ago

@sandeepgahlawat I'm also with the same issue as you. Did you find any solution for this?

sandeepgahlawat commented 4 years ago

@giovanniantonaccio yes, I did figure out a way to fix it, and below is what I did. In case you still have doubts feel free to ask.

      <Box
        width="100%"
        height="100%"
        id="drawer-container"
        position="relative"
        bgcolor="white"
        component="div"
        style={{ overflowY: "scroll", overflowX: "hidden" }}
       >
        <Drawer
          open={props.openDrawer}
          onClose={() => {}}
          elevation={5}
          PaperProps={{ style: { position: "absolute", width: "486px" } }}
          BackdropProps={{ style: { position: "absolute" } }}
          ModalProps={{
            container: document.getElementById("drawer-container"),
            style: { position: "absolute" },
          }}
          SlideProps={{
            onExiting: (node) => {
              node.style.webkitTransform = "scaleX(0)";
              node.style.transform = "scaleX(0)";
              node.style.transformOrigin = "top left ";
            },
          }}
        >
          <DrawerContent />
        </Drawer>
      </Box>
giovanniantonaccio commented 4 years ago

Thanks @sandeepgahlawat! Is solved the problem!

frankpape commented 4 years ago

The solution described by @LeroyDoornebal doesn't seem to work.

Here's a code sandbox. The drawer still covers the whole page. Any advice about what I'm doing wrong?

Edit: figured it out. The drawer container needs to be visible and take up space (e.g., by setting a width and height). I've updated the code sandbox to demonstrate.

Edit 2: OK, that doesn't quite do it either. The drawer still covers the whole page upon loading the sandbox, but then moves into the correct position upon re-rendering, when the code is changed.

Edit 3: After some experimentation, it seems to work if the drawer variant is "permanent", and fail (but then fix itself after re-rendering) when "temporary".

sandeepgahlawat commented 4 years ago

The solution described by @LeroyDoornebal doesn't seem to work.

Here's a code sandbox. The drawer still covers the whole page. Any advice about what I'm doing wrong?

Edit: figured it out. The drawer container needs to be visible and take up space (e.g., by setting a width and height). I've updated the code sandbox to demonstrate.

Edit 2: OK, that doesn't quite do it either. The drawer still covers the whole page upon loading the sandbox, but then moves into the correct position upon re-rendering, when the code is changed.

Edit 3: After some experimentation, it seems to work if the drawer variant is "permanent", and fail (but then fix itself after re-rendering) when "temporary".

just remove variant prop and it will work perfectly

mnemanja commented 4 years ago

@sandeepgahlawat removing the prop will default to "temporary". https://material-ui.com/api/drawer/

mmartinsky commented 4 years ago

It also doesn't seem to work properly with an anchor='bottom'. I modified @frankpape 's example to use a button trigger with setState, and you can see the behavior isn't working as intended (100px blue box sliding up from the green box, and disappearing into the white on toggle off).

https://codesandbox.io/s/material-demo-7nor0?file=/demo.js

miaowang commented 4 years ago

Can someone help me with this problem: I used similar approach but with a element higher than the screen height.

https://codesandbox.io/s/material-demo-forked-x8wi2?file=/demo.js

Works fine in Firefox. But in Chrome and Edge the browser scrolls to the top of the drawer. How can I prevent the scrolling?

edylucut155 commented 4 years ago

@mmartinsky I have a similar problem to yours. Did you manage to fix it?

JakeH91 commented 3 years ago

Hi ! I tried the example above, but there is this error : "document is not defined". Do you have any information about this. How should I define it ? Thanks in advance

@DorineLam I was getting the same error, so I scrapped that idea and instead did this:

<Drawer
  ModalProps={{
    style: { 
          position: 'relative',
          top: 'unset',
          left: 'unset',
          right: 'unset',
     }
  }}
  variant="permanent"
>
  <span>Some elements</span>
</Drawer>

The Drawer desperately wants to cling to one of the outer edges of the screen, so unsetting all the absolute positioning fixed that problem for me. Now I can put this drawer inside any element I want within my app.

LeroyDoornebal commented 3 years ago

Oh Irony, I never got notified for updates on this issue and stumbled into the problems with my implementation myself and found my way back to this thread. In the mean time a lot of updates have been done to material ui, so probably this problem has already been solved, but for people that are still stuck on older versions, I managed to implement it slightly different to get around the weird animations when you change the anchor.

  <Box style={{ overflow: 'hidden' }}>
    <Backdrop open={open} style={{ zIndex: 1199, position: 'absolute' }} />
    <MuiDrawer
      anchor={fromSide}
      open={open}
      onClose={onClose}
      PaperProps={{
        style: {
          width: width || '90%',
          position: 'absolute',
          maxWidth: maxWidth || 'initial',
          border: 'none'
        }
      }}
      ModalProps={{
        container: document.getElementById('some-id`), // Or ref ofcourse :)
        disableEnforceFocus: true,
        style: { position: 'absolute' }
      }}
      variant="persistent"
      {...props}
    >
      {children}
    </MuiDrawer>
  </Box>

The biggest issue I had with using the "permanent" variant is that it did not work out of the box with the backdrop, so I separated that by using the backdrop directly. Be aware that this implementation worked for my specific use case, maybe it gives you some ideas.

filipe-gomes commented 3 years ago

Hi thanks, it worked! But when I try to set the anchor to "right" (I need to open the drawer from the right side) it opens weirdly (shaking). Any solution for that?

@neomib did you ever figure out a way around this? This "shaking" effect is the only thing keeping this from working for me.

elyobo commented 3 years ago

I'd also be interested in an officially supported way to put drawers in other components (we're using them in cards); we've bashed something together along the lines of @LeroyDoornebal's solution, which avoids the shaky animations but still gives odd animation behaviour (looks more like a fade sometimes, can't get any to slide both in and out correctly).

filipe-gomes commented 3 years ago

@elyobo how did you avoid the shaky animations? Is your drawer temporary and coming from the right?

elyobo commented 3 years ago

No, using Leroy's approach above which uses a persistent drawer and a manually added backdrop. I have one coming from the left (not shaky, but seems to fade in then slide out on close) and one from the bottom (slides in but then seems to fade out on close).

Patchrik commented 3 years ago

@elyobo Do you mind sharing how you got the drawer to live inside the MUI Card. I can't seem to get the drawer to grab onto the card's id to use it as a container.

elyobo commented 3 years ago

Here's the gist of our CardDrawer which is used inside various Card components. Not everything will apply (e.g. theme overrides, assumption of an app bar at the top of the card) and I don't recall exactly what was done or why at this point, and (as noted) it's far from perfect either, animations are still a bit weird.

We also haven't upgraded to v5 yet, nor even looked at the upgrade path, so YMMV.

import { useRef } from 'react'
import PropTypes from 'prop-types'
import rgba from 'hex-to-rgba'

import Backdrop from '@material-ui/core/Backdrop'
import Drawer from '@material-ui/core/Drawer'
import { makeStyles, withStyles } from '@material-ui/core/styles'

const useStyles = makeStyles(theme => ({
  root: {
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    pointerEvents: 'none',
    position: 'absolute',
    overflow: 'hidden',
  },
  backdrop: {
    position: ['absolute', '!important'],
    backgroundColor: rgba(theme.palette.background.default, 0.67),
    pointerEvents: 'auto',
    zIndex: [theme.zIndex.appBar - 11, '!important'],
  },
  modal: {
    position: ['absolute', '!important'],
    zIndex: [theme.zIndex.appBar - 1, '!important'],
  },
}))

const bottomAnchorStyle = ({ fullHeight }) => (
  fullHeight
    ? { height: '100%' }
    // Note: maxHeight ensures a clickable backdrop to close the drawer
    : { borderTop: 'none', maxHeight: '90%' }
)

const StyledDrawer = withStyles(theme => ({
  paper: {
    zIndex: theme.zIndex.appBar - 10,
    position: 'absolute',
    pointerEvents: 'all',
  },
  paperAnchorLeft: ({ width }) => ({ width }),
  paperAnchorRight: ({ width }) => ({ width }),
  paperAnchorBottom: bottomAnchorStyle,
  paperAnchorDockedLeft: ({ width }) => ({ width }),
  paperAnchorDockedRight: ({ width }) => ({ width }),
  paperAnchorDockedBottom: bottomAnchorStyle,
}))(({ fullHeight, width, ...props }) => <Drawer {...props} />)

const CardDrawer = ({
  children,
  ModalProps,
  ...props
}) => {
  const rootRef = useRef(null)
  const classes = useStyles()
  const { onClose, open } = props

  return (
    <>
      <div
        ref={rootRef}
        className={classes.root}
      />
      <Backdrop
        onClick={onClose}
        open={open}
        className={classes.backdrop}
      />
      <StyledDrawer
        ModalProps={{
          ...ModalProps,
          className: classes.modal,
          container: () => rootRef.current,
          disableEnforceFocus: true,
          keepMounted: true,
        }}
        {...props}
        variant="persistent"
      >
        {children}
      </StyledDrawer>
    </>
  )
}

CardDrawer.propTypes = {
  children: PropTypes.node,
  fullHeight: PropTypes.bool,
  ModalProps: PropTypes.shape({}),
  onClose: PropTypes.func,
  open: PropTypes.bool.isRequired,
  width: PropTypes.oneOfType([
    PropTypes.number,
    PropTypes.string,
  ]),
}

CardDrawer.defaultProps = {
  children: undefined,
  fullHeight: false,
  ModalProps: {},
  onClose: undefined,
  width: 320,
}

export default CardDrawer
mdere-lightfeatherIO commented 3 years ago

@giovanniantonaccio yes, I did figure out a way to fix it, and below is what I did. In case you still have doubts feel free to ask.

      <Box
        width="100%"
        height="100%"
        id="drawer-container"
        position="relative"
        bgcolor="white"
        component="div"
        style={{ overflowY: "scroll", overflowX: "hidden" }}
       >
        <Drawer
          open={props.openDrawer}
          onClose={() => {}}
          elevation={5}
          PaperProps={{ style: { position: "absolute", width: "486px" } }}
          BackdropProps={{ style: { position: "absolute" } }}
          ModalProps={{
            container: document.getElementById("drawer-container"),
            style: { position: "absolute" },
          }}
          SlideProps={{
            onExiting: (node) => {
              node.style.webkitTransform = "scaleX(0)";
              node.style.transform = "scaleX(0)";
              node.style.transformOrigin = "top left ";
            },
          }}
        >
          <DrawerContent />
        </Drawer>
      </Box>

Have anyone gotten this solution to work on NextJS (SSR) with using document.

dberardo-com commented 3 years ago

i am trying to position a backdrop absolutely like in the examples above, just i am using Popovers as modal element.

The problem i have now is that popovers are not correctly positioned, because their position seems to be calculated from the visible area and not from the parent component coordinates. How can i fix this?

nichita-pasecinic commented 2 years ago

Is it possible though to have a swipeable (from bottom) drawer inside a fixed width and height container ? Basically the drawer should be triggered from bottom and it should not hover nor footer nor header and actionable only from page content.

Argyle-ClumsyLabs commented 2 years ago

After a lot of trial and error, finally got rid of the shaky animation (where the outer container itself seemed to be sliding up/down as well) by adding the line keepMounted: true to ModalProps. It brings completely smooth animation to the drawer. Thanks @elyobo !!

Final code:

<Drawer className="card-drawer"
            open={showDrawer}
            anchor={"bottom"}
            onClose={() => {
                closeDrawer()
            }}
            PaperProps={{ style: { position: 'absolute' } }}
            BackdropProps={{ style: { position: 'absolute' } }}
            ModalProps={{
                container: document.getElementById('container-drawer-is-within'),
                style: { position: 'absolute' },
                keepMounted: true,    // <=============== THIS
            }}
            variant="temporary"
        >
anish749 commented 1 year ago

For some weirdly odd reason, i was mounting one of the drawers on open, which wasn't affected by shaking animation, the other drawer which was written like normal code suffered the shakiness.

it feels like a hack, but here is what I did..

const [open, setOpen] = useState(...) // usual stuff.

// render
{open && ( // <---- this trick removed the shakiness
  <Drawer 
     open={open}
    ... the modal props with container ref and style as suggested
)}

Another AI suggestion that worked perfectly was to just disable Sliding on the Drawer using:

    SlideProps={{ timeout: { enter: 0, exit: 0 } }}
ekesel commented 11 months ago

Here's How You can implement in Typescript using React Hooks,

const containerRef = useRef<Element | (() => Element | null) | null | undefined>(); <Container className={contentStyle.container} ref={(node) => { containerRef.current = node; }} sx={{ '&.MuiContainer-root': { 'paddingLeft': 'unset', 'paddingRight': 'unset' } }}> `<Drawer anchor={'right'} open={_overlayContext?.overlay?.show} onClose={toggleOverlayDrawer('right', false)} PaperProps={{ style: { position: 'absolute' } }} slotProps={{ backdrop: { style: { position: 'absolute' } } }} ModalProps={{ container: containerRef.current ? containerRef.current : null, style: { position: 'absolute', zIndex: 1400 }, keepMounted: true, }} variant="temporary"

`

stevensturkop commented 8 months ago

Hi thanks, it worked! But when I try to set the anchor to "right" (I need to open the drawer from the right side) it opens weirdly (shaking). Any solution for that?

Late to the party, although variant='persistent' fixes the glitch for the regular Drawer, it also disables the swipe feature on mobile for the SwipeableDrawer, so the workaround that I found is to add the onEnter function, which forces the drawer to start from the right.

Note: the reason behind node.clientWidth - 1 is that in the code they use < > comparisons so it cannot be strictly equal. Note2: Replacing container: ... by container: () => { ... } will fix the error ReferenceError: document is not defined

const SwipeableDrawerStyled = emotionStyled(SwipeableDrawer)`
  &.${drawerClasses.root} {
    position: absolute;
    overflow: hidden;
    width: 100%;
    height: 100%;
  }

  .${paperClasses.root} {
    position: absolute;
    width: 100%;
    padding: ${PAGE_GUTTER};

    ::-webkit-scrollbar {
      display: none;
    }
  }
`;

  <SwipeableDrawerStyled
      anchor='right'
      open={isOpen}
      hideBackdrop
      elevation={0}
      onClose={toggleDrawer}
      onOpen={toggleDrawer}
      ModalProps={{
        // Making the container a function fixes this issue: "ReferenceError: document is not defined"
        container: () => document.getElementById('root-wrapper'),
      }}
      SlideProps={{
        onEnter(node) {
          // This fixes the issue with the drawer glitching when drawer anchor is "right"
          // Known issue: https://github.com/mui/material-ui/issues/11749#issuecomment-518521194
          // variant='persistent' fixes the glitch issue however it disables the swipeable feature on mobile
          node.style.transform = `translateX(${node.clientWidth - 1}px)`;
        },
      }}
tronghieu60s commented 2 months ago

For anyone that still wonders about the entire solution, the following worked for me.

Please note that I used document.getElementById here and that requires that you set the ID property to that value if you want the drawer to be contained in that element. It should also be possible to use a ref in this case, and that is preferred in my opinion.

Step 1: Set an ID attribute for the element that you want to contain the drawer elements

<div id="drawer-container" style="position: relative">
  <span>Some elements</span>
</div>

Make sure that you add position: relative to this element.

Step 2: Set correct styling and reference to container element on Drawer element.

<Drawer
  open={true}
  onClose={() => {}}
  PaperProps={{ style: { position: 'absolute' } }}
  slotProps={{ backdrop: { style: { position: 'absolute' } } }} // <=============== Fix backdrop by slotProps
  ModalProps={{
    container: document.getElementById('drawer-container'),
    style: { position: 'absolute' }
  }}
  keepMounted: true,    // <===============  remove shaky animation
  variant="temporary"
>
  <span>Some elements</span>
</Drawer>

The things to take away from the example above are the PaperProps, BackdropProps and the ModalProps props. For them to be contained within the container element that we created in step one, these elements have to be absolutely positioned.

Also note the document.getElementById to get a reference to the container element.

I edited this code, it worked for me!