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 d3616b70d98..8eb425f82cb 100644 --- a/packages/flutter/lib/src/material/dropdown_menu_form_field.dart +++ b/packages/flutter/lib/src/material/dropdown_menu_form_field.dart @@ -96,7 +96,7 @@ class DropdownMenuFormField extends FormField { textAlign: textAlign, inputDecorationTheme: inputDecorationTheme, menuStyle: menuStyle, - controller: controller, + controller: state.textFieldController, initialSelection: state.value, onSelected: field.didChange, focusNode: focusNode, @@ -139,8 +139,14 @@ class DropdownMenuFormField extends FormField { class _DropdownMenuFormFieldState extends FormFieldState { DropdownMenuFormField get _dropdownMenuFormField => widget as DropdownMenuFormField; + // The controller used to restore the selected item. RestorableTextEditingController? _restorableController; + // The controller used to reset the content of the DropdownMenu inner TextField. + TextEditingController? _localTextFieldController; + TextEditingController get textFieldController => + _dropdownMenuFormField.controller ?? (_localTextFieldController ??= TextEditingController()); + @override void initState() { super.initState(); @@ -163,11 +169,16 @@ class _DropdownMenuFormFieldState extends FormFieldState { if (oldWidget.initialValue != widget.initialValue && !hasInteractedByUser) { setValue(widget.initialValue); } + if (oldWidget.controller != _dropdownMenuFormField.controller) { + _localTextFieldController?.dispose(); + _localTextFieldController = null; + } } @override void dispose() { _restorableController?.dispose(); + _localTextFieldController?.dispose(); super.dispose(); } @@ -183,6 +194,9 @@ class _DropdownMenuFormFieldState extends FormFieldState { super.reset(); _dropdownMenuFormField.onSelected?.call(value); _updateRestorableController(widget.initialValue); + if (widget.initialValue == null) { + textFieldController.clear(); + } } void _updateRestorableController(T? value) { 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 14a6ab92216..4f884b504e3 100644 --- a/packages/flutter/test/material/dropdown_menu_form_field_test.dart +++ b/packages/flutter/test/material/dropdown_menu_form_field_test.dart @@ -506,13 +506,14 @@ void main() { ), ); - // Check default value. - DropdownMenu dropdownMenu = tester.widget(find.byType(DropdownMenu)); - expect(dropdownMenu.controller, null); - final TextEditingController controller = TextEditingController(); addTearDown(controller.dispose); + // Check default value. + DropdownMenu dropdownMenu = tester.widget(find.byType(DropdownMenu)); + expect(dropdownMenu.controller, isNotNull); // A default controller is created. + expect(dropdownMenu.controller, isNot(controller)); + await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -967,6 +968,92 @@ void main() { expect(fieldKey.currentState!.value, MenuItem.menuItem0); }); + // Regression test for https://github.com/flutter/flutter/issues/174578. + testWidgets( + 'Inner text field is cleared on reset when initialSelection is null - Default controller', + (WidgetTester tester) async { + final GlobalKey> fieldKey = GlobalKey>(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DropdownMenuFormField(key: fieldKey, dropdownMenuEntries: menuEntries), + ), + ), + ); + + final TextField textField = tester.widget(find.byType(TextField)); + + // Select menuItem1. + await tester.tap(find.byType(DropdownMenu)); + await tester.pump(); + await tester.tap(findMenuItem(MenuItem.menuItem1)); + await tester.pump(); + expect(fieldKey.currentState!.value, MenuItem.menuItem1); + expect( + textField.controller?.value, + const TextEditingValue(text: 'Item 1', selection: TextSelection.collapsed(offset: 6)), + ); + + // After reset the text field content is cleared. + fieldKey.currentState!.reset(); + await tester.pump(); + + expect(fieldKey.currentState!.value, null); + expect( + textField.controller?.value, + const TextEditingValue(selection: TextSelection.collapsed(offset: 0)), + ); + }, + ); + + // Regression test for https://github.com/flutter/flutter/issues/174578. + testWidgets( + 'Inner text field is cleared on reset when initialSelection is null - Custom controller', + (WidgetTester tester) async { + final GlobalKey> fieldKey = GlobalKey>(); + final TextEditingController controller = TextEditingController(); + addTearDown(controller.dispose); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DropdownMenuFormField( + key: fieldKey, + controller: controller, + dropdownMenuEntries: menuEntries, + ), + ), + ), + ); + + // Custom controller is correctly passed to the inner TextField. + final TextField textField = tester.widget(find.byType(TextField)); + expect(textField.controller, controller); + + // Select menuItem1. + await tester.tap(find.byType(DropdownMenu)); + await tester.pump(); + await tester.tap(findMenuItem(MenuItem.menuItem1)); + await tester.pump(); + expect(fieldKey.currentState!.value, MenuItem.menuItem1); + expect( + textField.controller?.value, + const TextEditingValue(text: 'Item 1', selection: TextSelection.collapsed(offset: 6)), + ); + + // After reset the text field content is cleared. + fieldKey.currentState!.reset(); + await tester.pump(); + + expect(fieldKey.currentState!.value, null); + expect( + controller.value, + const TextEditingValue(selection: TextSelection.collapsed(offset: 0)), + ); + }, + ); + testWidgets('isValid and hasError results are correct', (WidgetTester tester) async { final GlobalKey> fieldKey = GlobalKey>();