diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart index 14fd7ec1a29..9c11c5523e8 100644 --- a/packages/flutter/lib/src/material/dropdown_menu.dart +++ b/packages/flutter/lib/src/material/dropdown_menu.dart @@ -102,6 +102,18 @@ class DropdownMenuEntry { 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 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 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> createState() => _DropdownMenuState(); } @@ -722,6 +748,7 @@ class _DropdownMenuState extends State> { 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 extends State> { currentHighlight = widget.enableSearch ? i : null; widget.onSelected?.call(entry.value); _enableFilter = false; + if (widget.closeBehavior == DropdownMenuCloseBehavior.self) { + _controller.close(); + } } : null, requestFocusOnHover: false, diff --git a/packages/flutter/test/material/dropdown_menu_test.dart b/packages/flutter/test/material/dropdown_menu_test.dart index c0d1811bc2b..3aa9ab2f831 100644 --- a/packages/flutter/test/material/dropdown_menu_test.dart +++ b/packages/flutter/test/material/dropdown_menu_test.dart @@ -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: [ + DropdownMenu( + 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), findsOneWidget); + + MenuAnchor dropdownMenuAnchor = tester.widget(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), 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), findsOneWidget); + + // Tap the menu item to open the dropdown menu. + await tester.tap(find.byType(TextField)); + await tester.pumpAndSettle(); + dropdownMenuAnchor = tester.widget(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), findsOneWidget); + + // Tap the menu item to open the dropdown menu. + await tester.tap(find.byType(TextField)); + await tester.pumpAndSettle(); + dropdownMenuAnchor = tester.widget(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 {