diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart index 5a27cc25cbe..dc786318da8 100644 --- a/packages/flutter/lib/src/material/dropdown_menu.dart +++ b/packages/flutter/lib/src/material/dropdown_menu.dart @@ -173,7 +173,7 @@ enum DropdownMenuCloseBehavior { /// The [DropdownMenu] uses a [TextField] as the "anchor". /// * [TextField], which is a text input widget that uses an [InputDecoration]. /// * [DropdownMenuEntry], which is used to build the [MenuItemButton] in the [DropdownMenu] list. -class DropdownMenu extends StatefulWidget { +class DropdownMenu extends StatefulWidget { /// Creates a const [DropdownMenu]. /// /// The leading and trailing icons in the text field can be customized by using @@ -690,7 +690,7 @@ class DropdownMenu extends StatefulWidget { State> createState() => _DropdownMenuState(); } -class _DropdownMenuState extends State> { +class _DropdownMenuState extends State> { static const Map _editableShortcuts = { SingleActivator(LogicalKeyboardKey.arrowLeft): ExtendSelectionByCharacterIntent( forward: false, diff --git a/packages/flutter/lib/src/material/dropdown_menu_form_field.dart b/packages/flutter/lib/src/material/dropdown_menu_form_field.dart index 752e686da87..cf47b8e0290 100644 --- a/packages/flutter/lib/src/material/dropdown_menu_form_field.dart +++ b/packages/flutter/lib/src/material/dropdown_menu_form_field.dart @@ -28,7 +28,7 @@ import 'menu_style.dart'; /// /// * [DropdownMenu], which is the underlying text field without the [Form] /// integration. -class DropdownMenuFormField extends FormField { +class DropdownMenuFormField extends FormField { /// Creates a [DropdownMenu] widget that is a [FormField]. /// /// For a description of the `onSaved`, `validator`, or `autovalidateMode` @@ -164,7 +164,7 @@ class DropdownMenuFormField extends FormField { FormFieldState createState() => _DropdownMenuFormFieldState(); } -class _DropdownMenuFormFieldState extends FormFieldState { +class _DropdownMenuFormFieldState extends FormFieldState { DropdownMenuFormField get _dropdownMenuFormField => widget as DropdownMenuFormField; // The controller used to restore the selected item. diff --git a/packages/flutter/test/material/dropdown_menu_form_field_test.dart b/packages/flutter/test/material/dropdown_menu_form_field_test.dart index 984fbd675d9..ef0f5fc5d72 100644 --- a/packages/flutter/test/material/dropdown_menu_form_field_test.dart +++ b/packages/flutter/test/material/dropdown_menu_form_field_test.dart @@ -32,6 +32,14 @@ void main() { return find.widgetWithText(MenuItemButton, menuItem.label).last; } + Finder findMenuItemButton(String label) { + // For each menu items there are two MenuItemButton widgets. + // The last one is the real button item in the menu. + // The first one is not visible, it is part of _DropdownMenuBody + // which is used to compute the dropdown width. + return find.widgetWithText(MenuItemButton, label).last; + } + testWidgets('Creates an underlying DropdownMenu', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -1394,4 +1402,47 @@ void main() { ); expect(tester.getSize(find.byType(DropdownMenuFormField)), Size.zero); }); + + // Regression test for https://github.com/flutter/flutter/issues/180121. + testWidgets('Allow null entry to clear selection', (WidgetTester tester) async { + final controller = TextEditingController(); + addTearDown(controller.dispose); + + const selectNoneLabel = 'Select none'; + final nullableMenuItems = >[ + const DropdownMenuEntry(value: null, label: selectNoneLabel), + const DropdownMenuEntry(value: 'a', label: 'A'), + const DropdownMenuEntry(value: 'b', label: 'B'), + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return DropdownMenuFormField( + controller: controller, + requestFocusOnTap: true, + enableFilter: true, + dropdownMenuEntries: nullableMenuItems, + onSelected: (_) { + setState(() {}); + }, + ); + }, + ), + ), + ), + ); + + // Open the menu. + await tester.tap(find.byType(DropdownMenu)); + await tester.pump(); + + // Select the 'None' item. + await tester.tap(findMenuItemButton(selectNoneLabel)); + await tester.pumpAndSettle(); + + expect(controller.text, selectNoneLabel); + }); } diff --git a/packages/flutter/test/material/dropdown_menu_test.dart b/packages/flutter/test/material/dropdown_menu_test.dart index 6283b2f0213..a30c618d8dc 100644 --- a/packages/flutter/test/material/dropdown_menu_test.dart +++ b/packages/flutter/test/material/dropdown_menu_test.dart @@ -5333,6 +5333,49 @@ void main() { shouldFocusPrevious: textInputAction == TextInputAction.previous, ); }, variant: focusVariants); + + // Regression test for https://github.com/flutter/flutter/issues/180121. + testWidgets('Allow null entry to clear selection', (WidgetTester tester) async { + final controller = TextEditingController(); + addTearDown(controller.dispose); + + const selectNoneLabel = 'Select none'; + final nullableMenuItems = >[ + const DropdownMenuEntry(value: null, label: selectNoneLabel), + const DropdownMenuEntry(value: 'a', label: 'A'), + const DropdownMenuEntry(value: 'b', label: 'B'), + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return DropdownMenu( + controller: controller, + requestFocusOnTap: true, + enableFilter: true, + dropdownMenuEntries: nullableMenuItems, + onSelected: (_) { + setState(() {}); + }, + ); + }, + ), + ), + ), + ); + + // Open the menu. + await tester.tap(find.byType(DropdownMenu)); + await tester.pump(); + + // Select the 'None' item. + await tester.tap(findMenuItemButton(selectNoneLabel)); + await tester.pumpAndSettle(); + + expect(controller.text, selectNoneLabel); + }); } enum TestMenu {