Skip to content

Commit 2b8072e

Browse files
authored
Fix DropdownMenu menu does not follow the text field (flutter#154667)
## Description This PR fixes the `DropdownMenu` menu position when the keyboard appear on mobile device. ## Related Issue Fixes flutter#149037. ## Tests Adds 2 tests.
1 parent fea5d1e commit 2b8072e

File tree

4 files changed

+134
-5
lines changed

4 files changed

+134
-5
lines changed

packages/flutter/lib/src/material/dropdown_menu.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
804804
controller: _controller,
805805
menuChildren: menu,
806806
crossAxisUnconstrained: false,
807+
layerLink: LayerLink(),
807808
builder: (BuildContext context, MenuController controller, Widget? child) {
808809
assert(_initialMenu != null);
809810
final Widget trailingButton = Padding(

packages/flutter/lib/src/material/menu_anchor.dart

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ class MenuAnchor extends StatefulWidget {
144144
this.childFocusNode,
145145
this.style,
146146
this.alignmentOffset = Offset.zero,
147+
this.layerLink,
147148
this.clipBehavior = Clip.hardEdge,
148149
@Deprecated(
149150
'Use consumeOutsideTap instead. '
@@ -205,6 +206,13 @@ class MenuAnchor extends StatefulWidget {
205206
/// {@endtemplate}
206207
final Offset? alignmentOffset;
207208

209+
/// An optional [LayerLink] to attach the menu to the widget that this
210+
/// [MenuAnchor] surrounds.
211+
///
212+
/// When provided, the menu will follow the widget that this [MenuAnchor]
213+
/// surrounds if it moves because of view insets changes.
214+
final LayerLink? layerLink;
215+
208216
/// {@macro flutter.material.Material.clipBehavior}
209217
///
210218
/// Defaults to [Clip.hardEdge].
@@ -387,11 +395,20 @@ class _MenuAnchorState extends State<MenuAnchor> {
387395

388396
@override
389397
Widget build(BuildContext context) {
398+
Widget contents = _buildContents(context);
399+
if (widget.layerLink != null) {
400+
contents = CompositedTransformTarget(
401+
link: widget.layerLink!,
402+
child: contents,
403+
);
404+
}
405+
390406
Widget child = OverlayPortal(
391407
controller: _overlayController,
392408
overlayChildBuilder: (BuildContext context) {
393409
return _Submenu(
394410
anchor: this,
411+
layerLink: widget.layerLink,
395412
menuStyle: widget.style,
396413
alignmentOffset: widget.alignmentOffset ?? Offset.zero,
397414
menuPosition: _menuPosition,
@@ -400,7 +417,7 @@ class _MenuAnchorState extends State<MenuAnchor> {
400417
crossAxisUnconstrained: widget.crossAxisUnconstrained,
401418
);
402419
},
403-
child: _buildContents(context),
420+
child: contents,
404421
);
405422

406423
if (!widget.anchorTapClosesMenu) {
@@ -3497,6 +3514,7 @@ class _MenuPanelState extends State<_MenuPanel> {
34973514
class _Submenu extends StatelessWidget {
34983515
const _Submenu({
34993516
required this.anchor,
3517+
required this.layerLink,
35003518
required this.menuStyle,
35013519
required this.menuPosition,
35023520
required this.alignmentOffset,
@@ -3506,6 +3524,7 @@ class _Submenu extends StatelessWidget {
35063524
});
35073525

35083526
final _MenuAnchorState anchor;
3527+
final LayerLink? layerLink;
35093528
final MenuStyle? menuStyle;
35103529
final Offset? menuPosition;
35113530
final Offset alignmentOffset;
@@ -3553,12 +3572,17 @@ class _Submenu extends StatelessWidget {
35533572
.clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity);
35543573
final BuildContext anchorContext = anchor._anchorKey.currentContext!;
35553574
final RenderBox overlay = Overlay.of(anchorContext).context.findRenderObject()! as RenderBox;
3556-
final RenderBox anchorBox = anchorContext.findRenderObject()! as RenderBox;
3557-
final Offset upperLeft = anchorBox.localToGlobal(Offset(dx, -dy), ancestor: overlay);
3558-
final Offset bottomRight = anchorBox.localToGlobal(anchorBox.paintBounds.bottomRight, ancestor: overlay);
3575+
3576+
Offset upperLeft = Offset.zero;
3577+
Offset bottomRight = Offset.zero;
3578+
if (layerLink == null) {
3579+
final RenderBox anchorBox = anchorContext.findRenderObject()! as RenderBox;
3580+
upperLeft = anchorBox.localToGlobal(Offset(dx, -dy), ancestor: overlay);
3581+
bottomRight = anchorBox.localToGlobal(anchorBox.paintBounds.bottomRight, ancestor: overlay);
3582+
}
35593583
final Rect anchorRect = Rect.fromPoints(upperLeft, bottomRight);
35603584

3561-
return Theme(
3585+
Widget child = Theme(
35623586
data: Theme.of(context).copyWith(
35633587
visualDensity: visualDensity,
35643588
),
@@ -3609,6 +3633,16 @@ class _Submenu extends StatelessWidget {
36093633
),
36103634
),
36113635
);
3636+
3637+
if (layerLink != null) {
3638+
child = CompositedTransformFollower(
3639+
link: layerLink!,
3640+
targetAnchor: Alignment.bottomLeft,
3641+
child: child,
3642+
);
3643+
}
3644+
3645+
return child;
36123646
}
36133647
}
36143648

packages/flutter/test/material/dropdown_menu_test.dart

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2791,6 +2791,47 @@ void main() {
27912791
await tester.pumpAndSettle();
27922792
expect(controller.offset, 0.0);
27932793
});
2794+
2795+
// Regression test for https://github.com/flutter/flutter/issues/149037.
2796+
testWidgets('Dropdown menu follows the text field when keyboard opens', (WidgetTester tester) async {
2797+
Widget boilerplate(double bottomInsets) {
2798+
return MaterialApp(
2799+
home: MediaQuery(
2800+
data: MediaQueryData(viewInsets: EdgeInsets.only(bottom: bottomInsets)),
2801+
child: Scaffold(
2802+
body: Center(
2803+
child: DropdownMenu<TestMenu>(dropdownMenuEntries: menuChildren),
2804+
),
2805+
),
2806+
),
2807+
);
2808+
}
2809+
2810+
// Build once without bottom insets and open the menu.
2811+
await tester.pumpWidget(boilerplate(0.0));
2812+
await tester.tap(find.byType(TextField).first);
2813+
await tester.pump();
2814+
2815+
Finder findMenuPanels() {
2816+
return find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MenuPanel');
2817+
}
2818+
2819+
// Menu vertical position is just under the text field.
2820+
expect(
2821+
tester.getRect(findMenuPanels()).top,
2822+
tester.getRect(find.byType(TextField).first).bottom,
2823+
);
2824+
2825+
// Simulate the keyboard opening resizing the view.
2826+
await tester.pumpWidget(boilerplate(100.0));
2827+
await tester.pump();
2828+
2829+
// Menu vertical position is just under the text field.
2830+
expect(
2831+
tester.getRect(findMenuPanels()).top,
2832+
tester.getRect(find.byType(TextField).first).bottom,
2833+
);
2834+
});
27942835
}
27952836

27962837
enum TestMenu {

packages/flutter/test/material/menu_anchor_test.dart

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3800,6 +3800,59 @@ void main() {
38003800
]),
38013801
);
38023802
});
3803+
3804+
testWidgets('Menu follows content position when a LayerLink is provided', (WidgetTester tester) async {
3805+
final MenuController controller = MenuController();
3806+
final UniqueKey contentKey = UniqueKey();
3807+
3808+
Widget boilerplate(double bottomInsets) {
3809+
return MaterialApp(
3810+
home: MediaQuery(
3811+
data: MediaQueryData(
3812+
viewInsets: EdgeInsets.only(bottom: bottomInsets),
3813+
),
3814+
child: Scaffold(
3815+
body: Center(
3816+
child: MenuAnchor(
3817+
controller: controller,
3818+
layerLink: LayerLink(),
3819+
menuChildren: <Widget>[
3820+
MenuItemButton(
3821+
onPressed: () {},
3822+
child: const Text('Button 1'),
3823+
),
3824+
],
3825+
builder: (BuildContext context, MenuController controller, Widget? child) {
3826+
return SizedBox(key: contentKey, width: 100, height: 100);
3827+
},
3828+
),
3829+
),
3830+
),
3831+
),
3832+
);
3833+
}
3834+
3835+
// Build once without bottom insets and open the menu.
3836+
await tester.pumpWidget(boilerplate(0.0));
3837+
controller.open();
3838+
await tester.pump();
3839+
3840+
// Menu vertical position is just under the content.
3841+
expect(
3842+
tester.getRect(findMenuPanels()).top,
3843+
tester.getRect(find.byKey(contentKey)).bottom,
3844+
);
3845+
3846+
// Simulate the keyboard opening resizing the view.
3847+
await tester.pumpWidget(boilerplate(100.0));
3848+
await tester.pump();
3849+
3850+
// Menu vertical position is just under the content.
3851+
expect(
3852+
tester.getRect(findMenuPanels()).top,
3853+
tester.getRect(find.byKey(contentKey)).bottom,
3854+
);
3855+
});
38033856
});
38043857

38053858
group('LocalizedShortcutLabeler', () {

0 commit comments

Comments
 (0)