From d28111a9b27b1750bed72c20356c9da5c519698e Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 27 May 2025 16:04:25 -0700 Subject: [PATCH 1/8] Return `IPopupResult` --- .../Extensions/PopupExtensionsTests.cs | 130 ++++++++++++++++-- .../Extensions/PopupExtensions.shared.cs | 77 +++++++++-- .../Services/PopupService.shared.cs | 55 ++------ 3 files changed, 195 insertions(+), 67 deletions(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs b/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs index aa03a19941..352ca52b4d 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs @@ -36,34 +36,34 @@ public async Task ClosePopup_TokenExpired_ShouldThrowOperationCancelledException { // Arrange var cts = new CancellationTokenSource(); - + // Act await cts.CancelAsync(); - + // Assert await Assert.ThrowsAsync(() => navigation.ClosePopupAsync(cts.Token)); } - + [Fact(Timeout = (int)TestDuration.Short)] public async Task ClosePopup_NoExistingPopup_ShouldThrowPopupNotFoundException() { // Arrange - + // Act - + // Assert await Assert.ThrowsAsync(() => navigation.ClosePopupAsync(TestContext.Current.CancellationToken)); } - + [Fact(Timeout = (int)TestDuration.Short)] public async Task ClosePopup_PopupBlocked_ShouldThrowPopupBlockedException() { // Arrange - + // Act navigation.ShowPopup(new Button()); await navigation.PushModalAsync(new ContentPage()); - + // Assert await Assert.ThrowsAsync(() => navigation.ClosePopupAsync(TestContext.Current.CancellationToken)); } @@ -80,10 +80,10 @@ public async Task ShowPopupAsync_WithPopupType_ShowsPopupAndClosesPopup() // Assert Assert.Single(navigation.ModalStack); Assert.IsType(navigation.ModalStack[0]); - + // Act await navigation.ClosePopupAsync(TestContext.Current.CancellationToken); - + // Assert Assert.Empty(navigation.ModalStack); } @@ -106,10 +106,10 @@ public async Task ShowPopupAsync_Shell_WithPopupType_ShowsPopupAndClosesPopup() // Assert Assert.Single(shellNavigation.ModalStack); Assert.IsType(shellNavigation.ModalStack[0]); - + // Act await navigation.ClosePopupAsync(TestContext.Current.CancellationToken); - + // Assert Assert.Empty(navigation.ModalStack); } @@ -127,7 +127,7 @@ public void ShowPopupAsync_WithViewType_ShowsPopup() Assert.Single(navigation.ModalStack); Assert.IsType(navigation.ModalStack[0]); } - + [Fact] public void ShowPopupAsync_WithViewType_SetsCorrectDefaults() { @@ -138,7 +138,7 @@ public void ShowPopupAsync_WithViewType_SetsCorrectDefaults() // Act navigation.ShowPopup(label); - + popupPage = (PopupPage)navigation.ModalStack[0]; autogeneratedPopup = (Popup)(((Border)popupPage.Content.Children[0]).Content ?? throw new InvalidOperationException("Border Content cannot be null")); @@ -1276,6 +1276,108 @@ void HandlePopupClosed(object? sender, IPopupResult e) popupClosedTCS.SetResult(e); } } + + [Fact(Timeout = (int)TestDuration.Short)] + public async Task ClosePopupAsync_ShouldClosePopupUsingNavigationAndReturnResult() + { + // Arrange + + if (Application.Current?.Windows[0].Page is not Page page) + { + throw new InvalidOperationException("Page cannot be null"); + } + + // Act + page.ShowPopup(new MockPopup()); + + // Assert + Assert.Single(page.Navigation.ModalStack); + Assert.IsType(page.Navigation.ModalStack[0]); + + // Act + var popupResult = await page.ClosePopupAsync(page.Navigation, TestContext.Current.CancellationToken); + + // Assert + Assert.Empty(page.Navigation.ModalStack); + Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup); + } + + [Fact(Timeout = (int)TestDuration.Short)] + public async Task ClosePopupAsync_ShouldClosePopupUsingPageAndReturnResult() + { + // Arrange + if (Application.Current?.Windows[0].Page is not Page page) + { + throw new InvalidOperationException("Page cannot be null"); + } + + // Act + page.ShowPopup(new MockPopup()); + + // Assert + Assert.Single(page.Navigation.ModalStack); + Assert.IsType(page.Navigation.ModalStack[0]); + + // Act + var popupResult = await page.ClosePopupAsync(page, TestContext.Current.CancellationToken); + + // Assert + Assert.Empty(page.Navigation.ModalStack); + Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup); + } + + [Fact(Timeout = (int)TestDuration.Short)] + public async Task ClosePopupAsyncT_ShouldClosePopupUsingNavigationAndReturnResult() + { + // Arrange + const int expectedResult = 2; + if (Application.Current?.Windows[0].Page is not Page page) + { + throw new InvalidOperationException("Page cannot be null"); + } + + // Act + page.ShowPopup(new Popup()); + + // Assert + Assert.Single(page.Navigation.ModalStack); + Assert.IsType(page.Navigation.ModalStack[0]); + + // Act + var popupResult = await page.ClosePopupAsync(expectedResult, TestContext.Current.CancellationToken); + + // Assert + Assert.Empty(page.Navigation.ModalStack); + Assert.Equal(expectedResult, popupResult.Result); + Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup); + } + + [Fact(Timeout = (int)TestDuration.Short)] + public async Task ClosePopupAsyncT_ShouldClosePopupUsingPageAndReturnResult() + { + // Arrange + const int expectedResult = 2; + + if (Application.Current?.Windows[0].Page is not Page page) + { + throw new InvalidOperationException("Page cannot be null"); + } + + // Act + page.ShowPopup(new MockPopup()); + + // Assert + Assert.Single(page.Navigation.ModalStack); + Assert.IsType(page.Navigation.ModalStack[0]); + + // Act + var popupResult = await page.ClosePopupAsync(expectedResult, TestContext.Current.CancellationToken); + + // Assert + Assert.Empty(page.Navigation.ModalStack); + Assert.Equal(expectedResult, popupResult.Result); + Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup); + } } sealed class ViewWithIQueryAttributable : Button, IQueryAttributable diff --git a/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs b/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs index 8a67fbc7f6..846848efed 100644 --- a/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs +++ b/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs @@ -197,28 +197,76 @@ void HandlePopupClosed(object? sender, IPopupResult e) } /// - /// CloseAsync the Visible Popup + /// Close the Most Recent Popup /// - public static Task ClosePopupAsync(this Page page, CancellationToken token = default) + public static Task ClosePopupAsync(this Page page, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(page); return ClosePopupAsync(page.Navigation, token); } - + /// - /// CloseAsync the Visible Popup + /// Close the Most Recent Popup /// - public static Task ClosePopupAsync(this INavigation navigation, CancellationToken token = default) + public static async Task ClosePopupAsync(this INavigation navigation, CancellationToken token = default) { + ArgumentNullException.ThrowIfNull(navigation); token.ThrowIfCancellationRequested(); - + + var popupClosedTCS = new TaskCompletionSource(); + + var currentVisibleModalPage = Shell.Current is null + ? navigation.ModalStack.LastOrDefault() + : Shell.Current.Navigation.ModalStack.LastOrDefault(); + + if (currentVisibleModalPage is null) + { + throw new PopupNotFoundException(); + } + + if (currentVisibleModalPage is not PopupPage popupPage) + { + throw new PopupBlockedException(currentVisibleModalPage); + } + + popupPage.PopupClosed += HandlePopupPageClosed; + await popupPage.CloseAsync(new PopupResult(false), token); + + var popupResult = await popupClosedTCS.Task; + return popupResult; + + void HandlePopupPageClosed(object? sender, IPopupResult e) + { + popupPage.PopupClosed -= HandlePopupPageClosed; + popupClosedTCS.SetResult(e); + } + } + + /// + /// Close the Most Recent Popup Return a Result + /// + public static Task> ClosePopupAsync(this Page page, TResult result, CancellationToken token = default) + { + ArgumentNullException.ThrowIfNull(page); + + return ClosePopupAsync(page.Navigation, result, token); + } + + /// + /// Close the Most Recent Popup Return a Result + /// + public static async Task> ClosePopupAsync(this INavigation navigation, TResult result, CancellationToken token = default) + { ArgumentNullException.ThrowIfNull(navigation); + token.ThrowIfCancellationRequested(); + + var popupClosedTCS = new TaskCompletionSource(); var currentVisibleModalPage = Shell.Current is null ? navigation.ModalStack.LastOrDefault() : Shell.Current.Navigation.ModalStack.LastOrDefault(); - + if (currentVisibleModalPage is null) { throw new PopupNotFoundException(); @@ -229,10 +277,21 @@ public static Task ClosePopupAsync(this INavigation navigation, CancellationToke throw new PopupBlockedException(currentVisibleModalPage); } - return popupPage.CloseAsync(new PopupResult(false), token); + popupPage.PopupClosed += HandlePopupPageClosed; + + await popupPage.CloseAsync(new PopupResult(result, false), token); + + var popupResult = await popupClosedTCS.Task; + return GetPopupResult(popupResult); + + void HandlePopupPageClosed(object? sender, IPopupResult e) + { + popupPage.PopupClosed -= HandlePopupPageClosed; + popupClosedTCS.SetResult(e); + } } - internal static PopupResult GetPopupResult(in IPopupResult result) + static PopupResult GetPopupResult(in IPopupResult result) { return result switch { diff --git a/src/CommunityToolkit.Maui/Services/PopupService.shared.cs b/src/CommunityToolkit.Maui/Services/PopupService.shared.cs index cf226ef2d9..a72243d25d 100644 --- a/src/CommunityToolkit.Maui/Services/PopupService.shared.cs +++ b/src/CommunityToolkit.Maui/Services/PopupService.shared.cs @@ -45,7 +45,7 @@ public void ShowPopup(Page page, IPopupOptions? options = null) where T : not public void ShowPopup(INavigation navigation, IPopupOptions? options = null) where T : notnull { ArgumentNullException.ThrowIfNull(navigation); - + var popupContent = GetPopupContent(serviceProvider.GetRequiredService()); navigation.ShowPopup(popupContent, options); @@ -56,7 +56,7 @@ public void ShowPopup(INavigation navigation, IPopupOptions? options = null) public void ShowPopup(Shell shell, IPopupOptions? options = null, IDictionary? shellParameters = null) where T : notnull { ArgumentNullException.ThrowIfNull(shell); - + var popupContent = GetPopupContent(serviceProvider.GetRequiredService()); shell.ShowPopup(popupContent, options, shellParameters); } @@ -74,7 +74,7 @@ public Task ShowPopupAsync(INavigation navigation, IPopupOption where T : notnull { ArgumentNullException.ThrowIfNull(navigation); - + token.ThrowIfCancellationRequested(); var popupContent = GetPopupContent(serviceProvider.GetRequiredService()); @@ -86,7 +86,7 @@ public Task ShowPopupAsync(INavigation navigation, IPopupOption public Task ShowPopupAsync(Shell shell, IPopupOptions? options, IDictionary? shellParameters, CancellationToken token) where T : notnull { ArgumentNullException.ThrowIfNull(shell); - + token.ThrowIfCancellationRequested(); var popupContent = GetPopupContent(serviceProvider.GetRequiredService()); @@ -109,7 +109,7 @@ public Task> ShowPopupAsync(INavigation naviga where T : notnull { ArgumentNullException.ThrowIfNull(navigation); - + token.ThrowIfCancellationRequested(); var popupContent = GetPopupContent(serviceProvider.GetRequiredService()); @@ -120,7 +120,7 @@ public Task> ShowPopupAsync(INavigation naviga public Task> ShowPopupAsync(Shell shell, IPopupOptions? options, IDictionary? shellParameters = null, CancellationToken token = default) where T : notnull { ArgumentNullException.ThrowIfNull(shell); - + token.ThrowIfCancellationRequested(); var popupContent = GetPopupContent(serviceProvider.GetRequiredService()); @@ -142,49 +142,21 @@ public Task> ClosePopupAsync(Page page, T result, Cancellatio } /// - public async Task ClosePopupAsync(INavigation navigation, CancellationToken cancellationToken = default) + public Task ClosePopupAsync(INavigation navigation, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(navigation); cancellationToken.ThrowIfCancellationRequested(); - - var popupClosedTCS = new TaskCompletionSource(); - - var popupPage = GetCurrentPopupPage(navigation); - popupPage.PopupClosed += HandlePopupPageClosed; - - await popupPage.CloseAsync(new PopupResult(false), cancellationToken); - - var popupResult = await popupClosedTCS.Task; - return popupResult; - - void HandlePopupPageClosed(object? sender, IPopupResult e) - { - popupPage.PopupClosed -= HandlePopupPageClosed; - popupClosedTCS.SetResult(e); - } + + return navigation.ClosePopupAsync(cancellationToken); } /// - public async Task> ClosePopupAsync(INavigation navigation, T result, CancellationToken cancellationToken = default) + public Task> ClosePopupAsync(INavigation navigation, T result, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(navigation); cancellationToken.ThrowIfCancellationRequested(); - var popupClosedTCS = new TaskCompletionSource(); - - var popupPage = GetCurrentPopupPage(navigation); - popupPage.PopupClosed += HandlePopupPageClosed; - - await popupPage.CloseAsync(new PopupResult(result, false), cancellationToken); - - var popupResult = await popupClosedTCS.Task; - return PopupExtensions.GetPopupResult(popupResult); - - void HandlePopupPageClosed(object? sender, IPopupResult e) - { - popupPage.PopupClosed -= HandlePopupPageClosed; - popupClosedTCS.SetResult(e); - } + return navigation.ClosePopupAsync(result, cancellationToken); } internal static void AddPopup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TPopupView>( @@ -217,11 +189,6 @@ void HandlePopupPageClosed(object? sender, IPopupResult e) services.TryAdd(new ServiceDescriptor(typeof(TPopupViewModel), _ => viewModel, lifetime)); } - // All popups are now displayed in a PopupPage (ContentPage) that is pushed modally to the screen, e.g. Navigation.PushModalAsync(popupPage) - // We can use the ModalStack to retrieve the most recent popupPage by retrieving all PopupPages and return the most recent from the ModalStack - static PopupPage GetCurrentPopupPage(INavigation navigation) => - navigation.ModalStack.OfType().LastOrDefault() ?? throw new PopupNotFoundException(); - View GetPopupContent(T bindingContext) { if (bindingContext is View view) From 3e12edf60414c597653fc55876df810beb26bb4f Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 27 May 2025 16:07:20 -0700 Subject: [PATCH 2/8] `dotnet format` --- .../Services/PopupServiceTests.cs | 76 +++++++++---------- .../Interfaces/IPopupService.shared.cs | 8 +- .../Views/Popup/Popup.shared.cs | 4 +- .../Views/Popup/PopupPage.shared.cs | 6 +- 4 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs b/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs index 2ba4cfefa7..aa8720f1b0 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs @@ -63,13 +63,13 @@ public void ShowPopupAsync_UsingNavigation_WithViewType_ShowsPopup() Assert.Single(navigation.ModalStack); Assert.IsType(navigation.ModalStack[0]); } - + [Fact] public void ShowPopupAsync_UsingPage_WithViewType_ShowsPopup() { // Arrange var popupService = ServiceProvider.GetRequiredService(); - + if (Application.Current?.Windows[0].Page is not Page page) { throw new InvalidOperationException("Page cannot be null"); @@ -96,13 +96,13 @@ public async Task ShowPopupAsync_AwaitingShowPopupAsync_EnsurePreviousPopupClose // Assert Assert.Empty(navigation.ModalStack); } - + [Fact(Timeout = (int)TestDuration.Short)] public async Task ShowPopupAsync_UsingPage_AwaitingShowPopupAsync_EnsurePreviousPopupClosed() { // Arrange var popupService = ServiceProvider.GetRequiredService(); - + if (Application.Current?.Windows[0].Page is not Page page) { throw new InvalidOperationException("Page cannot be null"); @@ -246,7 +246,7 @@ public async Task ShowPopupAsyncShouldValidateProperBindingContext() // Act await popupService.ShowPopupAsync(page.Navigation, PopupOptions.Empty, TestContext.Current.CancellationToken); - + // Assert Assert.Same(popupInstance.BindingContext, popupViewModel); } @@ -265,7 +265,7 @@ public async Task ShowPopupAsyncShouldReturnResultOnceClosed() // Act var result = await popupService.ShowPopupAsync(page.Navigation, PopupOptions.Empty, CancellationToken.None); - + // Assert Assert.Same(mockPopup.Result, result.Result); Assert.False(result.WasDismissedByTappingOutsideOfPopup); @@ -288,7 +288,7 @@ public async Task ShowPopupTAsyncShouldReturnResultOnceClosed() // Assert Assert.False(result.WasDismissedByTappingOutsideOfPopup); } - + [Fact(Timeout = (int)TestDuration.Short)] public async Task ShowPopupAsync_UsingNavigation_ShouldThrowArgumentNullException_WhenNavigationIsNull() { @@ -312,7 +312,7 @@ public async Task ShowPopupAsync_UsingPage_ShouldThrowArgumentNullException_When await Assert.ThrowsAsync(() => popupService.ShowPopupAsync((Page?)null, PopupOptions.Empty, CancellationToken.None)); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - + [Fact] public void ShowPopup_UsingPage_ShouldThrowArgumentNullException_WhenNavigationIsNull() { @@ -322,10 +322,10 @@ public void ShowPopup_UsingPage_ShouldThrowArgumentNullException_WhenNavigationI // Act // Assert #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - Assert.Throws(() => popupService.ShowPopup((Page?)null,PopupOptions.Empty)); + Assert.Throws(() => popupService.ShowPopup((Page?)null, PopupOptions.Empty)); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - + [Fact(Timeout = (int)TestDuration.Short)] public async Task ClosePopupAsync_UsingPage_ShouldThrowArgumentNullException_WhenPageIsNull() { @@ -338,7 +338,7 @@ public async Task ClosePopupAsync_UsingPage_ShouldThrowArgumentNullException_Whe await Assert.ThrowsAsync(() => popupService.ClosePopupAsync((Page?)null, TestContext.Current.CancellationToken)); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - + [Fact(Timeout = (int)TestDuration.Short)] public async Task ClosePopupAsyncT_UsingPage_ShouldThrowArgumentNullException_WhenPageIsNull() { @@ -351,7 +351,7 @@ public async Task ClosePopupAsyncT_UsingPage_ShouldThrowArgumentNullException_Wh await Assert.ThrowsAsync(() => popupService.ClosePopupAsync((Page?)null, 2, TestContext.Current.CancellationToken)); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - + [Fact(Timeout = (int)TestDuration.Short)] public async Task ClosePopupAsync_UsingNavigation_ShouldThrowArgumentNullException_WhenNavigationIsNull() { @@ -363,7 +363,7 @@ public async Task ClosePopupAsync_UsingNavigation_ShouldThrowArgumentNullExcepti await Assert.ThrowsAsync(() => popupService.ClosePopupAsync((INavigation?)null, TestContext.Current.CancellationToken)); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - + [Fact(Timeout = (int)TestDuration.Short)] public async Task ClosePopupAsyncT_UsingNavigation_ShouldThrowArgumentNullException_WhenNavigationIsNull() { @@ -416,105 +416,105 @@ public async Task ClosePopupAsync_ShouldClosePopupUsingNavigationAndReturnResult { // Arrange var popupService = ServiceProvider.GetRequiredService(); - - + + if (Application.Current?.Windows[0].Page is not Page page) { throw new InvalidOperationException("Page cannot be null"); } - + // Act popupService.ShowPopup(page.Navigation); - + // Assert Assert.Single(page.Navigation.ModalStack); Assert.IsType(page.Navigation.ModalStack[0]); - + // Act var popupResult = await popupService.ClosePopupAsync(page.Navigation, TestContext.Current.CancellationToken); - + // Assert Assert.Empty(page.Navigation.ModalStack); Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup); } - + [Fact(Timeout = (int)TestDuration.Short)] public async Task ClosePopupAsync_ShouldClosePopupUsingPageAndReturnResult() { // Arrange var popupService = ServiceProvider.GetRequiredService(); - - + + if (Application.Current?.Windows[0].Page is not Page page) { throw new InvalidOperationException("Page cannot be null"); } - + // Act popupService.ShowPopup(page); - + // Assert Assert.Single(page.Navigation.ModalStack); Assert.IsType(page.Navigation.ModalStack[0]); - + // Act var popupResult = await popupService.ClosePopupAsync(page, TestContext.Current.CancellationToken); - + // Assert Assert.Empty(page.Navigation.ModalStack); Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup); } - + [Fact(Timeout = (int)TestDuration.Short)] public async Task ClosePopupAsyncT_ShouldClosePopupUsingNavigationAndReturnResult() { // Arrange const int expectedResult = 2; var popupService = ServiceProvider.GetRequiredService(); - + if (Application.Current?.Windows[0].Page is not Page page) { throw new InvalidOperationException("Page cannot be null"); } - + // Act popupService.ShowPopup(page.Navigation); - + // Assert Assert.Single(page.Navigation.ModalStack); Assert.IsType(page.Navigation.ModalStack[0]); - + // Act var popupResult = await popupService.ClosePopupAsync(page.Navigation, expectedResult, TestContext.Current.CancellationToken); - + // Assert Assert.Empty(page.Navigation.ModalStack); Assert.Equal(expectedResult, popupResult.Result); Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup); } - + [Fact(Timeout = (int)TestDuration.Short)] public async Task ClosePopupAsyncT_ShouldClosePopupUsingPageAndReturnResult() { // Arrange const int expectedResult = 2; var popupService = ServiceProvider.GetRequiredService(); - + if (Application.Current?.Windows[0].Page is not Page page) { throw new InvalidOperationException("Page cannot be null"); } - + // Act popupService.ShowPopup(page); - + // Assert Assert.Single(page.Navigation.ModalStack); Assert.IsType(page.Navigation.ModalStack[0]); - + // Act var popupResult = await popupService.ClosePopupAsync(page.Navigation, expectedResult, TestContext.Current.CancellationToken); - + // Assert Assert.Empty(page.Navigation.ModalStack); Assert.Equal(expectedResult, popupResult.Result); diff --git a/src/CommunityToolkit.Maui/Interfaces/IPopupService.shared.cs b/src/CommunityToolkit.Maui/Interfaces/IPopupService.shared.cs index bbf9931580..2273b5232c 100644 --- a/src/CommunityToolkit.Maui/Interfaces/IPopupService.shared.cs +++ b/src/CommunityToolkit.Maui/Interfaces/IPopupService.shared.cs @@ -15,7 +15,7 @@ public interface IPopupService /// The that enable support for customizing the display and behavior of the presented popup. void ShowPopup(Page page, IPopupOptions? options = null) where T : notnull; - + /// /// Shows a popup with the specified options. /// @@ -34,7 +34,7 @@ void ShowPopup(INavigation navigation, IPopupOptions? options = null) /// Parameters that will be passed into the view or its associated BindingContext if they implement . void ShowPopup(Shell shell, IPopupOptions? options = null, IDictionary? shellParameters = null) where T : notnull; - + /// /// Shows a popup with the specified options. /// @@ -68,7 +68,7 @@ Task ShowPopupAsync(INavigation navigation, IPopupOptions? opti /// An when the popup is closed or the is cancelled. Make sure to check the value to determine how the popup was closed. Task ShowPopupAsync(Shell shell, IPopupOptions? options, IDictionary? shellParameters = null, CancellationToken cancellationToken = default) where T : notnull; - + /// /// Shows a popup with the specified options. /// @@ -105,7 +105,7 @@ Task> ShowPopupAsync(INavigation navigation, I /// An when the popup is closed or the is cancelled. Make sure to check the value to determine how the popup was closed. Task> ShowPopupAsync(Shell shell, IPopupOptions? options = null, IDictionary? shellParameters = null, CancellationToken cancellationToken = default) where T : notnull; - + /// /// Closes the current popup. /// diff --git a/src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs index cfce72a440..a8a742b3e0 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs @@ -131,5 +131,5 @@ public partial class Popup : Popup } sealed class PopupNotFoundException() : InvalidPopupOperationException($"Unable to close popup: could not locate {nameof(PopupPage)}. {nameof(PopupExtensions.ShowPopup)} or {nameof(PopupExtensions.ShowPopupAsync)} must be called before {nameof(Popup.CloseAsync)}. If using a custom implementation of {nameof(Popup)}, override the {nameof(Popup.CloseAsync)} method"); -sealed class PopupBlockedException(in Page currentVisibleModalPage): InvalidPopupOperationException($"Unable to close Popup because it is blocked by the Modal Page {currentVisibleModalPage.GetType().FullName}. Please call `{nameof(Page.Navigation)}.{nameof(Page.Navigation.PopModalAsync)}()` to first remove {currentVisibleModalPage.GetType().FullName} from the {nameof(Page.Navigation.ModalStack)}"); -class InvalidPopupOperationException(in string message) : InvalidOperationException(message); +sealed class PopupBlockedException(in Page currentVisibleModalPage) : InvalidPopupOperationException($"Unable to close Popup because it is blocked by the Modal Page {currentVisibleModalPage.GetType().FullName}. Please call `{nameof(Page.Navigation)}.{nameof(Page.Navigation.PopModalAsync)}()` to first remove {currentVisibleModalPage.GetType().FullName} from the {nameof(Page.Navigation.ModalStack)}"); +class InvalidPopupOperationException(in string message) : InvalidOperationException(message); \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs index 5bf0f6864a..fb1c03a075 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs @@ -220,21 +220,21 @@ sealed partial class BorderStrokeConverter : BaseConverterOneWay public override Brush? ConvertFrom(Shape? value, CultureInfo? culture) => value?.Stroke; } } - + sealed partial class PaddingConverter : BaseConverterOneWay { public override Thickness DefaultConvertReturnValue { get; set; } = PopupDefaults.Padding; public override Thickness ConvertFrom(Thickness value, CultureInfo? culture) => value == default ? PopupDefaults.Padding : value; } - + sealed partial class HorizontalOptionsConverter : BaseConverterOneWay { public override LayoutOptions DefaultConvertReturnValue { get; set; } = PopupDefaults.HorizontalOptions; public override LayoutOptions ConvertFrom(LayoutOptions value, CultureInfo? culture) => value == LayoutOptions.Fill ? PopupDefaults.HorizontalOptions : value; } - + sealed partial class VerticalOptionsConverter : BaseConverterOneWay { public override LayoutOptions DefaultConvertReturnValue { get; set; } = PopupDefaults.VerticalOptions; From fb6f6a2cae11cd072b5b36b3f340f46f0d21e265 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 27 May 2025 16:48:45 -0700 Subject: [PATCH 3/8] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Extensions/PopupExtensions.shared.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs b/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs index 846848efed..ccc43c6de6 100644 --- a/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs +++ b/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs @@ -197,7 +197,7 @@ void HandlePopupClosed(object? sender, IPopupResult e) } /// - /// Close the Most Recent Popup + /// Closes the most recent popup and returns an that provides details about the closure. /// public static Task ClosePopupAsync(this Page page, CancellationToken token = default) { @@ -244,7 +244,7 @@ void HandlePopupPageClosed(object? sender, IPopupResult e) } /// - /// Close the Most Recent Popup Return a Result + /// Closes the most recent popup and returns a result of type . /// public static Task> ClosePopupAsync(this Page page, TResult result, CancellationToken token = default) { From cc7ad6d73aebe9c1af874779961593d51afa0187 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 27 May 2025 16:49:07 -0700 Subject: [PATCH 4/8] Add `GetMostRecentPopupPage()` --- .../Extensions/PopupExtensions.shared.cs | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs b/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs index ccc43c6de6..e1f80e0866 100644 --- a/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs +++ b/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs @@ -216,19 +216,7 @@ public static async Task ClosePopupAsync(this INavigation navigati var popupClosedTCS = new TaskCompletionSource(); - var currentVisibleModalPage = Shell.Current is null - ? navigation.ModalStack.LastOrDefault() - : Shell.Current.Navigation.ModalStack.LastOrDefault(); - - if (currentVisibleModalPage is null) - { - throw new PopupNotFoundException(); - } - - if (currentVisibleModalPage is not PopupPage popupPage) - { - throw new PopupBlockedException(currentVisibleModalPage); - } + var popupPage = GetMostRecentPopupPage(navigation); popupPage.PopupClosed += HandlePopupPageClosed; await popupPage.CloseAsync(new PopupResult(false), token); @@ -263,6 +251,24 @@ public static async Task> ClosePopupAsync(this IN var popupClosedTCS = new TaskCompletionSource(); + var popupPage = GetMostRecentPopupPage(navigation); + + popupPage.PopupClosed += HandlePopupPageClosed; + + await popupPage.CloseAsync(new PopupResult(result, false), token); + + var popupResult = await popupClosedTCS.Task; + return GetPopupResult(popupResult); + + void HandlePopupPageClosed(object? sender, IPopupResult e) + { + popupPage.PopupClosed -= HandlePopupPageClosed; + popupClosedTCS.SetResult(e); + } + } + + static PopupPage GetMostRecentPopupPage(in INavigation navigation) + { var currentVisibleModalPage = Shell.Current is null ? navigation.ModalStack.LastOrDefault() : Shell.Current.Navigation.ModalStack.LastOrDefault(); @@ -277,18 +283,7 @@ public static async Task> ClosePopupAsync(this IN throw new PopupBlockedException(currentVisibleModalPage); } - popupPage.PopupClosed += HandlePopupPageClosed; - - await popupPage.CloseAsync(new PopupResult(result, false), token); - - var popupResult = await popupClosedTCS.Task; - return GetPopupResult(popupResult); - - void HandlePopupPageClosed(object? sender, IPopupResult e) - { - popupPage.PopupClosed -= HandlePopupPageClosed; - popupClosedTCS.SetResult(e); - } + return popupPage; } static PopupResult GetPopupResult(in IPopupResult result) From 2d2222dbb9c8d5fd91af2a408fc78dea79af3da1 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 27 May 2025 16:53:07 -0700 Subject: [PATCH 5/8] Update XML --- .../Extensions/PopupExtensions.shared.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs b/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs index e1f80e0866..04ce998be9 100644 --- a/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs +++ b/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs @@ -207,7 +207,7 @@ public static Task ClosePopupAsync(this Page page, CancellationTok } /// - /// Close the Most Recent Popup + /// Closes the most recent popup and returns an that provides details about the closure. /// public static async Task ClosePopupAsync(this INavigation navigation, CancellationToken token = default) { @@ -242,7 +242,7 @@ public static Task> ClosePopupAsync(this Page pag } /// - /// Close the Most Recent Popup Return a Result + /// Closes the most recent popup and returns an that provides details about the closure. /// public static async Task> ClosePopupAsync(this INavigation navigation, TResult result, CancellationToken token = default) { From b9014e1d6b60a9bad4cb30b212b8c712e7aeeb6c Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 27 May 2025 16:54:23 -0700 Subject: [PATCH 6/8] Update src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Extensions/PopupExtensionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs b/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs index 352ca52b4d..1fcff62960 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs @@ -1295,7 +1295,7 @@ public async Task ClosePopupAsync_ShouldClosePopupUsingNavigationAndReturnResult Assert.IsType(page.Navigation.ModalStack[0]); // Act - var popupResult = await page.ClosePopupAsync(page.Navigation, TestContext.Current.CancellationToken); + var popupResult = await page.ClosePopupAsync(TestContext.Current.CancellationToken); // Assert Assert.Empty(page.Navigation.ModalStack); From 8a09f6346a87d7c091aaa1c4ab8241e28318eef4 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 27 May 2025 16:58:37 -0700 Subject: [PATCH 7/8] Add Unit Tests --- .../Extensions/PopupExtensionsTests.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs b/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs index 1fcff62960..506f64289d 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs @@ -68,6 +68,58 @@ public async Task ClosePopup_PopupBlocked_ShouldThrowPopupBlockedException() await Assert.ThrowsAsync(() => navigation.ClosePopupAsync(TestContext.Current.CancellationToken)); } + [Fact(Timeout = (int)TestDuration.Short)] + public async Task ClosePopup_NullPage_ShouldThrowArgumentNullException() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + await Assert.ThrowsAsync(() => PopupExtensions.ClosePopupAsync((Page?)null, TestContext.Current.CancellationToken)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + } + + [Fact(Timeout = (int)TestDuration.Short)] + public async Task ClosePopup_NullNavigation_ShouldThrowArgumentNullException() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + await Assert.ThrowsAsync(() => PopupExtensions.ClosePopupAsync((INavigation?)null, TestContext.Current.CancellationToken)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + } + + [Fact(Timeout = (int)TestDuration.Short)] + public async Task ClosePopupT_NullPage_ShouldThrowArgumentNullException() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + await Assert.ThrowsAsync(() => PopupExtensions.ClosePopupAsync((Page?)null, 2, TestContext.Current.CancellationToken)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + } + + [Fact(Timeout = (int)TestDuration.Short)] + public async Task ClosePopupT_NullNavigation_ShouldThrowArgumentNullException() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + await Assert.ThrowsAsync(() => PopupExtensions.ClosePopupAsync((INavigation?)null, 2, TestContext.Current.CancellationToken)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + } + [Fact(Timeout = (int)TestDuration.Short)] public async Task ShowPopupAsync_WithPopupType_ShowsPopupAndClosesPopup() { From 206a81d90df49064d04cd7a2eefb3ca5d9759164 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 27 May 2025 16:59:42 -0700 Subject: [PATCH 8/8] Update XML --- .../Extensions/PopupExtensions.shared.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs b/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs index 04ce998be9..752946f673 100644 --- a/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs +++ b/src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs @@ -232,7 +232,7 @@ void HandlePopupPageClosed(object? sender, IPopupResult e) } /// - /// Closes the most recent popup and returns a result of type . + /// Closes the most recent popup and returns an that provides details about the closure. /// public static Task> ClosePopupAsync(this Page page, TResult result, CancellationToken token = default) { @@ -242,7 +242,7 @@ public static Task> ClosePopupAsync(this Page pag } /// - /// Closes the most recent popup and returns an that provides details about the closure. + /// Closes the most recent popup and returns an that provides details about the closure. /// public static async Task> ClosePopupAsync(this INavigation navigation, TResult result, CancellationToken token = default) {