harden ClideAnchoredOverlay positioning + add an anchored test harness (T-288)
Root-cause of the theme-picker friction: the primitive's focus model is fine (keyboard nav reaches a ClideMenu through the overlay), but the shared harness() uses Overlay(canSizeOverlay) + a zero-size MediaQuery, which mispositions an anchored follower off-screen and defeats autoFlip. - autoFlip now reads the real view size (View.physicalSize) instead of MediaQuery.size, so it flips correctly even when MediaQuery is overridden. - Drop the inner Align in the follower — it pegged the panel to a corner of the full-screen follower box and broke hit-testing for non-top-left anchors, so end-aligned menu items weren't mouse-tappable. - Add anchoredHarness() — a properly-sized Overlay tree for testing popover content (the remaining migrations will use it). Tests: keyboard nav through the overlay, an end-aligned item is mouse-tappable, and autoFlip flips below->above near the bottom edge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -161,14 +161,18 @@ class _ClideAnchoredOverlayState extends State<ClideAnchoredOverlay> {
|
||||
var side = widget.side;
|
||||
if (!widget.autoFlip || widget.centered) return side;
|
||||
final box = context.findRenderObject();
|
||||
final media = MediaQuery.maybeOf(context);
|
||||
if (box is RenderBox && box.hasSize && media != null) {
|
||||
// Use the real view size, not MediaQuery — the latter can be overridden to
|
||||
// zero (e.g. the shared test harness), which would defeat the flip.
|
||||
final view = View.maybeOf(context);
|
||||
if (box is RenderBox && box.hasSize && view != null) {
|
||||
final rect = box.localToGlobal(Offset.zero) & box.size;
|
||||
final h = media.size.height;
|
||||
if (side == ClideAnchorSide.below && rect.bottom > h * 0.6) {
|
||||
side = ClideAnchorSide.above;
|
||||
} else if (side == ClideAnchorSide.above && rect.top < h * 0.4) {
|
||||
side = ClideAnchorSide.below;
|
||||
final h = view.physicalSize.height / view.devicePixelRatio;
|
||||
if (h > 0) {
|
||||
if (side == ClideAnchorSide.below && rect.bottom > h * 0.6) {
|
||||
side = ClideAnchorSide.above;
|
||||
} else if (side == ClideAnchorSide.above && rect.top < h * 0.4) {
|
||||
side = ClideAnchorSide.below;
|
||||
}
|
||||
}
|
||||
}
|
||||
return side;
|
||||
@@ -264,13 +268,17 @@ class _ClideAnchoredOverlayState extends State<ClideAnchoredOverlay> {
|
||||
final flipped = _resolvedSide != widget.side;
|
||||
final off = flipped ? Offset(widget.offset.dx, -widget.offset.dy) : widget.offset;
|
||||
final (target, follower) = _alignments(_resolvedSide);
|
||||
// The Stack lays the follower out loosely, so the (shrink-wrapping) panel
|
||||
// sizes to its content; the follower layer's transform alone positions it.
|
||||
// An Align here would peg the panel to a corner of the full-screen follower
|
||||
// box and break hit-testing for non-top-left anchors.
|
||||
positioned = CompositedTransformFollower(
|
||||
link: _link,
|
||||
showWhenUnlinked: false,
|
||||
targetAnchor: target,
|
||||
followerAnchor: follower,
|
||||
offset: off,
|
||||
child: Align(alignment: follower, child: content),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user