Unverified Commit 40776d01 authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

Allow users to customize search algorithm in `DropdownMenu` (#136848)

Fixes #136735

This PR is to add a searchCallback to allow users to customize the search algorithm. This feature is used to fix b/305662376 which needs an exact match algorithm.
parent 01eef7ca
...@@ -21,6 +21,13 @@ import 'text_field.dart'; ...@@ -21,6 +21,13 @@ import 'text_field.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
/// A callback function that returns the index of the item that matches the
/// current contents of a text field.
///
/// If a match doesn't exist then null must be returned.
///
/// Used by [DropdownMenu.searchCallback].
typedef SearchCallback<T> = int? Function(List<DropdownMenuEntry<T>> entries, String query);
// Navigation shortcuts to move the selected menu items up or down. // Navigation shortcuts to move the selected menu items up or down.
Map<ShortcutActivator, Intent> _kMenuTraversalShortcuts = <ShortcutActivator, Intent> { Map<ShortcutActivator, Intent> _kMenuTraversalShortcuts = <ShortcutActivator, Intent> {
...@@ -150,6 +157,7 @@ class DropdownMenu<T> extends StatefulWidget { ...@@ -150,6 +157,7 @@ class DropdownMenu<T> extends StatefulWidget {
this.onSelected, this.onSelected,
this.requestFocusOnTap, this.requestFocusOnTap,
this.expandedInsets, this.expandedInsets,
this.searchCallback,
required this.dropdownMenuEntries, required this.dropdownMenuEntries,
}); });
...@@ -303,6 +311,34 @@ class DropdownMenu<T> extends StatefulWidget { ...@@ -303,6 +311,34 @@ class DropdownMenu<T> extends StatefulWidget {
/// Defaults to null. /// Defaults to null.
final EdgeInsets? expandedInsets; final EdgeInsets? expandedInsets;
/// When [DropdownMenu.enableSearch] is true, this callback is used to compute
/// the index of the search result to be highlighted.
///
/// {@tool snippet}
///
/// In this example the `searchCallback` returns the index of the search result
/// that exactly matches the query.
///
/// ```dart
/// DropdownMenu<Text>(
/// searchCallback: (List<DropdownMenuEntry<Text>> entries, String query) {
/// if (query.isEmpty) {
/// return null;
/// }
/// final int index = entries.indexWhere((DropdownMenuEntry<Text> entry) => entry.label == query);
///
/// return index != -1 ? index : null;
/// },
/// dropdownMenuEntries: const <DropdownMenuEntry<Text>>[],
/// )
/// ```
/// {@end-tool}
///
/// Defaults to null. If this is null and [DropdownMenu.enableSearch] is true,
/// the default function will return the index of the first matching result
/// which contains the contents of the text input field.
final SearchCallback<T>? searchCallback;
@override @override
State<DropdownMenu<T>> createState() => _DropdownMenuState<T>(); State<DropdownMenu<T>> createState() => _DropdownMenuState<T>();
} }
...@@ -564,7 +600,11 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { ...@@ -564,7 +600,11 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
} }
if (widget.enableSearch) { if (widget.enableSearch) {
currentHighlight = search(filteredEntries, _textEditingController); if (widget.searchCallback != null) {
currentHighlight = widget.searchCallback!.call(filteredEntries, _textEditingController.text);
} else {
currentHighlight = search(filteredEntries, _textEditingController);
}
if (currentHighlight != null) { if (currentHighlight != null) {
scrollToHighlight(); scrollToHighlight();
} }
......
...@@ -1726,6 +1726,85 @@ void main() { ...@@ -1726,6 +1726,85 @@ void main() {
// so there are some extra padding before "Item 1". // so there are some extra padding before "Item 1".
expect(tester.getTopLeft(find.text('Item 1').last).dx, 48.0); expect(tester.getTopLeft(find.text('Item 1').last).dx, 48.0);
}); });
testWidgetsWithLeakTracking('DropdownMenu can have customized search algorithm', (WidgetTester tester) async {
final ThemeData theme = ThemeData();
Widget dropdownMenu({ SearchCallback<int>? searchCallback }) {
return MaterialApp(
theme: theme,
home: Scaffold(
body: DropdownMenu<int>(
requestFocusOnTap: true,
searchCallback: searchCallback,
dropdownMenuEntries: const <DropdownMenuEntry<int>>[
DropdownMenuEntry<int>(value: 0, label: 'All'),
DropdownMenuEntry<int>(value: 1, label: 'Unread'),
DropdownMenuEntry<int>(value: 2, label: 'Read'),
],
),
)
);
}
void checkExpectedHighlight({String? searchResult, required List<String> otherItems}) {
if (searchResult != null) {
final Finder material = find.descendant(
of: find.widgetWithText(MenuItemButton, searchResult).last,
matching: find.byType(Material),
);
final Material itemMaterial = tester.widget<Material>(material);
expect(itemMaterial.color, theme.colorScheme.onSurface.withOpacity(0.12));
}
for (final String nonHighlight in otherItems) {
final Finder material = find.descendant(
of: find.widgetWithText(MenuItemButton, nonHighlight).last,
matching: find.byType(Material),
);
final Material itemMaterial = tester.widget<Material>(material);
expect(itemMaterial.color, Colors.transparent);
}
}
// Test default.
await tester.pumpWidget(dropdownMenu());
await tester.pump();
await tester.tap(find.byType(DropdownMenu<int>));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), 'read');
await tester.pump();
checkExpectedHighlight(searchResult: 'Unread', otherItems: <String>['All', 'Read']); // Because "Unread" contains "read".
// Test custom search algorithm.
await tester.pumpWidget(dropdownMenu(
searchCallback: (_, __) => 0
));
await tester.pump();
await tester.enterText(find.byType(TextField), 'read');
await tester.pump();
checkExpectedHighlight(searchResult: 'All', otherItems: <String>['Unread', 'Read']); // Because the search result should always be index 0.
// Test custom search algorithm - exact match.
await tester.pumpWidget(dropdownMenu(
searchCallback: (List<DropdownMenuEntry<int>> entries, String query) {
if (query.isEmpty) {
return null;
}
final int index = entries.indexWhere((DropdownMenuEntry<int> entry) => entry.label == query);
return index != -1 ? index : null;
},
));
await tester.pump();
await tester.enterText(find.byType(TextField), 'read');
await tester.pump();
checkExpectedHighlight(otherItems: <String>['All', 'Unread', 'Read']); // Because it's case sensitive.
await tester.enterText(find.byType(TextField), 'Read');
await tester.pump();
checkExpectedHighlight(searchResult: 'Read', otherItems: <String>['All', 'Unread']);
});
} }
enum TestMenu { enum TestMenu {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment