From 336ae2de7962d4ecc39d74ccf6211c1c3dc65b49 Mon Sep 17 00:00:00 2001 From: Ludwik Trammer Date: Mon, 3 May 2021 19:06:53 +0200 Subject: [PATCH] Add "onTap" callback to PopupMenuItem (#81686) --- .../flutter/lib/src/material/popup_menu.dart | 6 + .../test/material/popup_menu_test.dart | 133 ++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index 2e3f2ef685..e5b05e2294 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -219,6 +219,7 @@ class PopupMenuItem extends PopupMenuEntry { const PopupMenuItem({ Key? key, this.value, + this.onTap, this.enabled = true, this.height = kMinInteractiveDimension, this.padding, @@ -232,6 +233,9 @@ class PopupMenuItem extends PopupMenuEntry { /// The value that will be returned by [showMenu] if this entry is selected. final T? value; + /// Called when the menu item is tapped. + final VoidCallback? onTap; + /// Whether the user is permitted to select this item. /// /// Defaults to true. If this is false, then the item will not react to @@ -319,6 +323,8 @@ class PopupMenuItemState> extends State { /// the menu route. @protected void handleTap() { + widget.onTap?.call(); + Navigator.pop(context, widget.value); } diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart index 8e06b62507..14ada4baed 100644 --- a/packages/flutter/test/material/popup_menu_test.dart +++ b/packages/flutter/test/material/popup_menu_test.dart @@ -289,6 +289,139 @@ void main() { expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isTrue); }); + testWidgets('PopupMenuItem onTap callback is called when defined', (WidgetTester tester) async { + final List menuItemTapCounters = [0, 0]; + + await tester.pumpWidget( + TestApp( + textDirection: TextDirection.ltr, + child: Material( + child: RepaintBoundary( + child: PopupMenuButton( + child: const Text('Actions'), + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + child: const Text('First option'), + onTap: () { + menuItemTapCounters[0]++; + }, + ), + PopupMenuItem( + child: const Text('Second option'), + onTap: () { + menuItemTapCounters[1]++; + }, + ), + const PopupMenuItem( + child: Text('Option without onTap'), + ), + ], + ), + ), + ), + ), + ); + + // Tap the first tiem + await tester.tap(find.text('Actions')); + await tester.pumpAndSettle(); + await tester.tap(find.text('First option')); + await tester.pumpAndSettle(); + expect(menuItemTapCounters, [1, 0]); + + // Tap the item again + await tester.tap(find.text('Actions')); + await tester.pumpAndSettle(); + await tester.tap(find.text('First option')); + await tester.pumpAndSettle(); + expect(menuItemTapCounters, [2, 0]); + + // Tap a different item + await tester.tap(find.text('Actions')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Second option')); + await tester.pumpAndSettle(); + expect(menuItemTapCounters, [2, 1]); + + // Tap an iteem without onTap + await tester.tap(find.text('Actions')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Option without onTap')); + await tester.pumpAndSettle(); + expect(menuItemTapCounters, [2, 1]); + }); + + testWidgets('PopupMenuItem can have both onTap and value', (WidgetTester tester) async { + final List menuItemTapCounters = [0, 0]; + String? selected; + + await tester.pumpWidget( + TestApp( + textDirection: TextDirection.ltr, + child: Material( + child: RepaintBoundary( + child: PopupMenuButton( + child: const Text('Actions'), + onSelected: (String value) { selected = value; }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + child: const Text('First option'), + value: 'first', + onTap: () { + menuItemTapCounters[0]++; + }, + ), + PopupMenuItem( + child: const Text('Second option'), + value: 'second', + onTap: () { + menuItemTapCounters[1]++; + }, + ), + const PopupMenuItem( + child: Text('Option without onTap'), + value: 'third', + ), + ], + ), + ), + ), + ), + ); + + // Tap the first item + await tester.tap(find.text('Actions')); + await tester.pumpAndSettle(); + await tester.tap(find.text('First option')); + await tester.pumpAndSettle(); + expect(menuItemTapCounters, [1, 0]); + expect(selected, 'first'); + + // Tap the item again + await tester.tap(find.text('Actions')); + await tester.pumpAndSettle(); + await tester.tap(find.text('First option')); + await tester.pumpAndSettle(); + expect(menuItemTapCounters, [2, 0]); + expect(selected, 'first'); + + // Tap a different item + await tester.tap(find.text('Actions')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Second option')); + await tester.pumpAndSettle(); + expect(menuItemTapCounters, [2, 1]); + expect(selected, 'second'); + + // Tap an iteem without onTap + await tester.tap(find.text('Actions')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Option without onTap')); + await tester.pumpAndSettle(); + expect(menuItemTapCounters, [2, 1]); + expect(selected, 'third'); + }); + testWidgets('PopupMenuItem is only focusable when enabled', (WidgetTester tester) async { final Key popupButtonKey = UniqueKey(); final GlobalKey childKey = GlobalKey();