Skip to content

Commit d49bbd7

Browse files
pictosPedro JesusTheCodeTraveler
authored
(Popup) Enable Popup v2 Inside Custom NavigationPage (#2919)
* remove navBar * fix navigation * Add Sample * Add Unit Tests * Refactor `Close` * `dotnet format` * Fix typo --------- Co-authored-by: Pedro Jesus <pedrojesus@Pedros-MacBook-Pro.local> Co-authored-by: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com>
1 parent 778c618 commit d49bbd7

File tree

4 files changed

+172
-8
lines changed

4 files changed

+172
-8
lines changed

samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/PopupsPage.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
<Button Text="Updating Popup" Command="{Binding UpdatingPopupCommand}" />
3636

3737
<Button Text="Show Popup content" Command="{Binding ShowPopupContentCommand}" />
38+
39+
<Button Text="Show Popup in a Modal Page in a Custom Navigation Page" Clicked="HandleModalPopupInCustomNavigationPage" />
3840

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

samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/PopupsPage.xaml.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,29 @@ async void HandleComplexPopupClicked(object? sender, EventArgs e)
169169
// Display Popup Result as a Toast
170170
await Toast.Make($"You entered {popupResult.Result}").Show(CancellationToken.None);
171171
}
172+
}
173+
174+
async void HandleModalPopupInCustomNavigationPage(object? sender, EventArgs eventArgs)
175+
{
176+
var modalPopupPage = new ContentPage
177+
{
178+
Content = new VerticalStackLayout
179+
{
180+
Spacing = 24,
181+
Children =
182+
{
183+
new Button()
184+
.Text("Show Popup")
185+
.Invoke(button => button.Command = new Command(async () => await popupService.ShowPopupAsync<ButtonPopup>(Shell.Current))),
186+
187+
new Button()
188+
.Text("Back")
189+
.Invoke(button => button.Command = new Command(async () => await Navigation.PopModalAsync()))
190+
}
191+
}.Center()
192+
};
172193

194+
var customNavigationPage = new NavigationPage(modalPopupPage);
195+
await Shell.Current.Navigation.PushModalAsync(customNavigationPage, true);
173196
}
174197
}

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

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Nito.AsyncEx;
99
using Xunit;
1010
using Application = Microsoft.Maui.Controls.Application;
11+
using NavigationPage = Microsoft.Maui.Controls.NavigationPage;
1112
using Page = Microsoft.Maui.Controls.Page;
1213

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

173+
[Fact]
174+
public async Task PopupPageT_CloseWhenUsingCustomNavigationPage_ShouldClose()
175+
{
176+
// Arrange
177+
if (Application.Current?.Windows[0].Page?.Navigation is not INavigation navigation)
178+
{
179+
throw new InvalidOperationException("Unable to locate Navigation page");
180+
}
181+
182+
bool wasPopupPageClosed = false;
183+
184+
var view = new ContentView();
185+
var popupOptions = new MockPopupOptions();
186+
var popupPage = new PopupPage<string>(view, popupOptions);
187+
popupPage.PopupClosed += HandlePopupPageClosed;
188+
189+
var onAppearingPage = new ContentPage();
190+
var customNavigationPage = new NavigationPage(onAppearingPage);
191+
onAppearingPage.NavigatedTo += HandlePageNavigatedTo;
192+
193+
// Act
194+
await Shell.Current.Navigation.PushModalAsync(customNavigationPage, true);
195+
await popupPage.CloseAsync(new PopupResult(false), CancellationToken.None);
196+
197+
// Assert
198+
Assert.True(wasPopupPageClosed);
199+
200+
async void HandlePageNavigatedTo(object? sender, NavigatedToEventArgs e)
201+
{
202+
if (!e.WasPreviousPageACommunityToolkitPopupPage())
203+
{
204+
await customNavigationPage.Navigation.PushModalAsync(popupPage);
205+
}
206+
}
207+
208+
void HandlePopupPageClosed(object? sender, IPopupResult e)
209+
{
210+
wasPopupPageClosed = true;
211+
}
212+
}
213+
214+
[Fact]
215+
public async Task PopupPageT_CloseAfterAdditionalModalPageToCustomNavigationPage_ShouldThrowPopupBlockedException()
216+
{
217+
// Arrange
218+
bool wasPopupPageClosed = false;
219+
220+
var view = new ContentView();
221+
var popupOptions = new MockPopupOptions();
222+
var firstPopupPage = new PopupPage<string>(view, popupOptions);
223+
firstPopupPage.PopupClosed += HandlePopupPageClosed;
224+
225+
var onAppearingPage = new ContentPage();
226+
var customNavigationPage = new NavigationPage(onAppearingPage);
227+
onAppearingPage.NavigatedTo += HandlePageNavigatedTo;
228+
229+
var secondPopupPage = new PopupPage<string>(new Button(), popupOptions);
230+
231+
// Act
232+
await Shell.Current.Navigation.PushModalAsync(customNavigationPage, true);
233+
await customNavigationPage.Navigation.PushModalAsync(secondPopupPage);
234+
235+
// Assert
236+
await Assert.ThrowsAsync<PopupBlockedException>(async () => await firstPopupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
237+
await Assert.ThrowsAnyAsync<InvalidPopupOperationException>(async () => await firstPopupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
238+
await Assert.ThrowsAnyAsync<InvalidOperationException>(async () => await firstPopupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
239+
Assert.False(wasPopupPageClosed);
240+
241+
async void HandlePageNavigatedTo(object? sender, NavigatedToEventArgs e)
242+
{
243+
if (!e.WasPreviousPageACommunityToolkitPopupPage())
244+
{
245+
await customNavigationPage.Navigation.PushModalAsync(firstPopupPage);
246+
}
247+
}
248+
249+
void HandlePopupPageClosed(object? sender, IPopupResult e)
250+
{
251+
wasPopupPageClosed = true;
252+
}
253+
}
254+
255+
[Fact]
256+
public async Task PopupPageT_CloseAfterAdditionalModalPageToCustomNavigationPage_ShouldThrowPopupNotFound()
257+
{
258+
// Arrange
259+
if (Application.Current?.Windows[0].Page?.Navigation is not INavigation navigation)
260+
{
261+
throw new InvalidOperationException("Unable to locate Navigation page");
262+
}
263+
264+
bool wasPopupPageClosed = false;
265+
266+
var view = new ContentView();
267+
var popupOptions = new MockPopupOptions();
268+
var popupPage = new PopupPage<string>(view, popupOptions);
269+
popupPage.PopupClosed += HandlePopupPageClosed;
270+
271+
var onAppearingPage = new ContentPage();
272+
var customNavigationPage = new NavigationPage(onAppearingPage);
273+
onAppearingPage.NavigatedTo += HandlePageNavigatedTo;
274+
275+
// Act
276+
await Shell.Current.Navigation.PushModalAsync(customNavigationPage, true);
277+
await customNavigationPage.Navigation.PushModalAsync(new ContentPage());
278+
279+
// Assert
280+
await Assert.ThrowsAsync<PopupNotFoundException>(async () => await popupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
281+
await Assert.ThrowsAnyAsync<InvalidPopupOperationException>(async () => await popupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
282+
await Assert.ThrowsAnyAsync<InvalidOperationException>(async () => await popupPage.CloseAsync(new PopupResult(false), CancellationToken.None));
283+
Assert.False(wasPopupPageClosed);
284+
285+
async void HandlePageNavigatedTo(object? sender, NavigatedToEventArgs e)
286+
{
287+
if (!e.WasPreviousPageACommunityToolkitPopupPage())
288+
{
289+
await customNavigationPage.Navigation.PushModalAsync(popupPage);
290+
}
291+
}
292+
293+
void HandlePopupPageClosed(object? sender, IPopupResult e)
294+
{
295+
wasPopupPageClosed = true;
296+
}
297+
}
298+
172299
[Fact]
173300
public void PopupPageT_Close_ShouldThrowOperationCanceledException_WhenTokenIsCancelled()
174301
{

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

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
using System.ComponentModel;
22
using System.Globalization;
3-
using System.Windows.Input;
43
using CommunityToolkit.Maui.Converters;
54
using CommunityToolkit.Maui.Core;
6-
using CommunityToolkit.Maui.Extensions;
75
using Microsoft.Maui.Controls.PlatformConfiguration;
86
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
97
using Microsoft.Maui.Controls.Shapes;
8+
using NavigationPage = Microsoft.Maui.Controls.NavigationPage;
9+
using Page = Microsoft.Maui.Controls.Page;
1010

1111
namespace CommunityToolkit.Maui.Views;
1212

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

6767
Shell.SetPresentationMode(this, PresentationMode.ModalNotAnimated);
6868
On<iOS>().SetModalPresentationStyle(UIModalPresentationStyle.OverFullScreen);
69+
NavigationPage.SetHasNavigationBar(this, false);
6970
}
7071

7172
public event EventHandler<IPopupResult>? PopupClosed;
@@ -81,17 +82,28 @@ public async Task CloseAsync(PopupResult result, CancellationToken token = defau
8182
// 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.
8283
token.ThrowIfCancellationRequested();
8384

84-
var popupPageToClose = Navigation.ModalStack.OfType<PopupPage>().LastOrDefault(popupPage => popupPage.Content == Content);
85-
86-
if (popupPageToClose is null)
85+
// Handle edge case where a Popup was pushed inside a custom IPageContainer (e.g. a NavigationPage) on the Modal Stack
86+
var customPageContainer = Navigation.ModalStack.OfType<IPageContainer<Page>>().LastOrDefault();
87+
if (customPageContainer is not null && customPageContainer.CurrentPage is not PopupPage)
8788
{
8889
throw new PopupNotFoundException();
8990
}
9091

91-
if (Navigation.ModalStack[^1] is Microsoft.Maui.Controls.Page currentVisibleModalPage
92-
&& currentVisibleModalPage != popupPageToClose)
92+
var popupPageToClose = customPageContainer?.CurrentPage as PopupPage
93+
?? Navigation.ModalStack.OfType<PopupPage>().LastOrDefault()
94+
?? throw new PopupNotFoundException();
95+
96+
// PopModalAsync will pop the last (top) page from the ModalStack
97+
// Ensure that the PopupPage the user is attempting to close is the last (top) page on the Modal stack before calling Navigation.PopModalAsync
98+
if (Navigation.ModalStack[^1] is IPageContainer<Page> { CurrentPage: PopupPage visiblePopupPageInCustomPageContainer }
99+
&& visiblePopupPageInCustomPageContainer.Content != Content)
100+
{
101+
throw new PopupBlockedException(popupPageToClose);
102+
}
103+
else if (Navigation.ModalStack[^1] is ContentPage currentVisibleModalPage
104+
&& currentVisibleModalPage.Content != Content)
93105
{
94-
throw new PopupBlockedException(currentVisibleModalPage);
106+
throw new PopupBlockedException(popupPageToClose);
95107
}
96108

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

0 commit comments

Comments
 (0)