Commit 52d47751 authored by Tom Larsen's avatar Tom Larsen Committed by Ian Hickson

Add onCancelled callback to PopupMenu (#14226)

* Add onCancelled callback to PopupMenu

* Fix spelling, don't call onCanceled if disposed, improve documentation.
parent 2e449f06
......@@ -685,6 +685,12 @@ Future<T> showMenu<T>({
/// Used by [PopupMenuButton.onSelected].
typedef void PopupMenuItemSelected<T>(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<T> 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<T> 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<T> 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<T> extends State<PopupMenuButton<T>> {
position: position,
)
.then<void>((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);
});
......
......@@ -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: <Widget>[
new PopupMenuButton<int>(
key: noCallbackKey,
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
value: 1,
child: const Text('Tap me please!'),
),
];
},
),
new PopupMenuButton<int>(
key: withCallbackKey,
onCanceled: () => cancels++,
itemBuilder: (BuildContext context) {
popupContext = context;
return <PopupMenuEntry<int>>[
const PopupMenuItem<int>(
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(
......
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