Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -35,6 +35,8 @@
<Button Text="Updating Popup" Command="{Binding UpdatingPopupCommand}" />

<Button Text="Show Popup content" Command="{Binding ShowPopupContentCommand}" />

<Button Text="Show Popup in a Modal Page in a Custom Navigation Page" Clicked="HandleModalPopupInCustomNavigationPage" />

<Button Text="Custom Positioning Popup" Clicked="HandlePopupPositionButtonClicked" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,29 @@ async void HandleComplexPopupClicked(object? sender, EventArgs e)
// Display Popup Result as a Toast
await Toast.Make($"You entered {popupResult.Result}").Show(CancellationToken.None);
}
}

async void HandleModalPopupInCustomNavigationPage(object? sender, EventArgs eventArgs)
{
var modalPopupPage = new ContentPage
{
Content = new VerticalStackLayout
{
Spacing = 24,
Children =
{
new Button()
.Text("Show Popup")
.Invoke(button => button.Command = new Command(async () => await popupService.ShowPopupAsync<ButtonPopup>(Shell.Current))),

new Button()
.Text("Back")
.Invoke(button => button.Command = new Command(async () => await Navigation.PopModalAsync()))
}
}.Center()
};

var customNavigationPage = new NavigationPage(modalPopupPage);
await Shell.Current.Navigation.PushModalAsync(customNavigationPage, true);
}
}
127 changes: 127 additions & 0 deletions src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Nito.AsyncEx;
using Xunit;
using Application = Microsoft.Maui.Controls.Application;
using NavigationPage = Microsoft.Maui.Controls.NavigationPage;
using Page = Microsoft.Maui.Controls.Page;

namespace CommunityToolkit.Maui.UnitTests.Views;
Expand Down Expand Up @@ -169,6 +170,132 @@ public async Task PopupPageT_CloseAfterAdditionalModalPage_ShouldThrowInvalidOpe
await Assert.ThrowsAnyAsync<InvalidOperationException>(async () => await popupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
}

[Fact]
public async Task PopupPageT_CloseWhenUsingCustomNavigationPage_ShouldClose()
{
// Arrange
if (Application.Current?.Windows[0].Page?.Navigation is not INavigation navigation)
{
throw new InvalidOperationException("Unable to locate Navigation page");
}

bool wasPopupPageClosed = false;

var view = new ContentView();
var popupOptions = new MockPopupOptions();
var popupPage = new PopupPage<string>(view, popupOptions);
popupPage.PopupClosed += HandlePopupPageClosed;

var onAppearingPage = new ContentPage();
var customNavigationPage = new NavigationPage(onAppearingPage);
onAppearingPage.NavigatedTo += HandlePageNavigatedTo;

// Act
await Shell.Current.Navigation.PushModalAsync(customNavigationPage, true);
await popupPage.CloseAsync(new PopupResult(false), CancellationToken.None);

// Assert
Assert.True(wasPopupPageClosed);

async void HandlePageNavigatedTo(object? sender, NavigatedToEventArgs e)
{
if (!e.WasPreviousPageACommunityToolkitPopupPage())
{
await customNavigationPage.Navigation.PushModalAsync(popupPage);
}
}

void HandlePopupPageClosed(object? sender, IPopupResult e)
{
wasPopupPageClosed = true;
}
}

[Fact]
public async Task PopupPageT_CloseAfterAdditionalModalPageToCustomNavigationPage_ShouldThrowPopupBlockedException()
{
// Arrange
bool wasPopupPageClosed = false;

var view = new ContentView();
var popupOptions = new MockPopupOptions();
var firstPopupPage = new PopupPage<string>(view, popupOptions);
firstPopupPage.PopupClosed += HandlePopupPageClosed;

var onAppearingPage = new ContentPage();
var customNavigationPage = new NavigationPage(onAppearingPage);
onAppearingPage.NavigatedTo += HandlePageNavigatedTo;

var secondPopupPage = new PopupPage<string>(new Button(), popupOptions);

// Act
await Shell.Current.Navigation.PushModalAsync(customNavigationPage, true);
await customNavigationPage.Navigation.PushModalAsync(secondPopupPage);

// Assert
await Assert.ThrowsAsync<PopupBlockedException>(async () => await firstPopupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
await Assert.ThrowsAnyAsync<InvalidPopupOperationException>(async () => await firstPopupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
await Assert.ThrowsAnyAsync<InvalidOperationException>(async () => await firstPopupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
Assert.False(wasPopupPageClosed);

async void HandlePageNavigatedTo(object? sender, NavigatedToEventArgs e)
{
if (!e.WasPreviousPageACommunityToolkitPopupPage())
{
await customNavigationPage.Navigation.PushModalAsync(firstPopupPage);
}
}

void HandlePopupPageClosed(object? sender, IPopupResult e)
{
wasPopupPageClosed = true;
}
}

[Fact]
public async Task PopupPageT_CloseAfterAdditionalModalPageToCustomNavigationPage_ShouldThrowPopupNotFound()
{
// Arrange
if (Application.Current?.Windows[0].Page?.Navigation is not INavigation navigation)
{
throw new InvalidOperationException("Unable to locate Navigation page");
}

bool wasPopupPageClosed = false;

var view = new ContentView();
var popupOptions = new MockPopupOptions();
var popupPage = new PopupPage<string>(view, popupOptions);
popupPage.PopupClosed += HandlePopupPageClosed;

var onAppearingPage = new ContentPage();
var customNavigationPage = new NavigationPage(onAppearingPage);
onAppearingPage.NavigatedTo += HandlePageNavigatedTo;

// Act
await Shell.Current.Navigation.PushModalAsync(customNavigationPage, true);
await customNavigationPage.Navigation.PushModalAsync(new ContentPage());

// Assert
await Assert.ThrowsAsync<PopupNotFoundException>(async () => await popupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
await Assert.ThrowsAnyAsync<InvalidPopupOperationException>(async () => await popupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
await Assert.ThrowsAnyAsync<InvalidOperationException>(async () => await popupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
Assert.False(wasPopupPageClosed);

async void HandlePageNavigatedTo(object? sender, NavigatedToEventArgs e)
{
if (!e.WasPreviousPageACommunityToolkitPopupPage())
{
await customNavigationPage.Navigation.PushModalAsync(popupPage);
}
}

void HandlePopupPageClosed(object? sender, IPopupResult e)
{
wasPopupPageClosed = true;
}
}

[Fact]
public void PopupPageT_Close_ShouldThrowOperationCanceledException_WhenTokenIsCancelled()
{
Expand Down
28 changes: 20 additions & 8 deletions src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using System.ComponentModel;
using System.Globalization;
using System.Windows.Input;
using CommunityToolkit.Maui.Converters;
using CommunityToolkit.Maui.Core;
using CommunityToolkit.Maui.Extensions;
using Microsoft.Maui.Controls.PlatformConfiguration;
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
using Microsoft.Maui.Controls.Shapes;
using NavigationPage = Microsoft.Maui.Controls.NavigationPage;
using Page = Microsoft.Maui.Controls.Page;

namespace CommunityToolkit.Maui.Views;

Expand Down Expand Up @@ -66,6 +66,7 @@ public PopupPage(Popup popup, IPopupOptions? popupOptions)

Shell.SetPresentationMode(this, PresentationMode.ModalNotAnimated);
On<iOS>().SetModalPresentationStyle(UIModalPresentationStyle.OverFullScreen);
NavigationPage.SetHasNavigationBar(this, false);
}

public event EventHandler<IPopupResult>? PopupClosed;
Expand All @@ -81,17 +82,28 @@ public async Task CloseAsync(PopupResult result, CancellationToken token = defau
// It may feel a bit redundant, given that we again call `ThrowIfCancellationRequested` later in this method, however, this ensures we propagate the correct Exception to the developer.
token.ThrowIfCancellationRequested();

var popupPageToClose = Navigation.ModalStack.OfType<PopupPage>().LastOrDefault(popupPage => popupPage.Content == Content);

if (popupPageToClose is null)
// Handle edge case where a Popup was pushed inside a custom IPageContainer (e.g. a NavigationPage) on the Modal Stack
var customPageContainer = Navigation.ModalStack.OfType<IPageContainer<Page>>().LastOrDefault();
if (customPageContainer is not null && customPageContainer.CurrentPage is not PopupPage)
{
throw new PopupNotFoundException();
}

if (Navigation.ModalStack[^1] is Microsoft.Maui.Controls.Page currentVisibleModalPage
&& currentVisibleModalPage != popupPageToClose)
var popupPageToClose = customPageContainer?.CurrentPage as PopupPage
?? Navigation.ModalStack.OfType<PopupPage>().LastOrDefault()
?? throw new PopupNotFoundException();

// PopModalAsync will pop the last (top) page from the ModalStack
// Ensure that the PopupPage the user is attempting to close is the last (top) page on the Modal stack before calling Navigation.PopModalAsync
if (Navigation.ModalStack[^1] is IPageContainer<Page> { CurrentPage: PopupPage visiblePopupPageInCustomPageContainer }
&& visiblePopupPageInCustomPageContainer.Content != Content)
{
throw new PopupBlockedException(popupPageToClose);
}
else if (Navigation.ModalStack[^1] is ContentPage currentVisibleModalPage
&& currentVisibleModalPage.Content != Content)
{
throw new PopupBlockedException(currentVisibleModalPage);
throw new PopupBlockedException(popupPageToClose);
}

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