Unverified Commit 91cb2618 authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

Make `DropdownMenu` be able to scroll to the highlighted item when searching. (#129740)

Fixes #120349

This PR enables `DropdownMenu` to automatically scroll to the first matching item when `enableSearch` is true.

<details><summary>video example</summary>

https://github.com/flutter/flutter/assets/36861262/1a7a956c-c186-44ca-9a52-d94dc21cac8a

</details>
parent a2dc0ed8
...@@ -284,6 +284,7 @@ class DropdownMenu<T> extends StatefulWidget { ...@@ -284,6 +284,7 @@ class DropdownMenu<T> extends StatefulWidget {
class _DropdownMenuState<T> extends State<DropdownMenu<T>> { class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
final GlobalKey _anchorKey = GlobalKey(); final GlobalKey _anchorKey = GlobalKey();
final GlobalKey _leadingKey = GlobalKey(); final GlobalKey _leadingKey = GlobalKey();
late List<GlobalKey> buttonItemKeys;
final MenuController _controller = MenuController(); final MenuController _controller = MenuController();
late final TextEditingController _textEditingController; late final TextEditingController _textEditingController;
late bool _enableFilter; late bool _enableFilter;
...@@ -299,6 +300,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { ...@@ -299,6 +300,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
_textEditingController = widget.controller ?? TextEditingController(); _textEditingController = widget.controller ?? TextEditingController();
_enableFilter = widget.enableFilter; _enableFilter = widget.enableFilter;
filteredEntries = widget.dropdownMenuEntries; filteredEntries = widget.dropdownMenuEntries;
buttonItemKeys = List<GlobalKey>.generate(filteredEntries.length, (int index) => GlobalKey());
_menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled); _menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled);
final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection); final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection);
...@@ -313,7 +315,15 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { ...@@ -313,7 +315,15 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
@override @override
void didUpdateWidget(DropdownMenu<T> oldWidget) { void didUpdateWidget(DropdownMenu<T> oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.enableSearch != widget.enableSearch) {
if (!widget.enableSearch) {
currentHighlight = null;
}
}
if (oldWidget.dropdownMenuEntries != widget.dropdownMenuEntries) { if (oldWidget.dropdownMenuEntries != widget.dropdownMenuEntries) {
currentHighlight = null;
filteredEntries = widget.dropdownMenuEntries;
buttonItemKeys = List<GlobalKey>.generate(filteredEntries.length, (int index) => GlobalKey());
_menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled); _menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled);
} }
if (oldWidget.leadingIcon != widget.leadingIcon) { if (oldWidget.leadingIcon != widget.leadingIcon) {
...@@ -354,11 +364,20 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { ...@@ -354,11 +364,20 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
}); });
} }
void scrollToHighlight() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final BuildContext? highlightContext = buttonItemKeys[currentHighlight!].currentContext;
if (highlightContext != null) {
Scrollable.ensureVisible(highlightContext);
}
});
}
double? getWidth(GlobalKey key) { double? getWidth(GlobalKey key) {
final BuildContext? context = key.currentContext; final BuildContext? context = key.currentContext;
if (context != null) { if (context != null) {
final RenderBox box = context.findRenderObject()! as RenderBox; final RenderBox box = context.findRenderObject()! as RenderBox;
return box.size.width; return box.hasSize ? box.size.width : null;
} }
return null; return null;
} }
...@@ -384,7 +403,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { ...@@ -384,7 +403,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
List<DropdownMenuEntry<T>> filteredEntries, List<DropdownMenuEntry<T>> filteredEntries,
TextEditingController textEditingController, TextEditingController textEditingController,
TextDirection textDirection, TextDirection textDirection,
{ int? focusedIndex } { int? focusedIndex, bool enableScrollToHighlight = true}
) { ) {
final List<Widget> result = <Widget>[]; final List<Widget> result = <Widget>[];
final double padding = leadingPadding ?? _kDefaultHorizontalPadding; final double padding = leadingPadding ?? _kDefaultHorizontalPadding;
...@@ -416,6 +435,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { ...@@ -416,6 +435,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
: effectiveStyle; : effectiveStyle;
final MenuItemButton menuItemButton = MenuItemButton( final MenuItemButton menuItemButton = MenuItemButton(
key: enableScrollToHighlight ? buttonItemKeys[i] : null,
style: effectiveStyle, style: effectiveStyle,
leadingIcon: entry.leadingIcon, leadingIcon: entry.leadingIcon,
trailingIcon: entry.trailingIcon, trailingIcon: entry.trailingIcon,
...@@ -490,7 +510,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { ...@@ -490,7 +510,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final TextDirection textDirection = Directionality.of(context); final TextDirection textDirection = Directionality.of(context);
_initialMenu ??= _buildButtons(widget.dropdownMenuEntries, _textEditingController, textDirection); _initialMenu ??= _buildButtons(widget.dropdownMenuEntries, _textEditingController, textDirection, enableScrollToHighlight: false);
final DropdownMenuThemeData theme = DropdownMenuTheme.of(context); final DropdownMenuThemeData theme = DropdownMenuTheme.of(context);
final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context); final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context);
...@@ -500,6 +520,9 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { ...@@ -500,6 +520,9 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
if (widget.enableSearch) { if (widget.enableSearch) {
currentHighlight = search(filteredEntries, _textEditingController); currentHighlight = search(filteredEntries, _textEditingController);
if (currentHighlight != null) {
scrollToHighlight();
}
} }
final List<Widget> menu = _buildButtons(filteredEntries, _textEditingController, textDirection, focusedIndex: currentHighlight); final List<Widget> menu = _buildButtons(filteredEntries, _textEditingController, textDirection, focusedIndex: currentHighlight);
......
...@@ -1315,6 +1315,29 @@ void main() { ...@@ -1315,6 +1315,29 @@ void main() {
await tester.pumpWidget(buildFrame()); await tester.pumpWidget(buildFrame());
expect(find.text(errorText), findsOneWidget); expect(find.text(errorText), findsOneWidget);
}); });
testWidgets('Can scroll to the highlighted item', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: DropdownMenu<TestMenu>(
requestFocusOnTap: true,
menuHeight: 100, // Give a small number so the list can only show 2 or 3 items.
dropdownMenuEntries: menuChildren,
),
),
));
await tester.pumpAndSettle();
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
expect(find.text('Item 5').hitTestable(), findsNothing);
await tester.enterText(find.byType(TextField), '5');
await tester.pumpAndSettle();
// Item 5 should show up.
expect(find.text('Item 5').hitTestable(), findsOneWidget);
});
} }
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