Fix expanded DropdownMenu panel is shorter than text field (#174443)

## Description

This PR fixes DropdownMenu menu panel being shorter than the TextField
when it expands to full-screen width.

## Before

The menu panel is shorter than the text field:

<img width="297" height="130" alt="image"
src="https://github.com/user-attachments/assets/db42bd01-94d8-47fb-9331-ebd78111b931"
/>

## After

The menu panel expands as close as possible to the edge similarly to the
text field.

<img width="297" height="130" alt="image"
src="https://github.com/user-attachments/assets/b7d8f2aa-a668-439a-8195-9b9ce48dba6b"
/>


## Code sample

<details><summary>Code sample for recordings</summary>

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const DropdownMenuExample());
}

class DropdownMenuExample extends StatefulWidget {
  const DropdownMenuExample({super.key});

  @override
  State<DropdownMenuExample> createState() => _DropdownMenuExampleState();
}

class _DropdownMenuExampleState extends State<DropdownMenuExample> {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: DropdownMenu<int>(
          expandedInsets: EdgeInsets.zero,
          dropdownMenuEntries: <DropdownMenuEntry<int>>[
            DropdownMenuEntry<int>(value: 0, label: 'Flutter'),
          ],
        ),
      ),
    );
  }
}


``` 
</details> 

## Implementation details

MenuAnchor automatically adds a default padding, see
2c21273bfd/packages/flutter/lib/src/material/menu_anchor.dart (L81-L82)

This PR add a property to menu anchor to expose the view padding.
DropdownMenu sets this padding to EdgeInsets.zero to opt-out from the
default 8 padding.


## Related Issue

Fixes [DropdownMenu children is shorter than the TextField when it
expands to full-screen
width](https://github.com/flutter/flutter/issues/172680)

## Tests

Adds 2 tests.

---------

Co-authored-by: Qun Cheng <36861262+QuncCccccc@users.noreply.github.com>
This commit is contained in:
Bruno Leroux 2025-09-04 08:35:20 +02:00 committed by GitHub
parent 251f4a8d5b
commit 6e9286d6eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 92 additions and 5 deletions

View File

@ -1110,6 +1110,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
Widget menuAnchor = MenuAnchor(
style: effectiveMenuStyle,
alignmentOffset: widget.alignmentOffset,
reservedPadding: EdgeInsets.zero,
controller: _controller,
menuChildren: menu,
crossAxisUnconstrained: false,

View File

@ -161,6 +161,7 @@ class MenuAnchor extends StatefulWidget {
this.childFocusNode,
this.style,
this.alignmentOffset = Offset.zero,
this.reservedPadding,
this.layerLink,
this.clipBehavior = Clip.hardEdge,
@Deprecated(
@ -318,6 +319,11 @@ class MenuAnchor extends StatefulWidget {
/// to rebuild this child when those change.
final Widget? child;
/// The padding between the edge of the safe area and the menu panel.
///
/// Defaults to EdgeInsets.all(8).
final EdgeInsetsGeometry? reservedPadding;
@override
State<MenuAnchor> createState() => _MenuAnchorState();
@ -406,6 +412,7 @@ class _MenuAnchorState extends State<MenuAnchor> {
menuPosition: position,
anchor: this,
alignmentOffset: widget.alignmentOffset ?? Offset.zero,
reservedPadding: widget.reservedPadding ?? const EdgeInsets.all(_kMenuViewPadding),
);
}
@ -2920,6 +2927,7 @@ class _MenuLayout extends SingleChildLayoutDelegate {
required this.avoidBounds,
required this.orientation,
required this.parentOrientation,
required this.reservedPadding,
});
// Rectangle of underlying button, relative to the overlay's dimensions.
@ -2951,13 +2959,14 @@ class _MenuLayout extends SingleChildLayoutDelegate {
// The orientation of this menu's parent.
final Axis parentOrientation;
// How close to the edge of the safe area the menu will be placed.
final EdgeInsetsGeometry reservedPadding;
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
// The menu can be at most the size of the overlay minus _kMenuViewPadding
// pixels in each direction.
return BoxConstraints.loose(
constraints.biggest,
).deflate(const EdgeInsets.all(_kMenuViewPadding));
// The menu can be at most the size of the overlay minus the view padding
// in each direction.
return BoxConstraints.loose(constraints.biggest).deflate(reservedPadding);
}
@override
@ -3289,6 +3298,7 @@ class _Submenu extends StatelessWidget {
this.crossAxisUnconstrained = true,
required this.menuChildren,
required this.menuScopeNode,
required this.reservedPadding,
});
final FocusScopeNode menuScopeNode;
@ -3301,6 +3311,7 @@ class _Submenu extends StatelessWidget {
final Clip clipBehavior;
final bool crossAxisUnconstrained;
final List<Widget> menuChildren;
final EdgeInsetsGeometry reservedPadding;
@override
Widget build(BuildContext context) {
@ -3399,6 +3410,7 @@ class _Submenu extends StatelessWidget {
menuPosition: menuPosition.position,
orientation: anchor._orientation,
parentOrientation: anchor._parent?._orientation ?? Axis.horizontal,
reservedPadding: reservedPadding,
),
child: menuPanel,
);

View File

@ -927,6 +927,31 @@ void main() {
expect(buttonSize.width, parentWidth - 35.0 - 20.0);
});
// Regression test for https://github.com/flutter/flutter/issues/172680.
testWidgets('Menu panel width can expand to full-screen width', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: DropdownMenu<int>(
expandedInsets: EdgeInsets.zero,
dropdownMenuEntries: <DropdownMenuEntry<int>>[
DropdownMenuEntry<int>(value: 0, label: 'Flutter'),
],
),
),
),
);
final double dropdownWidth = tester.getSize(find.byType(DropdownMenu<int>)).width;
expect(dropdownWidth, 800);
await tester.tap(find.byType(DropdownMenu<int>));
await tester.pump();
final double menuWidth = tester.getSize(findMenuItemButton('Flutter')).width;
expect(dropdownWidth, menuWidth);
});
testWidgets(
'Material2 - The menuHeight property can be used to show a shorter scrollable menu list instead of the complete list',
(WidgetTester tester) async {

View File

@ -5047,6 +5047,55 @@ void main() {
expect(find.byIcon(disabledIcon), findsOneWidget);
});
});
testWidgets('Menu panel default reserved padding', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: MenuAnchor(
controller: controller,
menuChildren: const <Widget>[SizedBox(width: 800, height: 24)],
builder: (BuildContext context, MenuController controller, Widget? child) {
return const SizedBox(width: 800, height: 24);
},
),
),
),
),
);
controller.open();
await tester.pump();
const double defaultReservedPadding = 8.0; // See _kMenuViewPadding.
expect(tester.getRect(findMenuPanels()).width, 800.0 - defaultReservedPadding * 2);
});
testWidgets('Menu panel accepts custom reserved padding', (WidgetTester tester) async {
const EdgeInsetsGeometry reservedPadding = EdgeInsets.symmetric(horizontal: 13.0);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: MenuAnchor(
controller: controller,
reservedPadding: reservedPadding,
menuChildren: const <Widget>[SizedBox(width: 800, height: 24)],
builder: (BuildContext context, MenuController controller, Widget? child) {
return const SizedBox(width: 800, height: 24);
},
),
),
),
),
);
controller.open();
await tester.pump();
expect(tester.getRect(findMenuPanels()).width, 800.0 - reservedPadding.horizontal);
});
}
List<Widget> createTestMenus({