Popover | CoUI

Popover

팝오버 컴포넌트

Popover#

트리거 요소에 앵커된 플로팅 패널을 표시합니다. 12-direction 배치, viewport 충돌 시 자동 flip/shift, 외부 클릭/ESC 닫기, 모달 backdrop, 명시적 controller 제어, 트리거 follow를 지원합니다.

Live Preview#

Web
Flutter
Loading Flutter...
class PopoverDefaultExample extends StatefulComponent {
  const PopoverDefaultExample({super.key});

  @override
  State<PopoverDefaultExample> createState() => _PopoverDefaultExampleState();
}

class _PopoverDefaultExampleState extends State<PopoverDefaultExample> {
  final _controller = CoPopoverController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Component build(BuildContext context) {
    return CoPopover(
      controller: _controller,
      openTrigger: CorePopoverTrigger.manual,
      trigger: CoButton(
        onPressed: _controller.toggle,
        variant: CoreButtonVariant.outline,
        child: Text('Open Popover').labelLarge,
      ),
      content: Text('This is the popover content.').bodyMedium,
    );
  }
}
class PopoverDefaultExample extends StatefulWidget {
  const PopoverDefaultExample({super.key});

  @override
  State<PopoverDefaultExample> createState() => _PopoverDefaultExampleState();
}

class _PopoverDefaultExampleState extends State<PopoverDefaultExample> {
  final _controller = CoPopoverController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CoPopover(
      controller: _controller,
      openTrigger: CorePopoverTrigger.manual,
      trigger: CoButton(
        onPressed: _controller.toggle,
        variant: CoreButtonVariant.outline,
        child: Text('Open Popover').labelLarge,
      ),
      content: Text('This is the popover content.').bodyMedium,
    );
  }
}

기본 사용법 (Basic Usage)#

final controller = CoPopoverController();

CoPopover(
  controller: controller,
  openTrigger: CorePopoverTrigger.manual,
  trigger: CoButton(
    onPressed: controller.toggle,
    variant: CoreButtonVariant.outline,
    child: Text('Open Popover').labelLarge,
  ),
  content: Text('This is the popover content.').bodyMedium,
)
final controller = CoPopoverController();

CoPopover(
  controller: controller,
  openTrigger: CorePopoverTrigger.manual,
  trigger: CoButton(
    onPressed: controller.toggle,
    variant: CoreButtonVariant.outline,
    child: Text('Open Popover').labelLarge,
  ),
  content: Text('This is the popover content.').bodyMedium,
)

명시적 controller 제어#

trigger 외부에서 open/close를 직접 제어해야 할 때 CoPopoverController를 주입합니다. 색상 피커의 swatch toggle, form field의 dropdown 등이 대표 예시.

final controller = CoPopoverController();

CoPopover(
  controller: controller,
  trigger: CoButton(
    onPressed: controller.toggle,
    child: const Text('Toggle'),
  ),
  content: const Text('Driven by controller.toggle()'),
)

controller.show(), controller.close(), controller.toggle(), controller.isOpen, controller.addListener() 모두 양 플랫폼 동일.

스타일 시스템 — popoverStyle (Epic #1302)#

CoPopover 의 panel chrome / 슬롯 미세 조정은 단일 popoverStyle (CorePopoverStyle) 으로 흐릅니다. 동작 정책 (placement, openTrigger, modal, dismiss*, collision, *Constraint) 은 위젯 파라미터.

CoPopover(
  placement: CorePopoverPlacement.bottomStart,
  trigger: CoButton(child: Text('Open')),
  content: ...,
  popoverStyle: CorePopoverStyle(
    panelBackgroundColor: cs.surface,
    panelBorderColor: cs.outline,
    panelBorderRadius: 12,
    panelPadding: 16,
    gap: 8,
    triggerSizing: CorePopoverTriggerSizing.expand,
    openAnimationDuration: Duration(milliseconds: 150),
    closeAnimationDuration: Duration(milliseconds: 100),
    triggerStyle: CoreButtonStyle(           // chrome 미세 조정
      paddingH: 16,
      labelStyle: CoreTextStyle(fontWeight: CoreFontWeight.semiBold),
    ),
  ),
)

CorePopoverStyle 필드#

필드타입설명
panelBackgroundColor / panelBorderColor Color? / String? panel 배경 / 보더 색
panelBorderWidth / panelBorderRadius double? panel 보더 두께 / 반경
panelPaddingdouble?panel 내부 패딩
panelShadow List<BoxShadow>? / String? panel 그림자
gapdouble?trigger ↔ panel 간격
triggerSizing CorePopoverTriggerSizing? trigger wrapper 사이징 (intrinsic / expand / matchPanel)
openAnimationDuration Duration? open 애니메이션 (기본 150ms)
closeAnimationDuration Duration? close 애니메이션 (기본 100ms)
triggerStyle CoreButtonStyle? trigger 가 CoButton 일 때 chrome 미세 조정 (variant 변경은 위젯 슬롯 주입)

asChild 패턴 — trigger variant 변경#

trigger 의 시맨틱 (variant) 변경이 필요할 때는 popoverStyle.triggerStyle 안에 packed 하지 말고 위젯 자체를 슬롯으로 주입합니다 (Epic #1302 원칙 8):

CoPopover(
  trigger: CoButton(variant: .outline, child: Text('Filter')),  // ← 변형 직접
  content: ...,
)

popoverStyle.triggerStyle 은 chrome 미세 조정용입니다 (padding / labelStyle 등).

Resolve chain#

design system default
  → CorePopoverTheme.style                       // 프로젝트 공통
  → 부모 컴포넌트 슬롯 오버라이드 (예: CoreSelectStyle.popoverStyle)
  → widget.popoverStyle                          // 인스턴스별

Props / Parameters#

속성타입기본값설명
trigger Widget / Component 필수 팝오버를 열 트리거 요소
content Widget / Component 필수 팝오버 콘텐츠
placement CorePopoverPlacement bottom 12-direction 배치
controller CoPopoverController? null 외부 imperative 제어
openTrigger CorePopoverTrigger click click / hover / focus / manual
modal bool false 배경 backdrop으로 다른 입력 차단
dismissOnOutsideTap bool true 외부 클릭 시 닫기
dismissOnEscape bool true ESC 키로 닫기
collision Set<CorePopoverCollision>? null (flip+shift) viewport 충돌 정책
widthConstraint CorePopoverConstraint? null (flexible) width 사이징 정책
heightConstraint CorePopoverConstraint? null (flexible) height 사이징 정책
popoverStyle CorePopoverStyle? null panel chrome / nested triggerStyle 묶음
onClose VoidCallback? null 닫힘 콜백

배치 옵션 (12-direction Placement)#

// 기본 4방향
CorePopoverPlacement.top
CorePopoverPlacement.bottom
CorePopoverPlacement.left
CorePopoverPlacement.right

// 8개 코너 (자동 flip/shift 시 활용)
CorePopoverPlacement.topStart
CorePopoverPlacement.topEnd
CorePopoverPlacement.bottomStart
CorePopoverPlacement.bottomEnd
CorePopoverPlacement.leftStart
CorePopoverPlacement.leftEnd
CorePopoverPlacement.rightStart
CorePopoverPlacement.rightEnd

충돌 정책#

// 기본 (flip + shift) — viewport 부딪히면 반대편으로 flip, 안 되면 axis shift
collision: const {CorePopoverCollision.flip, CorePopoverCollision.shift}

// flip만 — 반대편 공간 부족하면 그대로 잘림
collision: const {CorePopoverCollision.flip}

// 충돌 무시
collision: const {CorePopoverCollision.none}

Constraint (anchor-relative 사이징)#

// panel 너비를 trigger와 정확히 일치 (Select 패턴)
widthConstraint: CorePopoverConstraint.anchorFixedSize

// panel 너비 ≤ trigger 너비
widthConstraint: CorePopoverConstraint.anchorMaxSize

// panel 너비 ≥ trigger 너비
widthConstraint: CorePopoverConstraint.anchorMinSize

// content 자연 너비 (기본)
widthConstraint: CorePopoverConstraint.flexible

테마 커스터마이징#

CoreComponentTheme(
  popover: CorePopoverTheme(
    style: CorePopoverStyle(
      panelBackgroundColor: cs.surface,
      panelBorderColor: cs.outline,
      panelBorderRadius: 12,
      panelPadding: 20,
      gap: 12,
      openAnimationDuration: Duration(milliseconds: 250),
      closeAnimationDuration: Duration(milliseconds: 100),
    ),
  ),
)

크로스 플랫폼 패리티#

항목FlutterWeb
표시 방식 Overlay.of(rootOverlay: true).insert CoOverlayHost portal (popover 레이어, position: fixed)
Trigger 이벤트 GestureDetector / MouseRegion / Focus DOM click / mouseenter / focus
외부 클릭 닫기 Listener overlay barrier document.addEventListener('click')
ESC 닫기 FocusScope + dismissBackdropFocus document.addEventListener('keydown')
자동 flip/shift RenderShiftedBox collision getBoundingClientRect + viewport 비교
Anchor follow Ticker 매 프레임 ResizeObserver + window scroll/resize
Modal backdrop OverlayEntry barrier portal 레이어의 <div fixed inset-0 z-{N}>
Open animation TweenAnimationBuilder (scale 0.9→1.0, opacity 0→1) CSS transition (scale-90→scale-100, opacity-0→opacity-100)
Open duration CorePopoverTokens.openAnimationDuration (150ms) 동일 — inline transition-duration: 150ms
Close duration CorePopoverTokens.closeAnimationDuration (100ms) 동일 — close 시 transition-duration 가 100ms × 2/3 = 66ms 로 emit (CSS 가 Interval(0, 2/3) 를 직접 표현 못 해 가시 시간 압축으로 등가 매핑)
Open curve Curves.linear (CorePopoverCurve.linear) transition-timing-function: linear
Close curve Interval(0, 2/3) (CorePopoverCurve.accelerate23) linear timing + duration 단축으로 동등 효과
Controller API 동일 (CoPopoverController) 동일 (CoPopoverController)

관련 컴포넌트#

  • Tooltip: 호버 시 짧은 텍스트 정보
  • Dialog: 모달 방식의 대화 상자
  • HoverCard: 호버 시 카드 형태 정보