Popover

Popover (팝오버)

내용을 다른 요소 위에 표시하는 데 사용하는 Popover 컴포넌트에 대해 알아봅니다. Modal 기반으로 동작하며, click-away 시 자동으로 닫혀요.

출처: 문서

본문

Popover는 다른 내용 위에 어떤 내용을 표시하는 데 사용할 수 있어요.

Popover 컴포넌트를 사용할 때 알아 둘 점:

  • 이 컴포넌트는 Modal 컴포넌트를 기반으로 만들어졌습니다.
  • Popover는 Popper와 달리, 기본적으로 스크롤을 차단하고 click-away 시 닫힙니다.

기본 Popover (Basic Popover)

import * as React from 'react';
import Popover from '@mui/material/Popover';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';

export default function BasicPopover() {
  const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);

  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    setAnchorEl(event.currentTarget);
  };

  const handleClose = () => {
    setAnchorEl(null);
  };

  const open = Boolean(anchorEl);
  const id = open ? 'simple-popover' : undefined;

  return (
    <div>
      <Button aria-describedby={id} variant="contained" onClick={handleClick}>
        Open Popover
      </Button>
      <Popover
        id={id}
        open={open}
        anchorEl={anchorEl}
        onClose={handleClose}
        anchorOrigin={{
          vertical: 'bottom',
          horizontal: 'left',
        }}
      >
        <Typography sx={{ p: 2 }}>The content of the Popover.</Typography>
      </Popover>
    </div>
  );
}

앵커 플레이그라운드 (Anchor playground)

라디오 버튼으로 anchorOrigin과 transformOrigin 위치를 조정해 보세요. anchorReference를 anchorPosition 또는 anchorEl로 설정할 수도 있어요. anchorPosition일 때 컴포넌트는 anchorEl 대신 anchorPosition prop을 참조하며, 이를 조정해 팝오버의 위치를 설정할 수 있습니다.

import * as React from 'react';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';
import FormControlLabel from '@mui/material/FormControlLabel';
import RadioGroup from '@mui/material/RadioGroup';
import Radio from '@mui/material/Radio';
import Grid from '@mui/material/Grid';
import { green } from '@mui/material/colors';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Popover from '@mui/material/Popover';
import Input from '@mui/material/Input';
import InputLabel from '@mui/material/InputLabel';
import { HighlightedCode } from '@mui/internal-core-docs/HighlightedCode';

const inlineStyles = {
  anchorVertical: {
    top: {
      top: -5,
    },
    center: {
      top: 'calc(50% - 5px)',
    },
    bottom: {
      bottom: -5,
    },
  },
  anchorHorizontal: {
    left: {
      left: -5,
    },
    center: {
      left: 'calc(50% - 5px)',
    },
    right: {
      right: -5,
    },
  },
};

function AnchorPlayground() {
  const anchorRef = React.useRef();

  const [state, setState] = React.useState({
    open: false,
    anchorOriginVertical: 'top',
    anchorOriginHorizontal: 'left',
    transformOriginVertical: 'top',
    transformOriginHorizontal: 'left',
    positionTop: 200, // Just so the popover can be spotted more easily
    positionLeft: 400, // Same as above
    anchorReference: 'anchorEl',
  });

  const {
    open,
    anchorOriginVertical,
    anchorOriginHorizontal,
    transformOriginVertical,
    transformOriginHorizontal,
    positionTop,
    positionLeft,
    anchorReference,
  } = state;

  const handleChange = (event) => {
    setState({
      ...state,
      [event.target.name]: event.target.value,
    });
  };

  const handleNumberInputChange = (key) => (event) => {
    setState({
      ...state,
      [key]: parseInt(event.target.value, 10),
    });
  };

  const handleClickButton = () => {
    setState({
      ...state,
      open: true,
    });
  };

  const handleClose = () => {
    setState({
      ...state,
      open: false,
    });
  };

  let mode = '';

  if (anchorReference === 'anchorPosition') {
    mode = `
  anchorReference="${anchorReference}"
  anchorPosition={{ top: ${positionTop}, left: ${positionLeft} }}`;
  }

  const jsx = `
<Popover ${mode}
  anchorOrigin={{
    vertical: '${anchorOriginVertical}',
    horizontal: '${anchorOriginHorizontal}',
  }}
  transformOrigin={{
    vertical: '${transformOriginVertical}',
    horizontal: '${transformOriginHorizontal}',
  }}
>
  The content of the Popover.
</Popover>
`;

  const radioAnchorClasses = {
    color: green[600],
    '&.Mui-checked': {
      color: green[500],
    },
  };

  return (
    <div>
      <Grid container sx={{ justifyContent: 'center' }}>
        <Grid sx={{ position: 'relative', mb: 4 }}>
          <Button ref={anchorRef} variant="contained" onClick={handleClickButton}>
            Open Popover
          </Button>
          {anchorReference === 'anchorEl' && (
            <Box
              sx={{
                bgcolor: green[500],
                width: 10,
                height: 10,
                borderRadius: '50%',
                position: 'absolute',
              }}
              style={{
                ...inlineStyles.anchorVertical[anchorOriginVertical],
                ...inlineStyles.anchorHorizontal[anchorOriginHorizontal],
              }}
            />
          )}
        </Grid>
      </Grid>
      <Popover
        open={open}
        anchorEl={anchorRef.current}
        anchorReference={anchorReference}
        anchorPosition={{
          top: positionTop,
          left: positionLeft,
        }}
        onClose={handleClose}
        anchorOrigin={{
          vertical: anchorOriginVertical,
          horizontal: anchorOriginHorizontal,
        }}
        transformOrigin={{
          vertical: transformOriginVertical,
          horizontal: transformOriginHorizontal,
        }}
      >
        <Typography sx={{ m: 2 }}>The content of the Popover.</Typography>
      </Popover>
      <Grid container spacing={2}>
        <Grid
          size={{
            xs: 12,
            sm: 6,
          }}
        >
          <FormControl component="fieldset">
            <FormLabel component="legend">anchorReference</FormLabel>
            <RadioGroup
              row
              aria-label="anchor reference"
              name="anchorReference"
              value={anchorReference}
              onChange={handleChange}
            >
              <FormControlLabel
                value="anchorEl"
                control={<Radio />}
                label="anchorEl"
              />
              <FormControlLabel
                value="anchorPosition"
                control={<Radio />}
                label="anchorPosition"
              />
            </RadioGroup>
          </FormControl>
        </Grid>
        <Grid
          size={{
            xs: 12,
            sm: 6,
          }}
        >
          <FormControl variant="standard">
            <InputLabel htmlFor="position-top">anchorPosition.top</InputLabel>
            <Input
              id="position-top"
              type="number"
              value={positionTop}
              onChange={handleNumberInputChange('positionTop')}
            />
          </FormControl>
          &nbsp;
          <FormControl variant="standard">
            <InputLabel htmlFor="position-left">anchorPosition.left</InputLabel>
            <Input
              id="position-left"
              type="number"
              value={positionLeft}
              onChange={handleNumberInputChange('positionLeft')}
            />
          </FormControl>
        </Grid>
        <Grid
          size={{
            xs: 12,
            sm: 6,
          }}
        >
          <FormControl component="fieldset">
            <FormLabel component="legend">anchorOrigin.vertical</FormLabel>
            <RadioGroup
              aria-label="anchor origin vertical"
              name="anchorOriginVertical"
              value={anchorOriginVertical}
              onChange={handleChange}
            >
              <FormControlLabel
                value="top"
                control={<Radio sx={radioAnchorClasses} />}
                label="Top"
              />
              <FormControlLabel
                value="center"
                control={<Radio sx={radioAnchorClasses} />}
                label="Center"
              />
              <FormControlLabel
                value="bottom"
                control={<Radio sx={radioAnchorClasses} />}
                label="Bottom"
              />
            </RadioGroup>
          </FormControl>
        </Grid>
        <Grid
          size={{
            xs: 12,
            sm: 6,
          }}
        >
          <FormControl component="fieldset">
            <FormLabel component="legend">transformOrigin.vertical</FormLabel>
            <RadioGroup
              aria-label="transform origin vertical"
              name="transformOriginVertical"
              value={transformOriginVertical}
              onChange={handleChange}
            >
              <FormControlLabel value="top" control={<Radio />} label="Top" />
              <FormControlLabel
                value="center"
                control={<Radio color="primary" />}
                label="Center"
              />
              <FormControlLabel
                value="bottom"
                control={<Radio color="primary" />}
                label="Bottom"
              />
            </RadioGroup>
          </FormControl>
        </Grid>
        <Grid
          size={{
            xs: 12,
            sm: 6,
          }}
        >
          <FormControl component="fieldset">
            <FormLabel component="legend">anchorOrigin.horizontal</FormLabel>
            <RadioGroup
              row
              aria-label="anchor origin horizontal"
              name="anchorOriginHorizontal"
              value={anchorOriginHorizontal}
              onChange={handleChange}
            >
              <FormControlLabel
                value="left"
                control={<Radio sx={radioAnchorClasses} />}
                label="Left"
              />
              <FormControlLabel
                value="center"
                control={<Radio sx={radioAnchorClasses} />}
                label="Center"
              />
              <FormControlLabel
                value="right"
                control={<Radio sx={radioAnchorClasses} />}
                label="Right"
              />
            </RadioGroup>
          </FormControl>
        </Grid>
        <Grid
          size={{
            xs: 12,
            sm: 6,
          }}
        >
          <FormControl component="fieldset">
            <FormLabel component="legend">transformOrigin.horizontal</FormLabel>
            <RadioGroup
              row
              aria-label="transform origin horizontal"
              name="transformOriginHorizontal"
              value={transformOriginHorizontal}
              onChange={handleChange}
            >
              <FormControlLabel
                value="left"
                control={<Radio color="primary" />}
                label="Left"
              />
              <FormControlLabel
                value="center"
                control={<Radio color="primary" />}
                label="Center"
              />
              <FormControlLabel
                value="right"
                control={<Radio color="primary" />}
                label="Right"
              />
            </RadioGroup>
          </FormControl>
        </Grid>
      </Grid>
      <HighlightedCode code={jsx} language="jsx" />
    </div>
  );
}

export default AnchorPlayground;

마우스 호버 상호작용 (Mouse hover interaction)

이 데모는 Popover 컴포넌트를 mouseenter와 mouseleave 이벤트와 함께 사용해 팝오버 동작을 구현하는 방법을 보여줍니다.

import * as React from 'react';
import Popover from '@mui/material/Popover';
import Typography from '@mui/material/Typography';

export default function MouseHoverPopover() {
  const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);

  const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorEl(event.currentTarget);
  };

  const handlePopoverClose = () => {
    setAnchorEl(null);
  };

  const open = Boolean(anchorEl);

  return (
    <div>
      <Typography
        aria-owns={open ? 'mouse-over-popover' : undefined}
        aria-haspopup="true"
        onMouseEnter={handlePopoverOpen}
        onMouseLeave={handlePopoverClose}
      >
        Hover with a Popover.
      </Typography>
      <Popover
        id="mouse-over-popover"
        sx={{ pointerEvents: 'none' }}
        open={open}
        anchorEl={anchorEl}
        anchorOrigin={{
          vertical: 'bottom',
          horizontal: 'left',
        }}
        transformOrigin={{
          vertical: 'top',
          horizontal: 'left',
        }}
        onClose={handlePopoverClose}
        disableRestoreFocus
      >
        <Typography sx={{ p: 1 }}>I use Popover.</Typography>
      </Popover>
    </div>
  );
}

가상 요소 (Virtual element)

anchorEl prop의 값은 가짜 DOM 요소에 대한 참조일 수 있어요. 다음 인터페이스의 객체를 제공해야 합니다.

interface PopoverVirtualElement {
  nodeType: 1;
  getBoundingClientRect: () => DOMRect;
}

텍스트의 일부를 강조하면 팝오버가 보입니다.

import * as React from 'react';
import Popover, { PopoverProps } from '@mui/material/Popover';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';

export default function VirtualElementPopover() {
  const [open, setOpen] = React.useState(false);
  const [anchorEl, setAnchorEl] = React.useState<PopoverProps['anchorEl']>(null);

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

  const handleMouseUp = () => {
    const selection = window.getSelection();

    // Skip if selection has a length of 0
    if (!selection || selection.anchorOffset === selection.focusOffset) {
      return;
    }

    const getBoundingClientRect = () => {
      return selection.getRangeAt(0).getBoundingClientRect();
    };

    setOpen(true);

    setAnchorEl({ getBoundingClientRect, nodeType: 1 });
  };

  const id = open ? 'virtual-element-popover' : undefined;

  return (
    <div>
      <Typography aria-describedby={id} onMouseUp={handleMouseUp}>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ipsum purus,
        bibendum sit amet vulputate eget, porta semper ligula. Donec bibendum
        vulputate erat, ac fringilla mi finibus nec. Donec ac dolor sed dolor
        porttitor blandit vel vel purus. Fusce vel malesuada ligula. Nam quis
        vehicula ante, eu finibus est. Proin ullamcorper fermentum orci, quis finibus
        massa. Nunc lobortis, massa ut rutrum ultrices, metus metus finibus ex, sit
        amet facilisis neque enim sed neque. Quisque accumsan metus vel maximus
        consequat. Suspendisse lacinia tellus a libero volutpat maximus.
      </Typography>
      <Popover
        id={id}
        open={open}
        anchorEl={anchorEl}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
        onClose={handleClose}
        disableAutoFocus
      >
        <Paper>
          <Typography sx={{ p: 2 }}>The content of the Popover.</Typography>
        </Paper>
      </Popover>
    </div>
  );
}

가상 요소의 속성에 대한 자세한 내용은 다음 자료들을 참고하세요.

:::warning Popover 컴포넌트에 가상 요소를 사용하려면 nodeType 속성이 필요합니다. 이는 Popper나 Tooltip 컴포넌트에 사용되는 가상 요소와는 다릅니다. 둘 다 이 속성이 필요하지 않아요. :::

전환 (Transitions)

Popover는 기본적으로 Grow를 사용합니다. 다른 전환 효과로 바꾸거나 전환 관련 props를 넘기고 싶다면 slots.transition과 slotProps.transition을 쓰면 돼요.

보조 프로젝트 (Supplementary projects)

더 고급 사용 사례의 경우, 다음을 활용할 수 있을 거예요.

material-ui-popup-state

stars npm downloads

material-ui-popup-state 패키지는 대부분의 경우 팝오버 상태를 대신 관리해 줍니다.

import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import Popover from '@mui/material/Popover';
import PopupState, { bindTrigger, bindPopover } from 'material-ui-popup-state';

export default function PopoverPopupState() {
  return (
    <PopupState variant="popover" popupId="demo-popup-popover">
      {(popupState) => (
        <div>
          <Button variant="contained" {...bindTrigger(popupState)}>
            Open Popover
          </Button>
          <Popover
            {...bindPopover(popupState)}
            anchorOrigin={{
              vertical: 'bottom',
              horizontal: 'center',
            }}
            transformOrigin={{
              vertical: 'top',
              horizontal: 'center',
            }}
          >
            <Typography sx={{ p: 2 }}>The content of the Popover.</Typography>
          </Popover>
        </div>
      )}
    </PopupState>
  );
}

Grow API

Demos

이 React 컴포넌트의 사용 예시와 자세한 내용은 컴포넌트 데모 페이지에서 확인할 수 있어요.

Import

import Grow from '@mui/material/Grow';
// or
import { Grow } from '@mui/material';

Props

Name Type Default Required Description
children element - Yes
addEndListener function(node: HTMLElement, done: Function) => void - No
appear bool true No
disablePrefersReducedMotion bool false No
easing { enter?: string, exit?: string } | string - No
in bool - No
timeout 'auto' | number | { appear?: number, enter?: number, exit?: number } 'auto' No

참고: ref는 루트 요소 (HTMLDivElement).

그 외에 제공된 props는 루트 요소 (Transition).

Inheritance

위에서 명시적으로 다루진 않았지만, Transition 컴포넌트의 props도 Grow에서 사용할 수 있어요. 일부 컴포넌트는 react-transition-group을 기본 지원합니다.

Source code

이 페이지에서 원하는 정보를 찾지 못했다면, 더 자세한 내용은 컴포넌트 구현을 살펴보는 것도 좋아요.

Popover API

Demos

이 React 컴포넌트의 사용 예시와 자세한 내용은 컴포넌트 데모 페이지에서 확인할 수 있어요.

Import

import Popover from '@mui/material/Popover';
// or
import { Popover } from '@mui/material';

Props

Name Type Default Required Description
open bool - Yes
action ref - No
anchorEl HTML element | func - No
anchorOrigin { horizontal: 'center' | 'left' | 'right' | number, vertical: 'bottom' | 'center' | 'top' | number } `{
vertical: 'top',
horizontal: 'left',
}` No
anchorPosition { left: number, top: number } - No
anchorReference 'anchorEl' | 'anchorPosition' | 'none' 'anchorEl' No
children node - No
classes object - No Override or extend the styles applied to the component.
container HTML element | func - No
disableAutoFocus bool false No
disableScrollLock bool false No
elevation integer 8 No
marginThreshold number 16 No
onClose func - No
slotProps { backdrop?: func | object, paper?: func | object, root?: func | object, transition?: func | object } {} No
slots { backdrop?: elementType, paper?: elementType, root?: elementType, transition?: elementType } {} No
sx Array<func | object | bool> | func | object - No The system prop that allows defining system overrides as well as additional CSS styles.
transformOrigin { horizontal: 'center' | 'left' | 'right' | number, vertical: 'bottom' | 'center' | 'top' | number } `{
vertical: 'top',
horizontal: 'left',
}` No
transitionDuration 'auto' | number | { appear?: number, enter?: number, exit?: number } 'auto' No

참고: ref는 루트 요소 (HTMLDivElement).

그 외에 제공된 props는 루트 요소 (Modal).

Inheritance

위에서 명시적으로 다루진 않았지만, Modal 컴포넌트의 props도 Popover에서 사용할 수 있어요.

Slots

Name Default Class Description
root Modal .MuiPopover-root The component used for the root slot.
paper Paper .MuiPopover-paper The component used for the paper slot.
transition Grow - The component used for the transition slot.
backdrop Backdrop - The component used for the backdrop slot.

Source code

이 페이지에서 원하는 정보를 찾지 못했다면, 더 자세한 내용은 컴포넌트 구현을 살펴보는 것도 좋아요.

더 알아보기 (Learn more)