Popover
Popover (팝오버)
내용을 다른 요소 위에 표시하는 데 사용하는 Popover 컴포넌트에 대해 알아봅니다. Modal 기반으로 동작하며, click-away 시 자동으로 닫혀요.
출처: 문서
본문
Popover는 다른 내용 위에 어떤 내용을 표시하는 데 사용할 수 있어요.
Popover 컴포넌트를 사용할 때 알아 둘 점:
기본 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>
<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
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
이 페이지에서 원하는 정보를 찾지 못했다면, 더 자세한 내용은 컴포넌트 구현을 살펴보는 것도 좋아요.