From 173356b74fb03fa678efcf0958c0027c4dd18156 Mon Sep 17 00:00:00 2001 From: Shaun Lawrence <17139988+bijington@users.noreply.github.com> Date: Tue, 17 Jun 2025 20:29:03 +0100 Subject: [PATCH 01/10] Prevent a tap inside the content from closing a popup --- .../Views/Popup/PopupPage.shared.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs index 8cd851161c..353f24fa23 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) { @@ -176,7 +175,7 @@ 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, ICommand tapOutsideOfPopupCommand) { Background = BackgroundColor = null; @@ -186,6 +185,12 @@ public PopupPageLayout(in Popup popupContent, in IPopupOptions options) Content = popupContent }; + var backgroundGrid = new Grid(); + + backgroundGrid.GestureRecognizers.Add(new TapGestureRecognizer { Command = tapOutsideOfPopupCommand }); + + Children.Add(backgroundGrid); + // 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); From f4c0da6eeebf981d9785da3bb7321b5bbc6504b6 Mon Sep 17 00:00:00 2001 From: Shaun Lawrence <17139988+bijington@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:27:32 +0100 Subject: [PATCH 02/10] Revert "Prevent a tap inside the content from closing a popup" This reverts commit 173356b74fb03fa678efcf0958c0027c4dd18156. --- .../Views/Popup/PopupPage.shared.cs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs index 353f24fa23..8cd851161c 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs @@ -1,6 +1,5 @@ using System.ComponentModel; using System.Globalization; -using System.Windows.Input; using CommunityToolkit.Maui.Converters; using CommunityToolkit.Maui.Core; using CommunityToolkit.Maui.Extensions; @@ -42,14 +41,16 @@ 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); - - // 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); + + Content.GestureRecognizers.Add(new TapGestureRecognizer { Command = tapOutsideOfPopupCommand }); if (popupOptions is BindableObject bindablePopupOptions) { @@ -175,7 +176,7 @@ void IQueryAttributable.ApplyQueryAttributes(IDictionary query) internal sealed partial class PopupPageLayout : Grid { - public PopupPageLayout(in Popup popupContent, in IPopupOptions options, ICommand tapOutsideOfPopupCommand) + public PopupPageLayout(in Popup popupContent, in IPopupOptions options) { Background = BackgroundColor = null; @@ -185,12 +186,6 @@ public PopupPageLayout(in Popup popupContent, in IPopupOptions options, ICommand Content = popupContent }; - var backgroundGrid = new Grid(); - - backgroundGrid.GestureRecognizers.Add(new TapGestureRecognizer { Command = tapOutsideOfPopupCommand }); - - Children.Add(backgroundGrid); - // 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); From b5338a9e0957eb5c63d8f78deb74c4e5f8559f12 Mon Sep 17 00:00:00 2001 From: Shaun Lawrence <17139988+bijington@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:50:38 +0100 Subject: [PATCH 03/10] Make use of a BoxView as a background handler of taps --- .../Views/Popup/PopupPage.shared.cs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs index 8cd851161c..202bf8b114 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) { @@ -176,7 +175,7 @@ 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, ICommand tapOutsideOfPopupCommand) { Background = BackgroundColor = null; @@ -185,6 +184,15 @@ public PopupPageLayout(in Popup popupContent, in IPopupOptions options) BackgroundColor = popupContent.BackgroundColor ??= PopupDefaults.BackgroundColor, Content = popupContent }; + + var backgroundGrid = new BoxView + { + BackgroundColor = Colors.Transparent, + }; + + backgroundGrid.GestureRecognizers.Add(new TapGestureRecognizer { Command = tapOutsideOfPopupCommand }); + + Children.Add(backgroundGrid); // Bind `Popup` values through to Border using OneWay Bindings border.SetBinding(Border.MarginProperty, static (Popup popup) => popup.Margin, source: popupContent, mode: BindingMode.OneWay); From d17876eee22278ff84028694ce45ff5ce90934fb Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:30:35 -0700 Subject: [PATCH 04/10] Add GestureRecognizer to Prevent Popup from closing when tapped inside Refactored PopupPageLayout to expose the Border via a property instead of accessing it through the Children collection. Updated PopupPage to add gesture recognizers directly and simplified content initialization. Adjusted related unit tests to use the new Border property for improved clarity and maintainability. --- .../Extensions/PopupExtensionsTests.cs | 8 +-- .../Services/PopupServiceTests.cs | 2 +- .../Views/Popup/PopupPageTests.cs | 2 +- .../Views/Popup/PopupPage.shared.cs | 51 +++++++++---------- 4 files changed, 29 insertions(+), 34 deletions(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs b/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs index 45f0907a86..2c3aa10a62 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs @@ -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.Border; 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.Border; 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.Border; 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.Border; var popup = (Popup)(border.Content ?? throw new InvalidCastException()); // Assert diff --git a/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs b/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs index e4abe072d5..4c04c1023a 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.Border; 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..4c3fe761bf 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs @@ -472,7 +472,7 @@ public void PopupPage_ShouldRespectLayoutOptions() // Act var popupPage = new PopupPage(view, PopupOptions.Empty); - var border = (Border)popupPage.Content.Children[0]; + var border = popupPage.Content.Border; // Assert Assert.Equal(LayoutOptions.Start, border.VerticalOptions); diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs index 202bf8b114..bb87f6b756 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs @@ -1,6 +1,5 @@ using System.ComponentModel; using System.Globalization; -using System.Windows.Input; using CommunityToolkit.Maui.Converters; using CommunityToolkit.Maui.Core; using CommunityToolkit.Maui.Extensions; @@ -42,14 +41,16 @@ 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); - - // 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); + + Content.GestureRecognizers.Add(new TapGestureRecognizer { Command = tapOutsideOfPopupCommand }); if (popupOptions is BindableObject bindablePopupOptions) { @@ -103,7 +104,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 @@ -111,7 +112,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; } @@ -175,42 +176,36 @@ void IQueryAttributable.ApplyQueryAttributes(IDictionary query) internal sealed partial class PopupPageLayout : Grid { - public PopupPageLayout(in Popup popupContent, in IPopupOptions options, ICommand tapOutsideOfPopupCommand) + public PopupPageLayout(in Popup popupContent, in IPopupOptions options) { Background = BackgroundColor = null; - var border = new Border + Border = new Border { BackgroundColor = popupContent.BackgroundColor ??= PopupDefaults.BackgroundColor, Content = popupContent }; - - var backgroundGrid = new BoxView - { - BackgroundColor = Colors.Transparent, - }; - - backgroundGrid.GestureRecognizers.Add(new TapGestureRecognizer { Command = tapOutsideOfPopupCommand }); - - Children.Add(backgroundGrid); + Border.GestureRecognizers.Add(new TapGestureRecognizer()); // Blocks `tapOutsideOfPopupCommand` from closing the Popup when the content is tapped // 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); + 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); // 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); + 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); - Children.Add(border); + Children.Add(Border); } + public Border Border { get; } + sealed partial class BorderStrokeThicknessConverter : BaseConverterOneWay { public override double DefaultConvertReturnValue { get; set; } = PopupOptionsDefaults.BorderStrokeThickness; From 3896462e160a3de03c518f26b8e9390071829e90 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:33:19 -0700 Subject: [PATCH 05/10] Fix NuGet Security Vulnerability --- .../CommunityToolkit.Maui.UnitTests.csproj | 1 + 1 file changed, 1 insertion(+) 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 @@ + From b99d9803a6c7ff27d19bac0386a554a535a4f053 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:42:19 -0700 Subject: [PATCH 06/10] Revert "Add GestureRecognizer to Prevent Popup from closing when tapped inside" This reverts commit d17876eee22278ff84028694ce45ff5ce90934fb. --- .../Extensions/PopupExtensionsTests.cs | 8 +-- .../Services/PopupServiceTests.cs | 2 +- .../Views/Popup/PopupPageTests.cs | 2 +- .../Views/Popup/PopupPage.shared.cs | 51 ++++++++++--------- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs b/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs index 2c3aa10a62..45f0907a86 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs @@ -435,7 +435,7 @@ public void ShowPopupAsync_WithCustomOptions_AppliesOptions() var popupPage = (PopupPage)navigation.ModalStack[0]; var popupPageContent = popupPage.Content; - var border = popupPageContent.Border; + var border = (Border)popupPageContent.Children[0]; 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 = popupPageContent.Border; + var border = (Border)popupPageContent.Children[0]; 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 = popupPageContent.Border; + var border = (Border)popupPageContent.Children[0]; 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 = popupPageContent.Border; + var border = (Border)popupPageContent.Children[0]; var popup = (Popup)(border.Content ?? throw new InvalidCastException()); // Assert diff --git a/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs b/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs index 4c04c1023a..e4abe072d5 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 = popupPageLayout.Border; + var border = (Border)popupPageLayout.Children[0]; 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 4c3fe761bf..40c537be4f 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs @@ -472,7 +472,7 @@ public void PopupPage_ShouldRespectLayoutOptions() // Act var popupPage = new PopupPage(view, PopupOptions.Empty); - var border = popupPage.Content.Border; + var border = (Border)popupPage.Content.Children[0]; // Assert Assert.Equal(LayoutOptions.Start, border.VerticalOptions); diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs index bb87f6b756..202bf8b114 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,36 +175,42 @@ 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, ICommand tapOutsideOfPopupCommand) { Background = BackgroundColor = null; - Border = new Border + var border = new Border { BackgroundColor = popupContent.BackgroundColor ??= PopupDefaults.BackgroundColor, Content = popupContent }; - Border.GestureRecognizers.Add(new TapGestureRecognizer()); // Blocks `tapOutsideOfPopupCommand` from closing the Popup when the content is tapped + + var backgroundGrid = new BoxView + { + BackgroundColor = Colors.Transparent, + }; + + backgroundGrid.GestureRecognizers.Add(new TapGestureRecognizer { Command = tapOutsideOfPopupCommand }); + + Children.Add(backgroundGrid); // 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); + 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); // 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); + 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); - Children.Add(Border); + Children.Add(border); } - public Border Border { get; } - sealed partial class BorderStrokeThicknessConverter : BaseConverterOneWay { public override double DefaultConvertReturnValue { get; set; } = PopupOptionsDefaults.BorderStrokeThickness; From da52c7f37690a140183069ba37efb6fd668ace17 Mon Sep 17 00:00:00 2001 From: Shaun Lawrence <17139988+bijington@users.noreply.github.com> Date: Wed, 18 Jun 2025 19:40:12 +0100 Subject: [PATCH 07/10] Fix unit tests --- .../Extensions/PopupExtensionsTests.cs | 25 +++++++++-------- .../Services/PopupServiceTests.cs | 2 +- .../Views/Popup/PopupPageTests.cs | 28 +++++++++++++------ 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs b/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs index 45f0907a86..6bd4ac27a1 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)(((Border)popupPage.Content.Children.Last()).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.Children.OfType().Single(); 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.Children.OfType().Single(); 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.Children.OfType().Single(); 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.Children.OfType().Single(); 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..d32d18faf9 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.Children.OfType().Single(); 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..f4c9eb774b 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.Children.OfType().Single(); // 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) From d429c5029ade00228956717d72809df0937f2f12 Mon Sep 17 00:00:00 2001 From: Shaun Lawrence <17139988+bijington@users.noreply.github.com> Date: Wed, 18 Jun 2025 20:08:37 +0100 Subject: [PATCH 08/10] Remove the coalescing assignment that won't be invoked --- src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs index 202bf8b114..10fbf8edf6 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs @@ -49,7 +49,7 @@ public PopupPage(Popup popup, IPopupOptions popupOptions) }, () => popupOptions.CanBeDismissedByTappingOutsideOfPopup); // 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); + base.Content = new PopupPageLayout(popup, popupOptions, tapOutsideOfPopupCommand); if (popupOptions is BindableObject bindablePopupOptions) { From 14871d6507378ed1b1e200d31c4295c206e66b17 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:34:39 -0700 Subject: [PATCH 09/10] Add `in`, Rename to `tappableBackground `, Move Border Initialization after `tappableBackground ` code --- .../Views/Popup/PopupPage.shared.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs index 10fbf8edf6..730d770f97 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs @@ -47,7 +47,7 @@ public PopupPage(Popup popup, IPopupOptions popupOptions) popupOptions.OnTappingOutsideOfPopup?.Invoke(); await CloseAsync(new PopupResult(true)); }, () => popupOptions.CanBeDismissedByTappingOutsideOfPopup); - + // 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); @@ -103,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 @@ -111,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; } @@ -175,24 +175,24 @@ void IQueryAttributable.ApplyQueryAttributes(IDictionary query) internal sealed partial class PopupPageLayout : Grid { - public PopupPageLayout(in Popup popupContent, in IPopupOptions options, ICommand tapOutsideOfPopupCommand) + public PopupPageLayout(in Popup popupContent, in IPopupOptions options, in ICommand tapOutsideOfPopupCommand) { Background = BackgroundColor = null; + 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 + var border = new Border { BackgroundColor = popupContent.BackgroundColor ??= PopupDefaults.BackgroundColor, Content = popupContent }; - - var backgroundGrid = new BoxView - { - BackgroundColor = Colors.Transparent, - }; - - backgroundGrid.GestureRecognizers.Add(new TapGestureRecognizer { Command = tapOutsideOfPopupCommand }); - - Children.Add(backgroundGrid); // Bind `Popup` values through to Border using OneWay Bindings border.SetBinding(Border.MarginProperty, static (Popup popup) => popup.Margin, source: popupContent, mode: BindingMode.OneWay); From fd619fb51ace6ed711b113efe04cfc0bb4e54e4d Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:45:51 -0700 Subject: [PATCH 10/10] Add `public Border PopupBorder { get; }` and simplify unit tests --- .../Extensions/PopupExtensionsTests.cs | 10 +++---- .../Services/PopupServiceTests.cs | 2 +- .../Views/Popup/PopupPageTests.cs | 2 +- .../Views/Popup/PopupPage.shared.cs | 26 ++++++++++--------- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs b/src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs index 6bd4ac27a1..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.Last()).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 = popupPageContent.Children.OfType().Single(); + 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 = popupPageContent.Children.OfType().Single(); + 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 = popupPageContent.Children.OfType().Single(); + 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 = popupPageContent.Children.OfType().Single(); + var border = popupPageContent.PopupBorder; var popup = (Popup)(border.Content ?? throw new InvalidCastException()); // Assert diff --git a/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs b/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs index d32d18faf9..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 = popupPageLayout.Children.OfType().Single(); + 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 f4c9eb774b..1efd44b9f3 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs @@ -481,7 +481,7 @@ public void PopupPage_ShouldRespectLayoutOptions() // Act var popupPage = new PopupPage(view, PopupOptions.Empty); - var border = popupPage.Content.Children.OfType().Single(); + var border = popupPage.Content.PopupBorder; // Assert Assert.Equal(LayoutOptions.Start, border.VerticalOptions); diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs index 730d770f97..13cacf49b0 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs @@ -188,29 +188,31 @@ public PopupPageLayout(in Popup popupContent, in IPopupOptions options, in IComm 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 - var border = new Border + 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;