Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 4dd7065

Browse files
authored
Add onOpened callback to PopupMenuButton (#103753)
1 parent 5fcd9c2 commit 4dd7065

File tree

2 files changed

+84
-1
lines changed

2 files changed

+84
-1
lines changed

packages/flutter/lib/src/material/popup_menu.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,7 @@ class PopupMenuButton<T> extends StatefulWidget {
10091009
super.key,
10101010
required this.itemBuilder,
10111011
this.initialValue,
1012+
this.onOpened,
10121013
this.onSelected,
10131014
this.onCanceled,
10141015
this.tooltip,
@@ -1039,6 +1040,9 @@ class PopupMenuButton<T> extends StatefulWidget {
10391040
/// The value of the menu item, if any, that should be highlighted when the menu opens.
10401041
final T? initialValue;
10411042

1043+
/// Called when the popup menu is shown.
1044+
final VoidCallback? onOpened;
1045+
10421046
/// Called when the user selects a value from the popup menu created by this button.
10431047
///
10441048
/// If the popup menu is dismissed without selecting a value, [onCanceled] is
@@ -1204,6 +1208,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
12041208
final List<PopupMenuEntry<T>> items = widget.itemBuilder(context);
12051209
// Only show the menu if there is something to show
12061210
if (items.isNotEmpty) {
1211+
widget.onOpened?.call();
12071212
showMenu<T?>(
12081213
context: context,
12091214
elevation: widget.elevation ?? popupMenuTheme.elevation,

packages/flutter/test/material/popup_menu_test.dart

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,75 @@ void main() {
6464
expect(find.text('Next'), findsOneWidget);
6565
});
6666

67+
testWidgets('PopupMenuButton calls onOpened callback when the menu is opened', (WidgetTester tester) async {
68+
int opens = 0;
69+
late BuildContext popupContext;
70+
final Key noItemsKey = UniqueKey();
71+
final Key noCallbackKey = UniqueKey();
72+
final Key withCallbackKey = UniqueKey();
73+
74+
await tester.pumpWidget(
75+
MaterialApp(
76+
home: Material(
77+
child: Column(
78+
children: <Widget>[
79+
PopupMenuButton<int>(
80+
key: noItemsKey,
81+
itemBuilder: (BuildContext context) {
82+
return <PopupMenuEntry<int>>[];
83+
},
84+
onOpened: () => opens++,
85+
),
86+
PopupMenuButton<int>(
87+
key: noCallbackKey,
88+
itemBuilder: (BuildContext context) {
89+
popupContext = context;
90+
return <PopupMenuEntry<int>>[
91+
const PopupMenuItem<int>(
92+
value: 1,
93+
child: Text('Tap me please!'),
94+
),
95+
];
96+
},
97+
),
98+
PopupMenuButton<int>(
99+
key: withCallbackKey,
100+
itemBuilder: (BuildContext context) {
101+
return <PopupMenuEntry<int>>[
102+
const PopupMenuItem<int>(
103+
value: 1,
104+
child: Text('Tap me, too!'),
105+
),
106+
];
107+
},
108+
onOpened: () => opens++,
109+
),
110+
],
111+
),
112+
),
113+
),
114+
);
115+
116+
// Make sure callback is not called when the menu is not shown
117+
await tester.tap(find.byKey(noItemsKey));
118+
await tester.pump();
119+
expect(opens, equals(0));
120+
121+
// Make sure everything works if no callback is provided
122+
await tester.tap(find.byKey(noCallbackKey));
123+
await tester.pump();
124+
expect(opens, equals(0));
125+
126+
// Close the opened menu
127+
Navigator.of(popupContext).pop();
128+
await tester.pump();
129+
130+
// Make sure callback is called when the button is tapped
131+
await tester.tap(find.byKey(withCallbackKey));
132+
await tester.pump();
133+
expect(opens, equals(1));
134+
});
135+
67136
testWidgets('PopupMenuButton calls onCanceled callback when an item is not selected', (WidgetTester tester) async {
68137
int cancels = 0;
69138
late BuildContext popupContext;
@@ -130,9 +199,10 @@ void main() {
130199
expect(cancels, equals(2));
131200
});
132201

133-
testWidgets('disabled PopupMenuButton will not call itemBuilder, onSelected or onCanceled', (WidgetTester tester) async {
202+
testWidgets('disabled PopupMenuButton will not call itemBuilder, onOpened, onSelected or onCanceled', (WidgetTester tester) async {
134203
final GlobalKey popupButtonKey = GlobalKey();
135204
bool itemBuilderCalled = false;
205+
bool onOpenedCalled = false;
136206
bool onSelectedCalled = false;
137207
bool onCanceledCalled = false;
138208

@@ -158,6 +228,7 @@ void main() {
158228
),
159229
];
160230
},
231+
onOpened: ()=> onOpenedCalled = true,
161232
onSelected: (int selected) => onSelectedCalled = true,
162233
onCanceled: () => onCanceledCalled = true,
163234
),
@@ -177,6 +248,7 @@ void main() {
177248
await tester.tap(find.byKey(popupButtonKey));
178249
await tester.pumpAndSettle();
179250
expect(itemBuilderCalled, isFalse);
251+
expect(onOpenedCalled, isFalse);
180252
expect(onSelectedCalled, isFalse);
181253

182254
// Try to bring up the popup menu and tap outside it to cancel the menu
@@ -185,6 +257,7 @@ void main() {
185257
await tester.tapAt(Offset.zero);
186258
await tester.pumpAndSettle();
187259
expect(itemBuilderCalled, isFalse);
260+
expect(onOpenedCalled, isFalse);
188261
expect(onCanceledCalled, isFalse);
189262

190263
// Test again, with directional navigation mode and after focusing the button.
@@ -198,6 +271,7 @@ void main() {
198271
await tester.tap(find.byKey(popupButtonKey));
199272
await tester.pumpAndSettle();
200273
expect(itemBuilderCalled, isFalse);
274+
expect(onOpenedCalled, isFalse);
201275
expect(onSelectedCalled, isFalse);
202276

203277
// Try to bring up the popup menu and tap outside it to cancel the menu
@@ -206,13 +280,15 @@ void main() {
206280
await tester.tapAt(Offset.zero);
207281
await tester.pumpAndSettle();
208282
expect(itemBuilderCalled, isFalse);
283+
expect(onOpenedCalled, isFalse);
209284
expect(onCanceledCalled, isFalse);
210285
});
211286

212287
testWidgets('disabled PopupMenuButton is not focusable', (WidgetTester tester) async {
213288
final Key popupButtonKey = UniqueKey();
214289
final GlobalKey childKey = GlobalKey();
215290
bool itemBuilderCalled = false;
291+
bool onOpenedCalled = false;
216292
bool onSelectedCalled = false;
217293

218294
await tester.pumpWidget(
@@ -233,6 +309,7 @@ void main() {
233309
),
234310
];
235311
},
312+
onOpened: () => onOpenedCalled = true,
236313
onSelected: (int selected) => onSelectedCalled = true,
237314
),
238315
],
@@ -245,6 +322,7 @@ void main() {
245322

246323
expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isFalse);
247324
expect(itemBuilderCalled, isFalse);
325+
expect(onOpenedCalled, isFalse);
248326
expect(onSelectedCalled, isFalse);
249327
});
250328

0 commit comments

Comments
 (0)