From b61335bd3badf03f7ddff9bd143e2084da88de99 Mon Sep 17 00:00:00 2001 From: Bruno Leroux Date: Thu, 13 Feb 2025 10:48:25 +0100 Subject: [PATCH] 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. --- .../lib/src/material/dropdown_menu.dart | 19 ++++- .../test/material/dropdown_menu_test.dart | 70 +++++++++++++++++-- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart index 1de959f1a5..3e87f457e1 100644 --- a/packages/flutter/lib/src/material/dropdown_menu.dart +++ b/packages/flutter/lib/src/material/dropdown_menu.dart @@ -1079,11 +1079,24 @@ class _DropdownMenuState extends State> { ? textField : _DropdownMenuBody( 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: [ textField, ..._initialMenu!.map( (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, leadingButton, ], @@ -1317,9 +1330,11 @@ class _RenderDropdownMenuBody extends RenderBox continue; } final double maxIntrinsicWidth = child.getMinIntrinsicWidth(height); + // Add the width of leading icon. if (child == lastChild) { width += maxIntrinsicWidth; } + // Add the width of trailing icon. if (child == childBefore(lastChild!)) { width += maxIntrinsicWidth; } @@ -1344,11 +1359,11 @@ class _RenderDropdownMenuBody extends RenderBox continue; } final double maxIntrinsicWidth = child.getMaxIntrinsicWidth(height); - // Add the width of leading Icon. + // Add the width of leading icon. if (child == lastChild) { width += maxIntrinsicWidth; } - // Add the width of trailing Icon. + // Add the width of trailing icon. if (child == childBefore(lastChild!)) { width += maxIntrinsicWidth; } diff --git a/packages/flutter/test/material/dropdown_menu_test.dart b/packages/flutter/test/material/dropdown_menu_test.dart index 529557a7ab..07bd856fe0 100644 --- a/packages/flutter/test/material/dropdown_menu_test.dart +++ b/packages/flutter/test/material/dropdown_menu_test.dart @@ -718,6 +718,64 @@ void main() { 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( + dropdownMenuEntries: >[ + DropdownMenuEntry( + value: 0, + label: 'Flutter', + labelWidget: SizedBox(width: entryLabelWidth), + ), + ], + ), + ), + ), + ); + + final double width = tester.getSize(find.byType(DropdownMenu)).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( + label: SizedBox(width: labelWidth), + dropdownMenuEntries: >[ + DropdownMenuEntry( + value: 0, + label: 'Flutter', + labelWidget: SizedBox(width: entryLabelWidth), + ), + ], + ), + ), + ), + ); + + final double width = tester.getSize(find.byType(DropdownMenu)).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', ( WidgetTester tester, ) async { @@ -962,7 +1020,7 @@ void main() { // Default text field (without leading icon). 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); await tester.tap(find.byType(DropdownMenu)); @@ -985,7 +1043,7 @@ void main() { final Finder leadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last; 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); await tester.tap(find.byType(DropdownMenu)); @@ -1009,7 +1067,7 @@ void main() { final Finder largeLeadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last; 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); await tester.tap(find.byType(DropdownMenu)); @@ -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); await tester.tap(find.byType(DropdownMenu)); @@ -1072,7 +1130,7 @@ void main() { final Finder leadingIcon = find.widgetWithIcon(SizedBox, Icons.search).last; final double iconWidth = tester.getSize(leadingIcon).width; final Offset dropdownMenuTopRight = tester.getTopRight(find.byType(DropdownMenu)); - final Finder updatedLabel = find.text('label'); + final Finder updatedLabel = find.text('label').first; final Offset updatedLabelTopRight = tester.getTopRight(updatedLabel); await tester.tap(find.byType(DropdownMenu)); @@ -1110,7 +1168,7 @@ void main() { final Offset updatedDropdownMenuTopRight = tester.getTopRight( find.byType(DropdownMenu), ); - final Finder updatedLabel1 = find.text('label'); + final Finder updatedLabel1 = find.text('label').first; final Offset updatedLabelTopRight1 = tester.getTopRight(updatedLabel1); await tester.tap(find.byType(DropdownMenu));