Popover

Popover

팝오버는 트리거 요소를 기준으로 위치가 정해지는 오버레이 요소입니다. 이 오버레이에는 다양하고 자유로운 형태의 콘텐츠를 표시할 수 있습니다.

Examples

팝오버를 가장 간단하게 구축하려면 Popover 컴포넌트를 불러와 컴파운드 패턴으로 연결된 Popover.TriggerPopover.Content를 구성하면 됩니다.

Popover.Trigger를 클릭하면 자동으로 Popover.Content가 열리거나 닫힙니다. 콘텐츠가 열려 있을 때 내용 외부를 클릭하거나 Escape 키를 누르면 자동으로 팝 오버가 닫힙니다.

import { Popover } from "core-zero";
 
function Example() {
  return (
    <Popover>
      <Popover.Trigger>click</Popover.Trigger>
      <Popover.Content>
        <div>Popover content</div>
      </Popover.Content>
    </Popover>
  );
}

Controlled

React의 기본 훅을 활용하여 팝오버 컴포넌트를 프로그래밍 방식으로 제어할 수 있습니다.

import { useState } from "react";
import { Popover } from "core-zero";
 
function Example() {
  const [isOpen, setOpen] = useState(false);
 
  const handleChange = (isOpen: boolean) => {
    setOpen(isOpen);
  };
 
  return (
    <Popover isOpen={isOpen} onChange={handleChange}>
      <Popover.Trigger>click</Popover.Trigger>
      <Popover.Content>Popover content</Popover.Content>
    </Popover>
  );
}

팝오버를 더 세밀하게 제어하고 맞춤화하려면 core-zero가 제공하는 usePopover 훅을 활용할 수 있습니다. rootProps에는 팝오버의 상태를 포함한 컴포넌트를 제어하기 위한 모든 속성과 로직이 담겨 있습니다. 이를 통해 로직을 재사용하고 커스터마이즈할 수 있습니다.

import { Popover, usePopover } from "core-zero";
 
function Example() {
  const { rootProps } = usePopover({ defaultOpen: false });
 
  return (
    <Popover {...rootProps}>
      <Popover.Trigger>click</Popover.Trigger>
      <Popover.Content>
        <div>Popover content</div>
      </Popover.Content>
    </Popover>
  );
}

usePopover 훅을 사용하면 컴파운드 패턴 없이 트리거 컴포넌트를 원하는 대로 커스터마이즈할 수 있습니다.

import { PopoverContent, usePopover } from "core-zero";
 
function Example() {
  const { triggerProps, popoverContentProps } =
    usePopover <
    HTMLButtonElement >
    {
      defaultOpen: false,
    };
 
  return (
    <div>
      <button {...triggerProps}>click</button>
      <PopoverContent {...popoverContentProps}>
        <div>Popover content</div>
      </PopoverContent>
    </div>
  );
}

이처럼 팝오버 컴포넌트는 간단한 사용법과 고급 커스터마이징 기능을 모두 제공합니다. 사용자는 자신의 상황에 맞는 적절한 방식을 선택하여 활용할 수 있습니다.

Styling

팝오버는 헤드리스 컴포넌트이기 때문에 기본적으로 스타일이 지정되어 있지 않습니다. 사용자는 자신의 전사적인 상황에 맞춰 스타일을 적용하면 됩니다.

사용자가 적절한 스타일링을 할 수 있도록 헤드리스 컴포넌트는 다양한 상태를 추적합니다. 팝오버가 열려 있는지 닫혀 있는지, 포커스, 활성화, 호버 상태인지 등을 추적하여 사용자에게 제공합니다.

Using Data attributes

헤드리스 컴포넌트는 추적한 상태를 data-* 속성으로 각 컴포넌트에 노출합니다. 예를 들어, 팝오버 컴포넌트가 열려 있으면 data-open 속성이 트리거 컴포넌트와 콘텐츠 컴포넌트에 표시됩니다.

이러한 속성을 CSS Attribute selectors를 사용하여 스타일링을 적용할 수 있습니다. 아래는 tailwind를 사용한 예시입니다.

function Example() {
  return (
    <Popover offset={5}>
      <Popover.Trigger className="text-black/50 data-[active]:text-black data-[hover]:text-black data-[focus]:outline-1 data-[focus]:outline-black">
        popover
      </Popover.Trigger>
      <Popover.Content className="data-[closed]:animate-opacity-show-reverse data-[open]:animate-opacity-show data-[hover]:ring-2 data-[hover]:ring-primary">
        <div>Popover content</div>
      </Popover.Content>
    </Popover>
  );
}

Component API 섹션에서 사용 가능한 data 속성들을 확인할 수 있습니다.

Using render props

컴포넌트는 render props를 통해 현재 상태 정보를 노출합니다. 이를 활용하여 상태에 따라 동적으로 스타일을 적용하거나 다른 내용을 렌더링할 수 있습니다.

import { ChevronUpIcon } from "@radix-ui/react-icons";
 
function Example() {
  return (
    <Popover placement="top">
      <Popover.Trigger>click</Popover.Trigger>
      <Popover.Content>
        {({ isOpen, placement }) => (
          <>
            {isOpen && (
              <ChevronUpIcon
                className={clsx(
                  "h-6 w-6 text-black/50",
                  placement === "top" && "rotate-180",
                  placement === "left" && "rotate-90",
                  placement === "top" && "rotate-270",
                )}
              />
            )}
            <div>{placement}</div>
          </>
        )}
      </Popover.Content>
    </Popover>
  );
}

Component API 섹션에서 사용 가능한 render props를 확인할 수 있습니다.

Positioning

Placement

팝오버의 앵커 요소에 대한 위치는 placement prop으로 조정할 수 있습니다. 또한 팝오버는 공간이 부족할 경우 자동으로 반대 방향으로 전환됩니다.

function Example() {
  return (
    <Popover placement={"left"}>
      <Popover.Trigger>click</Popover.Trigger>
      <Popover.Content>
        <div>Popover content</div>
      </Popover.Content>
    </Popover>
  );
}

Offset

팝오버의 앵커 요소에 대한 오프셋은 offset 속성을 사용하여 조정할 수 있습니다. offset 속성은 요소와 앵커 요소 사이의 주축을 따라 적용되는 간격을 제어합니다

function Example() {
  return (
    <Popover offset={50}>
      <Popover.Trigger>click</Popover.Trigger>
      <Popover.Content>
        <div>Popover content</div>
      </Popover.Content>
    </Popover>
  );
}

Flipping

기본적으로 usePopover는 원래 배치 위치가 화면 밖으로 렌더링되는 상황에서 주축을 따라 팝오버를 뒤집으려고 시도합니다. 이는 shouldFlip={false}를 설정하여 재정의할 수 있습니다.

function Example() {
  return (
    <Popover shouldFlip={false}>
      <Popover.Trigger>click</Popover.Trigger>
      <Popover.Content>
        <div>Popover content</div>
      </Popover.Content>
    </Popover>
  );
}

Using With usePopover hook

팝오버 컴포넌트의 상태 제어, 로직 확장, 또는 트리거 컴포넌트를 자체적으로 구현하는 등 유연한 커스터마이징이 필요한 경우,usePopover 훅을 활용할 수 있습니다. usePopover 훅은 팝오버 컴포넌트를 구축하는 데 필요한 모든 요소를 포함하고 있으며, 이를 통해 팝오버 컴포넌트를 완벽하게 제어할 수 있습니다.

function Example() {
  const { rootProps, isOpen } = usePopover({
    defaultOpen: false,
    placement: "left",
    offset: 50,
  });
 
  return (
    <Popover {...rootProps}>
      <Popover.Trigger>click</Popover.Trigger>
      <Popover.Content>
        <div>Popover content</div>
      </Popover.Content>
      {isOpen && <div>Popover is open</div>}
    </Popover>
  );
}

외부에서 상태나 ref를 관리해야 하는 경우, usePopover 훅에 인자를 전달할 수 있습니다. onChange prop을 사용하면 팝오버의 상태를 제어할 수 있습니다.

function Example() {
  const ref = useRef < HTMLButtonElement > null;
  const [isOpen, setOpen] = useState(false);
  const { rootProps } = usePopover({
    isOpen,
    onChange: setOpen,
    triggerRef: ref,
  });
 
  return (
    <Popover {...rootProps}>
      <Popover.Trigger>click</Popover.Trigger>
      <Popover.Content>
        <div>Popover content</div>
      </Popover.Content>
      {isOpen && <div>Popover is open</div>}
    </Popover>
  );
}

Customize logic

usePopover 훅을 활용하면 팝오버 제어 로직을 재사용하고 사용자 정의하여 팝오버의 동작을 원하는 대로 조정할 수 있습니다.

function Example() {
  const { rootProps, toggle, close } = usePopover({
    defaultOpen: false,
  });
 
  const handleClose = () => {
    logger(//...);
    close();
  };
 
  const handleToggle = () => {
	  logger(//...);
    toggle();
  };
 
  return (
    <Popover {...rootProps} onToggle={handleToggle}>
      <Popover.Trigger>click</Popover.Trigger>
      <Popover.Content>
        <button onClick={handleClose}>close</button>
        <div>Popover content</div>
      </Popover.Content>
    </Popover>
  );
}

Customizing component

usePopover 훅을 사용하면 Popover 컴포넌트가 제공하는 기본 트리거 컴포넌트 대신 자신만의 커스텀 트리거 컴포넌트를 만들 수 있습니다. usePopover로부터 triggerProps를 받아 이를 커스텀 컴포넌트에 전달하기만 하면 됩니다.

function Example() {
  const { rootProps, triggerProps } =
    usePopover <
    HTMLDivElement >
    {
      defaultOpen: false,
    };
 
  return (
    <>
      <div {...triggerProps}>click</div>
      <Popover {...rootProps}>
        <Popover.Content>
          <div>Popover content</div>
        </Popover.Content>
      </Popover>
    </>
  );
}

usePopovertriggerPropspopoverContentProps를 전달하면 컴파운드 패턴 없이 팝오버를 사용할 수 있습니다. usePopover 훅으로 팝오버의 모든 기능을 완벽하게 제어할 수 있습니다.

function Example() {
  const { triggerProps, popoverContentProps } =
    usePopover <
    HTMLDivElement >
    {
      defaultOpen: false,
    };
 
  return (
    <div>
      <PopoverTrigger as="div" {...triggerProps}>
        click
      </PopoverTrigger>
      <PopoverContent {...popoverContentProps} placement="left">
        <div>Popover content</div>
      </PopoverContent>
    </div>
  );
}

Rendering as different elements

기본적으로 Popover와 그 하위 컴포넌트들은 각각 해당 컴포넌트에 적합한 기본 요소로 렌더링됩니다. PopoverContent컴포넌트는 <div>로 렌더링되며, PopoverButton 컴포넌트는 <button>으로 렌더링됩니다.

as prop을 사용하여 컴포넌트를 다른 요소나 사용자 정의 컴포넌트로 렌더링할 수 있습니다. 이때 사용자 정의 컴포넌트가 ref를 전달 (opens in a new tab)하도록 해야 올바르게 연결할 수 있습니다.

const MyCustomButton = forwardRef<HTMLButtonElement, any>(function (props, ref) {
  return <button className="..." ref={ref} {...props} />;
});
 
function Example() {
  const { triggerProps, popoverContentProps } = usePopover({
    defaultOpen: false,
  });
 
  return (
    <>
      <PopoverTrigger {...triggerProps} as={MyCustomButton}>
        click
      </PopoverTrigger>
      <PopoverContent {...popoverContentProps}>
        <div>Popover content</div>
      </PopoverContent>
    </>
  );
}

Keyboard interaction

KeyDescription
Space팝오버 트리거라 focus 되어 있을 때 팝오버 열기/닫기
Enter팝오버 트리거라 focus 되어 있을 때 팝오버 열기/닫기
Tabfocus 할 수 있는 다음 요소로 focus 이동
Shift + Tabfocus 할 수 있는 이전 요소로 focus 이동
ESC팝오버를 닫고 팝오버 트리거로 focus

Component API

Popover

NameDefaultTypeDescription
isOpenfalseboolean팝오버가 기본적으로 열려있는지 여부(controlled).
defaultOpenfalseboolean팝오버가 기본적으로 열려있는지 여부(uncontrolled).
onChange-(isOpen: boolean) => void팝오버의 열린 상태가 변경되면 호출되는 핸들러
onOpen-void팝오버가 열릴 때 호출되는 핸들러
onToggle-void팝오버의 상태가 변경될 때 호출되는 핸들러
onClose-void팝오버의 닫힐 때 변경될 때 호출되는 핸들러
offset8number요소와 앵커의 주축에 따른 거리
placement“bottom”"top" | "right" | "bottom" | "left”앵커의 위지 기준으로 요소 배치
shouldFliptrueboolean렌더링할 공긴이 부족할 경우 방향을 뒤집을지 여부
triggerRefReact.RefObjectReact.RefObject트리거 요소의 ref를 외부에서 주입 받을 경우 사용
popoverRefReact.RefObjectReact.RefObject팝오버 컨텐츠 요소의 ref를 외부에서 주입 받을 경우 사용

PopoverTrigger(Popover.trigger)

NameDefaultTypeDescription
as‘button’React.ElementType팝오버 트리거가 렌더링 되어야하는 요소 혹은 구성요소
Data AttributeRender PropTypeDescription
data-openisOpenboolean팝오버가 열려있는지 여부
data-focusisFocusboolean팝오버 트리거에 포커스가 있는지 여부
data-activeisActiveboolean팝오버 트리거가 활성 상태인지 눌려진 상태인지 여부
data-hoverisHoveredboolean팝오버 트리거에 마우스 호버 여부

PopoverContent(Popover.content)

NameDefaultTypeDescription
asReact.ElementType팝오버 컨텐츠가 렌더링 되어야하는 요소 혹은 구성요소
Data AttributeRender PropTypeDescription
data-openisOpenboolean팝오버가 열려있는지 여부
data-closed-boolean팝오버가 닫혀있는지 여부
data-placementplacement"top" | "right" | "bottom" | "left”팝오버의 배치
data-focusisFocusboolean팝오버 컨텐츠에 포커스가 있는지 여부
data-activeisActiveboolean팝오버 컨텐츠가 활성 상태인지 눌려진 상태인지 여부
data-hoverisHoveredboolean팝오버 컨텐츠에 마우스 호버 여부

usePopover

props

usePopover의 props는 Popover 컴포넌트의 props와 동일합니다. usePopover는 팝오버 컴포넌트를 제어하고 확장하기 위한 로직을 제공합니다.

NameDefaultTypeDescription
isOpenfalseboolean팝오버가 기본적으로 열려있는지 여부(controlled).
defaultOpenfalseboolean팝오버가 기본적으로 열려있는지 여부(uncontrolled).
onChange-(isOpen: boolean) => void팝오버의 열린 상태가 변경되면 호출되는 핸들러
onOpen-void팝오버가 열릴 때 호출되는 핸들러
onToggle-void팝오버의 상태가 변경될 때 호출되는 핸들러
onClose-void팝오버의 닫힐 때 변경될 때 호출되는 핸들러
offset8number요소와 앵커의 주축에 따른 거리
placement“bottom”"top" | "right" | "bottom" | "left”앵커의 위지 기준으로 요소 배치
shouldFliptrueboolean렌더링할 공긴이 부족할 경우 방향을 뒤집을지 여부
triggerRefReact.RefObjectReact.RefObject트리거 요소의 ref를 외부에서 주입 받을 경우 사용
popoverRefReact.RefObjectReact.RefObject팝오버 컨텐츠 요소의 ref를 외부에서 주입 받을 경우 사용
return
NameDescription
rootProps컴파운드 패턴 사용 시 루트 컴포넌트에 구조 분해 할당으로 주입
triggerProps트리거 컴포넌트를 독립적으로 사용하거나 사용자 지정 구성요소에 적용할 때는 구조 분해 할당으로 속성을 주입
popoverContentProps컨텐츠 컴포넌트를 독립적으로 사용할 때 구조 분해 할당으로 속성을 주입
isOpen팝오버의 열기 상태
setOpen팝오버의 상태를 변경
open팝오버를 열기 상태로 변경
close팝오버를 닫기 상태로 변경
toggle팝오버의 열고 닫기 상태를 변경