Fix DropdownMenu default width does not take label into account (#161219)

## Description

This PR fixes `DropdownMenu` default width when a long label is
provided.

Before:

The width is based on the longest menu item (which can be smaller than
the label):


![image](https://github.com/user-attachments/assets/c8d543a3-12c6-4f8e-ba89-e067a09f8653)

After:

The width also depends on the label width when it is longer than the
menu items.


![image](https://github.com/user-attachments/assets/afde7e35-1da0-46a9-900d-8fd119c30317)


## Related Issue

Fixes [DropdownMenu default width does not take label into
account](https://github.com/flutter/flutter/issues/136678)

## Tests

Adds 2 tests.
This commit is contained in:
Bruno Leroux 2025-02-13 10:48:25 +01:00 committed by GitHub
parent 60d0bfcca4
commit b61335bd3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 81 additions and 8 deletions

View File

@ -1079,11 +1079,24 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
? textField ? textField
: _DropdownMenuBody( : _DropdownMenuBody(
width: widget.width, width: widget.width,
// The children, except the text field, are used to compute the preferred width,
// which is the width of the longest children, plus the width of trailingButton
// and leadingButton.
//
// See _RenderDropdownMenuBody layout logic.
children: <Widget>[ children: <Widget>[
textField, textField,
..._initialMenu!.map( ..._initialMenu!.map(
(Widget item) => ExcludeFocus(excluding: !controller.isOpen, child: item), (Widget item) => ExcludeFocus(excluding: !controller.isOpen, child: item),
), ),
if (widget.label != null)
ExcludeSemantics(
child: Padding(
// See RenderEditable.floatingCursorAddedMargin for the default horizontal padding.
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: DefaultTextStyle(style: effectiveTextStyle!, child: widget.label!),
),
),
trailingButton, trailingButton,
leadingButton, leadingButton,
], ],
@ -1317,9 +1330,11 @@ class _RenderDropdownMenuBody extends RenderBox
continue; continue;
} }
final double maxIntrinsicWidth = child.getMinIntrinsicWidth(height); final double maxIntrinsicWidth = child.getMinIntrinsicWidth(height);
// Add the width of leading icon.
if (child == lastChild) { if (child == lastChild) {
width += maxIntrinsicWidth; width += maxIntrinsicWidth;
} }
// Add the width of trailing icon.
if (child == childBefore(lastChild!)) { if (child == childBefore(lastChild!)) {
width += maxIntrinsicWidth; width += maxIntrinsicWidth;
} }
@ -1344,11 +1359,11 @@ class _RenderDropdownMenuBody extends RenderBox
continue; continue;
} }
final double maxIntrinsicWidth = child.getMaxIntrinsicWidth(height); final double maxIntrinsicWidth = child.getMaxIntrinsicWidth(height);
// Add the width of leading Icon. // Add the width of leading icon.
if (child == lastChild) { if (child == lastChild) {
width += maxIntrinsicWidth; width += maxIntrinsicWidth;
} }
// Add the width of trailing Icon. // Add the width of trailing icon.
if (child == childBefore(lastChild!)) { if (child == childBefore(lastChild!)) {
width += maxIntrinsicWidth; width += maxIntrinsicWidth;
} }

View File

@ -718,6 +718,64 @@ void main() {
expect(box.size.width, customWidth); expect(box.size.width, customWidth);
}); });
testWidgets('The width is determined by the menu entries', (WidgetTester tester) async {
const double entryLabelWidth = 100;
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: DropdownMenu<int>(
dropdownMenuEntries: <DropdownMenuEntry<int>>[
DropdownMenuEntry<int>(
value: 0,
label: 'Flutter',
labelWidget: SizedBox(width: entryLabelWidth),
),
],
),
),
),
);
final double width = tester.getSize(find.byType(DropdownMenu<int>)).width;
const double menuEntryPadding = 24.0; // See _kDefaultHorizontalPadding.
const double leadingWidth = 16.0;
const double trailingWidth = 56.0;
expect(width, entryLabelWidth + leadingWidth + trailingWidth + menuEntryPadding);
});
testWidgets('The width is determined by the label when it is longer than menu entries', (
WidgetTester tester,
) async {
const double labelWidth = 120;
const double entryLabelWidth = 100;
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: DropdownMenu<int>(
label: SizedBox(width: labelWidth),
dropdownMenuEntries: <DropdownMenuEntry<int>>[
DropdownMenuEntry<int>(
value: 0,
label: 'Flutter',
labelWidget: SizedBox(width: entryLabelWidth),
),
],
),
),
),
);
final double width = tester.getSize(find.byType(DropdownMenu<int>)).width;
const double leadingWidth = 16.0;
const double trailingWidth = 56.0;
const double labelPadding = 8.0; // See RenderEditable.floatingCursorAddedMargin.
expect(width, labelWidth + labelPadding + leadingWidth + trailingWidth);
});
testWidgets('The width of MenuAnchor respects MenuAnchor.expandedInsets', ( testWidgets('The width of MenuAnchor respects MenuAnchor.expandedInsets', (
WidgetTester tester, WidgetTester tester,
) async { ) async {
@ -962,7 +1020,7 @@ void main() {
// Default text field (without leading icon). // Default text field (without leading icon).
await tester.pumpWidget(buildTest(themeData, menuChildren, label: const Text('label'))); await tester.pumpWidget(buildTest(themeData, menuChildren, label: const Text('label')));
final Finder label = find.text('label'); final Finder label = find.text('label').first;
final Offset labelTopLeft = tester.getTopLeft(label); final Offset labelTopLeft = tester.getTopLeft(label);
await tester.tap(find.byType(DropdownMenu<TestMenu>)); await tester.tap(find.byType(DropdownMenu<TestMenu>));
@ -985,7 +1043,7 @@ void main() {
final Finder leadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last; final Finder leadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last;
final double iconWidth = tester.getSize(leadingIcon).width; final double iconWidth = tester.getSize(leadingIcon).width;
final Finder updatedLabel = find.text('label'); final Finder updatedLabel = find.text('label').first;
final Offset updatedLabelTopLeft = tester.getTopLeft(updatedLabel); final Offset updatedLabelTopLeft = tester.getTopLeft(updatedLabel);
await tester.tap(find.byType(DropdownMenu<TestMenu>)); await tester.tap(find.byType(DropdownMenu<TestMenu>));
@ -1009,7 +1067,7 @@ void main() {
final Finder largeLeadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last; final Finder largeLeadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last;
final double largeIconWidth = tester.getSize(largeLeadingIcon).width; final double largeIconWidth = tester.getSize(largeLeadingIcon).width;
final Finder updatedLabel1 = find.text('label'); final Finder updatedLabel1 = find.text('label').first;
final Offset updatedLabelTopLeft1 = tester.getTopLeft(updatedLabel1); final Offset updatedLabelTopLeft1 = tester.getTopLeft(updatedLabel1);
await tester.tap(find.byType(DropdownMenu<TestMenu>)); await tester.tap(find.byType(DropdownMenu<TestMenu>));
@ -1040,7 +1098,7 @@ void main() {
), ),
); );
final Finder label = find.text('label'); final Finder label = find.text('label').first;
final Offset labelTopRight = tester.getTopRight(label); final Offset labelTopRight = tester.getTopRight(label);
await tester.tap(find.byType(DropdownMenu<TestMenu>)); await tester.tap(find.byType(DropdownMenu<TestMenu>));
@ -1072,7 +1130,7 @@ void main() {
final Finder leadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last; final Finder leadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last;
final double iconWidth = tester.getSize(leadingIcon).width; final double iconWidth = tester.getSize(leadingIcon).width;
final Offset dropdownMenuTopRight = tester.getTopRight(find.byType(DropdownMenu<TestMenu>)); final Offset dropdownMenuTopRight = tester.getTopRight(find.byType(DropdownMenu<TestMenu>));
final Finder updatedLabel = find.text('label'); final Finder updatedLabel = find.text('label').first;
final Offset updatedLabelTopRight = tester.getTopRight(updatedLabel); final Offset updatedLabelTopRight = tester.getTopRight(updatedLabel);
await tester.tap(find.byType(DropdownMenu<TestMenu>)); await tester.tap(find.byType(DropdownMenu<TestMenu>));
@ -1110,7 +1168,7 @@ void main() {
final Offset updatedDropdownMenuTopRight = tester.getTopRight( final Offset updatedDropdownMenuTopRight = tester.getTopRight(
find.byType(DropdownMenu<TestMenu>), find.byType(DropdownMenu<TestMenu>),
); );
final Finder updatedLabel1 = find.text('label'); final Finder updatedLabel1 = find.text('label').first;
final Offset updatedLabelTopRight1 = tester.getTopRight(updatedLabel1); final Offset updatedLabelTopRight1 = tester.getTopRight(updatedLabel1);
await tester.tap(find.byType(DropdownMenu<TestMenu>)); await tester.tap(find.byType(DropdownMenu<TestMenu>));