Skip to content

Commit e9554ef

Browse files
Merge branch 'main' into feature/sl-improve-popup-unit-tests
2 parents d4613bb + 6133ad1 commit e9554ef

File tree

5 files changed

+65
-39
lines changed

5 files changed

+65
-39
lines changed

src/CommunityToolkit.Maui.UnitTests/CommunityToolkit.Maui.UnitTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<ItemGroup>
2222
<!--Fix vulnerabilities-->
2323
<PackageReference Include="System.Net.Http" Version="4.3.4" />
24+
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
2425
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
2526
</ItemGroup>
2627

src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ public void ShowPopupAsync_WithViewType_SetsCorrectDefaults()
192192
navigation.ShowPopup(label);
193193

194194
popupPage = (PopupPage)navigation.ModalStack[0];
195-
autogeneratedPopup = (Popup)(((Border)popupPage.Content.Children[0]).Content ?? throw new InvalidOperationException("Border Content cannot be null"));
195+
autogeneratedPopup = (Popup)(popupPage.Content.PopupBorder.Content ?? throw new InvalidOperationException("Border Content cannot be null"));
196196

197197
// Assert
198198
Assert.Equal(PopupDefaults.BackgroundColor, autogeneratedPopup.BackgroundColor);
@@ -435,7 +435,7 @@ public void ShowPopupAsync_WithCustomOptions_AppliesOptions()
435435

436436
var popupPage = (PopupPage)navigation.ModalStack[0];
437437
var popupPageContent = popupPage.Content;
438-
var border = (Border)popupPageContent.Children[0];
438+
var border = popupPageContent.PopupBorder;
439439
var popup = border.Content;
440440

441441
// Assert
@@ -507,7 +507,7 @@ public void ShowPopupAsync_Shell_WithCustomOptions_AppliesOptions()
507507

508508
var popupPage = (PopupPage)shellNavigation.ModalStack[0];
509509
var popupPageContent = popupPage.Content;
510-
var border = (Border)popupPageContent.Children[0];
510+
var border = popupPageContent.PopupBorder;
511511
var popup = border.Content;
512512

513513
// Assert
@@ -579,7 +579,7 @@ public void ShowPopupAsyncWithView_WithCustomOptions_AppliesOptions()
579579

580580
var popupPage = (PopupPage)navigation.ModalStack[0];
581581
var popupPageContent = popupPage.Content;
582-
var border = (Border)popupPageContent.Children[0];
582+
var border = popupPageContent.PopupBorder;
583583
var popup = (Popup)(border.Content ?? throw new InvalidCastException());
584584

585585
// Assert
@@ -660,7 +660,7 @@ public void ShowPopupAsyncWithView_Shell_WithCustomOptions_AppliesOptions()
660660

661661
var popupPage = (PopupPage)shellNavigation.ModalStack[0];
662662
var popupPageContent = popupPage.Content;
663-
var border = (Border)popupPageContent.Children[0];
663+
var border = popupPageContent.PopupBorder;
664664
var popup = (Popup)(border.Content ?? throw new InvalidCastException());
665665

666666
// Assert
@@ -1149,7 +1149,7 @@ public async Task ShowPopupAsync_ReferenceTypeShouldReturnNull_WhenPopupTapGestu
11491149
var popupPage = (PopupPage)navigation.ModalStack[0];
11501150
popupPage.PopupClosed += HandlePopupClosed;
11511151

1152-
var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
1152+
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
11531153
tapGestureRecognizer.Command?.Execute(null);
11541154

11551155
var popupClosedResult = await popupClosedTCS.Task;
@@ -1184,7 +1184,7 @@ public async Task ShowPopupAsync_Shell_ReferenceTypeShouldReturnNull_WhenPopupTa
11841184
var popupPage = (PopupPage)shellNavigation.ModalStack[0];
11851185
popupPage.PopupClosed += HandlePopupClosed;
11861186

1187-
var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
1187+
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
11881188
tapGestureRecognizer.Command?.Execute(null);
11891189

11901190
var popupClosedResult = await popupClosedTCS.Task;
@@ -1212,7 +1212,7 @@ public async Task ShowPopupAsync_NullableValueTypeShouldReturnResult_WhenPopupIs
12121212
var popupPage = (PopupPage)navigation.ModalStack[0];
12131213
popupPage.PopupClosed += HandlePopupClosed;
12141214

1215-
var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
1215+
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
12161216
tapGestureRecognizer.Command?.Execute(null);
12171217

12181218
var popupClosedResult = await popupClosedTCS.Task;
@@ -1247,7 +1247,7 @@ public async Task ShowPopupAsync_Shell_NullableValueTypeShouldReturnResult_WhenP
12471247
var popupPage = (PopupPage)shellNavigation.ModalStack[0];
12481248
popupPage.PopupClosed += HandlePopupClosed;
12491249

1250-
var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
1250+
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
12511251
tapGestureRecognizer.Command?.Execute(null);
12521252

12531253
var popupClosedResult = await popupClosedTCS.Task;
@@ -1275,7 +1275,7 @@ public async Task ShowPopupAsync_ValueTypeShouldReturnResult_WhenPopupIsClosedBy
12751275
var popupPage = (PopupPage)navigation.ModalStack[0];
12761276
popupPage.PopupClosed += HandlePopupClosed;
12771277

1278-
var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
1278+
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
12791279
tapGestureRecognizer.Command?.Execute(null);
12801280

12811281
var popupClosedResult = await popupClosedTCS.Task;
@@ -1311,7 +1311,7 @@ public async Task ShowPopupAsync_Shell_ValueTypeShouldReturnResult_WhenPopupIsCl
13111311
var popupPage = (PopupPage)shellNavigation.ModalStack[0];
13121312
popupPage.PopupClosed += HandlePopupClosed;
13131313

1314-
var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
1314+
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
13151315
tapGestureRecognizer.Command?.Execute(null);
13161316

13171317
var popupClosedResult = await popupClosedTCS.Task;
@@ -1459,6 +1459,9 @@ public async Task ShowPopupAsync_TaskShouldCompleteWhenCloseAsyncIsCalled()
14591459
Assert.Equal(expectedResult, popupResult.Result);
14601460
Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup);
14611461
}
1462+
1463+
static TapGestureRecognizer GetTapOutsideGestureRecognizer(PopupPage popupPage) =>
1464+
(TapGestureRecognizer)popupPage.Content.Children.OfType<BoxView>().Single().GestureRecognizers[0];
14621465
}
14631466

14641467
sealed class ViewWithIQueryAttributable : Button, IQueryAttributable

src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ public void ShowPopupAsync_WithCustomOptions_AppliesOptions()
164164

165165
var popupPage = (PopupPage)navigation.ModalStack[0];
166166
var popupPageLayout = popupPage.Content;
167-
var border = (Border)popupPageLayout.Children[0];
167+
var border = popupPageLayout.PopupBorder;
168168
var popup = border.Content;
169169

170170
// Assert

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

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -250,13 +250,22 @@ public void Constructor_WithViewAndPopupOptions_SetsCorrectProperties()
250250
Assert.Equal(UIModalPresentationStyle.OverFullScreen, popupPage.On<iOS>().ModalPresentationStyle());
251251

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

256257
// Verify PopupPageLayout structure
257258
var pageContent = popupPage.Content;
258-
Assert.Single(pageContent.Children);
259-
Assert.IsType<Border>(pageContent.Children[0]);
259+
Assert.Collection(
260+
pageContent.Children,
261+
first =>
262+
{
263+
first.Should().BeOfType<BoxView>();
264+
},
265+
second =>
266+
{
267+
second.Should().BeOfType<Border>();
268+
});
260269

261270
// Verify content binding context is set correctly
262271
Assert.Equal(view.BindingContext, pageContent.BindingContext);
@@ -316,7 +325,7 @@ public async Task TapGestureRecognizer_ShouldClosePopupWhenCanBeDismissedIsTrue(
316325

317326
var popupPage = new PopupPage(view, popupOptions);
318327

319-
var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
328+
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
320329
var command = tapGestureRecognizer.Command;
321330
Assert.NotNull(command);
322331

@@ -341,7 +350,7 @@ public void TapGestureRecognizer_ShouldNotExecuteWhenCanBeDismissedIsFalse()
341350
};
342351

343352
var popupPage = new PopupPage(view, popupOptions);
344-
var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
353+
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
345354
var command = tapGestureRecognizer.Command;
346355

347356
// Act & Assert
@@ -421,7 +430,7 @@ public void TappingOutside_ShouldNotClosePopup_WhenCanBeDismissedIsFalse()
421430
var popupPage = new PopupPage(view, popupOptions);
422431

423432
// Act
424-
var tapGestureRecognizer = (TapGestureRecognizer)popupPage.Content.GestureRecognizers[0];
433+
var tapGestureRecognizer = GetTapOutsideGestureRecognizer(popupPage);
425434
var command = tapGestureRecognizer.Command;
426435

427436
// Assert
@@ -472,12 +481,15 @@ public void PopupPage_ShouldRespectLayoutOptions()
472481

473482
// Act
474483
var popupPage = new PopupPage(view, PopupOptions.Empty);
475-
var border = (Border)popupPage.Content.Children[0];
484+
var border = popupPage.Content.PopupBorder;
476485

477486
// Assert
478487
Assert.Equal(LayoutOptions.Start, border.VerticalOptions);
479488
Assert.Equal(LayoutOptions.End, border.HorizontalOptions);
480489
}
490+
491+
static TapGestureRecognizer GetTapOutsideGestureRecognizer(PopupPage popupPage) =>
492+
(TapGestureRecognizer)popupPage.Content.Children.OfType<BoxView>().Single().GestureRecognizers[0];
481493

482494
// Helper class for testing protected methods
483495
sealed class TestablePopupPage(View view, IPopupOptions popupOptions) : PopupPage(view, popupOptions)

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

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.ComponentModel;
22
using System.Globalization;
3+
using System.Windows.Input;
34
using CommunityToolkit.Maui.Converters;
45
using CommunityToolkit.Maui.Core;
56
using CommunityToolkit.Maui.Extensions;
@@ -41,16 +42,14 @@ public PopupPage(Popup popup, IPopupOptions popupOptions)
4142
this.popup = popup;
4243
this.popupOptions = popupOptions;
4344

44-
// Only set the content if the parent constructor hasn't set the content already; don't override content if it already exists
45-
base.Content ??= new PopupPageLayout(popup, popupOptions);
46-
4745
tapOutsideOfPopupCommand = new Command(async () =>
4846
{
4947
popupOptions.OnTappingOutsideOfPopup?.Invoke();
5048
await CloseAsync(new PopupResult(true));
5149
}, () => popupOptions.CanBeDismissedByTappingOutsideOfPopup);
5250

53-
Content.GestureRecognizers.Add(new TapGestureRecognizer { Command = tapOutsideOfPopupCommand });
51+
// Only set the content if the parent constructor hasn't set the content already; don't override content if it already exists
52+
base.Content = new PopupPageLayout(popup, popupOptions, tapOutsideOfPopupCommand);
5453

5554
if (popupOptions is BindableObject bindablePopupOptions)
5655
{
@@ -104,15 +103,15 @@ public async Task CloseAsync(PopupResult result, CancellationToken token = defau
104103

105104
popupClosedEventManager.HandleEvent(this, result, nameof(PopupClosed));
106105
}
107-
106+
108107
protected override bool OnBackButtonPressed()
109108
{
110109
// Only close the Popup if PopupOptions.CanBeDismissedByTappingOutsideOfPopup is true
111110
if (popupOptions.CanBeDismissedByTappingOutsideOfPopup)
112111
{
113112
CloseAsync(new PopupResult(true), CancellationToken.None).SafeFireAndForget();
114113
}
115-
114+
116115
// Always return true to let the Android Operating System know that we are manually handling the Navigation request from the Android Back Button
117116
return true;
118117
}
@@ -176,33 +175,44 @@ void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
176175

177176
internal sealed partial class PopupPageLayout : Grid
178177
{
179-
public PopupPageLayout(in Popup popupContent, in IPopupOptions options)
178+
public PopupPageLayout(in Popup popupContent, in IPopupOptions options, in ICommand tapOutsideOfPopupCommand)
180179
{
181180
Background = BackgroundColor = null;
182181

183-
var border = new Border
182+
var tappableBackground = new BoxView
183+
{
184+
BackgroundColor = Colors.Transparent,
185+
HorizontalOptions = LayoutOptions.Fill,
186+
VerticalOptions = LayoutOptions.Fill
187+
};
188+
tappableBackground.GestureRecognizers.Add(new TapGestureRecognizer { Command = tapOutsideOfPopupCommand });
189+
Children.Add(tappableBackground); // Add the Tappable Background to the PopupPageLayout Grid before adding the Border to ensure the Border is displayed on top
190+
191+
PopupBorder = new Border
184192
{
185193
BackgroundColor = popupContent.BackgroundColor ??= PopupDefaults.BackgroundColor,
186194
Content = popupContent
187195
};
188196

189197
// Bind `Popup` values through to Border using OneWay Bindings
190-
border.SetBinding(Border.MarginProperty, static (Popup popup) => popup.Margin, source: popupContent, mode: BindingMode.OneWay);
191-
border.SetBinding(Border.PaddingProperty, static (Popup popup) => popup.Padding, source: popupContent, mode: BindingMode.OneWay);
192-
border.SetBinding(Border.BackgroundProperty, static (Popup popup) => popup.Background, source: popupContent, mode: BindingMode.OneWay);
193-
border.SetBinding(Border.BackgroundColorProperty, static (Popup popup) => popup.BackgroundColor, source: popupContent, mode: BindingMode.OneWay);
194-
border.SetBinding(Border.VerticalOptionsProperty, static (Popup popup) => popup.VerticalOptions, source: popupContent, mode: BindingMode.OneWay);
195-
border.SetBinding(Border.HorizontalOptionsProperty, static (Popup popup) => popup.HorizontalOptions, source: popupContent, mode: BindingMode.OneWay);
198+
PopupBorder.SetBinding(Border.MarginProperty, static (Popup popup) => popup.Margin, source: popupContent, mode: BindingMode.OneWay);
199+
PopupBorder.SetBinding(Border.PaddingProperty, static (Popup popup) => popup.Padding, source: popupContent, mode: BindingMode.OneWay);
200+
PopupBorder.SetBinding(Border.BackgroundProperty, static (Popup popup) => popup.Background, source: popupContent, mode: BindingMode.OneWay);
201+
PopupBorder.SetBinding(Border.BackgroundColorProperty, static (Popup popup) => popup.BackgroundColor, source: popupContent, mode: BindingMode.OneWay);
202+
PopupBorder.SetBinding(Border.VerticalOptionsProperty, static (Popup popup) => popup.VerticalOptions, source: popupContent, mode: BindingMode.OneWay);
203+
PopupBorder.SetBinding(Border.HorizontalOptionsProperty, static (Popup popup) => popup.HorizontalOptions, source: popupContent, mode: BindingMode.OneWay);
196204

197205
// Bind `PopupOptions` values through to Border using OneWay Bindings
198-
border.SetBinding(Border.ShadowProperty, static (IPopupOptions options) => options.Shadow, source: options, mode: BindingMode.OneWay);
199-
border.SetBinding(Border.StrokeProperty, static (IPopupOptions options) => options.Shape, source: options, converter: new BorderStrokeConverter(), mode: BindingMode.OneWay);
200-
border.SetBinding(Border.StrokeShapeProperty, static (IPopupOptions options) => options.Shape, source: options, mode: BindingMode.OneWay);
201-
border.SetBinding(Border.StrokeThicknessProperty, static (IPopupOptions options) => options.Shape, source: options, converter: new BorderStrokeThicknessConverter(), mode: BindingMode.OneWay);
206+
PopupBorder.SetBinding(Border.ShadowProperty, static (IPopupOptions options) => options.Shadow, source: options, mode: BindingMode.OneWay);
207+
PopupBorder.SetBinding(Border.StrokeProperty, static (IPopupOptions options) => options.Shape, source: options, converter: new BorderStrokeConverter(), mode: BindingMode.OneWay);
208+
PopupBorder.SetBinding(Border.StrokeShapeProperty, static (IPopupOptions options) => options.Shape, source: options, mode: BindingMode.OneWay);
209+
PopupBorder.SetBinding(Border.StrokeThicknessProperty, static (IPopupOptions options) => options.Shape, source: options, converter: new BorderStrokeThicknessConverter(), mode: BindingMode.OneWay);
202210

203-
Children.Add(border);
211+
Children.Add(PopupBorder);
204212
}
205213

214+
public Border PopupBorder { get; }
215+
206216
sealed partial class BorderStrokeThicknessConverter : BaseConverterOneWay<Shape?, double>
207217
{
208218
public override double DefaultConvertReturnValue { get; set; } = PopupOptionsDefaults.BorderStrokeThickness;

0 commit comments

Comments
 (0)