Added filter callback on dropdown menu (#143939)

DropdownMenu can now customize its filter using the new parameter DropdownMenu.filterCallback, similar to DropdownMenu.searchCallback.
This commit is contained in:
Dacian Florea 2024-06-03 21:19:29 +03:00 committed by GitHub
parent be724796aa
commit a92318dd98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 109 additions and 2 deletions

View File

@ -25,6 +25,12 @@ import 'theme_data.dart';
// late BuildContext context;
// late FocusNode myFocusNode;
/// A callback function that returns the list of the items that matches the
/// current applied filter.
///
/// Used by [DropdownMenu.filterCallback].
typedef FilterCallback<T> = List<DropdownMenuEntry<T>> Function(List<DropdownMenuEntry<T>> entries, String filter);
/// A callback function that returns the index of the item that matches the
/// current contents of a text field.
///
@ -163,10 +169,11 @@ class DropdownMenu<T> extends StatefulWidget {
this.focusNode,
this.requestFocusOnTap,
this.expandedInsets,
this.filterCallback,
this.searchCallback,
required this.dropdownMenuEntries,
this.inputFormatters,
});
}) : assert(filterCallback == null || enableFilter);
/// Determine if the [DropdownMenu] is enabled.
///
@ -382,6 +389,41 @@ class DropdownMenu<T> extends StatefulWidget {
/// Defaults to null.
final EdgeInsets? expandedInsets;
/// When [DropdownMenu.enableFilter] is true, this callback is used to
/// compute the list of filtered items.
///
/// {@tool snippet}
///
/// In this example the `filterCallback` returns the items that contains the
/// trimmed query.
///
/// ```dart
/// DropdownMenu<Text>(
/// enableFilter: true,
/// filterCallback: (List<DropdownMenuEntry<Text>> entries, String filter) {
/// final String trimmedFilter = filter.trim().toLowerCase();
/// if (trimmedFilter.isEmpty) {
/// return entries;
/// }
///
/// return entries
/// .where((DropdownMenuEntry<Text> entry) =>
/// entry.label.toLowerCase().contains(trimmedFilter),
/// )
/// .toList();
/// },
/// dropdownMenuEntries: const <DropdownMenuEntry<Text>>[],
/// )
/// ```
/// {@end-tool}
///
/// Defaults to null. If this parameter is null and the
/// [DropdownMenu.enableFilter] property is set to true, the default behavior
/// will return a filtered list. The filtered list will contain items
/// that match the text provided by the input field, with a case-insensitive
/// comparison. When this is not null, `enableFilter` must be set to true.
final FilterCallback<T>? filterCallback;
/// When [DropdownMenu.enableSearch] is true, this callback is used to compute
/// the index of the search result to be highlighted.
///
@ -691,7 +733,8 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context);
if (_enableFilter) {
filteredEntries = filter(widget.dropdownMenuEntries, _localTextEditingController!);
filteredEntries = widget.filterCallback?.call(filteredEntries, _localTextEditingController!.text)
?? filter(widget.dropdownMenuEntries, _localTextEditingController!);
}
if (widget.enableSearch) {

View File

@ -1130,6 +1130,70 @@ void main() {
}
});
testWidgets('Enable filtering with custom filter callback that filter text case sensitive', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
requestFocusOnTap: true,
enableFilter: true,
filterCallback: (List<DropdownMenuEntry<TestMenu>> entries, String filter) {
return entries.where((DropdownMenuEntry<TestMenu> element) => element.label.contains(filter)).toList();
},
dropdownMenuEntries: menuChildren,
controller: controller,
),
),
));
// Open the menu.
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await tester.enterText(find.byType(TextField).first, 'item');
expect(controller.text, 'item');
await tester.pumpAndSettle();
for (final TestMenu menu in TestMenu.values) {
expect(find.widgetWithText(MenuItemButton, menu.label).hitTestable(), findsNothing);
}
await tester.enterText(find.byType(TextField).first, 'Item');
expect(controller.text, 'Item');
await tester.pumpAndSettle();
expect(find.widgetWithText(MenuItemButton, 'Item 0').hitTestable(), findsOneWidget);
expect(find.widgetWithText(MenuItemButton, 'Menu 1').hitTestable(), findsNothing);
expect(find.widgetWithText(MenuItemButton, 'Item 2').hitTestable(), findsOneWidget);
expect(find.widgetWithText(MenuItemButton, 'Item 3').hitTestable(), findsOneWidget);
expect(find.widgetWithText(MenuItemButton, 'Item 4').hitTestable(), findsOneWidget);
expect(find.widgetWithText(MenuItemButton, 'Item 5').hitTestable(), findsOneWidget);
});
testWidgets('Throw assertion error when enable filtering with custom filter callback and enableFilter set on False', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
expect((){
MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu<TestMenu>(
requestFocusOnTap: true,
filterCallback: (List<DropdownMenuEntry<TestMenu>> entries, String filter) {
return entries.where((DropdownMenuEntry<TestMenu> element) => element.label.contains(filter)).toList();
},
dropdownMenuEntries: menuChildren,
controller: controller,
),
),
);
}, throwsAssertionError);
});
testWidgets('The controller can access the value in the input field', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
final TextEditingController controller = TextEditingController();