Introduce DropdownMenu.closeBehavior
to control menu closing behavior (#156405)
Fixes [Add option to control whether the root DropdownMenu can be closed or not](https://github.com/flutter/flutter/issues/139269) This introduces `DropdownMenu.closeBehavior` to provide control over `DropdownMenu` can be closed in nested menus. ### Code sample <details> <summary>expand to view the code sample</summary> ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatefulWidget { const MyApp({super.key}); @override State<MyApp> createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { String selectedValue = "1"; @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( body: Center( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Column( spacing: 16.0, mainAxisSize: MainAxisSize.min, children: [ const Text("DropdownMenuCloseBehavior.none"), MenuAnchor( menuChildren: <Widget>[ Padding( padding: const EdgeInsets.all(8.0), child: DropdownMenu( closeBehavior: DropdownMenuCloseBehavior.none, label: const Text('Menu'), initialSelection: selectedValue, onSelected: (String? value) { if (value != null) { setState(() { selectedValue = value; }); } }, dropdownMenuEntries: ["1", "2", "3"] .map( (it) => DropdownMenuEntry( value: it, label: it, ), ) .toList(), ), ) ], child: const Text('Open Menu'), builder: (context, controller, child) { return ElevatedButton( onPressed: () { controller.open(); }, child: child, ); }, ), ], ), Column( spacing: 16.0, mainAxisSize: MainAxisSize.min, children: [ const Text("DropdownMenuCloseBehavior.self"), MenuAnchor( menuChildren: <Widget>[ Padding( padding: const EdgeInsets.all(8.0), child: DropdownMenu( closeBehavior: DropdownMenuCloseBehavior.self, label: const Text('Menu'), initialSelection: selectedValue, onSelected: (String? value) { if (value != null) { setState(() { selectedValue = value; }); } }, dropdownMenuEntries: ["1", "2", "3"] .map( (it) => DropdownMenuEntry( value: it, label: it, ), ) .toList(), ), ) ], child: const Text('Open Menu'), builder: (context, controller, child) { return ElevatedButton( onPressed: () { controller.open(); }, child: child, ); }, ), ], ), Column( spacing: 16.0, mainAxisSize: MainAxisSize.min, children: [ const Text("DropdownMenuCloseBehavior.all"), MenuAnchor( menuChildren: <Widget>[ Padding( padding: const EdgeInsets.all(8.0), child: DropdownMenu( closeBehavior: DropdownMenuCloseBehavior.all, label: const Text('Menu'), initialSelection: selectedValue, onSelected: (String? value) { if (value != null) { setState(() { selectedValue = value; }); } }, dropdownMenuEntries: ["1", "2", "3"] .map( (it) => DropdownMenuEntry( value: it, label: it, ), ) .toList(), ), ) ], child: const Text('Open Menu'), builder: (context, controller, child) { return ElevatedButton( onPressed: () { controller.open(); }, child: child, ); }, ), ], ), ], ), ), ), ); } } ``` </details> ### Demo https://github.com/user-attachments/assets/1f79ea6e-c0c6-4dcf-8180-d9dcca1c22c5
This commit is contained in:
parent
56e2a9d380
commit
45033a29f9
@ -102,6 +102,18 @@ class DropdownMenuEntry<T> {
|
||||
final ButtonStyle? style;
|
||||
}
|
||||
|
||||
/// Defines the behavior for closing the dropdown menu when an item is selected.
|
||||
enum DropdownMenuCloseBehavior {
|
||||
/// Closes all open menus in the widget tree.
|
||||
all,
|
||||
|
||||
/// Closes only the current dropdown menu.
|
||||
self,
|
||||
|
||||
/// Does not close any menus.
|
||||
none,
|
||||
}
|
||||
|
||||
/// A dropdown menu that can be opened from a [TextField]. The selected
|
||||
/// menu item is displayed in that field.
|
||||
///
|
||||
@ -172,6 +184,7 @@ class DropdownMenu<T> extends StatefulWidget {
|
||||
this.alignmentOffset,
|
||||
required this.dropdownMenuEntries,
|
||||
this.inputFormatters,
|
||||
this.closeBehavior = DropdownMenuCloseBehavior.all,
|
||||
}) : assert(filterCallback == null || enableFilter);
|
||||
|
||||
/// Determine if the [DropdownMenu] is enabled.
|
||||
@ -473,6 +486,19 @@ class DropdownMenu<T> extends StatefulWidget {
|
||||
/// {@macro flutter.material.MenuAnchor.alignmentOffset}
|
||||
final Offset? alignmentOffset;
|
||||
|
||||
/// Defines the behavior for closing the dropdown menu when an item is selected.
|
||||
///
|
||||
/// The close behavior can be set to:
|
||||
/// * [DropdownMenuCloseBehavior.all]: Closes all open menus in the widget tree.
|
||||
/// * [DropdownMenuCloseBehavior.self]: Closes only the current dropdown menu.
|
||||
/// * [DropdownMenuCloseBehavior.none]: Does not close any menus.
|
||||
///
|
||||
/// This property allows fine-grained control over the menu's closing behavior,
|
||||
/// which can be useful for creating nested or complex menu structures.
|
||||
///
|
||||
/// Defaults to [DropdownMenuCloseBehavior.all].
|
||||
final DropdownMenuCloseBehavior closeBehavior;
|
||||
|
||||
@override
|
||||
State<DropdownMenu<T>> createState() => _DropdownMenuState<T>();
|
||||
}
|
||||
@ -722,6 +748,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
|
||||
style: effectiveStyle,
|
||||
leadingIcon: entry.leadingIcon,
|
||||
trailingIcon: entry.trailingIcon,
|
||||
closeOnActivate: widget.closeBehavior == DropdownMenuCloseBehavior.all,
|
||||
onPressed: entry.enabled && widget.enabled
|
||||
? () {
|
||||
_localTextEditingController?.value = TextEditingValue(
|
||||
@ -731,6 +758,9 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
|
||||
currentHighlight = widget.enableSearch ? i : null;
|
||||
widget.onSelected?.call(entry.value);
|
||||
_enableFilter = false;
|
||||
if (widget.closeBehavior == DropdownMenuCloseBehavior.self) {
|
||||
_controller.close();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
requestFocusOnHover: false,
|
||||
|
@ -3463,6 +3463,92 @@ void main() {
|
||||
textFieldPosition = tester.getTopLeft(find.byType(TextField));
|
||||
expect(textFieldPosition, equals(const Offset(16.0, 544.0)));
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/139269.
|
||||
testWidgets('DropdownMenu.closeBehavior controls menu closing behavior', (WidgetTester tester) async {
|
||||
Widget buildDropdownMenu({ DropdownMenuCloseBehavior closeBehavior = DropdownMenuCloseBehavior.all }) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
body: MenuAnchor(
|
||||
menuChildren: <Widget>[
|
||||
DropdownMenu<TestMenu>(
|
||||
closeBehavior: closeBehavior,
|
||||
dropdownMenuEntries: menuChildren,
|
||||
),
|
||||
],
|
||||
child: const Text('Open Menu'),
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
return ElevatedButton(
|
||||
onPressed: () => controller.open(),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Test closeBehavior set to all.
|
||||
await tester.pumpWidget(buildDropdownMenu());
|
||||
|
||||
// Tap the button to open the root anchor.
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
await tester.pumpAndSettle();
|
||||
// Tap the menu item to open the dropdown menu.
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(DropdownMenu<TestMenu>), findsOneWidget);
|
||||
|
||||
MenuAnchor dropdownMenuAnchor = tester.widget<MenuAnchor>(find.byType(MenuAnchor).last);
|
||||
expect(dropdownMenuAnchor.controller!.isOpen, true);
|
||||
|
||||
// Tap the dropdown menu item.
|
||||
await tester.tap(find.widgetWithText(MenuItemButton, TestMenu.mainMenu0.label).last);
|
||||
await tester.pumpAndSettle();
|
||||
// All menus should be closed.
|
||||
expect(find.byType(DropdownMenu<TestMenu>), findsNothing);
|
||||
expect(find.byType(MenuAnchor), findsOneWidget);
|
||||
|
||||
// Test closeBehavior set to self.
|
||||
await tester.pumpWidget(buildDropdownMenu(closeBehavior: DropdownMenuCloseBehavior.self));
|
||||
|
||||
// Tap the button to open the root anchor.
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(DropdownMenu<TestMenu>), findsOneWidget);
|
||||
|
||||
// Tap the menu item to open the dropdown menu.
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pumpAndSettle();
|
||||
dropdownMenuAnchor = tester.widget<MenuAnchor>(find.byType(MenuAnchor).last);
|
||||
expect(dropdownMenuAnchor.controller!.isOpen, true);
|
||||
|
||||
// Tap the menu item to open the dropdown menu.
|
||||
await tester.tap(find.widgetWithText(MenuItemButton, TestMenu.mainMenu0.label).last);
|
||||
await tester.pumpAndSettle();
|
||||
// Only the dropdown menu should be closed.
|
||||
expect(dropdownMenuAnchor.controller!.isOpen, false);
|
||||
|
||||
// Test closeBehavior set to none.
|
||||
await tester.pumpWidget(buildDropdownMenu(closeBehavior: DropdownMenuCloseBehavior.none));
|
||||
|
||||
// Tap the button to open the root anchor.
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(DropdownMenu<TestMenu>), findsOneWidget);
|
||||
|
||||
// Tap the menu item to open the dropdown menu.
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pumpAndSettle();
|
||||
dropdownMenuAnchor = tester.widget<MenuAnchor>(find.byType(MenuAnchor).last);
|
||||
expect(dropdownMenuAnchor.controller!.isOpen, true);
|
||||
|
||||
// Tap the dropdown menu item.
|
||||
await tester.tap(find.widgetWithText(MenuItemButton, TestMenu.mainMenu0.label).last);
|
||||
await tester.pumpAndSettle();
|
||||
// None of the menus should be closed.
|
||||
expect(dropdownMenuAnchor.controller!.isOpen, true);
|
||||
});
|
||||
}
|
||||
|
||||
enum TestMenu {
|
||||
|
Loading…
x
Reference in New Issue
Block a user