Browse Source

Ensure system back event is passed to child page once (#21246)

* ensure system back event is passed to child page once

* fix formatting in DrawerPage

* add tests

* add back default page handler for system back event, but only check OnSystemBackButtonPressed

* removed `OnSystemBackButtonPressed` checks in derived page classes

* nav page - send back event to current page before modal pages.

* Fix modal-first system back routing in NavigationPage

---------

Co-authored-by: Javier Suárez Ruiz <javiersuarezruiz@hotmail.com>
pull/21354/head
Emmanuel Hansen 5 months ago
committed by GitHub
parent
commit
593b5a2ff5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 13
      src/Avalonia.Controls/Page/CarouselPage.cs
  2. 13
      src/Avalonia.Controls/Page/DrawerPage.cs
  3. 12
      src/Avalonia.Controls/Page/NavigationPage.cs
  4. 17
      src/Avalonia.Controls/Page/Page.cs
  5. 32
      src/Avalonia.Controls/Page/TabbedPage.cs
  6. 32
      tests/Avalonia.Controls.UnitTests/CarouselPageTests.cs
  7. 75
      tests/Avalonia.Controls.UnitTests/DrawerPageTests.cs
  8. 37
      tests/Avalonia.Controls.UnitTests/NavigationPageTests.cs
  9. 40
      tests/Avalonia.Controls.UnitTests/PageNavigationHostTests.cs
  10. 46
      tests/Avalonia.Controls.UnitTests/TabbedPageTests.cs

13
src/Avalonia.Controls/Page/CarouselPage.cs

@ -55,6 +55,19 @@ namespace Avalonia.Controls
{
ItemsPanelProperty.OverrideDefaultValue<CarouselPage>(DefaultPanel);
FocusableProperty.OverrideDefaultValue<CarouselPage>(true);
PageNavigationSystemBackButtonPressedEvent.AddClassHandler<CarouselPage>((sender, eventArgs) =>
{
if (eventArgs.Handled)
return;
var pageEvent = new RoutedEventArgs(PageNavigationSystemBackButtonPressedEvent);
sender.CurrentPage?.RaiseEvent(pageEvent);
if (pageEvent.Handled)
{
eventArgs.Handled = true;
}
});
}
public CarouselPage()

13
src/Avalonia.Controls/Page/DrawerPage.cs

@ -295,6 +295,9 @@ namespace Avalonia.Controls
{
PageNavigationSystemBackButtonPressedEvent.AddClassHandler<DrawerPage>((sender, eventArgs) =>
{
if (eventArgs.Handled)
return;
if (sender.IsOpen
&& sender.DrawerBehavior != DrawerBehavior.Locked
&& sender.DrawerBehavior != DrawerBehavior.Disabled)
@ -302,6 +305,16 @@ namespace Avalonia.Controls
sender.IsOpen = false;
eventArgs.Handled = true;
}
else
{
var pageEvent = new RoutedEventArgs(PageNavigationSystemBackButtonPressedEvent);
sender.CurrentPage?.RaiseEvent(pageEvent);
if (pageEvent.Handled)
{
eventArgs.Handled = true;
}
}
});
}

12
src/Avalonia.Controls/Page/NavigationPage.cs

@ -234,6 +234,18 @@ namespace Avalonia.Controls
return;
}
if (sender.CurrentPage != null)
{
var forwarded = new RoutedEventArgs(PageNavigationSystemBackButtonPressedEvent);
sender.CurrentPage.RaiseEvent(forwarded);
if (forwarded.Handled)
eventArgs.Handled = true;
}
if (eventArgs.Handled)
return;
if (sender.StackDepth > 1)
{
eventArgs.Handled = true;

17
src/Avalonia.Controls/Page/Page.cs

@ -62,7 +62,7 @@ namespace Avalonia.Controls
public static readonly RoutedEvent<RoutedEventArgs> PageNavigationSystemBackButtonPressedEvent =
RoutedEvent.Register<Page, RoutedEventArgs>(
nameof(PageNavigationSystemBackButtonPressed),
RoutingStrategies.Bubble);
RoutingStrategies.Direct);
/// <summary>
/// Defines the <see cref="Navigation"/> property.
@ -75,18 +75,17 @@ namespace Avalonia.Controls
static Page()
{
PageNavigationSystemBackButtonPressedEvent.AddClassHandler<Page>((page, args) =>
AffectsMeasure<Page>(SafeAreaPaddingProperty);
PageNavigationSystemBackButtonPressedEvent.AddClassHandler<Page>((sender, eventArgs) =>
{
if (!args.Handled && page.OnSystemBackButtonPressed())
{
args.Handled = true;
if (eventArgs.Handled)
return;
}
page.CurrentPage?.RaiseEvent(args);
if (sender.OnSystemBackButtonPressed())
{
eventArgs.Handled = true;
}
});
AffectsMeasure<Page>(SafeAreaPaddingProperty);
}
/// <summary>

32
src/Avalonia.Controls/Page/TabbedPage.cs

@ -9,6 +9,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity;
using Avalonia.Threading;
namespace Avalonia.Controls
@ -87,6 +88,19 @@ namespace Avalonia.Controls
static TabbedPage()
{
FocusableProperty.OverrideDefaultValue<TabbedPage>(true);
PageNavigationSystemBackButtonPressedEvent.AddClassHandler<TabbedPage>((sender, eventArgs) =>
{
if (eventArgs.Handled)
return;
var forwarded = new RoutedEventArgs(PageNavigationSystemBackButtonPressedEvent);
sender.CurrentPage?.RaiseEvent(forwarded);
if (forwarded.Handled)
{
eventArgs.Handled = true;
}
});
}
/// <summary>
@ -289,13 +303,15 @@ namespace Avalonia.Controls
if (target < 0)
{
_ignoringDisabledSelection = true;
try { _tabControl.SelectedIndex = SelectedIndex; }
try
{ _tabControl.SelectedIndex = SelectedIndex; }
finally { _ignoringDisabledSelection = false; }
return;
}
_ignoringDisabledSelection = true;
try { _tabControl.SelectedIndex = target; }
try
{ _tabControl.SelectedIndex = target; }
finally { _ignoringDisabledSelection = false; }
if (target == SelectedIndex)
@ -658,12 +674,12 @@ namespace Avalonia.Controls
int delta = (e.SwipeDirection, isHorizontal, isRtl) switch
{
(SwipeDirection.Left, true, false) => +1,
(SwipeDirection.Right, true, false) => -1,
(SwipeDirection.Left, true, true) => -1,
(SwipeDirection.Right, true, true) => +1,
(SwipeDirection.Up, false, _) => -1,
(SwipeDirection.Down, false, _) => +1,
(SwipeDirection.Left, true, false) => +1,
(SwipeDirection.Right, true, false) => -1,
(SwipeDirection.Left, true, true) => -1,
(SwipeDirection.Right, true, true) => +1,
(SwipeDirection.Up, false, _) => -1,
(SwipeDirection.Down, false, _) => +1,
_ => 0
};

32
tests/Avalonia.Controls.UnitTests/CarouselPageTests.cs

@ -11,10 +11,11 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Threading;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
@ -542,6 +543,35 @@ public class CarouselPageTests
}
}
public class SystemBackButtonTests : ScopedTestBase
{
private static RoutedEventArgs RaiseBackButton(Page dp)
{
var args = new RoutedEventArgs(Page.PageNavigationSystemBackButtonPressedEvent);
dp.RaiseEvent(args);
return args;
}
[Fact]
public void Back_Event_Is_Forwarded_To_Content()
{
var cp = new CarouselPage();
var page = new ContentPage();
bool isRaised = false;
page.PageNavigationSystemBackButtonPressed += (s, e) =>
{
isRaised = true;
};
var root = new TestRoot { Child = cp };
((AvaloniaList<Page>)cp.Pages!).Add(page);
var args = RaiseBackButton(cp);
Assert.True(isRaised);
Assert.False(args.Handled);
}
}
public class LogicalChildrenTests : ScopedTestBase
{
[Fact]

75
tests/Avalonia.Controls.UnitTests/DrawerPageTests.cs

@ -1105,6 +1105,25 @@ public class DrawerPageTests
return args;
}
[Fact]
public void Back_Event_Is_Forwarded_To_Content()
{
var dp = new DrawerPage();
var page = new ContentPage();
bool isRaised = false;
page.PageNavigationSystemBackButtonPressed += (s, e) =>
{
isRaised = true;
};
var root = new TestRoot { Child = dp };
dp.CurrentPage = page;
var args = RaiseBackButton(dp);
Assert.True(isRaised);
Assert.False(args.Handled);
}
[Fact]
public void BackButton_ClosesOpenDrawer()
{
@ -1156,6 +1175,62 @@ public class DrawerPageTests
Assert.False(dp.IsOpen);
Assert.False(args.Handled);
}
[Fact]
public async Task BackButton_ForwardsThroughNavigationPageToModalBeforeCoveredPage()
{
var dp = new DrawerPage();
var nav = new NavigationPage();
var coveredPage = new BackHandlingPage { HandleBack = true };
var modal = new BackHandlingPage();
await nav.PushAsync(coveredPage);
await nav.PushModalAsync(modal);
dp.Content = nav;
var root = new TestRoot { Child = dp };
var args = RaiseBackButton(dp);
Assert.True(args.Handled);
Assert.Equal(0, coveredPage.BackButtonPressCount);
Assert.Equal(1, modal.BackButtonPressCount);
Assert.Empty(nav.ModalStack);
Assert.Same(coveredPage, nav.CurrentPage);
}
[Fact]
public async Task BackButton_ClosesOpenDrawerBeforeForwardingToNestedNavigationPage()
{
var dp = new DrawerPage { IsOpen = true };
var nav = new NavigationPage();
var coveredPage = new BackHandlingPage { HandleBack = true };
var modal = new BackHandlingPage();
await nav.PushAsync(coveredPage);
await nav.PushModalAsync(modal);
dp.Content = nav;
var root = new TestRoot { Child = dp };
var args = RaiseBackButton(dp);
Assert.True(args.Handled);
Assert.False(dp.IsOpen);
Assert.Equal(0, coveredPage.BackButtonPressCount);
Assert.Equal(0, modal.BackButtonPressCount);
Assert.Single(nav.ModalStack);
Assert.Same(modal, nav.ModalStack[0]);
}
private sealed class BackHandlingPage : ContentPage
{
public int BackButtonPressCount { get; private set; }
public bool HandleBack { get; set; }
protected override bool OnSystemBackButtonPressed()
{
BackButtonPressCount++;
return HandleBack;
}
}
}
public class IconTests : ScopedTestBase

37
tests/Avalonia.Controls.UnitTests/NavigationPageTests.cs

@ -1354,6 +1354,43 @@ public class NavigationPageTests
Assert.Same(modal, nav.ModalStack[0]);
}
[Fact]
public async Task BackButton_WithModal_DoesNotForwardToCoveredCurrentPage()
{
var nav = new NavigationPage();
var root = new BackHandlingPage { HandleBack = true };
var modal = new BackHandlingPage();
await nav.PushAsync(root);
await nav.PushModalAsync(modal);
var args = RaiseBackButton(nav);
Assert.True(args.Handled);
Assert.Equal(0, root.BackButtonPressCount);
Assert.Equal(1, modal.BackButtonPressCount);
Assert.Empty(nav.ModalStack);
Assert.Equal(1, nav.StackDepth);
Assert.Same(root, nav.CurrentPage);
}
[Fact]
public async Task BackButton_WithHandledModal_DoesNotForwardToCoveredCurrentPage()
{
var nav = new NavigationPage();
var root = new BackHandlingPage { HandleBack = true };
var modal = new BackHandlingPage { HandleBack = true };
await nav.PushAsync(root);
await nav.PushModalAsync(modal);
var args = RaiseBackButton(nav);
Assert.True(args.Handled);
Assert.Equal(0, root.BackButtonPressCount);
Assert.Equal(1, modal.BackButtonPressCount);
Assert.Single(nav.ModalStack);
Assert.Same(modal, nav.ModalStack[0]);
}
private sealed class BackHandlingPage : ContentPage
{
public int BackButtonPressCount { get; private set; }

40
tests/Avalonia.Controls.UnitTests/PageNavigationHostTests.cs

@ -1,3 +1,5 @@
using Avalonia.Collections;
using Avalonia.Interactivity;
using Avalonia.UnitTests;
using Xunit;
@ -163,4 +165,42 @@ public class PageNavigationHostTests
Assert.Same(second, host.Presenter!.Child);
}
}
public class SystemBackButtonTests : ScopedTestBase
{
[Fact]
public void BackRequested_ForwardsToNestedCurrentPageOnce()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var child = new ContentPage { Header = "Child" };
var parent = new CarouselPage
{
Pages = new AvaloniaList<Page> { child }
};
var host = new PageNavigationHost { Page = parent };
var window = new Window
{
Width = 400,
Height = 300,
Content = host
};
var raiseCount = 0;
child.PageNavigationSystemBackButtonPressed += (_, e) =>
{
raiseCount++;
e.Handled = true;
};
window.Show();
Assert.Same(child, parent.CurrentPage);
Assert.Same(parent, host.Presenter?.Child);
var args = new RoutedEventArgs(TopLevel.BackRequestedEvent);
window.RaiseEvent(args);
Assert.Equal(1, raiseCount);
Assert.True(args.Handled);
}
}
}

46
tests/Avalonia.Controls.UnitTests/TabbedPageTests.cs

@ -1423,6 +1423,52 @@ public class TabbedPageTests
}
}
public class SystemBackButtonTests : ScopedTestBase
{
private static RoutedEventArgs RaiseBackButton(Page dp)
{
var args = new RoutedEventArgs(Page.PageNavigationSystemBackButtonPressedEvent);
dp.RaiseEvent(args);
return args;
}
[Fact]
public void Back_Event_Is_Forwarded_To_Content()
{
var tp = new TestableTabbedPage();
var page1 = new ContentPage { Header = "Page1" };
var page2 = new ContentPage { Header = "Page2" };
var page3 = new ContentPage { Header = "Page3" };
var pages = new AvaloniaList<Page> { page1, page2, page3 };
tp.Pages = pages;
NotifyCollectionChangedEventArgs? received = null;
tp.PagesChanged += (_, e) => received = e;
bool isRaisedOnPage1 = false, isRaisedOnPage2 = false, isRaisedOnPage3 = false;
page1.PageNavigationSystemBackButtonPressed += (s, e) =>
{
isRaisedOnPage1 = true;
};
page2.PageNavigationSystemBackButtonPressed += (s, e) =>
{
isRaisedOnPage2 = true;
};
page3.PageNavigationSystemBackButtonPressed += (s, e) =>
{
isRaisedOnPage3 = true;
};
var root = new TestRoot { Child = tp };
tp.CallCommitSelection(1, page2);
var args = RaiseBackButton(tp);
Assert.False(isRaisedOnPage1);
Assert.True(isRaisedOnPage2);
Assert.False(isRaisedOnPage3);
Assert.False(args.Handled);
}
}
private sealed class TestableTabbedPage : TabbedPage
{
public void CallCommitSelection(int index, Page? page) => CommitSelection(index, page);

Loading…
Cancel
Save