diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs index e403a8e4c08e..d594eedbd557 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs @@ -233,9 +233,13 @@ protected async virtual void OnNavigateBack() { try { - // Call OnBackButtonPressed to allow the page to intercept navigation - if (Page?.SendBackButtonPressed() == true) + // Route through Shell.OnBackButtonPressed so that Shell subclass overrides + // are invoked consistently for both the navigation bar back button and the + // hardware/system back button (fixes dotnet/maui#9095). + if (_shell?.SendBackButtonPressed() == true) + { return; + } await Page.Navigation.PopAsync(); } diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue9095.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue9095.cs new file mode 100644 index 000000000000..e471d5bdf016 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue9095.cs @@ -0,0 +1,162 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 9095, + "Shell toolbar back button doesn't fire Shell.OnBackButtonPressed on Android and iOS", + PlatformAffected.Android)] +public class Issue9095 : TestShell +{ + internal static bool BackButtonPressedCalledReturnFalse; + internal static bool ContentPageBackButtonPressedCalledReturnFalse; + + protected override void Init() + { + Routing.RegisterRoute(nameof(Issue9095SecondPage), typeof(Issue9095SecondPage)); + Routing.RegisterRoute(nameof(Issue9095ReturnFalsePage), typeof(Issue9095ReturnFalsePage)); + AddContentPage(new Issue9095RootPage()); + } + + protected override bool OnBackButtonPressed() + { + // Set static flag to prove Shell.OnBackButtonPressed was called + BackButtonPressedCalledReturnFalse = true; + + if (CurrentPage is Issue9095SecondPage secondPage) + { + secondPage.UpdateStatus("OnBackButtonPressed Called"); + } + + // Delegate to base which calls ContentPage.OnBackButtonPressed internally. + // If ContentPage returns true → base returns true (navigation blocked). + // If ContentPage returns false → base pops the stack (navigation proceeds). + return base.OnBackButtonPressed(); + } + + + public class Issue9095RootPage : ContentPage + { + readonly Label _returnFalseStatusLabel; + readonly Label _contentPageReturnFalseStatusLabel; + + public Issue9095RootPage() + { + Title = "HomePage"; + BackButtonPressedCalledReturnFalse = false; + ContentPageBackButtonPressedCalledReturnFalse = false; + + var navigateButton = new Button + { + Text = "Go to Second Page", + AutomationId = "NavigateButton" + }; + + navigateButton.Clicked += async (s, e) => + await Shell.Current.GoToAsync(nameof(Issue9095SecondPage)); + + var navigateReturnFalseButton = new Button + { + Text = "Go to Return False Page", + AutomationId = "NavigateReturnFalseButton" + }; + + navigateReturnFalseButton.Clicked += async (s, e) => + await Shell.Current.GoToAsync(nameof(Issue9095ReturnFalsePage)); + + _returnFalseStatusLabel = new Label + { + Text = "Waiting", + AutomationId = "ReturnFalseStatusLabel" + }; + + _contentPageReturnFalseStatusLabel = new Label + { + Text = "Waiting", + AutomationId = "ContentPageReturnFalseStatusLabel" + }; + + Content = new VerticalStackLayout + { + Children = { navigateButton, navigateReturnFalseButton, _returnFalseStatusLabel, _contentPageReturnFalseStatusLabel } + }; + } + + protected override void OnAppearing() + { + base.OnAppearing(); + if (BackButtonPressedCalledReturnFalse) + { + _returnFalseStatusLabel.Text = "OnBackButtonPressed Called And Returned False"; + BackButtonPressedCalledReturnFalse = false; + } + if (ContentPageBackButtonPressedCalledReturnFalse) + { + _contentPageReturnFalseStatusLabel.Text = "ContentPage OnBackButtonPressed Called And Returned False"; + ContentPageBackButtonPressedCalledReturnFalse = false; + } + } + } + + public class Issue9095SecondPage : ContentPage + { + readonly Label _statusLabel; + readonly Label _contentPageStatusLabel; + + public Issue9095SecondPage() + { + Title = "Second Page"; + + _statusLabel = new Label + { + Text = "OnBackButtonPressed Not Called", + AutomationId = "BackButtonPressedLabel" + }; + + _contentPageStatusLabel = new Label + { + Text = "ContentPage OnBackButtonPressed Not Called", + AutomationId = "ContentPageBackButtonLabel" + }; + + Content = new VerticalStackLayout + { + Children = { _statusLabel, _contentPageStatusLabel } + }; + } + + public void UpdateStatus(string text) + { + _statusLabel.Text = text; + } + + protected override bool OnBackButtonPressed() + { + _contentPageStatusLabel.Text = "ContentPage OnBackButtonPressed Called"; + return true; + } + } + + public class Issue9095ReturnFalsePage : ContentPage + { + public Issue9095ReturnFalsePage() + { + Title = "Return False Page"; + + Content = new VerticalStackLayout + { + Children = + { + new Label + { + Text = "Press back to test OnBackButtonPressed returning false", + AutomationId = "ReturnFalsePageLabel" + } + } + }; + } + + protected override bool OnBackButtonPressed() + { + ContentPageBackButtonPressedCalledReturnFalse = true; + return false; + } + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue9095.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue9095.cs new file mode 100644 index 000000000000..4b3e0c1ac8f9 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue9095.cs @@ -0,0 +1,83 @@ +#if ANDROID // iOS/Mac fix pending in https://github.com/dotnet/maui/pull/35072 +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue9095 : _IssuesUITest +{ + public Issue9095(TestDevice device) : base(device) { } + + public override string Issue => "Shell toolbar back button doesn't fire Shell.OnBackButtonPressed on Android and iOS"; + + protected override bool ResetAfterEachTest => true; + + [Test] + [Category(UITestCategories.Shell)] + public void ShellOnBackButtonPressedShouldBeInvokedWhenPressingNavigationBarBackButton() + { + // Navigate to the second page + App.WaitForElement("NavigateButton"); + App.Tap("NavigateButton"); + + // Wait for the second page to appear + App.WaitForElement("BackButtonPressedLabel"); + + // Verify the initial state + var initialText = App.FindElement("BackButtonPressedLabel").GetText(); + Assert.That(initialText, Is.EqualTo("OnBackButtonPressed Not Called"), + "Label should show 'Not Called' before pressing back."); + + var initialContentPageText = App.FindElement("ContentPageBackButtonLabel").GetText(); + Assert.That(initialContentPageText, Is.EqualTo("ContentPage OnBackButtonPressed Not Called"), + "ContentPage label should show 'Not Called' before pressing back."); + + // Tap the Shell toolbar back button + if (App is AppiumIOSApp iosApp && HelperExtensions.IsIOS26OrHigher(iosApp)) + App.TapBackArrow(); // iOS 26+ doesn't show the previous page title in the back button + else + App.TapBackArrow(Device is TestDevice.iOS or TestDevice.Mac ? "HomePage" : ""); + + // Shell.OnBackButtonPressed should have been called, updating the label. + // The second page remains visible because Shell.OnBackButtonPressed returns true. + var updatedText = App.WaitForElement("BackButtonPressedLabel").GetText(); + Assert.That(updatedText, Is.EqualTo("OnBackButtonPressed Called"), + "Shell.OnBackButtonPressed should be invoked when pressing the Shell toolbar back button."); + + // ContentPage.OnBackButtonPressed should also have been called. + var contentPageText = App.FindElement("ContentPageBackButtonLabel").GetText(); + Assert.That(contentPageText, Is.EqualTo("ContentPage OnBackButtonPressed Called"), + "ContentPage.OnBackButtonPressed should be invoked when pressing the Shell toolbar back button."); + } + + [Test] + [Category(UITestCategories.Shell)] + public void ShellOnBackButtonPressedReturnFalseShouldNavigateBack() + { + // Navigate to the return-false page + App.WaitForElement("NavigateReturnFalseButton"); + App.Tap("NavigateReturnFalseButton"); + + // Wait for the return-false page to appear + App.WaitForElement("ReturnFalsePageLabel"); + + // Tap the Shell toolbar back button + if (App is AppiumIOSApp iosApp && HelperExtensions.IsIOS26OrHigher(iosApp)) + App.TapBackArrow(); + else + App.TapBackArrow(Device is TestDevice.iOS or TestDevice.Mac ? "HomePage" : ""); + + // Shell.OnBackButtonPressed returned false, so navigation should proceed back to root. + // The labels on the root page confirm both Shell and ContentPage OnBackButtonPressed were called. + App.WaitForElement("ReturnFalseStatusLabel"); + var shellStatusText = App.FindElement("ReturnFalseStatusLabel").GetText(); + Assert.That(shellStatusText, Is.EqualTo("OnBackButtonPressed Called And Returned False"), + "Shell.OnBackButtonPressed should have been called even when returning false."); + + var contentPageStatusText = App.FindElement("ContentPageReturnFalseStatusLabel").GetText(); + Assert.That(contentPageStatusText, Is.EqualTo("ContentPage OnBackButtonPressed Called And Returned False"), + "ContentPage.OnBackButtonPressed should have been called even when returning false."); + } +} +#endif \ No newline at end of file