reactjs / react-modal

Accessible modal dialog component for React
http://reactcommunity.org/react-modal
MIT License
7.37k stars 809 forks source link

How can I position my modal relative to a parent element in my component? #925

Open cyruscuenca opened 2 years ago

cyruscuenca commented 2 years ago

Summary:

Steps to reproduce:

  1. Set app element to the app root: ReactModal.setAppElement('#root');
  2. Create a basic modal with props like these:
             </nav>
            <ReactModal
                isOpen={state.delete}
                onRequestClose={ () => { setState(prevState => ({ delete: !prevState.delete })) } }
                shouldCloseOnOverlayClick={true}
                className="h-auto m-auto card px-5 py-4"
                style={{
                    overlay: {
                        'background': 'rgba(138, 145, 153, .66)'
                    },
                    content: {
                        'width': '400px',
                        'marginTop': '25vh',
                    }
                }}
            >
  3. Modal is positioned like a fixed element in the center of the viewport.

What I want

I want to position this modal relative to it's parent component. This modal is for a user menu popup, and it needs to appear below a fixed navbar. The position of the user image is not static. The site is responsive, so the dropdown must be relative to that parent.

Link to example of issue:

https://www.google.com has a similar modal popup when you sign in with an account.

Here is an example image: https://imgur.com/KIykuAc

Any help would be apprecieted.

exaucae commented 2 years ago

Have you tried using parentSelector property from the docs?

cyruscuenca commented 2 years ago

I have it set to my root. I'll experiment with attaching it to the navigation bar by ID

cyruscuenca commented 2 years ago

I appended my modal to the profile image div in an attempt to position the modal relative to the image. However, the modal seems to behave as normal and create a fixed position overlay with a modal over it.

https://imgur.com/a/0QZN0Dx

cyruscuenca commented 2 years ago

Basically, I'm trying to position my modal like a dropdown.

kaichii commented 2 years ago

any updates?

cyruscuenca commented 2 years ago

Yes, I just had to set it to the correct element. Sorry for keeping this open!

C0DE-RUNNER commented 2 years ago

Hey, I've a similar requirement in my project. Hoe did you solve this issue?

COValhalla commented 2 years ago

I'm at a similar position. I need to have the modal appear where my mouse clicked on an image. I've set the parentSelector property to the parent of my image, but it is still responding with the default document.body functionality.

parentSelector={() => document.querySelector('.modalParent')}

diasbruno commented 2 years ago

@cyruscuenca @C0DE-RUNNER @kaichii @exaucae Have you looked in the resultant style for the components involved?

exaucae commented 2 years ago

below is my working code. one can provide a tiny wrapper library to ease everyone else life. I can do that in the coming weeks

import React, { ReactNode, RefObject, useCallback, useEffect, useState } from 'react';
import ReactModal from 'react-modal';
import { css } from '@emotion/css';
import { brandColors, spaces } from '../style';
import { devicePoints } from '../breakpoints';

export type MenuProps = {
  children: ReactNode;
  open: boolean;
  parentRef: RefObject<Element>;
  contentStyle: string;
  anchorOrigin: 'bottom-left' | 'bottom-right';
  onClose: () => void;
};

export const Menu = ({ children, parentRef, open, onClose, anchorOrigin, contentStyle }: MenuProps) => {
  const [contentRef, setContentRef] = useState(null);

  const handleClickOutsideContent = useCallback(
    (event: MouseEvent) => {
      const eventTarget = event.target as HTMLElement;

      const isClosable =
        open &&
        !parentRef?.current?.contains(eventTarget) &&
        !contentRef?.contains(eventTarget) &&
        !eventTarget.closest('.ReactModal__Overlay');

      if (isClosable) onClose();
    },
    [contentRef, open, parentRef, onClose]
  );

  const getRightPosition = useCallback(() => {
    if (parentRef?.current && contentRef) {
      const compStyles = window.getComputedStyle(parentRef.current);
      const parentPaddingRight = compStyles.getPropertyValue('padding-right').replace('px', '');

      const menuStyle = window.getComputedStyle(contentRef);
      const menuWidth = menuStyle.getPropertyValue('width').replace('px', '');

      switch (anchorOrigin) {
        case 'bottom-left':
          return Number(parentRef?.current?.clientWidth) - Number(parentPaddingRight);
        default:
          return Number(menuWidth) - Number(parentPaddingRight);
      }
    }
    return 0;
  }, [anchorOrigin, contentRef, parentRef]);

  useEffect(() => {
    document.addEventListener('click', handleClickOutsideContent);
    return () => {
      document.removeEventListener('click', handleClickOutsideContent);
    };
  }, [handleClickOutsideContent]);

  return (
    <ReactModal
      overlayClassName={css`
        position: fixed;
        width: 100vw;
        top: 0;
        left: unset;
        right: 0;
        bottom: 0;
        background-color: unset;
        @media (min-width: ${devicePoints.tablet}) {
          position: absolute;
          width: auto;
          right: ${getRightPosition()}px;
          top: unset;
        }
      `}
      className={css`
        position: static;
        inset: auto;
        width: 100vw;
        height: 100vh;
        border-radius: 0;
        padding: 0;
        margin: 0;
        border: 0;
        outline: none;
        z-index: auto;
        overflow: auto;
        box-shadow: 0 4px 12px 0 ${brandColors.neutrals.semiGrey};
        background-color: ${brandColors.neutrals.white};
        @media (min-width: ${devicePoints.tablet}) {
          position: inherit;
          z-index: 2;
          margin-top: ${spaces.s16};
        }
        ${contentStyle};
      `}
      portalClassName={css`
        position: relative;
      `}
      isOpen={open}
      onRequestClose={onClose}
      parentSelector={() => parentRef.current}
      contentRef={(node) => setContentRef(node)}
      ariaHideApp={false}
    >
      {children}
    </ReactModal>
  );
};

Example Usage:

export const BottomRight = () => {
  const [open, setOpen] = useState(false);
  const parentRef = useRef(null);

  const handleClose = () => {
    setOpen(false);
  };

  const handleOpen = () => {
    setOpen(true);
  };

  return (
      <>
        <button type="button" ref={parentRef} onClick={handleOpen}>
          Open bottom right Menu
        </button>
        <Menu
          anchorOrigin="bottom-right"
          onClose={handleClose}
          open={open}
          parentRef={parentRef}
          contentStyle={`
          @media (min-width: ${devicePoints.tablet}) {
            background-color: green;
          height: 250px;
          width: 200px;
        }
        `}
        >
          This is the bottom right menu content
        </Menu>
      </>
  );
};
diasbruno commented 2 years ago

Thanks, @exaucae. can you explain this use case?

exaucae commented 2 years ago

sure @diasbruno ,

I needed a menu component that could be positioned at the right or left of its parent element, like MUI react menu. But a constraint on our component library made us exclude MUI usage. Another alternative was react-menu but I faced an awkward configuration issue. Since we already used react-modal for our Dialogs, I went with it for our Menus.

diasbruno commented 2 years ago

Hmmm...I have seen this before. Using react-modal in this case is not the best option. Your solution looks great, I'd recommend extract this code to its own library or maybe instead of using react-modal behind the curtains, you can thing something more lightweight.

exaucae commented 2 years ago

Hmmm...I have seen this before. Using react-modal in this case is not the best option.

Indeed, came as last resort :)

Your solution looks great, I'd recommend extract this code to its own library or maybe instead of using react-modal behind the curtains, you can thing something more lightweight.

sure thing. Let's see what I can do in coming weeks