Skip to content

Add PopupExtensions.ClosePopup() #2671

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ namespace CommunityToolkit.Maui.Core.Handlers;
/// <summary>
/// Handler Popup control
/// </summary>
#if NET10_0_OR_GREATER
#error Remove PopupHandler
#endif
[Obsolete($"{nameof(PopupHandler)} is no longer used by {nameof(CommunityToolkit)}.{nameof(Maui)} and will be removed in .NET 10")]
public partial class PopupHandler
{
Expand Down
3 changes: 3 additions & 0 deletions src/CommunityToolkit.Maui.Core/Interfaces/IPopup.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ namespace CommunityToolkit.Maui.Core;
/// <summary>
/// Represents a small View that pops up at front the Page.
/// </summary>
#if NET10_0_OR_GREATER
#error Remove IPopup
#endif
[Obsolete($"{nameof(IPopup)} is no longer used by {nameof(CommunityToolkit)}.{nameof(Maui)} and will be removed in .NET 10")]
public interface IPopup : IElement, IVisualTreeElement, IAsynchronousHandler
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ namespace CommunityToolkit.Maui.Core.Views;
/// <summary>
/// The native implementation of Popup control.
/// </summary>
#if NET10_0_OR_GREATER
#error Remove MauiPopup
#endif
[Obsolete($"{nameof(MauiPopup)} is no longer used by {nameof(CommunityToolkit)}.{nameof(Maui)} and will be removed in .NET 10")]
public class MauiPopup : Dialog, IDialogInterfaceOnCancelListener
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ namespace CommunityToolkit.Maui.Core.Views;
/// </remarks>
/// <param name="mauiContext">An instance of <see cref="IMauiContext"/>.</param>
/// <exception cref="ArgumentNullException">If <paramref name="mauiContext"/> is null an exception will be thrown. </exception>
#if NET10_0_OR_GREATER
#error Remove MauiPopup
#endif
[Obsolete($"{nameof(MauiPopup)} is no longer used by {nameof(CommunityToolkit)}.{nameof(Maui)} and will be removed in .NET 10")]
public class MauiPopup(IMauiContext mauiContext) : UIViewController
{
Expand Down
3 changes: 3 additions & 0 deletions src/CommunityToolkit.Maui.Core/Views/Popup/MauiPopup.tizen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ namespace CommunityToolkit.Maui.Core.Views;
/// <summary>
/// The native implementation of Popup control.
/// </summary>
#if NET10_0_OR_GREATER
#error Remove MauiPopup
#endif
[Obsolete($"{nameof(MauiPopup)} is no longer used by {nameof(CommunityToolkit)}.{nameof(Maui)} and will be removed in .NET 10")]
public class MauiPopup : Popup
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ namespace CommunityToolkit.Maui.Core.Views;
/// <summary>
/// Extension class where Helper methods for Popup lives.
/// </summary>
#if NET10_0_OR_GREATER
#error Remove MauiPopup
#endif
[Obsolete($"{nameof(PopupExtensions)} is no longer used by {nameof(CommunityToolkit)}.{nameof(Maui)} and will be removed in .NET 10")]
public static class PopupExtensions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ namespace CommunityToolkit.Maui.Core.Views;
/// <summary>
/// Extension class where Helper methods for Popup lives.
/// </summary>
#if NET10_0_OR_GREATER
#error Remove PopupExtensions
#endif
[Obsolete($"{nameof(PopupExtensions)} is no longer used by {nameof(CommunityToolkit)}.{nameof(Maui)} and will be removed in .NET 10")]
public static class PopupExtensions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ namespace CommunityToolkit.Maui.Core.Views;
/// <summary>
/// Extension class where Helper methods for Popup lives.
/// </summary>
#if NET10_0_OR_GREATER
#error Remove PopupExtensions
#endif
[Obsolete($"{nameof(PopupExtensions)} is no longer used by {nameof(CommunityToolkit)}.{nameof(Maui)} and will be removed in .NET 10")]
public static class PopupExtensions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
/// <summary>
/// Displays Overlay in the Popup background.
/// </summary>
#if NET10_0_OR_GREATER
#error Remove PopupOverlay
#endif
[Obsolete($"{nameof(PopupOverlay)} is no longer used by {nameof(CommunityToolkit)}.{nameof(Maui)} and will be removed in .NET 10")]
class PopupOverlay : WindowOverlay
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,45 @@ public PopupExtensionsTests()
navigation = page.Navigation;
}

[Fact]
public void ShowPopupAsync_WithPopupType_ShowsPopup()
[Fact(Timeout = (int)TestDuration.Short)]
public async Task ClosePopup_TokenExpired_ShouldThrowOperationCancelledException()
{
// Arrange
var cts = new CancellationTokenSource();

// Act
await cts.CancelAsync();

// Assert
await Assert.ThrowsAsync<OperationCanceledException>(() => navigation.ClosePopup(cts.Token));
}

[Fact(Timeout = (int)TestDuration.Short)]
public async Task ClosePopup_NoExistingPopup_ShouldThrowPopupNotFoundException()
{
// Arrange

// Act

// Assert
await Assert.ThrowsAsync<PopupNotFoundException>(() => navigation.ClosePopup(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<PopupBlockedException>(() => navigation.ClosePopup(TestContext.Current.CancellationToken));
}

[Fact(Timeout = (int)TestDuration.Short)]
public async Task ShowPopupAsync_WithPopupType_ShowsPopupAndClosesPopup()
{
// Arrange
var selfClosingPopup = ServiceProvider.GetRequiredService<MockSelfClosingPopup>() ?? throw new InvalidOperationException();
Expand All @@ -43,10 +80,16 @@ public void ShowPopupAsync_WithPopupType_ShowsPopup()
// Assert
Assert.Single(navigation.ModalStack);
Assert.IsType<PopupPage>(navigation.ModalStack[0]);

// Act
await navigation.ClosePopup(TestContext.Current.CancellationToken);

// Assert
Assert.Empty(navigation.ModalStack);
}

[Fact]
public void ShowPopupAsync_Shell_WithPopupType_ShowsPopup()
[Fact(Timeout = (int)TestDuration.Short)]
public async Task ShowPopupAsync_Shell_WithPopupType_ShowsPopupAndClosesPopup()
{
// Arrange
var shell = new Shell();
Expand All @@ -63,6 +106,12 @@ public void ShowPopupAsync_Shell_WithPopupType_ShowsPopup()
// Assert
Assert.Single(shellNavigation.ModalStack);
Assert.IsType<PopupPage>(shellNavigation.ModalStack[0]);

// Act
await navigation.ClosePopup(TestContext.Current.CancellationToken);

// Assert
Assert.Empty(navigation.ModalStack);
}

[Fact]
Expand Down Expand Up @@ -138,7 +187,7 @@ public async Task ShowPopupAsync_AwaitingShowPopupAsync_EnsurePreviousPopupClose
}

[Fact(Timeout = (int)TestDuration.Short)]
public async Task qShowPopupAsync_Shell_AwaitingShowPopupAsync_EnsurePreviousPopupClosed()
public async Task ShowPopupAsync_Shell_AwaitingShowPopupAsync_EnsurePreviousPopupClosed()
{
// Arrange
var shell = new Shell();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ public async Task PopupPageT_CloseAfterAdditionalModalPage_ShouldThrowInvalidOpe
await navigation.PushModalAsync(new ContentPage());

// Assert
await Assert.ThrowsAsync<InvalidPopupOperationException>(async () => await popupPage.Close(new PopupResult(false), CancellationToken.None));
await Assert.ThrowsAsync<PopupBlockedException>(async () => await popupPage.Close(new PopupResult(false), CancellationToken.None));
await Assert.ThrowsAnyAsync<InvalidPopupOperationException>(async () => await popupPage.Close(new PopupResult(false), CancellationToken.None));
await Assert.ThrowsAnyAsync<InvalidOperationException>(async () => await popupPage.Close(new PopupResult(false), CancellationToken.None));
}

Expand Down
36 changes: 36 additions & 0 deletions src/CommunityToolkit.Maui/Extensions/PopupExtensions.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,42 @@ void HandlePopupClosed(object? sender, IPopupResult e)
}
}

/// <summary>
/// Close the Visible Popup
/// </summary>
public static Task ClosePopup(this Page page, CancellationToken token = default)
{
ArgumentNullException.ThrowIfNull(page);

return ClosePopup(page.Navigation, token);
}

/// <summary>
/// Close the Visible Popup
/// </summary>
public static Task ClosePopup(this INavigation navigation, CancellationToken token = default)
{
token.ThrowIfCancellationRequested();

ArgumentNullException.ThrowIfNull(navigation);

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);
}

return popupPage.Close(new PopupResult(false), token);
}

static PopupResult<T> GetPopupResult<T>(in IPopupResult result)
{
return result switch
Expand Down
5 changes: 3 additions & 2 deletions src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,6 @@ public partial class Popup<T> : Popup
public virtual Task Close(T result, CancellationToken token = default) => GetPopupPage().Close(new PopupResult<T>(result, false), token);
}

class InvalidPopupOperationException(string message) : InvalidOperationException(message);
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.Close)}. If using a custom implementation of {nameof(Popup)}, override the {nameof(Popup.Close)} method");
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.Close)}. If using a custom implementation of {nameof(Popup)}, override the {nameof(Popup.Close)} 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);
2 changes: 1 addition & 1 deletion src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public async Task Close(PopupResult result, CancellationToken token = default)
if (Navigation.ModalStack[^1] is Microsoft.Maui.Controls.Page currentVisibleModalPage
&& currentVisibleModalPage != popupPageToClose)
{
throw new InvalidPopupOperationException($"Unable to close Popup because it is blocked by the Modal Page {currentVisibleModalPage.GetType().FullName}. Please call `{nameof(Navigation)}.{nameof(Navigation.PopModalAsync)}()` to first remove {currentVisibleModalPage.GetType().FullName} from the {nameof(Navigation.ModalStack)}");
throw new PopupBlockedException(currentVisibleModalPage);
}

// We call `.ThrowIfCancellationRequested()` again to avoid a race condition where a developer cancels the CancellationToken after we check for an InvalidOperationException
Expand Down