Popover

Popover

주어진 타겟 요소에 상대적으로 팝오버 섹션을 표시하는 컴포넌트예요.

출처: 문서

본문

사용법 (Usage)

Popover로 타겟 요소에 상대적인 팝오버를 표시해요.

import { Popover, Text, Button } from '@mantine/core';

function Demo() {
  return (
    <Popover width={200} position="bottom" withArrow>
      <Popover.Target>
        <Button>Toggle popover</Button>
      </Popover.Target>
      <Popover.Dropdown>
        <Text size="xs">This is uncontrolled popover, it is opened when button is clicked</Text>
      </Popover.Dropdown>
    </Popover>
  );
}

제어 방식 (Controlled)

opened와 onChange prop으로 Popover 상태를 제어할 수 있어요.

import { useState } from 'react';
import { Button, Popover } from '@mantine/core';

function Demo() {
  const [opened, setOpened] = useState(false);
  return (
    <Popover opened={opened} onChange={setOpened}>
      <Popover.Target>
        <Button onClick={() => setOpened((o) => !o)}>Toggle popover</Button>
      </Popover.Target>
      <Popover.Dropdown>Dropdown</Popover.Dropdown>
    </Popover>
  );
}

마우스 이벤트를 사용하는 제어 예시:

import { useDisclosure } from '@mantine/hooks';
import { Popover, Text, Button } from '@mantine/core';

function Demo() {
  const [opened, { close, open }] = useDisclosure(false);
  return (
    <Popover opened={opened}>
      <Popover.Target>
        <Button onMouseEnter={open} onMouseLeave={close}>Hover to see popover</Button>
      </Popover.Target>
      <Popover.Dropdown>
        <Text size="xs">This popover is shown when user hovers the target element</Text>
      </Popover.Dropdown>
    </Popover>
  );
}

포커스 트랩 (Focus trap)

Popover.Dropdown 안에서 대화형 요소(인풋, 버튼 등)를 사용해야 한다면 trapFocus prop을 설정해요.

import { Popover, Button, TextInput } from '@mantine/core';

function Demo() {
  return (
    <Popover width={200} trapFocus>
      <Popover.Target><Button>Toggle popover</Button></Popover.Target>
      <Popover.Dropdown>
        <TextInput placeholder="Input inside popover" />
      </Popover.Dropdown>
    </Popover>
  );
}

인라인 요소 (Inline elements)

inline 미들웨어를 활성화하면 인라인 요소와 Popover를 사용할 수 있어요.

import { Popover, Mark, Text } from '@mantine/core';

function Demo() {
  return (
    <Text>
      Stantler's magnificent antlers were traded at high prices as works of art. As a result, this
      Pokémon was hunted close to extinction by those who were after the priceless antlers.{' '}
      <Popover middlewares={{ inline: true }} position="top">
        <Popover.Target>
          <Mark>When visiting a junkyard</Mark>
        </Popover.Target>
        <Popover.Dropdown>Inline dropdown</Popover.Dropdown>
      </Popover>
      , you may catch sight of it having an intense fight with Murkrow over shiny objects.
    </Text>
  );
}

같은 너비 (Same width)

width="target" prop을 설정하면 Popover 드롭다운이 타겟 요소와 같은 너비를 차지해요.

import { Popover, Text, Button } from '@mantine/core';

function Demo() {
  return (
    <Popover width="target">
      <Popover.Target><Button>Toggle popover</Button></Popover.Target>
      <Popover.Dropdown>
        <Text size="xs">This popover has same width as target, it is useful when you are building input dropdowns</Text>
      </Popover.Dropdown>
    </Popover>
  );
}

offset

offset prop을 숫자로 설정하면 타겟 요소에 상대적인 드롭다운 위치를 변경할 수 있어요. 이렇게 하면 메인 축에서만 드롭다운 오프셋을 제어할 수 있어요.

import { Popover, Button, Text } from '@mantine/core';

function Demo() {
  return (
    <Popover position="bottom" offset={20}>
      <Popover.Target><Button>Popover target</Button></Popover.Target>
      <Popover.Dropdown>
        <Text size="xs">Change position and offset to configure dropdown offset relative to target</Text>
      </Popover.Dropdown>
    </Popover>
  );
}

두 축 모두 오프셋을 제어하려면 mainAxis와 crossAxis 속성을 가진 객체를 전달해요.

import { Popover, Button, Text } from '@mantine/core';

function Demo() {
  return (
    <Popover offset={{ mainAxis: 10, crossAxis: 20 }}>
      {/* ... */}
    </Popover>
  );
}

미들웨어 (Middlewares)

middlewares prop으로 Floating UI 미들웨어를 활성화/비활성화할 수 있어요.

  • shift 미들웨어는 드롭다운을 뷰 안에 유지하도록 이동시켜요. 기본적으로 활성화돼 있어요.
  • flip 미들웨어는 드롭다운을 뷰 안에 유지하도록 배치를 변경해요. 기본적으로 활성화돼 있어요.
  • inline 미들웨어는 여러 줄에 걸친 인라인 참조 요소의 배치를 개선해요. 기본적으로 비활성화돼 있어요.
  • size 미들웨어는 드롭다운 크기를 조작해요. 기본적으로 비활성화돼 있어요.

shift와 flip 미들웨어를 끄는 예시:

import { Popover } from '@mantine/core';

function Demo() {
  return (
    <Popover middlewares={{ shift: false, flip: false }}>
      {/* Popover content */}
    </Popover>
  );
}

미들웨어 옵션 커스터마이즈 (Customize middleware options)

Floating UI 미들웨어 옵션을 커스터마이즈하려면 middlewares prop에 객체로 전달해요. 예를 들어 shift 미들웨어 패딩을 20px로 바꾸려면 다음 설정을 사용해요.

import { Popover } from '@mantine/core';

function Demo() {
  return (
    <Popover middlewares={{ shift: { padding: 20 } }}>
      {/* Popover content */}
    </Popover>
  );
}

드롭다운 화살표 (Dropdown arrow)

withArrow prop을 설정하면 드롭다운에 화살표를 추가할 수 있어요. 화살표는 transform: rotate(45deg)로 회전된 div 요소예요.

arrowPosition prop은 Popover 컴포넌트에서 position이 *-start와 *-end 값으로 설정될 때 화살표가 타겟 요소에 상대적으로 어떻게 배치되는지 결정해요. 기본값은 center예요. 가능하면 화살표가 타겟 요소의 중앙에 배치돼요.

arrowPosition을 side로 변경하면 화살표가 타겟 요소의 측면에 배치되고, arrowOffset prop으로 화살표 오프셋을 제어할 수 있어요. arrowPosition이 center일 때는 arrowOffset prop이 무시돼요.

오버레이와 함께 (With overlay)

withOverlay prop을 설정하면 드롭다운 뒤에 오버레이를 추가할 수 있어요. overlayProps prop으로 Overlay 컴포넌트에 추가 설정을 전달할 수 있어요.

import { Popover, Avatar, Text, Group, Anchor, Stack } from '@mantine/core';

function Demo() {
  return (
    <Popover withOverlay overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} width={280} position="bottom">
      <Popover.Target><Button variant="default">Toggle</Button></Popover.Target>
      <Popover.Dropdown>
        <Group wrap="nowrap">
          <Avatar src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-8.png" size={60} radius="md" />
          <div>
            <Text fw={600}>Mantine</Text>
            <Anchor href="https://x.com/mantinedev" size="xs" c="dimmed">@mantinedev</Anchor>
          </div>
        </Group>
        <Text size="xs" mt="md" c="dimmed">Customizable React components and hooks library with focus on usability, accessibility and developer experience</Text>
        <Group mt="sm" gap="xs">
          <Text size="xs">0 Following</Text>
          <Text size="xs">1,174 Followers</Text>
        </Group>
      </Popover.Dropdown>
    </Popover>
  );
}

분리 시 숨기기 (Hide detached)

hideDetached prop으로 타겟 요소가 스타일(display: none, visibility: hidden 등)로 숨겨지거나 DOM에서 제거되거나 뷰포트 밖으로 스크롤되었을 때 드롭다운이 어떻게 동작할지 설정할 수 있어요.

기본적으로 hideDetached는 활성화되어 있고, 드롭다운은 타겟 요소와 함께 숨겨져요. hideDetached={false}로 이 동작을 바꿀 수 있어요.

import { Box, Button, Group, Popover } from '@mantine/core';

function Demo() {
  return (
    <Group>
      <Box>
        <Popover>
          <Popover.Target><Button>Toggle popover</Button></Popover.Target>
          <Popover.Dropdown>This popover dropdown is hidden when detached</Popover.Dropdown>
        </Popover>
      </Box>
      <Box>
        <Popover hideDetached={false}>
          <Popover.Target><Button>Toggle popover</Button></Popover.Target>
          <Popover.Dropdown>This popover dropdown is visible when detached</Popover.Dropdown>
        </Popover>
      </Box>
    </Group>
  );
}

비활성 (Disabled)

disabled prop을 설정하면 Popover.Dropdown 렌더링을 막을 수 있어요.

import { Popover, Text, Button } from '@mantine/core';

function Demo() {
  return (
    <Popover disabled>
      <Popover.Target><Button>Toggle popover</Button></Popover.Target>
      <Popover.Dropdown>
        <Text size="xs">Disabled popover dropdown is always hidden</Text>
      </Popover.Dropdown>
    </Popover>
  );
}

외부 클릭 (Click outside)

기본적으로 Popover은 드롭다운 밖을 클릭하면 닫혀요. 이 동작을 비활성화하려면 closeOnClickOutside={false}를 설정해요.

clickOutsideEvents prop으로 외부 클릭 감지에 사용되는 이벤트를 설정할 수 있어요. 기본적으로 Popover은 mousedown과 touchstart 이벤트를 듣고 있어요. 이를 mouseup과 touchend 같은 다른 이벤트로 바꿀 수 있어요.

onDismiss

열림 상태를 제어해야 하지만 외부 클릭과 Escape 키로 팝오버를 닫고 싶다면 onDismiss prop을 사용해요.

import { useState } from 'react';
import { Button, Popover } from '@mantine/core';

function Demo() {
  const [opened, setOpened] = useState(false);
  return (
    <Popover opened={opened} onDismiss={() => setOpened(false)}>
      <Popover.Target>
        <Button onClick={() => setOpened((o) => !o)}>Toggle popover</Button>
      </Popover.Target>
      <Popover.Dropdown>Dropdown</Popover.Dropdown>
    </Popover>
  );
}

초기 포커스 (Initial focus)

Popover는 FocusTrap 컴포넌트로 포커스를 관리해요. 초기 포커스를 받을 요소에 data-autofocus 속성을 추가해요.

import { Popover } from '@mantine/core';

function Demo() {
  return (
    <Popover width={200} opened position="bottom">
      <Popover.Target><button>Target</button></Popover.Target>
      <Popover.Dropdown>
        <input data-autofocus placeholder="Initial focus" />
      </Popover.Dropdown>
    </Popover>
  );
}

Popover.Target children

Popover.Target은 단일 자식으로 요소나 컴포넌트를 요구해요. 문자열, 프래그먼트, 숫자, 여러 요소/컴포넌트는 지원되지 않고 오류를 던져요. 커스텀 컴포넌트는 루트 요소 ref를 얻는 prop을 제공해야 해요. 모든 Mantine 컴포넌트는 기본적으로 ref를 지원해요.

import { Popover, Button } from '@mantine/core';

function Demo() {
  return (
    <>
      <Popover.Target><button>Native button – ok</button></Popover.Target>
      {/* OK */}
      <Popover.Target><Button>Mantine component – ok</Button></Popover.Target>
      {/* String, Number, Fragment, Multiple nodes – NOT OK, will throw error */}
    </>
  );
}

필수 ref prop (Required ref prop)

Popover.Target 안에서 렌더링되는 커스텀 컴포넌트는 ref prop을 지원해야 해요.

// Example of code that WILL NOT WORK
import { Popover } from '@mantine/core';

// ❌ ref is not forwarded to the root element
function MyComponent() {
  return <div>My component</div>;
}

ref를 루트 요소에 전달하면 동작해요.

// Example of code that will work
import { Popover } from '@mantine/core';

// ✅ ref is forwarded to the root element
function MyComponent({ ref, ...others }: React.ComponentProps<'div'>) {
  return <div {...others} ref={ref}>My component</div>;
}

컨텍스트 메뉴 (Context menu)

Popover.ContextMenu를 사용하면 우클릭 시 커서 위치에 드롭다운을 열 수 있어요. Popover.Target을 대체하고 contextmenu 이벤트에 응답해야 할 요소를 감싸요. 브라우저의 기본 컨텍스트 메뉴는 억제되고 Popover.Dropdown이 커서 위치에 배치돼요. 다시 우클릭하면 드롭다운이 새 좌표로 재배치돼요. Popover.Dropdown에는 어떤 콘텐츠든 넣을 수 있어요. disabled를 설정하면 브라우저 기본 컨텍스트 메뉴를 복원할 수 있어요.

import { Avatar, Button, Group, Paper, Popover, Stack, Text } from '@mantine/core';

function Demo() {
  return (
    <Paper withBorder radius="md" p="xl" maw={400} mx="auto">
      <Text>Right-click anywhere inside this area</Text>
      <Text size="sm" c="dimmed">A popover will open at the cursor position</Text>

      <Popover.ContextMenu>
        <Popover.Target>
          <div>(right-click area)</div>
        </Popover.Target>
        <Popover.Dropdown w={280}>
          <Group wrap="nowrap">
            <Avatar size={44}>JD</Avatar>
            <Stack gap={0}>
              <Text fw={500}>Jane Doe</Text>
              <Text size="xs" c="dimmed">[email protected]</Text>
            </Stack>
          </Group>
          <Group mt="md">
            <Button size="xs">Message</Button>
            <Button size="xs" variant="default">Follow</Button>
          </Group>
        </Popover.Dropdown>
      </Popover.ContextMenu>
    </Paper>
  );
}

터치 기기 (Touch devices)

contextmenu 이벤트를 발생시키지 않는 터치 기기(특히 iOS Safari)에서는 길게 누르기로 드롭다운이 열려요. longPressDelay prop으로 드롭다운이 열리기 전 요소를 눌러야 하는 시간을 제어할 수 있고, 기본값은 500ms예요. 터치 기기에서 드롭다운 아래에 네이티브 텍스트 선택 콜아웃이 나타나는 것을 막기 위해, Popover.ContextMenu은 감싼 요소에서 텍스트 선택(user-select: none)을 비활성화해요.

중첩 팝오버 (Nested popovers)

중첩 팝오버는 Portal 없이 children을 렌더링해야 해요. 보통 팝오버 콘텐츠를 렌더링하는 컴포넌트의 prop으로 포털을 비활성화해야 해요. 예를 들어 Select에는 comboboxProps={{ withinPortal: false }} prop이 있어요. 팝오버 콘텐츠를 렌더링하는 데 사용하는 컴포넌트의 문서에서 포털을 비활성화하는 방법을 확인해요. 포털이 비활성화되지 않으면 외부 클릭으로 모든 팝오버가 닫혀요.

접근성 (Accessibility)

Popover는 WAI-ARIA 권장사항을 따릅니다.

  • 드롭다운 요소에는 role="dialog"와 aria-labelledby="target-id" 속성이 있어요
  • 타겟 요소에는 aria-haspopup="dialog", aria-expanded, aria-controls="dropdown-id" 속성이 있어요

비제어 Popover는 button 요소나 그것을 렌더링하는 컴포넌트(Button, ActionIcon 등)와 함께 쓸 때만 접근 가능해요. 다른 요소는 Space와 Enter 키 입력을 지원하지 않아요.

키보드 상호작용 (Keyboard interactions)

Key Description Condition
Escape 드롭다운 닫기 드롭다운 내부 포커스
Space/Enter 드롭다운 열기/닫기 타겟 요소 포커스

더 알아보기 (Learn more)