Fix DropdownMenuFormField does not clear text field content on reset … (#174937)

## 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.
This commit is contained in:
Bruno Leroux 2025-09-08 14:09:10 +02:00 committed by GitHub
parent 937f2b9ad4
commit 6ae27959de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 106 additions and 5 deletions

View File

@ -96,7 +96,7 @@ class DropdownMenuFormField<T> extends FormField<T> {
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<T> extends FormField<T> {
class _DropdownMenuFormFieldState<T> extends FormFieldState<T> {
DropdownMenuFormField<T> get _dropdownMenuFormField => widget as DropdownMenuFormField<T>;
// 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<T> extends FormFieldState<T> {
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<T> extends FormFieldState<T> {
super.reset();
_dropdownMenuFormField.onSelected?.call(value);
_updateRestorableController(widget.initialValue);
if (widget.initialValue == null) {
textFieldController.clear();
}
}
void _updateRestorableController(T? value) {

View File

@ -506,13 +506,14 @@ void main() {
),
);
// Check default value.
DropdownMenu<MenuItem> dropdownMenu = tester.widget(find.byType(DropdownMenu<MenuItem>));
expect(dropdownMenu.controller, null);
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
// Check default value.
DropdownMenu<MenuItem> dropdownMenu = tester.widget(find.byType(DropdownMenu<MenuItem>));
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<FormFieldState<MenuItem>> fieldKey = GlobalKey<FormFieldState<MenuItem>>();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: DropdownMenuFormField<MenuItem>(key: fieldKey, dropdownMenuEntries: menuEntries),
),
),
);
final TextField textField = tester.widget(find.byType(TextField));
// Select menuItem1.
await tester.tap(find.byType(DropdownMenu<MenuItem>));
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<FormFieldState<MenuItem>> fieldKey = GlobalKey<FormFieldState<MenuItem>>();
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: DropdownMenuFormField<MenuItem>(
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<MenuItem>));
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<FormFieldState<MenuItem>> fieldKey = GlobalKey<FormFieldState<MenuItem>>();