Popover#
트리거 요소에 앵커된 플로팅 패널을 표시합니다. 12-direction 배치, viewport 충돌 시 자동 flip/shift, 외부 클릭/ESC 닫기, 모달 backdrop, 명시적 controller 제어, 트리거 follow를 지원합니다.
Live Preview#
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 보더 두께 / 반경 |
panelPadding | double? | panel 내부 패딩 |
panelShadow |
List<BoxShadow>? / String? |
panel 그림자 |
gap | double? | 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),
),
),
)
크로스 플랫폼 패리티#
| 항목 | Flutter | Web |
|---|---|---|
| 표시 방식 | 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) |