Fix DropdownMenu filtering is broken (#177450)
## Description
This PR fixes `DropdownMenu` filtering.
This is mainly a revert of
https://github.com/flutter/flutter/pull/162062.
It adds a test to avoid a similar regression in the future.
It will reeopen https://github.com/flutter/flutter/issues/155660. A
future PR will try to fix that issue.
See
https://github.com/flutter/flutter/pull/174757#issuecomment-3430614390
for more context.
## Related Issue
Fixes [DropdownMenu filtering is
broken](https://github.com/flutter/flutter/issues/174609)
Reeopens [DropdownMenu.didUpdateWidget should re-match initialSelection
when dropdownMenuEntries have
changed](https://github.com/flutter/flutter/issues/155660)
## Tests
Adds 1 test.
Removes 3 tests (reverted tests from
https://github.com/flutter/flutter/pull/162062).
diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart
index 3d2dfe7..ddaebc0 100644
--- a/packages/flutter/lib/src/material/dropdown_menu.dart
+++ b/packages/flutter/lib/src/material/dropdown_menu.dart
@@ -662,8 +662,6 @@
TextEditingController get _effectiveTextEditingController =>
widget.controller ?? (_localTextEditingController ??= TextEditingController());
final FocusNode _internalFocudeNode = FocusNode();
- int? _selectedEntryIndex;
- late final void Function() _clearSelectedEntryIndex;
FocusNode? _localTrailingIconButtonFocusNode;
FocusNode get _trailingIconButtonFocusNode =>
@@ -672,8 +670,6 @@
@override
void initState() {
super.initState();
- _clearSelectedEntryIndex = () => _selectedEntryIndex = null;
- _effectiveTextEditingController.addListener(_clearSelectedEntryIndex);
_enableSearch = widget.enableSearch;
filteredEntries = widget.dropdownMenuEntries;
buttonItemKeys = List<GlobalKey>.generate(filteredEntries.length, (int index) => GlobalKey());
@@ -686,7 +682,6 @@
text: filteredEntries[index].label,
selection: TextSelection.collapsed(offset: filteredEntries[index].label.length),
);
- _selectedEntryIndex = index;
}
refreshLeadingPadding();
_controller = widget.menuController ?? MenuController();
@@ -694,7 +689,6 @@
@override
void dispose() {
- widget.controller?.removeListener(_clearSelectedEntryIndex);
_localTextEditingController?.dispose();
_localTextEditingController = null;
_internalFocudeNode.dispose();
@@ -707,11 +701,8 @@
void didUpdateWidget(DropdownMenu<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller != widget.controller) {
- oldWidget.controller?.removeListener(_clearSelectedEntryIndex);
_localTextEditingController?.dispose();
_localTextEditingController = null;
- _effectiveTextEditingController.addListener(_clearSelectedEntryIndex);
- _selectedEntryIndex = null;
}
if (oldWidget.enableFilter != widget.enableFilter) {
if (!widget.enableFilter) {
@@ -729,21 +720,6 @@
filteredEntries = widget.dropdownMenuEntries;
buttonItemKeys = List<GlobalKey>.generate(filteredEntries.length, (int index) => GlobalKey());
_menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled);
- if (_selectedEntryIndex != null) {
- final T oldSelectionValue = oldWidget.dropdownMenuEntries[_selectedEntryIndex!].value;
- final int index = filteredEntries.indexWhere(
- (DropdownMenuEntry<T> entry) => entry.value == oldSelectionValue,
- );
- if (index != -1) {
- _effectiveTextEditingController.value = TextEditingValue(
- text: filteredEntries[index].label,
- selection: TextSelection.collapsed(offset: filteredEntries[index].label.length),
- );
- _selectedEntryIndex = index;
- } else {
- _selectedEntryIndex = null;
- }
- }
}
if (oldWidget.leadingIcon != widget.leadingIcon) {
refreshLeadingPadding();
@@ -757,7 +733,6 @@
text: filteredEntries[index].label,
selection: TextSelection.collapsed(offset: filteredEntries[index].label.length),
);
- _selectedEntryIndex = index;
}
}
if (oldWidget.menuController != widget.menuController) {
@@ -963,7 +938,6 @@
text: entry.label,
selection: TextSelection.collapsed(offset: entry.label.length),
);
- _selectedEntryIndex = i;
currentHighlight = widget.enableSearch ? i : null;
widget.onSelected?.call(entry.value);
_enableFilter = false;
@@ -1056,7 +1030,6 @@
text: entry.label,
selection: TextSelection.collapsed(offset: entry.label.length),
);
- _selectedEntryIndex = currentHighlight;
widget.onSelected?.call(entry.value);
}
} else {
diff --git a/packages/flutter/test/material/dropdown_menu_test.dart b/packages/flutter/test/material/dropdown_menu_test.dart
index 5f19b26..e3ca967 100644
--- a/packages/flutter/test/material/dropdown_menu_test.dart
+++ b/packages/flutter/test/material/dropdown_menu_test.dart
@@ -2479,178 +2479,6 @@
},
);
- testWidgets('Rematch selection against the first entry with the same value', (
- WidgetTester tester,
- ) async {
- final TextEditingController controller = TextEditingController();
- addTearDown(controller.dispose);
-
- String selectionLabel = 'Initial label';
-
- await tester.pumpWidget(
- MaterialApp(
- home: StatefulBuilder(
- builder: (BuildContext context, StateSetter setState) {
- return Scaffold(
- body: DropdownMenu<TestMenu>(
- initialSelection: TestMenu.mainMenu0,
- dropdownMenuEntries: <DropdownMenuEntry<TestMenu>>[
- DropdownMenuEntry<TestMenu>(
- value: TestMenu.mainMenu0,
- label: '$selectionLabel 0',
- ),
- DropdownMenuEntry<TestMenu>(
- value: TestMenu.mainMenu1,
- label: '$selectionLabel 1',
- ),
- ],
- controller: controller,
- ),
- floatingActionButton: FloatingActionButton(
- onPressed: () => setState(() => selectionLabel = 'Updated label'),
- ),
- );
- },
- ),
- ),
- );
-
- // Open the menu
- await tester.tap(find.byType(DropdownMenu<TestMenu>));
- await tester.pump();
-
- // Select the second item
- await tester.tap(findMenuItemButton('$selectionLabel 1'));
- await tester.pump();
-
- // Update dropdownMenuEntries labels
- await tester.tap(find.byType(FloatingActionButton));
- await tester.pump();
-
- expect(controller.text, 'Updated label 1');
- });
-
- testWidgets('Forget selection if its value does not map to any entry', (
- WidgetTester tester,
- ) async {
- final TextEditingController controller = TextEditingController();
- addTearDown(controller.dispose);
-
- String selectionLabel = 'Initial label';
- bool selectionInEntries = true;
-
- await tester.pumpWidget(
- MaterialApp(
- home: StatefulBuilder(
- builder: (BuildContext context, StateSetter setState) {
- return Scaffold(
- body: Column(
- children: <Widget>[
- DropdownMenu<TestMenu>(
- initialSelection: TestMenu.mainMenu0,
- dropdownMenuEntries: <DropdownMenuEntry<TestMenu>>[
- DropdownMenuEntry<TestMenu>(
- value: TestMenu.mainMenu0,
- label: '$selectionLabel 0',
- ),
- if (selectionInEntries)
- DropdownMenuEntry<TestMenu>(
- value: TestMenu.mainMenu1,
- label: '$selectionLabel 1',
- ),
- ],
- controller: controller,
- ),
- ElevatedButton(
- onPressed: () => setState(() => selectionInEntries = !selectionInEntries),
- child: null,
- ),
- ],
- ),
- floatingActionButton: FloatingActionButton(
- onPressed: () => setState(() => selectionLabel = 'Updated label'),
- ),
- );
- },
- ),
- ),
- );
-
- // Open the menu
- await tester.tap(find.byType(DropdownMenu<TestMenu>));
- await tester.pump();
-
- // Select the second item
- await tester.tap(findMenuItemButton('$selectionLabel 1'));
- await tester.pump();
-
- // Update dropdownMenuEntries labels
- await tester.tap(find.byType(FloatingActionButton));
- // Remove second item from entires
- await tester.tap(find.byType(ElevatedButton));
- await tester.pump();
-
- expect(controller.text, 'Initial label 1');
-
- // Put second item back into entries
- await tester.tap(find.byType(ElevatedButton));
- await tester.pump();
-
- expect(controller.text, 'Initial label 1');
- });
-
- testWidgets(
- 'Do not rematch selection if the text field was edited progrmaticlly via controller',
- (WidgetTester tester) async {
- final TextEditingController controller = TextEditingController();
- addTearDown(controller.dispose);
-
- String selectionLabel = 'Initial label';
-
- await tester.pumpWidget(
- MaterialApp(
- home: StatefulBuilder(
- builder: (BuildContext context, StateSetter setState) {
- return Scaffold(
- body: Column(
- children: <Widget>[
- DropdownMenu<TestMenu>(
- initialSelection: TestMenu.mainMenu0,
- dropdownMenuEntries: <DropdownMenuEntry<TestMenu>>[
- DropdownMenuEntry<TestMenu>(
- value: TestMenu.mainMenu0,
- label: '$selectionLabel 0',
- ),
- ],
- controller: controller,
- ),
- ElevatedButton(
- onPressed: () => setState(() => controller.text = 'Controller Value'),
- child: null,
- ),
- ],
- ),
- floatingActionButton: FloatingActionButton(
- onPressed: () => setState(() => selectionLabel = 'Updated label'),
- ),
- );
- },
- ),
- ),
- );
-
- // Change the text field value via controller
- await tester.tap(find.byType(ElevatedButton));
- await tester.pump();
-
- // Update dropdownMenuEntries labels
- await tester.tap(find.byType(FloatingActionButton));
- await tester.pump();
-
- expect(controller.text, 'Controller Value');
- },
- );
-
testWidgets('The default text input field should not be focused on mobile platforms '
'when it is tapped', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
@@ -4981,6 +4809,50 @@
}, throwsAssertionError);
});
});
+
+ // Regression test for https://github.com/flutter/flutter/issues/174609.
+ testWidgets(
+ 'DropdownMenu keeps the selected item from filtered list after entries list is updated',
+ (WidgetTester tester) async {
+ final TextEditingController controller = TextEditingController();
+ addTearDown(controller.dispose);
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: StatefulBuilder(
+ builder: (BuildContext context, StateSetter setState) {
+ return DropdownMenu<TestMenu>(
+ controller: controller,
+ requestFocusOnTap: true,
+ enableFilter: true,
+ // toList() is used here to simulate list update.
+ dropdownMenuEntries: menuChildren.toList(),
+ onSelected: (_) {
+ setState(() {});
+ },
+ );
+ },
+ ),
+ ),
+ ),
+ );
+
+ // Open the menu.
+ await tester.tap(find.byType(DropdownMenu<TestMenu>));
+ await tester.pump();
+
+ // Filter the entries to only show 'Menu 1'.
+ await tester.enterText(find.byType(TextField).first, TestMenu.mainMenu1.label);
+ await tester.pump();
+
+ // Select the 'Menu 1' item.
+ await tester.tap(findMenuItemButton(TestMenu.mainMenu1.label));
+ await tester.pumpAndSettle();
+
+ expect(controller.text, TestMenu.mainMenu1.label);
+ },
+ );
}
enum TestMenu {