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 @@ -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();
}
Expand Down
162 changes: 162 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue9095.cs
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +8 to +9

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;
Comment on lines +85 to +93
}
}
}

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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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" : "");

Comment on lines +36 to +41
// 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()
{
Comment on lines +54 to +57
// 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
Loading