Skip to content

Commit 5b21b96

Browse files
authored
Merge branch 'main' into FixSnackbarRegistration
2 parents a1cc628 + 47c5535 commit 5b21b96

File tree

8 files changed

+119
-20
lines changed

8 files changed

+119
-20
lines changed

src/CommunityToolkit.Maui.UnitTests/BaseHandlerTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ static void InitializeServicesAndSetMockApplication(out IServiceProvider service
8181
var mockPopup = new MockSelfClosingPopup(mockPageViewModel, new());
8282

8383
PopupService.AddPopup(mockPopup, mockPageViewModel, appBuilder.Services, ServiceLifetime.Transient);
84+
8485
appBuilder.Services.AddTransientPopup<MockPopup>();
86+
appBuilder.Services.AddTransient<GarbageCollectionHeavySelfClosingPopup>();
8587
#endregion
8688

8789
var mauiApp = appBuilder.Build();

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,21 @@ public async Task ShowPopupAsyncWithView_Shell_ShouldValidateProperBindingContex
946946
Assert.Equal(shellParameterViewModelTextValue, view.BindingContext.Text);
947947
}
948948

949+
[Fact(Timeout = (int)TestDuration.Medium)]
950+
public async Task ShowPopupAsync_ShouldSuccessfullyCompleteAndReturnResultUnderHeavyGarbageCollection()
951+
{
952+
// Arrange
953+
var mockPopup = ServiceProvider.GetRequiredService<GarbageCollectionHeavySelfClosingPopup>();
954+
var selfClosingPopup = ServiceProvider.GetRequiredService<GarbageCollectionHeavySelfClosingPopup>() ?? throw new InvalidOperationException();
955+
956+
// Act
957+
var result = await navigation.ShowPopupAsync<object?>(selfClosingPopup, PopupOptions.Empty, TestContext.Current.CancellationToken);
958+
959+
// Assert
960+
Assert.Same(mockPopup.Result, result.Result);
961+
Assert.False(result.WasDismissedByTappingOutsideOfPopup);
962+
}
963+
949964
[Fact(Timeout = (int)TestDuration.Medium)]
950965
public async Task ShowPopupAsync_ShouldReturnResultOnceClosed()
951966
{
@@ -1430,7 +1445,7 @@ public async Task ClosePopupAsyncT_ShouldClosePopupUsingPageAndReturnResult()
14301445
Assert.Equal(expectedResult, popupResult.Result);
14311446
Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup);
14321447
}
1433-
1448+
14341449
[Fact(Timeout = (int)TestDuration.Short)]
14351450
public async Task ShowPopupAsync_TaskShouldCompleteWhenCloseAsyncIsCalled()
14361451
{
@@ -1460,7 +1475,7 @@ public async Task ShowPopupAsync_TaskShouldCompleteWhenCloseAsyncIsCalled()
14601475
Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup);
14611476
}
14621477

1463-
static TapGestureRecognizer GetTapOutsideGestureRecognizer(PopupPage popupPage) =>
1478+
static TapGestureRecognizer GetTapOutsideGestureRecognizer(PopupPage popupPage) =>
14641479
(TapGestureRecognizer)popupPage.Content.Children.OfType<BoxView>().Single().GestureRecognizers[0];
14651480
}
14661481

src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,9 @@ public async Task ClosePopupAsyncT_ShouldClosePopupUsingPageAndReturnResult()
522522
}
523523
}
524524

525-
sealed class MockSelfClosingPopup : Popup<object?>, IQueryAttributable
525+
class GarbageCollectionHeavySelfClosingPopup(MockPageViewModel viewModel, object? result = null) : MockSelfClosingPopup(viewModel, result);
526+
527+
class MockSelfClosingPopup : Popup<object?>, IQueryAttributable
526528
{
527529
public const int ExpectedResult = 2;
528530

@@ -548,7 +550,9 @@ async void HandleTick(object? sender, EventArgs e)
548550
timer.Tick -= HandleTick;
549551
try
550552
{
553+
GC.Collect();
551554
await CloseAsync(Result);
555+
GC.Collect();
552556
}
553557
catch (InvalidOperationException)
554558
{

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

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using CommunityToolkit.Maui.Core;
22
using CommunityToolkit.Maui.Extensions;
3-
using CommunityToolkit.Maui.UnitTests.Extensions;
4-
using CommunityToolkit.Maui.UnitTests.Services;
53
using CommunityToolkit.Maui.Views;
64
using FluentAssertions;
75
using Microsoft.Maui.Controls.PlatformConfiguration;
@@ -219,6 +217,50 @@ public void PopupPageT_Close_ShouldThrowOperationCanceledException_WhenTokenIsCa
219217
act.Should().ThrowAsync<OperationCanceledException>();
220218
}
221219

220+
[Fact]
221+
public void TapGestureRecognizer_VerifyCanBeDismissedByTappingOutsideOfPopup_ShouldNotExecuteWhenEitherFalse()
222+
{
223+
// Arrange
224+
var view = new Popup();
225+
var popupOptions = new PopupOptions();
226+
227+
// Act
228+
var popupPage = new PopupPage(view, popupOptions);
229+
var tapGestureRecognizer = popupPage.Content.Children.OfType<BoxView>().Single().GestureRecognizers.OfType<TapGestureRecognizer>().Single();
230+
231+
// Assert
232+
Assert.True(tapGestureRecognizer.Command?.CanExecute(null));
233+
234+
// Act
235+
view.CanBeDismissedByTappingOutsideOfPopup = false;
236+
popupOptions.CanBeDismissedByTappingOutsideOfPopup = false;
237+
238+
// Assert
239+
Assert.False(tapGestureRecognizer.Command?.CanExecute(null));
240+
241+
// Act
242+
view.CanBeDismissedByTappingOutsideOfPopup = true;
243+
popupOptions.CanBeDismissedByTappingOutsideOfPopup = false;
244+
245+
// Assert
246+
Assert.False(tapGestureRecognizer.Command?.CanExecute(null));
247+
248+
// Act
249+
view.CanBeDismissedByTappingOutsideOfPopup = false;
250+
popupOptions.CanBeDismissedByTappingOutsideOfPopup = true;
251+
252+
// Assert
253+
Assert.False(tapGestureRecognizer.Command?.CanExecute(null));
254+
255+
// Act
256+
view.CanBeDismissedByTappingOutsideOfPopup = true;
257+
popupOptions.CanBeDismissedByTappingOutsideOfPopup = true;
258+
259+
// Assert
260+
Assert.True(tapGestureRecognizer.Command?.CanExecute(null));
261+
262+
}
263+
222264
[Fact]
223265
public void Constructor_WithViewAndPopupOptions_SetsCorrectProperties()
224266
{
@@ -487,8 +529,8 @@ public void PopupPage_ShouldRespectLayoutOptions()
487529
Assert.Equal(LayoutOptions.Start, border.VerticalOptions);
488530
Assert.Equal(LayoutOptions.End, border.HorizontalOptions);
489531
}
490-
491-
static TapGestureRecognizer GetTapOutsideGestureRecognizer(PopupPage popupPage) =>
532+
533+
static TapGestureRecognizer GetTapOutsideGestureRecognizer(PopupPage popupPage) =>
492534
(TapGestureRecognizer)popupPage.Content.Children.OfType<BoxView>().Single().GestureRecognizers[0];
493535

494536
// Helper class for testing protected methods

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ public void PopupBackgroundColor_DefaultValue_ShouldBeWhite()
1515
Assert.Equal(PopupDefaults.BackgroundColor, Colors.White);
1616
}
1717

18+
[Fact]
19+
public void CanBeDismissedByTappingOutsideOfPopup_DefaultValue_ShouldBeTrue()
20+
{
21+
var popup = new Popup();
22+
Assert.Equal(PopupDefaults.CanBeDismissedByTappingOutsideOfPopup, popup.CanBeDismissedByTappingOutsideOfPopup);
23+
}
24+
1825
[Fact]
1926
public void Margin_DefaultValue_ShouldBeDefaultThickness()
2027
{
@@ -151,7 +158,7 @@ public async Task PopupT_Close_ShouldNotThrowExceptionWhenCloseIsOverridden()
151158
await popup.CloseAsync(TestContext.Current.CancellationToken);
152159
await popup.CloseAsync("Hello", TestContext.Current.CancellationToken);
153160
}
154-
161+
155162
[Fact(Timeout = (int)TestDuration.Short)]
156163
public async Task ShowPopupAsync_TaskShouldCompleteWhenPopupCloseAsyncIsCalled()
157164
{

src/CommunityToolkit.Maui/Primitives/Defaults/PopupDefaults.shared.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@ static class PopupDefaults
3131
/// Default value for <see cref="VisualElement.BackgroundColor"/> BackgroundColor
3232
/// </summary>
3333
public static Color BackgroundColor { get; } = Colors.White;
34+
35+
/// <summary>
36+
/// Default value for <see cref="Popup.CanBeDismissedByTappingOutsideOfPopup"/>
37+
/// </summary>
38+
public const bool CanBeDismissedByTappingOutsideOfPopup = PopupOptionsDefaults.CanBeDismissedByTappingOutsideOfPopup;
3439
}

src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ public partial class Popup : ContentView
2727
/// </summary>
2828
public static new readonly BindableProperty VerticalOptionsProperty = View.VerticalOptionsProperty;
2929

30+
/// <summary>
31+
/// Backing BindableProperty for the <see cref="CanBeDismissedByTappingOutsideOfPopup"/> property.
32+
/// </summary>
33+
public static readonly BindableProperty CanBeDismissedByTappingOutsideOfPopupProperty = BindableProperty.Create(nameof(CanBeDismissedByTappingOutsideOfPopup), typeof(bool), typeof(Popup), PopupDefaults.CanBeDismissedByTappingOutsideOfPopup);
34+
3035
/// <summary>
3136
/// Initializes Popup
3237
/// </summary>
@@ -84,6 +89,17 @@ public Popup()
8489
set => base.VerticalOptions = value;
8590
}
8691

92+
/// <inheritdoc cref="IPopupOptions.CanBeDismissedByTappingOutsideOfPopup"/> />
93+
/// <remarks>
94+
/// When true and the user taps outside the popup, it will dismiss.
95+
/// On Android - when false the hardware back button is disabled.
96+
/// </remarks>
97+
public bool CanBeDismissedByTappingOutsideOfPopup
98+
{
99+
get => (bool)GetValue(CanBeDismissedByTappingOutsideOfPopupProperty);
100+
set => SetValue(CanBeDismissedByTappingOutsideOfPopupProperty, value);
101+
}
102+
87103
/// <summary>
88104
/// Close the Popup.
89105
/// </summary>

src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ partial class PopupPage : ContentPage, IQueryAttributable
2626
readonly Popup popup;
2727
readonly IPopupOptions popupOptions;
2828
readonly Command tapOutsideOfPopupCommand;
29-
readonly WeakEventManager popupClosedEventManager = new();
3029

3130
public PopupPage(View view, IPopupOptions popupOptions)
3231
: this(view as Popup ?? CreatePopupFromView<Popup>(view), popupOptions)
@@ -46,14 +45,15 @@ public PopupPage(Popup popup, IPopupOptions popupOptions)
4645
{
4746
popupOptions.OnTappingOutsideOfPopup?.Invoke();
4847
await CloseAsync(new PopupResult(true));
49-
}, () => popupOptions.CanBeDismissedByTappingOutsideOfPopup);
48+
}, () => GetCanBeDismissedByTappingOutsideOfPopup(popup, popupOptions));
5049

5150
// Only set the content if the parent constructor hasn't set the content already; don't override content if it already exists
5251
base.Content = new PopupPageLayout(popup, popupOptions, tapOutsideOfPopupCommand);
5352

53+
popup.PropertyChanged += HandlePopupPropertyChanged;
5454
if (popupOptions is BindableObject bindablePopupOptions)
5555
{
56-
bindablePopupOptions.PropertyChanged += HandlePopupPropertyChanged;
56+
bindablePopupOptions.PropertyChanged += HandlePopupOptionsPropertyChanged;
5757
}
5858

5959
this.SetBinding(BindingContextProperty, static (Popup x) => x.BindingContext, source: popup, mode: BindingMode.OneWay);
@@ -63,11 +63,7 @@ public PopupPage(Popup popup, IPopupOptions popupOptions)
6363
On<iOS>().SetModalPresentationStyle(UIModalPresentationStyle.OverFullScreen);
6464
}
6565

66-
public event EventHandler<IPopupResult> PopupClosed
67-
{
68-
add => popupClosedEventManager.AddEventHandler(value);
69-
remove => popupClosedEventManager.RemoveEventHandler(value);
70-
}
66+
public event EventHandler<IPopupResult>? PopupClosed;
7167

7268
// Prevent Content from being set by external class
7369
// Casts `PopupPage.Content` to return typeof(PopupPageLayout)
@@ -101,13 +97,13 @@ public async Task CloseAsync(PopupResult result, CancellationToken token = defau
10197
token.ThrowIfCancellationRequested();
10298
await Navigation.PopModalAsync(false).WaitAsync(token);
10399

104-
popupClosedEventManager.HandleEvent(this, result, nameof(PopupClosed));
100+
PopupClosed?.Invoke(this, result);
105101
}
106102

107103
protected override bool OnBackButtonPressed()
108104
{
109-
// Only close the Popup if PopupOptions.CanBeDismissedByTappingOutsideOfPopup is true
110-
if (popupOptions.CanBeDismissedByTappingOutsideOfPopup)
105+
// Only close the Popup if CanBeDismissedByTappingOutsideOfPopup is true
106+
if (GetCanBeDismissedByTappingOutsideOfPopup(popup, popupOptions))
111107
{
112108
CloseAsync(new PopupResult(true), CancellationToken.None).SafeFireAndForget();
113109
}
@@ -152,14 +148,26 @@ protected override void OnNavigatedTo(NavigatedToEventArgs args)
152148
return popup;
153149
}
154150

155-
void HandlePopupPropertyChanged(object? sender, PropertyChangedEventArgs e)
151+
// Only dismiss when a user taps outside Popup when **both** Popup.CanBeDismissedByTappingOutsideOfPopup and PopupOptions.CanBeDismissedByTappingOutsideOfPopup are true
152+
// If either value is false, do not dismiss Popup
153+
static bool GetCanBeDismissedByTappingOutsideOfPopup(in Popup popup, in IPopupOptions popupOptions) => popup.CanBeDismissedByTappingOutsideOfPopup & popupOptions.CanBeDismissedByTappingOutsideOfPopup;
154+
155+
void HandlePopupOptionsPropertyChanged(object? sender, PropertyChangedEventArgs e)
156156
{
157157
if (e.PropertyName == nameof(IPopupOptions.CanBeDismissedByTappingOutsideOfPopup))
158158
{
159159
tapOutsideOfPopupCommand.ChangeCanExecute();
160160
}
161161
}
162162

163+
void HandlePopupPropertyChanged(object? sender, PropertyChangedEventArgs e)
164+
{
165+
if (e.PropertyName == Popup.CanBeDismissedByTappingOutsideOfPopupProperty.PropertyName)
166+
{
167+
tapOutsideOfPopupCommand.ChangeCanExecute();
168+
}
169+
}
170+
163171
void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
164172
{
165173
if (popup is IQueryAttributable popupIQueryAttributable)

0 commit comments

Comments
 (0)