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 {
class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
final GlobalKey _anchorKey = GlobalKey();
final GlobalKey _leadingKey = GlobalKey();
late List<GlobalKey> buttonItemKeys;
final MenuController _controller = MenuController();
late final TextEditingController _textEditingController;
late bool _enableFilter;
......@@ -299,6 +300,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
_textEditingController = widget.controller ?? TextEditingController();
_enableFilter = widget.enableFilter;
filteredEntries = widget.dropdownMenuEntries;
buttonItemKeys = List<GlobalKey>.generate(filteredEntries.length, (int index) => GlobalKey());
_menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled);
final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection);
......@@ -313,7 +315,15 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
@override
void didUpdateWidget(DropdownMenu<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.enableSearch != widget.enableSearch) {
if (!widget.enableSearch) {
currentHighlight = null;
}
}
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);
}
if (oldWidget.leadingIcon != widget.leadingIcon) {
......@@ -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) {
final BuildContext? context = key.currentContext;
if (context != null) {
final RenderBox box = context.findRenderObject()! as RenderBox;
return box.size.width;
return box.hasSize ? box.size.width : null;
}
return null;
}
......@@ -384,7 +403,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
List<DropdownMenuEntry<T>> filteredEntries,
TextEditingController textEditingController,
TextDirection textDirection,
{ int? focusedIndex }
{ int? focusedIndex, bool enableScrollToHighlight = true}
) {
final List<Widget> result = <Widget>[];
final double padding = leadingPadding ?? _kDefaultHorizontalPadding;
......@@ -416,6 +435,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
: effectiveStyle;
final MenuItemButton menuItemButton = MenuItemButton(
key: enableScrollToHighlight ? buttonItemKeys[i] : null,
style: effectiveStyle,
leadingIcon: entry.leadingIcon,
trailingIcon: entry.trailingIcon,
......@@ -490,7 +510,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
@override
Widget build(BuildContext 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 defaults = _DropdownMenuDefaultsM3(context);
......@@ -500,6 +520,9 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
if (widget.enableSearch) {
currentHighlight = search(filteredEntries, _textEditingController);
if (currentHighlight != null) {
scrollToHighlight();
}
}
final List<Widget> menu = _buildButtons(filteredEntries, _textEditingController, textDirection, focusedIndex: currentHighlight);
......
......@@ -1315,6 +1315,29 @@ void main() {
await tester.pumpWidget(buildFrame());
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 {
......
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