diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/PopupsPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/PopupsPage.xaml
index 82ca4cb591..46d0348745 100644
--- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/PopupsPage.xaml
+++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/PopupsPage.xaml
@@ -35,6 +35,8 @@
+
+
diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/PopupsPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/PopupsPage.xaml.cs
index 79c4a2fb70..59a1404a62 100644
--- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/PopupsPage.xaml.cs
+++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/PopupsPage.xaml.cs
@@ -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(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);
}
}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs
index e808445700..7f312479c0 100644
--- a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs
+++ b/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs
@@ -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;
@@ -169,6 +170,132 @@ public async Task PopupPageT_CloseAfterAdditionalModalPage_ShouldThrowInvalidOpe
await Assert.ThrowsAnyAsync(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(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(view, popupOptions);
+ firstPopupPage.PopupClosed += HandlePopupPageClosed;
+
+ var onAppearingPage = new ContentPage();
+ var customNavigationPage = new NavigationPage(onAppearingPage);
+ onAppearingPage.NavigatedTo += HandlePageNavigatedTo;
+
+ var secondPopupPage = new PopupPage(new Button(), popupOptions);
+
+ // Act
+ await Shell.Current.Navigation.PushModalAsync(customNavigationPage, true);
+ await customNavigationPage.Navigation.PushModalAsync(secondPopupPage);
+
+ // Assert
+ await Assert.ThrowsAsync(async () => await firstPopupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
+ await Assert.ThrowsAnyAsync(async () => await firstPopupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
+ await Assert.ThrowsAnyAsync(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(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(async () => await popupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
+ await Assert.ThrowsAnyAsync(async () => await popupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
+ await Assert.ThrowsAnyAsync(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()
{
diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs
index fc00b1ae89..d711bdaedb 100644
--- a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs
+++ b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs
@@ -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;
@@ -66,6 +66,7 @@ public PopupPage(Popup popup, IPopupOptions? popupOptions)
Shell.SetPresentationMode(this, PresentationMode.ModalNotAnimated);
On().SetModalPresentationStyle(UIModalPresentationStyle.OverFullScreen);
+ NavigationPage.SetHasNavigationBar(this, false);
}
public event EventHandler? PopupClosed;
@@ -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().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>().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().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 { 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