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:
@@ -97,5 +97,78 @@ void main() {
|
||||
expect(find.text('panel'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
// A ClideAnchoredOverlay hosting a ClideMenu of A/B/C, for the focus +
|
||||
// positioning tests below.
|
||||
Widget anchoredMenu(
|
||||
ClideOverlayController c,
|
||||
void Function(String) onPick, {
|
||||
ClideAnchorSide side = ClideAnchorSide.below,
|
||||
ClideAnchorAlign align = ClideAnchorAlign.start,
|
||||
bool autoFlip = false,
|
||||
}) {
|
||||
return ClideAnchoredOverlay(
|
||||
controller: c,
|
||||
side: side,
|
||||
align: align,
|
||||
autoFlip: autoFlip,
|
||||
anchor: const SizedBox(width: 80, height: 24, child: ClideText('trigger')),
|
||||
overlayBuilder: (ctx, ctrl) => ClideMenu(
|
||||
onClose: ctrl.close,
|
||||
entries: [
|
||||
ClideMenuItem(label: 'A', onSelect: () => onPick('A')),
|
||||
ClideMenuItem(label: 'B', onSelect: () => onPick('B')),
|
||||
ClideMenuItem(label: 'C', onSelect: () => onPick('C')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('keyboard nav reaches a ClideMenu through the overlay', (tester) async {
|
||||
final c = ClideOverlayController();
|
||||
addTearDown(c.dispose);
|
||||
var picked = '';
|
||||
await tester.pumpWidget(anchoredHarness(f, anchoredMenu(c, (v) => picked = v)));
|
||||
c.open();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // A
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // B
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
expect(picked, 'B');
|
||||
});
|
||||
|
||||
testWidgets('an end-aligned menu item is mouse-tappable (no Align hit-offset)', (tester) async {
|
||||
final c = ClideOverlayController();
|
||||
addTearDown(c.dispose);
|
||||
var picked = '';
|
||||
// Right-edge trigger + end alignment — the menu extends left, on-screen.
|
||||
await tester.pumpWidget(anchoredHarness(
|
||||
f,
|
||||
anchoredMenu(c, (v) => picked = v, align: ClideAnchorAlign.end),
|
||||
alignment: Alignment.topRight,
|
||||
));
|
||||
c.open();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('C'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(picked, 'C');
|
||||
});
|
||||
|
||||
testWidgets('autoFlip flips below to above when the anchor is near the bottom', (tester) async {
|
||||
final c = ClideOverlayController();
|
||||
addTearDown(c.dispose);
|
||||
await tester.pumpWidget(anchoredHarness(
|
||||
f,
|
||||
anchoredMenu(c, (_) {}, side: ClideAnchorSide.below, autoFlip: true),
|
||||
alignment: Alignment.bottomLeft,
|
||||
));
|
||||
c.open();
|
||||
await tester.pumpAndSettle();
|
||||
// The panel (its first item) sits ABOVE the trigger, not below.
|
||||
final triggerTop = tester.getRect(find.text('trigger')).top;
|
||||
final panelBottom = tester.getRect(find.text('A')).bottom;
|
||||
expect(panelBottom, lessThanOrEqualTo(triggerTop));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user