Skip to content

Commit a92318d

Browse files
authored
Added filter callback on dropdown menu (#143939)
DropdownMenu can now customize its filter using the new parameter DropdownMenu.filterCallback, similar to DropdownMenu.searchCallback.
1 parent be72479 commit a92318d

File tree

2 files changed

+109
-2
lines changed

2 files changed

+109
-2
lines changed

packages/flutter/lib/src/material/dropdown_menu.dart

+45-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ import 'theme_data.dart';
2525
// late BuildContext context;
2626
// late FocusNode myFocusNode;
2727

28+
/// A callback function that returns the list of the items that matches the
29+
/// current applied filter.
30+
///
31+
/// Used by [DropdownMenu.filterCallback].
32+
typedef FilterCallback<T> = List<DropdownMenuEntry<T>> Function(List<DropdownMenuEntry<T>> entries, String filter);
33+
2834
/// A callback function that returns the index of the item that matches the
2935
/// current contents of a text field.
3036
///
@@ -163,10 +169,11 @@ class DropdownMenu<T> extends StatefulWidget {
163169
this.focusNode,
164170
this.requestFocusOnTap,
165171
this.expandedInsets,
172+
this.filterCallback,
166173
this.searchCallback,
167174
required this.dropdownMenuEntries,
168175
this.inputFormatters,
169-
});
176+
}) : assert(filterCallback == null || enableFilter);
170177

171178
/// Determine if the [DropdownMenu] is enabled.
172179
///
@@ -382,6 +389,41 @@ class DropdownMenu<T> extends StatefulWidget {
382389
/// Defaults to null.
383390
final EdgeInsets? expandedInsets;
384391

392+
/// When [DropdownMenu.enableFilter] is true, this callback is used to
393+
/// compute the list of filtered items.
394+
///
395+
/// {@tool snippet}
396+
///
397+
/// In this example the `filterCallback` returns the items that contains the
398+
/// trimmed query.
399+
///
400+
/// ```dart
401+
/// DropdownMenu<Text>(
402+
/// enableFilter: true,
403+
/// filterCallback: (List<DropdownMenuEntry<Text>> entries, String filter) {
404+
/// final String trimmedFilter = filter.trim().toLowerCase();
405+
/// if (trimmedFilter.isEmpty) {
406+
/// return entries;
407+
/// }
408+
///
409+
/// return entries
410+
/// .where((DropdownMenuEntry<Text> entry) =>
411+
/// entry.label.toLowerCase().contains(trimmedFilter),
412+
/// )
413+
/// .toList();
414+
/// },
415+
/// dropdownMenuEntries: const <DropdownMenuEntry<Text>>[],
416+
/// )
417+
/// ```
418+
/// {@end-tool}
419+
///
420+
/// Defaults to null. If this parameter is null and the
421+
/// [DropdownMenu.enableFilter] property is set to true, the default behavior
422+
/// will return a filtered list. The filtered list will contain items
423+
/// that match the text provided by the input field, with a case-insensitive
424+
/// comparison. When this is not null, `enableFilter` must be set to true.
425+
final FilterCallback<T>? filterCallback;
426+
385427
/// When [DropdownMenu.enableSearch] is true, this callback is used to compute
386428
/// the index of the search result to be highlighted.
387429
///
@@ -691,7 +733,8 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
691733
final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context);
692734

693735
if (_enableFilter) {
694-
filteredEntries = filter(widget.dropdownMenuEntries, _localTextEditingController!);
736+
filteredEntries = widget.filterCallback?.call(filteredEntries, _localTextEditingController!.text)
737+
?? filter(widget.dropdownMenuEntries, _localTextEditingController!);
695738
}
696739

697740
if (widget.enableSearch) {

packages/flutter/test/material/dropdown_menu_test.dart

+64
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,70 @@ void main() {
11301130
}
11311131
});
11321132

1133+
testWidgets('Enable filtering with custom filter callback that filter text case sensitive', (WidgetTester tester) async {
1134+
final ThemeData themeData = ThemeData();
1135+
final TextEditingController controller = TextEditingController();
1136+
addTearDown(controller.dispose);
1137+
1138+
await tester.pumpWidget(MaterialApp(
1139+
theme: themeData,
1140+
home: Scaffold(
1141+
body: DropdownMenu<TestMenu>(
1142+
requestFocusOnTap: true,
1143+
enableFilter: true,
1144+
filterCallback: (List<DropdownMenuEntry<TestMenu>> entries, String filter) {
1145+
return entries.where((DropdownMenuEntry<TestMenu> element) => element.label.contains(filter)).toList();
1146+
},
1147+
dropdownMenuEntries: menuChildren,
1148+
controller: controller,
1149+
),
1150+
),
1151+
));
1152+
1153+
// Open the menu.
1154+
await tester.tap(find.byType(DropdownMenu<TestMenu>));
1155+
await tester.pump();
1156+
1157+
await tester.enterText(find.byType(TextField).first, 'item');
1158+
expect(controller.text, 'item');
1159+
await tester.pumpAndSettle();
1160+
for (final TestMenu menu in TestMenu.values) {
1161+
expect(find.widgetWithText(MenuItemButton, menu.label).hitTestable(), findsNothing);
1162+
}
1163+
1164+
await tester.enterText(find.byType(TextField).first, 'Item');
1165+
expect(controller.text, 'Item');
1166+
await tester.pumpAndSettle();
1167+
expect(find.widgetWithText(MenuItemButton, 'Item 0').hitTestable(), findsOneWidget);
1168+
expect(find.widgetWithText(MenuItemButton, 'Menu 1').hitTestable(), findsNothing);
1169+
expect(find.widgetWithText(MenuItemButton, 'Item 2').hitTestable(), findsOneWidget);
1170+
expect(find.widgetWithText(MenuItemButton, 'Item 3').hitTestable(), findsOneWidget);
1171+
expect(find.widgetWithText(MenuItemButton, 'Item 4').hitTestable(), findsOneWidget);
1172+
expect(find.widgetWithText(MenuItemButton, 'Item 5').hitTestable(), findsOneWidget);
1173+
});
1174+
1175+
testWidgets('Throw assertion error when enable filtering with custom filter callback and enableFilter set on False', (WidgetTester tester) async {
1176+
final ThemeData themeData = ThemeData();
1177+
final TextEditingController controller = TextEditingController();
1178+
addTearDown(controller.dispose);
1179+
1180+
expect((){
1181+
MaterialApp(
1182+
theme: themeData,
1183+
home: Scaffold(
1184+
body: DropdownMenu<TestMenu>(
1185+
requestFocusOnTap: true,
1186+
filterCallback: (List<DropdownMenuEntry<TestMenu>> entries, String filter) {
1187+
return entries.where((DropdownMenuEntry<TestMenu> element) => element.label.contains(filter)).toList();
1188+
},
1189+
dropdownMenuEntries: menuChildren,
1190+
controller: controller,
1191+
),
1192+
),
1193+
);
1194+
}, throwsAssertionError);
1195+
});
1196+
11331197
testWidgets('The controller can access the value in the input field', (WidgetTester tester) async {
11341198
final ThemeData themeData = ThemeData();
11351199
final TextEditingController controller = TextEditingController();

0 commit comments

Comments
 (0)