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 @@ -21,6 +21,7 @@
<ItemGroup>
<!--Fix vulnerabilities-->
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<BoxView>().Single().GestureRecognizers[0];
}

sealed class ViewWithIQueryAttributable : Button, IQueryAttributable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 20 additions & 8 deletions src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,22 @@ public void Constructor_WithViewAndPopupOptions_SetsCorrectProperties()
Assert.Equal(UIModalPresentationStyle.OverFullScreen, popupPage.On<iOS>().ModalPresentationStyle());

// Verify content has tap gesture recognizer attached
Assert.Single(popupPage.Content.GestureRecognizers);
Assert.IsType<TapGestureRecognizer>(popupPage.Content.GestureRecognizers[0]);
var gestureRecognizers = popupPage.Content.Children.OfType<BoxView>().Single().GestureRecognizers;
Assert.Single(gestureRecognizers);
Assert.IsType<TapGestureRecognizer>(gestureRecognizers[0]);

// Verify PopupPageLayout structure
var pageContent = popupPage.Content;
Assert.Single(pageContent.Children);
Assert.IsType<Border>(pageContent.Children[0]);
Assert.Collection(
pageContent.Children,
first =>
{
first.Should().BeOfType<BoxView>();
},
second =>
{
second.Should().BeOfType<Border>();
});

// Verify content binding context is set correctly
Assert.Equal(view.BindingContext, pageContent.BindingContext);
Expand Down Expand Up @@ -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);

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<BoxView>().Single().GestureRecognizers[0];

// Helper class for testing protected methods
sealed class TestablePopupPage(View view, IPopupOptions popupOptions) : PopupPage(view, popupOptions)
Expand Down
48 changes: 29 additions & 19 deletions src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -104,15 +103,15 @@ 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
if (popupOptions.CanBeDismissedByTappingOutsideOfPopup)
{
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;
}
Expand Down Expand Up @@ -176,33 +175,44 @@ void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> 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<Shape?, double>
{
public override double DefaultConvertReturnValue { get; set; } = PopupOptionsDefaults.BorderStrokeThickness;
Expand Down
Loading