Popover
팝오버 컴포넌트
목차
트리거 요소에 앵커된 플로팅 패널을 표시합니다. 12-direction 배치, viewport 충돌 시 자동 flip/shift, 외부 클릭/ESC 닫기, 모달 backdrop, 명시적 controller 제어, 트리거 follow를 지원합니다.
기본 사용법 (Basic Usage)#
final controller = PopoverController();
Popover(
controller: controller,
openTrigger: CorePopoverTrigger.manual,
trigger: Button(
onPressed: controller.toggle,
variant: CoreButtonVariant.outline,
child: Text('Open Popover').labelLarge,
),
content: Text('This is the popover content.').bodyMedium,
)
final controller = PopoverController();
Popover(
controller: controller,
openTrigger: CorePopoverTrigger.manual,
trigger: Button(
onPressed: controller.toggle,
variant: CoreButtonVariant.outline,
child: Text('Open Popover').labelLarge,
),
content: Text('This is the popover content.').bodyMedium,
)
Live Preview#
코드 보기
class PopoverDefaultExample extends StatefulComponent {
const PopoverDefaultExample({super.key});
@override
State<PopoverDefaultExample> createState() => _PopoverDefaultExampleState();
}
class _PopoverDefaultExampleState extends State<PopoverDefaultExample> {
final _controller = PopoverController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Component build(BuildContext context) {
return Popover(
controller: _controller,
openTrigger: CorePopoverTrigger.manual,
trigger: Button(
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 = PopoverController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Popover(
controller: _controller,
openTrigger: CorePopoverTrigger.manual,
trigger: Button(
onPressed: _controller.toggle,
variant: CoreButtonVariant.outline,
child: Text('Open Popover').labelLarge,
),
content: Text('This is the popover content.').bodyMedium,
);
}
}
명시적 controller 제어#
trigger 외부에서 open/close를 직접 제어해야 할 때 PopoverController를
주입합니다. 색상 피커의 swatch toggle, form field의 dropdown 등이 대표 예시.
final controller = PopoverController();
Popover(
controller: controller,
trigger: Button(
onPressed: controller.toggle,
child: const Text('Toggle'),
),
content: const Text('Driven by controller.toggle()'),
)
controller.show(), controller.close(), controller.toggle(),
controller.isOpen, controller.addListener() 모두 양 플랫폼 동일.
Props / Parameters#
W는 Flutter의 Widget, Web의 Component를 뜻하는 합성 슬롯 타입입니다. PopoverController는 각 플랫폼 런타임에 연결되므로 인스턴스를 플랫폼 사이에 공유하지 않습니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
trigger | W | 필수 | 팝오버를 열 트리거 요소 |
content | W | 필수 | 팝오버 콘텐츠 |
placement |
CorePopoverPlacement |
bottom |
12-direction 배치 |
controller |
PopoverController? |
null |
외부 imperative 제어 |
openTrigger |
CorePopoverTrigger |
click |
click / hover / focus / manual |
modal |
bool |
false |
배경 backdrop으로 다른 입력 차단 |
dismissOnOutsideTap |
bool |
true |
외부 클릭 시 닫기 |
dismissOnEscape |
bool |
true |
ESC 키로 닫기 |
closeOnScroll |
bool |
false |
트리거 조상이 스크롤할 때 닫기. Web은 문서·뷰포트 스크롤도 포함 |
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 |
닫힘 콜백 |
기본 Popover는 스크롤해도 열려 있습니다. closeOnScroll: true를 지정하면
트리거 조상 스크롤에서 닫히며, 패널 안이나 무관한 스크롤 영역의 스크롤은
닫힘 대상이 아닙니다. Select는 별도 기본값 true를 전달하므로 두 컴포넌트의
기본 동작이 다릅니다. 이미 열린 상태에서 값을 바꿔도 새 정책이 적용됩니다.
스타일 시스템 (Style System)#
popoverStyle 슬롯#
Popover 의 panel chrome / 슬롯 미세 조정은 단일 popoverStyle
(CorePopoverStyle) 으로 흐릅니다. 동작 정책 (placement,
openTrigger, modal, dismiss*, collision,
*Constraint) 은 위젯
파라미터.
Popover(
placement: CorePopoverPlacement.bottomStart,
trigger: Button(child: Text('Open')),
content: ...,
popoverStyle: CorePopoverStyle(
panelBackgroundColor: cs.surface,
panelBorderColor: cs.outline,
panelBorderRadius: CoreBorderRadius.all(12),
panelPadding: CoreEdgeInsets.all(16),
placementOffset: 8,
triggerSizing: CorePopoverTriggerSizing.expand,
openAnimationDuration: Duration(milliseconds: 150),
closeAnimationDuration: Duration(milliseconds: 100),
triggerStyle: CoreButtonStyle( // chrome 미세 조정
paddingH: 16,
labelStyle: CoreTextStyle(fontWeight: CoreFontWeight.semiBold),
),
),
)
CorePopoverStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
panelStyle |
CorePopupStyle? |
Panel chrome override. Nested [CorePopupStyle] — the popover panel box is a composed
Popup
, so its chrome (background / border / radius / shadow / padding / min-width / content colour) is absorbed through this single nested slot and forwarded raw to
Popup
(child-component-composition).
null
→ [defaultPanelStyle] (which itself defers most fields to
CorePopupStyle
's defaults).
|
placementOffset |
double? |
Trigger-to-panel placement offset (logical px) — applied along the placement axis (top/bottom or left/right depending on
CorePopoverPlacement
) to push the floating panel away from the trigger edge. Maps to a Flutter
Offset
via
placement_ext.toPopoverOffset
and to the Web floating-controller placement-axis distance.
|
viewportPadding |
CoreEdgeInsets? |
Viewport-edge padding enforced when applying [CorePopoverCollision.shift] — the panel won't paint within this much of the visible viewport edge regardless of placement. Per CLAUDE.md rule 2 (multi-side padding/margin → [CoreEdgeInsets]), each side accepts its own value so popovers can avoid colliding with fixed headers, banners, or safe-area insets (e.g.
CoreEdgeInsets.only(top: 64)
reserves a 64-px sticky-header strip).
null
→ token default ([defaultViewportPadding]).
|
triggerSizing |
CorePopoverTriggerSizing? |
Trigger sizing behaviour (intrinsic / expand / matchPanel). |
openAnimationDuration |
Duration? |
Open animation duration override. |
closeAnimationDuration |
Duration? |
Close animation duration override. |
triggerStyle |
CoreButtonStyle? |
Trigger button style override. Nested
CoreButtonStyle
so any chrome / dimensional / nested-slot subset can be overridden without touching the panel chrome. To change the trigger's
variant
(e.g. outline vs primary), inject the trigger widget directly via
Popover(trigger:...)
rather than packing variant into this style (asChild pattern). Carries no
defaultTriggerStyle
. A popover diverges from
Button
's own chrome in
nothing
— it anchors whatever trigger it is handed — so the only fillable partial would restate the child's defaults, which
style-contract.md
forbids (the parent must not reach into the child's defaults) and which would silently stop tracking
Button
the next time those defaults move. Absence also keeps a live fallback reachable: DatePicker and TimePicker resolve their trailing icon as
merged.trailingIconStyle ?? popoverStyle.triggerStyle ?.trailingIconStyle ?? defaultTrailingIconStyle
, so a default carrying a
trailingIconStyle
would make that third rung dead on both platforms.
|
asChild 패턴 — trigger variant 변경#
trigger 의 시맨틱 (variant) 변경이 필요할 때는 popoverStyle.triggerStyle
안에 packed 하지 말고 위젯 자체를 슬롯으로 주입합니다:
Popover(
trigger: Button(variant: .outline, child: Text('Filter')), // ← 변형 직접
content: ...,
)
popoverStyle.triggerStyle 은 chrome 미세 조정용입니다 (padding /
labelStyle 등).
Resolve chain#
design system default
→ CorePopoverTheme.style // 프로젝트 공통
→ 부모 컴포넌트 슬롯 오버라이드 (예: CoreSelectStyle.popoverStyle)
→ widget.popoverStyle // 인스턴스별
빠른 오버라이드 (Chain)#
이미 만든 Popover 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class PopoverChainExample extends StatefulWidget {
const PopoverChainExample({super.key});
@override
State<PopoverChainExample> createState() => _PopoverChainExampleState();
}
class _PopoverChainExampleState extends State<PopoverChainExample> {
final _controller = PopoverController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Popover(
controller: _controller,
openTrigger: CorePopoverTrigger.manual,
trigger: Button(
onPressed: _controller.toggle,
variant: CoreButtonVariant.outline,
child: Text('Open Popover').labelLarge,
),
content: Text('This is the popover content.').bodyMedium,
).withStyle(
const CorePopoverStyle(
placementOffset: CoreSpace.space24,
openAnimationDuration: Duration(milliseconds: CoreDuration.moderate),
panelStyle: CorePopupStyle(
padding: CoreEdgeInsets.all(CoreSpace.space20),
borderRadius: CoreBorderRadius.all(CoreRadius.radius16),
borderWidth: CoreStrokeWidth.stroke2,
borderColor: CoreColor.token(CoreColors.primary),
),
),
);
}
}
class PopoverChainExample extends StatefulComponent {
const PopoverChainExample({super.key});
@override
State<PopoverChainExample> createState() => _PopoverChainExampleState();
}
class _PopoverChainExampleState extends State<PopoverChainExample> {
final _controller = PopoverController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Component build(BuildContext context) {
return Popover(
controller: _controller,
openTrigger: CorePopoverTrigger.manual,
trigger: Button(
onPressed: _controller.toggle,
variant: CoreButtonVariant.outline,
child: Text('Open Popover').labelLarge,
),
content: Text('This is the popover content.').bodyMedium,
).withStyle(
const CorePopoverStyle(
placementOffset: CoreSpace.space24,
openAnimationDuration: Duration(milliseconds: CoreDuration.moderate),
panelStyle: CorePopupStyle(
padding: CoreEdgeInsets.all(CoreSpace.space20),
borderRadius: CoreBorderRadius.all(CoreRadius.radius16),
borderWidth: CoreStrokeWidth.stroke2,
borderColor: CoreColor.token(CoreColors.primary),
),
),
);
}
}
테마 커스터마이징 (Theme Customization)#
CoreComponentTheme(
popover: CorePopoverTheme(
style: CorePopoverStyle(
panelBackgroundColor: cs.surface,
panelBorderColor: cs.outline,
panelBorderRadius: CoreBorderRadius.all(12),
panelPadding: CoreEdgeInsets.all(20),
placementOffset: 12,
openAnimationDuration: Duration(milliseconds: 250),
closeAnimationDuration: Duration(milliseconds: 100),
),
),
)
배치 옵션 (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
동작 스펙 (Behavior)#
openTrigger가 click이면 트리거 조작으로 열기/닫기를 토글하고, hover이면 진입/이탈, focus이면 포커스 변화, manual이면 controller로 표시를 제어합니다.
placement는 앵커 방향, collision은 viewport 충돌 시 flip/shift 정책, width/height constraint는 앵커와의 상대 사이징을 정합니다. 바깥 클릭과 Escape 닫기 플래그는 각 플랫폼의 overlay dismissal 경로에 전달되며 실제 키보드 제약은 아래 접근성 절을 확인하세요.
modal은 backdrop을 요청합니다. 닫힘은 onClose로 통보되며 자체 선택 값은 없습니다.
트리거와 판 사이 기본 간격은 CorePopoverStyle.defaultPlacementOffset(4)입니다. 오프셋을 따로 적지 않은 합성 컴포넌트(DatePicker · TimePicker · ColorPicker · PhoneInput · Fab 팝업)가 같은 값을 따르고, Select · Autocomplete 는 스스로 4 를 적습니다.
사용 가이드라인 (Usage Guidelines)#
트리거에 붙는 임의 콘텐츠 패널에는 Popover를 사용합니다. 짧은 도움말은 Tooltip, 호버 정보 카드는 HoverCard, 사용자 확인과 모달 대화는 Dialog, 선택 값 목록은 Select를 선택합니다.
위치·열림 상태를 이미 소유한 표면에 chrome만 필요하면 Popup을 합성합니다.
✅ Do#
키보드로 열어야 하는 트리거는 .manual + controller로
final controller = PopoverController();
Popover(
controller: controller,
openTrigger: CorePopoverTrigger.manual,
trigger: Button(
onPressed: controller.toggle,
child: const Text('Toggle'),
),
content: const Text('Driven by controller.toggle()'),
)
Button의 onPressed는 포커스된 상태에서 Enter/Space로도 활성화되므로, 키보드로 열어야 하는 트리거는 openTrigger: .manual + controller.toggle 조합이 안전합니다.
❌ Don't#
기본 .click 트리거가 Flutter에서 키보드로 열릴 거라 기대하지 않기
// ❌ 기본 .click 트리거가 키보드로 열릴 거라 기대
Popover(
openTrigger: CorePopoverTrigger.click, // 기본값
trigger: myFocusableTrigger,
content: someContent,
)
Flutter의 .click 트리거는 raw 포인터 Listener로 구현되어 있어 포커스된 트리거에서 Enter/Space를 눌러도 열리지 않습니다(Web은 열립니다). 키보드 접근이 필요하면 위 Do처럼 .manual + controller를 쓰세요.
접근성 (Accessibility)#
역할#
두 플랫폼이 다르게 말합니다.
| Flutter | Web | |
|---|---|---|
| 트리거 | Semantics(container: true, expanded: …) — 열림/닫힘 상태만, 역할 없음 |
aria-haspopup="dialog"
+
aria-expanded
+ 열려 있는 동안
aria-controls
(패널 id)
|
| 패널 | 합성된 Popup의 Semantics(container: true) — dialog 역할 없음 |
role="dialog" (+ data-placement) |
| modal 배경 | modal: true일 때만 BlockSemantics + ExcludeSemantics |
OverlayHost가 modal entry를 받으면 앱 subtree에
inert
; backdrop 자체는
aria-hidden="true"
|
aria-modal · aria-label/aria-labelledby · aria-describedby는
어느 플랫폼에도 없습니다 — 패널에는 접근 가능한 이름이 없습니다. Web 은 열려 있는 동안만 aria-controls 로 트리거를 패널에 잇습니다(닫혀 있을 때는 패널이 문서에 없어, 없는 id 를 가리키지 않도록 뺍니다).
Flutter 트리거의 expanded 는 팝오버가 열리고 닫힐 때마다 스스로 다시 그려집니다 — 컨트롤러로 열었든 바깥 탭·Escape 로 닫혔든, 팝오버를 합성한 컴포넌트가 다시 그려지지 않아도 상태가 따라옵니다.
키보드#
처리하는 키는 Escape 하나뿐이며, 그 조건이 플랫폼마다 다릅니다.
| 키 | Flutter | Web |
|---|---|---|
Escape |
modal: true 이고 dismissOnOutsideTap: true일 때만 닫힘 |
dismissOnEscape: true(기본값)이면 닫힘 — document 레벨 리스너라 modal 여부·포커스 위치와 무관 |
Flutter에서 dismissOnEscape는 Escape를 제어하지 않습니다. 이 파라미터는 내부적으로 오버레이의 dismissBackdropFocus(= FocusScope의 autofocus)로 전달되고, Escape 단축키는 modal && dismissOnOutsideTap일 때만 설치됩니다. 따라서 기본 설정(modal: false, dismissOnEscape: true)에서 Flutter는 Escape로 닫히지 않습니다. Web은 같은 설정에서 닫힙니다.
키보드로 여는 것도 갈립니다. openTrigger: .click일 때 Web은 트리거 래퍼의 click
이벤트를 듣기 때문에 안쪽 버튼을 Enter/Space로 활성화하면 합성된 DOM click이 올라와 열립니다. Flutter는 raw 포인터 Listener(TriggerTapDetector)를 쓰므로
포커스된 트리거에서 Enter/Space를 눌러도 열리지 않습니다.
패널 안쪽에는 방향키 이동 · Tab 순환 · Home/End 처리가 양 플랫폼 모두 없습니다.
포커스 관리#
-
Flutter (
modal: true) — 전용FocusScopeNode를 만들고, post-frame에requestFocus()로 포커스를 패널 안으로 옮깁니다. - Flutter (
modal: false, 기본값) — scope 노드가 없어 실질적인 포커스 격리가 없습니다. -
Web (
modal: true, OverlayHost 경유) — modal entry가 마운트되면 호스트가 앱 subtree에inert를 붙여 배경을 접근성 트리·Tab 순서·hit-test에서 제외합니다. 별도 JS 포커스 순환 트랩은 없으며 패널의 초기 포커스는 직접 이동하지 않습니다. - Web (
modal: false) — 앱 subtree에 inert를 붙이지 않아 배경이 계속 동작합니다. - Web (OverlayHost 없는 fallback) — 패널과 backdrop을 트리거 옆에 직접 렌더하며 호스트의 배경 inert 처리는 제공되지 않습니다. modal containment가 필요하면 OverlayHost 경로를 사용하세요.
닫을 때 트리거로 포커스를 되돌리는 동작은 양 플랫폼 모두 없습니다.
스크린 리더#
-
Web — 트리거는 dialog 팝업 있음 / 펼침·접힘으로 읽힙니다. 패널에는
role="dialog"가 있지만 이름과 초기 포커스 이동은 없습니다. modal + OverlayHost 경로는 앱 배경을 inert로 제외하고, non-modal 또는 호스트 없는 fallback은 배경 콘텐츠를 계속 노출합니다.modal: true이고dismissOnOutsideTap: true이면 패널 곁에 시각적으로 숨긴role="button"노드가 지역화된dismissLabel(ko: "닫기")을 이름으로 가져, 스크린 리더가 활성화하면 팝오버가 닫힙니다(tabindex="-1"이라 탭 순서에는 들지 않음 — 키보드에는 Escape가 있습니다). 이 이름은 닫기 동작의 이름이지 패널 내용의 이름이 아닙니다. -
Flutter — 트리거는 역할도 팝업 힌트도 없이 펼침/접힘 상태만 읽힙니다. 패널은 이름도 역할도 없는 시맨틱 컨테이너입니다.
modal: true이면 배경이 트리에서 제거되고 포커스가 패널로 들어가며,dismissOnOutsideTap: true까지 켜져 있으면 패널이 접근성dismiss동작을 노출합니다 — 지역화된dismissLabel(ko: "닫기")을 이름으로 가져, 스크린 리더의 닫기 제스처와 그 노드의 활성화가 팝오버를 닫습니다(이 이름은 닫기 동작의 이름이지 패널 내용의 이름이 아닙니다). 기본값modal: false에서는 배경이 그대로 읽히고 포커스도 이동하지 않으며 닫기 동작도 없습니다.
알려진 제약#
- Flutter는 패널에 dialog 역할을, 트리거에 팝업 힌트를 내보내지 않습니다(Web은 둘 다 내보냅니다).
- Flutter는 기본
.click트리거를 키보드로 열 수 없습니다. - Flutter의
dismissOnEscape는 이름과 달리 Escape를 켜지 않습니다(위 경고 참조). - Web의 호스트 기반 modal 배경 격리와 패널 초기 포커스·복귀는 별개입니다. 패널로 직접 포커스를 옮기거나 트리거로 복원하는 동작은 없습니다.
-
어느 플랫폼도 패널에 접근 가능한 이름을 주지 않고
aria-modal을 설정하지 않습니다. (모달 패널의dismissLabel은 두 플랫폼 모두 닫기 동작의 이름이지 패널의 이름이 아닙니다.) 트리거↔패널 연결(aria-controls)은 Web 만 합니다 — Flutter 에는 그 관계를 말할 시맨틱 속성이 없습니다. -
openTrigger: .hover는 양 플랫폼 모두 포인터 전용이라 포커스 대응이 없습니다. Web의.focus트리거는tabindex없는 래퍼<div>에 버블링하지 않는focus/blur를 바인딩합니다.
따라서 접근 가능한 이름과 포커스 관리가 필요한 용도(모달 대화 상자 등)에는 지금 이 컴포넌트만으로 충분하지 않습니다 — 이름은 content
안에서, 포커스 이동·복귀는 호출자 코드에서 직접 처리해야 합니다.
전역으로 보장되는 항목(동작 줄이기·고대비·색 강제 모드 등)은 전역 접근성 축을 참고하세요.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 표시 방식 | root Overlay의 entry | OverlayHost portal의 fixed 패널 |
| 트리거 이벤트 | click은 Listener 기반 TriggerTapDetector; hover/focus 래퍼 | DOM click/mouseenter/focus |
| 외부 클릭 닫기 | overlay barrier | document 클릭 리스너 |
| Escape 플래그 | overlay의 focus-dismiss 경로; 키 처리 제약은 접근성 절 참조 | document keydown |
| 스크린 리더의 닫기 | modal + dismissOnOutsideTap 패널의 dismiss 접근성 동작 |
같은 조건에서 패널 곁의 숨긴 role="button" 노드 (탭 순서 밖) |
| 충돌·앵커 추적 | render layout와 Ticker | DOM 위치 측정, resize/scroll 관찰 |
| 모달 배경 | OverlayEntry barrier와 시맨틱 차단 | portal backdrop + OverlayHost의 앱 subtree inert; inline fallback에는 호스트 격리 없음 |
| 등장·퇴장 | overlay 위젯의 애니메이션 | 패널 CSS 전환과 지연 unmount |
onClose 시점 | entry의 닫힘 future 완료 후 | 닫기 시작 시 호출 후 전환·unmount |
스타일의 등장·퇴장 시간은 Duration, 배치 간격과 viewport margin은 논리 px 단위입니다. 같은 Core 정책을 쓰더라도 focus·DOM 이벤트·overlay lifecycle은 위와 같이 각 런타임에서 실행됩니다.
레거시 vs 통일 비교 (Migration Notes)#
Flutter 의 앵커드 오버레이(Popover·Tooltip·HoverCard·DropdownMenu·Menubar·FormField)는 전부 이 문서의
Popover 가 쓰는 것과 같은 단일 엔진(PopoverOverlayHandler)을 직접 호출합니다. 이전 버전의 CoUI 를 참조하는 코드에는 컴포넌트마다 갈아끼우는 중앙
OverlayManager + PopoverController 경유 방식이 남아 있을 수 있습니다.
| 항목 | 레거시 (중앙 OverlayManager + 컴포넌트별 핸들러) |
통일 (PopoverOverlayHandler 직접 호출) |
|---|---|---|
| 위치·dismiss·theme capture 로직 | 컴포넌트마다 갈아끼우는 핸들러에 분산 | 엔진 하나에 집중 |
| 신규 앵커드 컴포넌트 추가 | 새 핸들러 작성 필요 | 같은 엔진을 그대로 호출 |
| public API | PopoverController |
동일 (PopoverController) — 사용자 코드 변경 불필요 |
이 사용자 API(PopoverController, Popover/Tooltip/HoverCard
생성자)는 두 방식 모두에서 동일합니다 — 엔진 통합은 내부 구현 정리이며 이 문서의 위 표는 라이브러리 유지보수 맥락을 이해하는 데 참고용입니다.