[기능] floating-ui로 Popover 위치 제어하기

짜장킴·2025년 9월 21일

프로젝트

목록 보기
27/38

floating-ui란?

  • Popover/Tooltip/Dropdown 위치 제어 라이브러리
  • 자동 위치 보정, 충돌 방지, offset, flip, shift 제공

floating-ui 사용 예시

  • placement: "right-start" : 기준 요소(reference)의 오른쪽 상단을 기준으로 패널을 붙임
  • whileElementsMounted: autoUpdate : 스크롤/리사이즈/레이아웃 변경 시 자동으로 재배치
  • middleware
    - offset(8) : 기준과 패널 사이 여백(px)
    - flip() : 배치 방향이 화면 밖으로 나갈 때 반대 방향으로 뒤집기
    - shift({ padding: 12 }) : 가장자리(뷰포트 경계)에서 안쪽으로 밀어넣기 / padding만큼 여백 유지
    - size({...}) : 가용 공간(가령 높이)에 맞춰 패널 크기(예: maxHeight)를 동적으로 조정

반환값

  • x, y : 계산된 좌표 / top/left에 그대로 바인딩
  • strategy : "absolute"(기본) 또는 "fixed"
  • refs.setReference : 기준 요소(보통 버튼, 아이콘 등)에 연결
  • refs.setFloating: 실제로 떠야 하는 요소(Popover 패널, Tooltip 등)에 연결

코드 예시

부모 컴포넌트

  • 클릭된 셀 DOM(dayEl)을 기준 요소(referenceEl) 로 저장
  • 이 값을 상태로 관리해 자식 모달에 props로 전달
  const handleDateClick = (arg: DateClickArg) => {
    setClickedDate(arg.date);
    setReferenceEl(arg.dayEl as HTMLElement);
    setIsOpen(true);
  };

자식 컴포넌트

  • useFloating 훅을 사용해 x, y, strategy 좌표와 refs(reference/floating 연결 ref)를 얻음
  • useEffect로 부모에서 받은 referenceEl을 refs.setReference에 연결
  • 실제 떠 있는 패널은 refs.setFloating을 ref로 달아서 자동 위치 계산 적용
import {
  useFloating,
  offset,
  flip,
  shift,
  size,
  autoUpdate,
} from "@floating-ui/react";
import { useEffect } from "react";

export function ReservationInfoModal({ isOpen, onClose, referenceEl }: Props) {
  const { x, y, refs, strategy } = useFloating({
    placement: "right-start",            
    whileElementsMounted: autoUpdate,
    middleware: [
      offset(8),                        
      flip(),                           
      shift({ padding: 12 }),           
      size({
        apply({ availableHeight, elements }) {
          // 뷰포트 가용 높이에 맞춰 최대 높이 제한 + 스크롤 가능
          Object.assign(elements.floating.style, {
            maxHeight: `${Math.max(availableHeight, 480)}px`,
            overflowY: "auto",
          });
        },
      }),
    ],
  });

  useEffect(() => {
    if (referenceEl) refs.setReference(referenceEl);
  }, [referenceEl, refs]);

  if (!isOpen || !referenceEl) return null;

  return (
    <>
      {/* 배경 클릭으로 닫기 (모달처럼 동작시키고 싶을 때) */}
      <div className="fixed inset-0 z-40" onClick={onClose} aria-hidden="true" />
      {/* floating 패널 */}
      <div
        ref={refs.setFloating}
        style={{ position: strategy, top: y ?? 0, left: x ?? 0 }}
        className="z-50 min-w-[370px] max-w-[430px] w-full rounded-[10px] border bg-white"
      >
        {/* Popover/내용… */}
      </div>
    </>
  );
}
profile
프론트엔드 취준생입니다.

0개의 댓글