From 6ae27959deb07ce2a1e072a08cbdf0b236b63b7c Mon Sep 17 00:00:00 2001 From: Bruno Leroux Date: Mon, 8 Sep 2025 14:09:10 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20DropdownMenuFormField=20does=20not=20clea?= =?UTF-8?q?r=20text=20field=20content=20on=20reset=20=E2=80=A6=20(#174937)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This PR fixes `DropdownMenuFormField` not clearing the text field value when the form is reset and DropdownMenuFormField.initialSelection is null. ## Related Issue Fixes [DropdownMenuFormField does not reset when calling FormState.reset](https://github.com/flutter/flutter/issues/174578) ## Tests Adds 2 tests. --- .../material/dropdown_menu_form_field.dart | 16 +++- .../dropdown_menu_form_field_test.dart | 95 ++++++++++++++++++- 2 files changed, 106 insertions(+), 5 deletions(-) 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>();