diff --git a/src/CommunityToolkit.Maui.UnitTests/CommunityToolkit.Maui.UnitTests.csproj b/src/CommunityToolkit.Maui.UnitTests/CommunityToolkit.Maui.UnitTests.csproj
index a892d1d124..27187601bf 100644
--- a/src/CommunityToolkit.Maui.UnitTests/CommunityToolkit.Maui.UnitTests.csproj
+++ b/src/CommunityToolkit.Maui.UnitTests/CommunityToolkit.Maui.UnitTests.csproj
@@ -21,6 +21,7 @@
+
diff --git a/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs b/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs
index 45f0907a86..cef2f4423c 100644
--- a/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs
+++ b/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs
@@ -192,7 +192,7 @@ public void ShowPopupAsync_WithViewType_SetsCorrectDefaults()
navigation.ShowPopup(label);
popupPage = (PopupPage)navigation.ModalStack[0];
- autogeneratedPopup = (Popup)(((Border)popupPage.Content.Children[0]).Content ?? throw new InvalidOperationException("Border Content cannot be null"));
+ autogeneratedPopup = (Popup)(popupPage.Content.PopupBorder.Content ?? throw new InvalidOperationException("Border Content cannot be null"));
// Assert
Assert.Equal(PopupDefaults.BackgroundColor, autogeneratedPopup.BackgroundColor);
@@ -435,7 +435,7 @@ public void ShowPopupAsync_WithCustomOptions_AppliesOptions()
var popupPage = (PopupPage)navigation.ModalStack[0];
var popupPageContent = popupPage.Content;
- var border = (Border)popupPageContent.Children[0];
+ var border = popupPageContent.PopupBorder;
var popup = border.Content;
// Assert
@@ -507,7 +507,7 @@ public void ShowPopupAsync_Shell_WithCustomOptions_AppliesOptions()
var popupPage = (PopupPage)shellNavigation.ModalStack[0];
var popupPageContent = popupPage.Content;
- var border = (Border)popupPageContent.Children[0];
+ var border = popupPageContent.PopupBorder;
var popup = border.Content;
// Assert
@@ -579,7 +579,7 @@ public void ShowPopupAsyncWithView_WithCustomOptions_AppliesOptions()
var popupPage = (PopupPage)navigation.ModalStack[0];
var popupPageContent = popupPage.Content;
- var border = (Border)popupPageContent.Children[0];
+ var border = popupPageContent.PopupBorder;
var popup = (Popup)(border.Content ?? throw new InvalidCastException());
// Assert
@@ -660,7 +660,7 @@ public void ShowPopupAsyncWithView_Shell_WithCustomOptions_AppliesOptions()
var popupPage = (PopupPage)shellNavigation.ModalStack[0];
var popupPageContent = popupPage.Content;
- var border = (Border)popupPageContent.Children[0];
+ var border = popupPageContent.PopupBorder;
var popup = (Popup)(border.Content ?? throw new InvalidCastException());
// Assert
@@ -1149,7 +1149,7 @@ public async Task ShowPopupAsync_ReferenceTypeShouldReturnNull_WhenPopupTapGestu
var popupPage = (PopupPage)navigation.ModalStack[0];
popupPage.PopupClosed += HandlePopupClosed;
- var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
+ var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
tapGestureRecognizer.Command?.Execute(null);
var popupClosedResult = await popupClosedTCS.Task;
@@ -1184,7 +1184,7 @@ public async Task ShowPopupAsync_Shell_ReferenceTypeShouldReturnNull_WhenPopupTa
var popupPage = (PopupPage)shellNavigation.ModalStack[0];
popupPage.PopupClosed += HandlePopupClosed;
- var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
+ var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
tapGestureRecognizer.Command?.Execute(null);
var popupClosedResult = await popupClosedTCS.Task;
@@ -1212,7 +1212,7 @@ public async Task ShowPopupAsync_NullableValueTypeShouldReturnResult_WhenPopupIs
var popupPage = (PopupPage)navigation.ModalStack[0];
popupPage.PopupClosed += HandlePopupClosed;
- var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
+ var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
tapGestureRecognizer.Command?.Execute(null);
var popupClosedResult = await popupClosedTCS.Task;
@@ -1247,7 +1247,7 @@ public async Task ShowPopupAsync_Shell_NullableValueTypeShouldReturnResult_WhenP
var popupPage = (PopupPage)shellNavigation.ModalStack[0];
popupPage.PopupClosed += HandlePopupClosed;
- var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
+ var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
tapGestureRecognizer.Command?.Execute(null);
var popupClosedResult = await popupClosedTCS.Task;
@@ -1275,7 +1275,7 @@ public async Task ShowPopupAsync_ValueTypeShouldReturnResult_WhenPopupIsClosedBy
var popupPage = (PopupPage)navigation.ModalStack[0];
popupPage.PopupClosed += HandlePopupClosed;
- var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
+ var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
tapGestureRecognizer.Command?.Execute(null);
var popupClosedResult = await popupClosedTCS.Task;
@@ -1311,7 +1311,7 @@ public async Task ShowPopupAsync_Shell_ValueTypeShouldReturnResult_WhenPopupIsCl
var popupPage = (PopupPage)shellNavigation.ModalStack[0];
popupPage.PopupClosed += HandlePopupClosed;
- var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
+ var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
tapGestureRecognizer.Command?.Execute(null);
var popupClosedResult = await popupClosedTCS.Task;
@@ -1459,6 +1459,9 @@ public async Task ShowPopupAsync_TaskShouldCompleteWhenCloseAsyncIsCalled()
Assert.Equal(expectedResult, popupResult.Result);
Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup);
}
+
+ static TapGestureRecognizer GetTapOutsideGestureRecognizer(PopupPage popupPage) =>
+ (TapGestureRecognizer)popupPage.Content.Children.OfType().Single().GestureRecognizers[0];
}
sealed class ViewWithIQueryAttributable : Button, IQueryAttributable
diff --git a/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs b/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs
index e4abe072d5..41f929d100 100644
--- a/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs
+++ b/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs
@@ -166,7 +166,7 @@ public void ShowPopupAsync_WithCustomOptions_AppliesOptions()
var popupPage = (PopupPage)navigation.ModalStack[0];
var popupPageLayout = popupPage.Content;
- var border = (Border)popupPageLayout.Children[0];
+ var border = popupPageLayout.PopupBorder;
var popup = border.Content;
// Assert
diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs
index 40c537be4f..1efd44b9f3 100644
--- a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs
+++ b/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs
@@ -250,13 +250,22 @@ public void Constructor_WithViewAndPopupOptions_SetsCorrectProperties()
Assert.Equal(UIModalPresentationStyle.OverFullScreen, popupPage.On().ModalPresentationStyle());
// Verify content has tap gesture recognizer attached
- Assert.Single(popupPage.Content.GestureRecognizers);
- Assert.IsType(popupPage.Content.GestureRecognizers[0]);
+ var gestureRecognizers = popupPage.Content.Children.OfType().Single().GestureRecognizers;
+ Assert.Single(gestureRecognizers);
+ Assert.IsType(gestureRecognizers[0]);
// Verify PopupPageLayout structure
var pageContent = popupPage.Content;
- Assert.Single(pageContent.Children);
- Assert.IsType(pageContent.Children[0]);
+ Assert.Collection(
+ pageContent.Children,
+ first =>
+ {
+ first.Should().BeOfType();
+ },
+ second =>
+ {
+ second.Should().BeOfType();
+ });
// Verify content binding context is set correctly
Assert.Equal(view.BindingContext, pageContent.BindingContext);
@@ -316,7 +325,7 @@ public async Task TapGestureRecognizer_ShouldClosePopupWhenCanBeDismissedIsTrue(
var popupPage = new PopupPage(view, popupOptions);
- var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
+ var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
var command = tapGestureRecognizer.Command;
Assert.NotNull(command);
@@ -341,7 +350,7 @@ public void TapGestureRecognizer_ShouldNotExecuteWhenCanBeDismissedIsFalse()
};
var popupPage = new PopupPage(view, popupOptions);
- var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
+ var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
var command = tapGestureRecognizer.Command;
// Act & Assert
@@ -421,7 +430,7 @@ public void TappingOutside_ShouldNotClosePopup_WhenCanBeDismissedIsFalse()
var popupPage = new PopupPage(view, popupOptions);
// Act
- var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
+ var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
var command = tapGestureRecognizer.Command;
// Assert
@@ -472,12 +481,15 @@ public void PopupPage_ShouldRespectLayoutOptions()
// Act
var popupPage = new PopupPage(view, PopupOptions.Empty);
- var border = (Border)popupPage.Content.Children[0];
+ var border = popupPage.Content.PopupBorder;
// Assert
Assert.Equal(LayoutOptions.Start, border.VerticalOptions);
Assert.Equal(LayoutOptions.End, border.HorizontalOptions);
}
+
+ static TapGestureRecognizer GetTapOutsideGestureRecognizer(PopupPage popupPage) =>
+ (TapGestureRecognizer)popupPage.Content.Children.OfType().Single().GestureRecognizers[0];
// Helper class for testing protected methods
sealed class TestablePopupPage(View view, IPopupOptions popupOptions) : PopupPage(view, popupOptions)
diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs
index 8cd851161c..13cacf49b0 100644
--- a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs
+++ b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs
@@ -1,5 +1,6 @@
using System.ComponentModel;
using System.Globalization;
+using System.Windows.Input;
using CommunityToolkit.Maui.Converters;
using CommunityToolkit.Maui.Core;
using CommunityToolkit.Maui.Extensions;
@@ -41,16 +42,14 @@ public PopupPage(Popup popup, IPopupOptions popupOptions)
this.popup = popup;
this.popupOptions = popupOptions;
- // Only set the content if the parent constructor hasn't set the content already; don't override content if it already exists
- base.Content ??= new PopupPageLayout(popup, popupOptions);
-
tapOutsideOfPopupCommand = new Command(async () =>
{
popupOptions.OnTappingOutsideOfPopup?.Invoke();
await CloseAsync(new PopupResult(true));
}, () => popupOptions.CanBeDismissedByTappingOutsideOfPopup);
- Content.GestureRecognizers.Add(new TapGestureRecognizer { Command = tapOutsideOfPopupCommand });
+ // Only set the content if the parent constructor hasn't set the content already; don't override content if it already exists
+ base.Content = new PopupPageLayout(popup, popupOptions, tapOutsideOfPopupCommand);
if (popupOptions is BindableObject bindablePopupOptions)
{
@@ -104,7 +103,7 @@ public async Task CloseAsync(PopupResult result, CancellationToken token = defau
popupClosedEventManager.HandleEvent(this, result, nameof(PopupClosed));
}
-
+
protected override bool OnBackButtonPressed()
{
// Only close the Popup if PopupOptions.CanBeDismissedByTappingOutsideOfPopup is true
@@ -112,7 +111,7 @@ protected override bool OnBackButtonPressed()
{
CloseAsync(new PopupResult(true), CancellationToken.None).SafeFireAndForget();
}
-
+
// Always return true to let the Android Operating System know that we are manually handling the Navigation request from the Android Back Button
return true;
}
@@ -176,33 +175,44 @@ void IQueryAttributable.ApplyQueryAttributes(IDictionary query)
internal sealed partial class PopupPageLayout : Grid
{
- public PopupPageLayout(in Popup popupContent, in IPopupOptions options)
+ public PopupPageLayout(in Popup popupContent, in IPopupOptions options, in ICommand tapOutsideOfPopupCommand)
{
Background = BackgroundColor = null;
- var border = new Border
+ var tappableBackground = new BoxView
+ {
+ BackgroundColor = Colors.Transparent,
+ HorizontalOptions = LayoutOptions.Fill,
+ VerticalOptions = LayoutOptions.Fill
+ };
+ tappableBackground.GestureRecognizers.Add(new TapGestureRecognizer { Command = tapOutsideOfPopupCommand });
+ Children.Add(tappableBackground); // Add the Tappable Background to the PopupPageLayout Grid before adding the Border to ensure the Border is displayed on top
+
+ PopupBorder = new Border
{
BackgroundColor = popupContent.BackgroundColor ??= PopupDefaults.BackgroundColor,
Content = popupContent
};
// Bind `Popup` values through to Border using OneWay Bindings
- border.SetBinding(Border.MarginProperty, static (Popup popup) => popup.Margin, source: popupContent, mode: BindingMode.OneWay);
- border.SetBinding(Border.PaddingProperty, static (Popup popup) => popup.Padding, source: popupContent, mode: BindingMode.OneWay);
- border.SetBinding(Border.BackgroundProperty, static (Popup popup) => popup.Background, source: popupContent, mode: BindingMode.OneWay);
- border.SetBinding(Border.BackgroundColorProperty, static (Popup popup) => popup.BackgroundColor, source: popupContent, mode: BindingMode.OneWay);
- border.SetBinding(Border.VerticalOptionsProperty, static (Popup popup) => popup.VerticalOptions, source: popupContent, mode: BindingMode.OneWay);
- border.SetBinding(Border.HorizontalOptionsProperty, static (Popup popup) => popup.HorizontalOptions, source: popupContent, mode: BindingMode.OneWay);
+ PopupBorder.SetBinding(Border.MarginProperty, static (Popup popup) => popup.Margin, source: popupContent, mode: BindingMode.OneWay);
+ PopupBorder.SetBinding(Border.PaddingProperty, static (Popup popup) => popup.Padding, source: popupContent, mode: BindingMode.OneWay);
+ PopupBorder.SetBinding(Border.BackgroundProperty, static (Popup popup) => popup.Background, source: popupContent, mode: BindingMode.OneWay);
+ PopupBorder.SetBinding(Border.BackgroundColorProperty, static (Popup popup) => popup.BackgroundColor, source: popupContent, mode: BindingMode.OneWay);
+ PopupBorder.SetBinding(Border.VerticalOptionsProperty, static (Popup popup) => popup.VerticalOptions, source: popupContent, mode: BindingMode.OneWay);
+ PopupBorder.SetBinding(Border.HorizontalOptionsProperty, static (Popup popup) => popup.HorizontalOptions, source: popupContent, mode: BindingMode.OneWay);
// Bind `PopupOptions` values through to Border using OneWay Bindings
- border.SetBinding(Border.ShadowProperty, static (IPopupOptions options) => options.Shadow, source: options, mode: BindingMode.OneWay);
- border.SetBinding(Border.StrokeProperty, static (IPopupOptions options) => options.Shape, source: options, converter: new BorderStrokeConverter(), mode: BindingMode.OneWay);
- border.SetBinding(Border.StrokeShapeProperty, static (IPopupOptions options) => options.Shape, source: options, mode: BindingMode.OneWay);
- border.SetBinding(Border.StrokeThicknessProperty, static (IPopupOptions options) => options.Shape, source: options, converter: new BorderStrokeThicknessConverter(), mode: BindingMode.OneWay);
+ PopupBorder.SetBinding(Border.ShadowProperty, static (IPopupOptions options) => options.Shadow, source: options, mode: BindingMode.OneWay);
+ PopupBorder.SetBinding(Border.StrokeProperty, static (IPopupOptions options) => options.Shape, source: options, converter: new BorderStrokeConverter(), mode: BindingMode.OneWay);
+ PopupBorder.SetBinding(Border.StrokeShapeProperty, static (IPopupOptions options) => options.Shape, source: options, mode: BindingMode.OneWay);
+ PopupBorder.SetBinding(Border.StrokeThicknessProperty, static (IPopupOptions options) => options.Shape, source: options, converter: new BorderStrokeThicknessConverter(), mode: BindingMode.OneWay);
- Children.Add(border);
+ Children.Add(PopupBorder);
}
+ public Border PopupBorder { get; }
+
sealed partial class BorderStrokeThicknessConverter : BaseConverterOneWay
{
public override double DefaultConvertReturnValue { get; set; } = PopupOptionsDefaults.BorderStrokeThickness;