From 52d47751a065bc41d177f381e169b0ed3ad71b80 Mon Sep 17 00:00:00 2001 From: Tom Larsen Date: Mon, 29 Jan 2018 23:10:43 -0500 Subject: [PATCH] Add onCancelled callback to PopupMenu (#14226) * Add onCancelled callback to PopupMenu * Fix spelling, don't call onCanceled if disposed, improve documentation. --- .../flutter/lib/src/material/popup_menu.dart | 22 ++++++- .../test/material/popup_menu_test.dart | 66 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index 442a22695d..92dfc5b99f 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -685,6 +685,12 @@ Future showMenu({ /// Used by [PopupMenuButton.onSelected]. typedef void PopupMenuItemSelected(T value); +/// Signature for the callback invoked when a [PopupMenuButton] is dismissed +/// without selecting an item. +/// +/// Used by [PopupMenuButton.onCanceled]. +typedef void PopupMenuCanceled(); + /// Signature used by [PopupMenuButton] to lazily construct the items shown when /// the button is pressed. /// @@ -750,6 +756,7 @@ class PopupMenuButton extends StatefulWidget { @required this.itemBuilder, this.initialValue, this.onSelected, + this.onCanceled, this.tooltip, this.elevation: 8.0, this.padding: const EdgeInsets.all(8.0), @@ -766,8 +773,16 @@ class PopupMenuButton extends StatefulWidget { final T initialValue; /// Called when the user selects a value from the popup menu created by this button. + /// + /// If the popup menu is dismissed without selecting a value, [onCanceled] is + /// called instead. final PopupMenuItemSelected onSelected; + /// Called when the user dismisses the popup menu without selecting an item. + /// + /// If the user selects a value, [onSelected] is called instead. + final PopupMenuCanceled onCanceled; + /// Text that describes the action that will occur when the button is pressed. /// /// This text is displayed when the user long-presses on the button and is @@ -814,8 +829,13 @@ class _PopupMenuButtonState extends State> { position: position, ) .then((T newValue) { - if (!mounted || newValue == null) + if (!mounted) return null; + if (newValue == null) { + if (widget.onCanceled != null) + widget.onCanceled(); + return null; + } if (widget.onSelected != null) widget.onSelected(newValue); }); diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart index b64df1c4a2..1ee60cdf1c 100644 --- a/packages/flutter/test/material/popup_menu_test.dart +++ b/packages/flutter/test/material/popup_menu_test.dart @@ -58,6 +58,72 @@ void main() { expect(find.text('Next'), findsOneWidget); }); + testWidgets('PopupMenuButton calls onCanceled callback when an item is not selected', (WidgetTester tester) async { + int cancels = 0; + BuildContext popupContext; + final Key noCallbackKey = new UniqueKey(); + final Key withCallbackKey = new UniqueKey(); + + await tester.pumpWidget( + new MaterialApp( + home: new Material( + child: new Column( + children: [ + new PopupMenuButton( + key: noCallbackKey, + itemBuilder: (BuildContext context) { + return >[ + const PopupMenuItem( + value: 1, + child: const Text('Tap me please!'), + ), + ]; + }, + ), + new PopupMenuButton( + key: withCallbackKey, + onCanceled: () => cancels++, + itemBuilder: (BuildContext context) { + popupContext = context; + return >[ + const PopupMenuItem( + value: 1, + child: const Text('Tap me, too!'), + ), + ]; + }, + ), + ], + ), + ), + ), + ); + + // Make sure everything works if no callback is provided + await tester.tap(find.byKey(noCallbackKey)); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + await tester.tapAt(const Offset(0.0, 0.0)); + await tester.pump(); + expect(cancels, equals(0)); + + // Make sure callback is called when a non-selection tap occurs + await tester.tap(find.byKey(withCallbackKey)); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + await tester.tapAt(const Offset(0.0, 0.0)); + await tester.pump(); + expect(cancels, equals(1)); + + // Make sure callback is called when back navigation occurs + await tester.tap(find.byKey(withCallbackKey)); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + Navigator.of(popupContext).pop(); + await tester.pump(); + expect(cancels, equals(2)); + }); + testWidgets('PopupMenuButton is horizontal on iOS', (WidgetTester tester) async { Widget build(TargetPlatform platform) { return new MaterialApp(