mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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).
This commit is contained in:
parent
f62f80bb31
commit
b05f001129
@ -662,8 +662,6 @@ class _DropdownMenuState<T extends Object> extends State<DropdownMenu<T>> {
|
||||
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 @@ class _DropdownMenuState<T extends Object> extends State<DropdownMenu<T>> {
|
||||
@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 @@ class _DropdownMenuState<T extends Object> extends State<DropdownMenu<T>> {
|
||||
text: filteredEntries[index].label,
|
||||
selection: TextSelection.collapsed(offset: filteredEntries[index].label.length),
|
||||
);
|
||||
_selectedEntryIndex = index;
|
||||
}
|
||||
refreshLeadingPadding();
|
||||
_controller = widget.menuController ?? MenuController();
|
||||
@ -694,7 +689,6 @@ class _DropdownMenuState<T extends Object> extends State<DropdownMenu<T>> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller?.removeListener(_clearSelectedEntryIndex);
|
||||
_localTextEditingController?.dispose();
|
||||
_localTextEditingController = null;
|
||||
_internalFocudeNode.dispose();
|
||||
@ -707,11 +701,8 @@ class _DropdownMenuState<T extends Object> extends State<DropdownMenu<T>> {
|
||||
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 @@ class _DropdownMenuState<T extends Object> extends State<DropdownMenu<T>> {
|
||||
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 @@ class _DropdownMenuState<T extends Object> extends State<DropdownMenu<T>> {
|
||||
text: filteredEntries[index].label,
|
||||
selection: TextSelection.collapsed(offset: filteredEntries[index].label.length),
|
||||
);
|
||||
_selectedEntryIndex = index;
|
||||
}
|
||||
}
|
||||
if (oldWidget.menuController != widget.menuController) {
|
||||
@ -963,7 +938,6 @@ class _DropdownMenuState<T extends Object> extends State<DropdownMenu<T>> {
|
||||
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 @@ class _DropdownMenuState<T extends Object> extends State<DropdownMenu<T>> {
|
||||
text: entry.label,
|
||||
selection: TextSelection.collapsed(offset: entry.label.length),
|
||||
);
|
||||
_selectedEntryIndex = currentHighlight;
|
||||
widget.onSelected?.call(entry.value);
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -2479,178 +2479,6 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
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 @@ void main() {
|
||||
}, 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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user