Skip to content

Commit 652eb1b

Browse files
Prevent Popup From Being Dismissed when Tapped Inside Popup (#2800)
* Workaround .NET MAUI touch based behaviour * Shift to checking the bounds of the touch * Refactor code * Update Unit Tests * Update Unit Tests * `dotnet format` --------- Co-authored-by: Shaun Lawrence <[email protected]> Co-authored-by: Brandon Minnick <[email protected]>
1 parent f344ac0 commit 652eb1b

File tree

10 files changed

+211
-130
lines changed

10 files changed

+211
-130
lines changed

samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ async void DisplayPopup(object sender, EventArgs e)
285285

286286
await this.ShowPopupAsync(popupMediaElement);
287287

288-
popupMediaElement.Stop();
289-
popupMediaElement.Source = null;
288+
popupMediaElement.Stop();
289+
popupMediaElement.Source = null;
290290
}
291291
}

samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/ComplexPopupViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public partial class ComplexPopupViewModel(IPopupService popupService) : Observa
77
{
88
readonly IPopupService popupService = popupService;
99
readonly INavigation navigation = Application.Current?.Windows[0].Page?.Navigation ?? throw new InvalidOperationException("Unable to locate INavigation");
10-
10+
1111
[ObservableProperty, NotifyCanExecuteChangedFor(nameof(ReturnButtonTappedCommand))]
1212
public partial string ReturnText { get; set; } = string.Empty;
1313

src/CommunityToolkit.Maui.UnitTests/Extensions/AppBuilderExtensionsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public void UseMauiCommunityToolkit_ShouldAssignValues()
110110
Shadow = null,
111111
Shape = null
112112
};
113-
113+
114114
Core.AppBuilderExtensions.ShouldUseStatusBarBehaviorOnAndroidModalPageOptionCompleted += HandleShouldUseStatusBarBehaviorOnAndroidModalPageOptionCompleted;
115115

116116
// Act

src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using CommunityToolkit.Maui.UnitTests.Services;
66
using CommunityToolkit.Maui.Views;
77
using Microsoft.Maui.Controls.Shapes;
8+
using Nito.AsyncEx;
89
using Xunit;
910

1011
namespace CommunityToolkit.Maui.UnitTests.Extensions;
@@ -1162,8 +1163,14 @@ public async Task ShowPopupAsync_ReferenceTypeShouldReturnNull_WhenPopupTapGestu
11621163
var popupPage = (PopupPage)navigation.ModalStack[0];
11631164
popupPage.PopupClosed += HandlePopupClosed;
11641165

1165-
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
1166-
tapGestureRecognizer.Command?.Execute(null);
1166+
try
1167+
{
1168+
// Run using AsyncContext to catch Exception thrown by fire-and-forget ICommand.Execute
1169+
AsyncContext.Run(() => Assert.True(popupPage.TryExecuteTapOutsideOfPopupCommand()));
1170+
}
1171+
catch (PopupNotFoundException) // PopupNotFoundException is expected here because `ShowPopup` was never called
1172+
{
1173+
}
11671174

11681175
var popupClosedResult = await popupClosedTCS.Task;
11691176
var showPopupResult = await showPopupTask;
@@ -1197,8 +1204,14 @@ public async Task ShowPopupAsync_Shell_ReferenceTypeShouldReturnNull_WhenPopupTa
11971204
var popupPage = (PopupPage)shellNavigation.ModalStack[0];
11981205
popupPage.PopupClosed += HandlePopupClosed;
11991206

1200-
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
1201-
tapGestureRecognizer.Command?.Execute(null);
1207+
try
1208+
{
1209+
// Run using AsyncContext to catch Exception thrown by fire-and-forget ICommand.Execute
1210+
AsyncContext.Run(() => Assert.True(popupPage.TryExecuteTapOutsideOfPopupCommand()));
1211+
}
1212+
catch (PopupNotFoundException) // PopupNotFoundException is expected here because `ShowPopup` was never called
1213+
{
1214+
}
12021215

12031216
var popupClosedResult = await popupClosedTCS.Task;
12041217
var showPopupResult = await showPopupTask;
@@ -1225,8 +1238,14 @@ public async Task ShowPopupAsync_NullableValueTypeShouldReturnResult_WhenPopupIs
12251238
var popupPage = (PopupPage)navigation.ModalStack[0];
12261239
popupPage.PopupClosed += HandlePopupClosed;
12271240

1228-
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
1229-
tapGestureRecognizer.Command?.Execute(null);
1241+
try
1242+
{
1243+
// Run using AsyncContext to catch Exception thrown by fire-and-forget ICommand.Execute
1244+
AsyncContext.Run(() => Assert.True(popupPage.TryExecuteTapOutsideOfPopupCommand()));
1245+
}
1246+
catch (PopupNotFoundException) // PopupNotFoundException is expected here because `ShowPopup` was never called
1247+
{
1248+
}
12301249

12311250
var popupClosedResult = await popupClosedTCS.Task;
12321251
var showPopupResult = await showPopupTask;
@@ -1260,8 +1279,14 @@ public async Task ShowPopupAsync_Shell_NullableValueTypeShouldReturnResult_WhenP
12601279
var popupPage = (PopupPage)shellNavigation.ModalStack[0];
12611280
popupPage.PopupClosed += HandlePopupClosed;
12621281

1263-
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
1264-
tapGestureRecognizer.Command?.Execute(null);
1282+
try
1283+
{
1284+
// Run using AsyncContext to catch Exception thrown by fire-and-forget ICommand.Execute
1285+
AsyncContext.Run(() => Assert.True(popupPage.TryExecuteTapOutsideOfPopupCommand()));
1286+
}
1287+
catch (PopupNotFoundException) // PopupNotFoundException is expected here because `ShowPopup` was never called
1288+
{
1289+
}
12651290

12661291
var popupClosedResult = await popupClosedTCS.Task;
12671292
var showPopupResult = await showPopupTask;
@@ -1288,8 +1313,14 @@ public async Task ShowPopupAsync_ValueTypeShouldReturnResult_WhenPopupIsClosedBy
12881313
var popupPage = (PopupPage)navigation.ModalStack[0];
12891314
popupPage.PopupClosed += HandlePopupClosed;
12901315

1291-
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
1292-
tapGestureRecognizer.Command?.Execute(null);
1316+
try
1317+
{
1318+
// Run using AsyncContext to catch Exception thrown by fire-and-forget ICommand.Execute
1319+
AsyncContext.Run(() => Assert.True(popupPage.TryExecuteTapOutsideOfPopupCommand()));
1320+
}
1321+
catch (PopupNotFoundException) // PopupNotFoundException is expected here because `ShowPopup` was never called
1322+
{
1323+
}
12931324

12941325
var popupClosedResult = await popupClosedTCS.Task;
12951326
var showPopupResult = await showPopupTask;
@@ -1324,8 +1355,14 @@ public async Task ShowPopupAsync_Shell_ValueTypeShouldReturnResult_WhenPopupIsCl
13241355
var popupPage = (PopupPage)shellNavigation.ModalStack[0];
13251356
popupPage.PopupClosed += HandlePopupClosed;
13261357

1327-
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
1328-
tapGestureRecognizer.Command?.Execute(null);
1358+
try
1359+
{
1360+
// Run using AsyncContext to catch Exception thrown by fire-and-forget ICommand.Execute
1361+
AsyncContext.Run(() => Assert.True(popupPage.TryExecuteTapOutsideOfPopupCommand()));
1362+
}
1363+
catch (PopupNotFoundException) // PopupNotFoundException is expected here because `ShowPopup` was never called
1364+
{
1365+
}
13291366

13301367
var popupClosedResult = await popupClosedTCS.Task;
13311368
var showPopupResult = await showPopupTask;
@@ -1472,9 +1509,6 @@ public async Task ShowPopupAsync_TaskShouldCompleteWhenCloseAsyncIsCalled()
14721509
Assert.Equal(expectedResult, popupResult.Result);
14731510
Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup);
14741511
}
1475-
1476-
static TapGestureRecognizer GetTapOutsideGestureRecognizer(PopupPage popupPage) =>
1477-
(TapGestureRecognizer)popupPage.Content.Children.OfType<BoxView>().Single().GestureRecognizers[0];
14781512
}
14791513

14801514
sealed class ViewWithIQueryAttributable : Button, IQueryAttributable

src/CommunityToolkit.Maui.UnitTests/Views/Popup/DefaultPopupOptionsSettingsTests.cs

Lines changed: 51 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,25 @@ public void Popup_SetPopupOptionsDefaultsNotCalled_UsesPopupOptionsDefaults()
2727
var popupPage = new PopupPage(new Popup(), null);
2828
var popupBorder = popupPage.Content.PopupBorder;
2929

30-
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
31-
3230
// Assert
33-
Assert.True(tapGestureRecognizer.Command?.CanExecute(null));
31+
try
32+
{
33+
// Run using AsyncContext to catch Exception thrown by fire-and-forget ICommand.Execute
34+
AsyncContext.Run(() => Assert.True(popupPage.TryExecuteTapOutsideOfPopupCommand()));
35+
}
36+
catch (PopupNotFoundException) // PopupNotFoundException is expected here because `ShowPopup` was never called
37+
{
38+
}
39+
3440
Assert.Equal(2, popupBorder.StrokeThickness);
3541
Assert.Equal(Colors.LightGray, popupBorder.Stroke);
3642
Assert.Equal(Colors.Black.WithAlpha(0.3f), popupPage.BackgroundColor);
37-
43+
3844
Assert.Equal(Colors.Black, popupBorder.Shadow.Brush);
3945
Assert.Equal(new(20, 20), popupBorder.Shadow.Offset);
4046
Assert.Equal(40, popupBorder.Shadow.Radius);
4147
Assert.Equal(0.8f, popupBorder.Shadow.Opacity);
42-
48+
4349
Assert.Equal(new CornerRadius(20, 20, 20, 20), ((RoundRectangle?)popupBorder.StrokeShape)?.CornerRadius);
4450
Assert.Equal(2, ((RoundRectangle?)popupBorder.StrokeShape)?.StrokeThickness);
4551
Assert.Equal(Colors.LightGray, ((RoundRectangle?)popupBorder.StrokeShape)?.Stroke);
@@ -52,19 +58,25 @@ public void Popup_SetPopupOptionsNotCalled_PopupOptionsEmptyUsed_UsesPopupOption
5258
var popupPage = new PopupPage(new Popup(), PopupOptions.Empty);
5359
var popupBorder = popupPage.Content.PopupBorder;
5460

55-
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
56-
5761
// Assert
58-
Assert.True(tapGestureRecognizer.Command?.CanExecute(null));
62+
try
63+
{
64+
// Run using AsyncContext to catch Exception thrown by fire-and-forget ICommand.Execute
65+
AsyncContext.Run(() => Assert.True(popupPage.TryExecuteTapOutsideOfPopupCommand()));
66+
}
67+
catch (PopupNotFoundException) // PopupNotFoundException is expected here because `ShowPopup` was never called
68+
{
69+
}
70+
5971
Assert.Equal(2, popupBorder.StrokeThickness);
6072
Assert.Equal(Colors.LightGray, popupBorder.Stroke);
6173
Assert.Equal(Colors.Black.WithAlpha(0.3f), popupPage.BackgroundColor);
62-
74+
6375
Assert.Equal(Colors.Black, popupBorder.Shadow.Brush);
6476
Assert.Equal(new(20, 20), popupBorder.Shadow.Offset);
6577
Assert.Equal(40, popupBorder.Shadow.Radius);
6678
Assert.Equal(0.8f, popupBorder.Shadow.Opacity);
67-
79+
6880
Assert.Equal(new CornerRadius(20, 20, 20, 20), ((RoundRectangle?)popupBorder.StrokeShape)?.CornerRadius);
6981
Assert.Equal(2, ((RoundRectangle?)popupBorder.StrokeShape)?.StrokeThickness);
7082
Assert.Equal(Colors.LightGray, ((RoundRectangle?)popupBorder.StrokeShape)?.Stroke);
@@ -90,20 +102,18 @@ public void Popup_SetPopupDefaultsCalled_UsesDefaultPopupOptionsSettings()
90102

91103
var popupPage = new PopupPage(new Popup(), null);
92104
var popupBorder = popupPage.Content.PopupBorder;
93-
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
94105

95106
// Act
96107
try
97108
{
98109
// Run using AsyncContext to catch Exception thrown by fire-and-forget ICommand.Execute
99-
AsyncContext.Run(() => { tapGestureRecognizer.Command?.Execute(null); });
110+
AsyncContext.Run(() => Assert.True(popupPage.TryExecuteTapOutsideOfPopupCommand()));
100111
}
101112
catch (PopupNotFoundException) // PopupNotFoundException is expected here because `ShowPopup` was never called
102113
{
103114
}
104115

105-
// // Assert
106-
Assert.True(tapGestureRecognizer.Command?.CanExecute(null));
116+
// Assert
107117
Assert.True(hasOnTappingOutsideOfPopupExecuted);
108118
Assert.Equal(defaultPopupSettings.PageOverlayColor, popupPage.BackgroundColor);
109119
Assert.Equal(defaultPopupSettings.Shadow, popupBorder.Shadow);
@@ -130,20 +140,18 @@ public void Popup_SetPopupDefaultsCalled_PopupOptionsOverridden_UsesProvidedPopu
130140

131141
var popupPage = new PopupPage(new Popup(), defaultPopupSettings);
132142
var popupBorder = popupPage.Content.PopupBorder;
133-
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
134143

135144
// Act
136145
try
137146
{
138147
// Run using AsyncContext to catch Exception thrown by fire-and-forget ICommand.Execute
139-
AsyncContext.Run(() => { tapGestureRecognizer.Command?.Execute(null); });
148+
AsyncContext.Run(() => Assert.True(popupPage.TryExecuteTapOutsideOfPopupCommand()));
140149
}
141150
catch (PopupNotFoundException) // PopupNotFoundException is expected here because `ShowPopup` was never called
142151
{
143152
}
144153

145154
// // Assert
146-
Assert.True(tapGestureRecognizer.Command?.CanExecute(null));
147155
Assert.True(hasOnTappingOutsideOfPopupExecuted);
148156
Assert.Equal(defaultPopupSettings.PageOverlayColor, popupPage.BackgroundColor);
149157
Assert.Equal(defaultPopupSettings.Shadow, popupBorder.Shadow);
@@ -157,19 +165,25 @@ public void View_SetPopupOptionsDefaultsNotCalled_UsesPopupOptionsDefaults()
157165
var popupPage = new PopupPage(new View(), null);
158166
var popupBorder = popupPage.Content.PopupBorder;
159167

160-
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
161-
162168
// Assert
163-
Assert.True(tapGestureRecognizer.Command?.CanExecute(null));
169+
try
170+
{
171+
// Run using AsyncContext to catch Exception thrown by fire-and-forget ICommand.Execute
172+
AsyncContext.Run(() => Assert.True(popupPage.TryExecuteTapOutsideOfPopupCommand()));
173+
}
174+
catch (PopupNotFoundException) // PopupNotFoundException is expected here because `ShowPopup` was never called
175+
{
176+
}
177+
164178
Assert.Equal(2, popupBorder.StrokeThickness);
165179
Assert.Equal(Colors.LightGray, popupBorder.Stroke);
166180
Assert.Equal(Colors.Black.WithAlpha(0.3f), popupPage.BackgroundColor);
167-
181+
168182
Assert.Equal(Colors.Black, popupBorder.Shadow.Brush);
169183
Assert.Equal(new(20, 20), popupBorder.Shadow.Offset);
170184
Assert.Equal(40, popupBorder.Shadow.Radius);
171185
Assert.Equal(0.8f, popupBorder.Shadow.Opacity);
172-
186+
173187
Assert.Equal(new CornerRadius(20, 20, 20, 20), ((RoundRectangle?)popupBorder.StrokeShape)?.CornerRadius);
174188
Assert.Equal(2, ((RoundRectangle?)popupBorder.StrokeShape)?.StrokeThickness);
175189
Assert.Equal(Colors.LightGray, ((RoundRectangle?)popupBorder.StrokeShape)?.Stroke);
@@ -182,19 +196,24 @@ public void View_SetPopupOptionsNotCalled_PopupOptionsEmptyUsed_UsesPopupOptions
182196
var popupPage = new PopupPage(new View(), PopupOptions.Empty);
183197
var popupBorder = popupPage.Content.PopupBorder;
184198

185-
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
186-
187-
// Assert
188-
Assert.True(tapGestureRecognizer.Command?.CanExecute(null));
199+
// Assert
200+
try
201+
{
202+
// Run using AsyncContext to catch Exception thrown by fire-and-forget ICommand.Execute
203+
AsyncContext.Run(() => Assert.True(popupPage.TryExecuteTapOutsideOfPopupCommand()));
204+
}
205+
catch (PopupNotFoundException) // PopupNotFoundException is expected here because `ShowPopup` was never called
206+
{
207+
}
189208
Assert.Equal(2, popupBorder.StrokeThickness);
190209
Assert.Equal(Colors.LightGray, popupBorder.Stroke);
191210
Assert.Equal(Colors.Black.WithAlpha(0.3f), popupPage.BackgroundColor);
192-
211+
193212
Assert.Equal(Colors.Black, popupBorder.Shadow.Brush);
194213
Assert.Equal(new(20, 20), popupBorder.Shadow.Offset);
195214
Assert.Equal(40, popupBorder.Shadow.Radius);
196215
Assert.Equal(0.8f, popupBorder.Shadow.Opacity);
197-
216+
198217
Assert.Equal(new CornerRadius(20, 20, 20, 20), ((RoundRectangle?)popupBorder.StrokeShape)?.CornerRadius);
199218
Assert.Equal(2, ((RoundRectangle?)popupBorder.StrokeShape)?.StrokeThickness);
200219
Assert.Equal(Colors.LightGray, ((RoundRectangle?)popupBorder.StrokeShape)?.Stroke);
@@ -220,20 +239,18 @@ public void View_SetPopupDefaultsCalled_UsesDefaultPopupOptionsSettings()
220239

221240
var popupPage = new PopupPage(new View(), null);
222241
var popupBorder = popupPage.Content.PopupBorder;
223-
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
224242

225243
// Act
226244
try
227245
{
228246
// Run using AsyncContext to catch Exception thrown by fire-and-forget ICommand.Execute
229-
AsyncContext.Run(() => { tapGestureRecognizer.Command?.Execute(null); });
247+
AsyncContext.Run(() => Assert.True(popupPage.TryExecuteTapOutsideOfPopupCommand()));
230248
}
231249
catch (PopupNotFoundException) // PopupNotFoundException is expected here because `ShowPopup` was never called
232250
{
233251
}
234252

235-
// // Assert
236-
Assert.True(tapGestureRecognizer.Command?.CanExecute(null));
253+
// Assert
237254
Assert.True(hasOnTappingOutsideOfPopupExecuted);
238255
Assert.Equal(defaultPopupSettings.PageOverlayColor, popupPage.BackgroundColor);
239256
Assert.Equal(defaultPopupSettings.Shadow, popupBorder.Shadow);
@@ -260,28 +277,21 @@ public void View_SetPopupDefaultsCalled_PopupOptionsOverridden_UsesProvidedPopup
260277

261278
var popupPage = new PopupPage(new View(), defaultPopupSettings);
262279
var popupBorder = popupPage.Content.PopupBorder;
263-
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
264280

265-
// Act
281+
// // Assert
266282
try
267283
{
268284
// Run using AsyncContext to catch Exception thrown by fire-and-forget ICommand.Execute
269-
AsyncContext.Run(() => { tapGestureRecognizer.Command?.Execute(null); });
285+
AsyncContext.Run(() => Assert.True(popupPage.TryExecuteTapOutsideOfPopupCommand()));
270286
}
271287
catch (PopupNotFoundException) // PopupNotFoundException is expected here because `ShowPopup` was never called
272288
{
273289
}
274290

275-
// // Assert
276-
Assert.True(tapGestureRecognizer.Command?.CanExecute(null));
277291
Assert.True(hasOnTappingOutsideOfPopupExecuted);
278292
Assert.Equal(defaultPopupSettings.PageOverlayColor, popupPage.BackgroundColor);
279293
Assert.Equal(defaultPopupSettings.Shadow, popupBorder.Shadow);
280294
Assert.Equal(defaultPopupSettings.Shape, popupBorder.StrokeShape);
281295
}
282-
283-
284-
static TapGestureRecognizer GetTapOutsideGestureRecognizer(PopupPage popupPage) =>
285-
(TapGestureRecognizer)popupPage.Content.Children.OfType<BoxView>().Single().GestureRecognizers[0];
286296
}
287297
#pragma warning restore CA1416

src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupOptionsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public void CanBeDismissedByTappingOutsideOfPopup_SetValue_ShouldBeUpdated()
2424
public void Shadow_DefaultValue_ShouldBeTrue()
2525
{
2626
var popupOptions = new PopupOptions();
27-
27+
2828
Assert.Equal(Colors.Black, popupOptions.Shadow?.Brush);
2929
Assert.Equal(new(20, 20), popupOptions.Shadow?.Offset);
3030
Assert.Equal(40, popupOptions.Shadow?.Radius);

0 commit comments

Comments
 (0)