diff --git a/src/Controls/src/Core/Shell/ShellSection.cs b/src/Controls/src/Core/Shell/ShellSection.cs index e17f4c12d855..21afc7668da3 100644 --- a/src/Controls/src/Core/Shell/ShellSection.cs +++ b/src/Controls/src/Core/Shell/ShellSection.cs @@ -844,10 +844,21 @@ protected virtual async Task OnPopToRootAsync(bool animated) RequestType = NavigationRequestType.PopToRoot }; - InvokeNavigationRequest(args); + PresentedPageDisappearing(); var oldStack = _navStack; _navStack = new List { null }; + // NOTE: + // We intentionally raise PresentedPageAppearing (and thus SendPageAppearing/OnAppearing) + // before issuing the platform navigation request and awaiting its completion. This matches + // the behavior used for single-level Pop navigation and keeps Shell lifecycle events + // consistent across navigation patterns. At this point the root page may not yet be + // visually presented by the native stack (the pop-to-root animation can still be in + // progress), but Shell-level state (e.g., tab bar visibility) must already be updated + // so the platform reads the correct value when it commits the native navigation. + PresentedPageAppearing(); + InvokeNavigationRequest(args); + if (args.Task != null) await args.Task; @@ -856,11 +867,14 @@ protected virtual async Task OnPopToRootAsync(bool animated) for (int i = 1; i < oldStack.Count; i++) { - oldStack[i].SendDisappearing(); + // RemovePage is called for all pages. SendDisappearing is only called for + // intermediate pages; the top page's disappearing event was already fired + // by PresentedPageDisappearing() above to avoid duplicate lifecycle events. + if (i < oldStack.Count - 1) + oldStack[i].SendDisappearing(); RemovePage(oldStack[i]); } - PresentedPageAppearing(); } protected virtual Task OnPushAsync(Page page, bool animated) diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/TabBarVisibilityAfterMultiLevelPopToRoot.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/TabBarVisibilityAfterMultiLevelPopToRoot.png new file mode 100644 index 000000000000..fd0ee9db07a1 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/TabBarVisibilityAfterMultiLevelPopToRoot.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue33351.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue33351.cs new file mode 100644 index 000000000000..41f32a354496 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue33351.cs @@ -0,0 +1,167 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 33351, "Changing Shell Tab Visibility when navigating back multiple pages ignores Shell Tab Visibility", PlatformAffected.All)] +public class Issue33351 : Shell +{ + public Issue33351() + { + Routing.RegisterRoute("Issue33351Page1", typeof(Issue33351Page1)); + Routing.RegisterRoute("Issue33351Page2", typeof(Issue33351Page2)); + + var tabBar = new TabBar + { + Items = + { + new MyTab + { + Title = "Tab 1", + ContentTemplate = new DataTemplate(() => new Issue33351MainPage()) + }, + new ShellContent + { + Title = "Tab 2", + ContentTemplate = new DataTemplate(() => new Issue33351SecondTabPage()) + } + } + }; + + Items.Add(tabBar); + } + + class MyTab : ShellContent + { + protected override void OnAppearing() + { + base.OnAppearing(); + + if (this.Parent != null) + Shell.SetTabBarIsVisible(this.Parent, true); + + Shell.Current.Navigating -= OnShellNavigating; + Shell.Current.Navigating += OnShellNavigating; + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + + Shell.Current.Navigating -= OnShellNavigating; + } + + void OnShellNavigating(object sender, ShellNavigatingEventArgs e) + { + if (this.Parent != null) + Shell.SetTabBarIsVisible(this.Parent, false); + } + } + + class Issue33351MainPage : ContentPage + { + public Issue33351MainPage() + { + Title = "Main Page"; + AutomationId = "RootPage"; + + var statusLabel = new Label + { + AutomationId = "TabBarVisibleLabel", + Text = "Tab Bar Visible" + }; + + var navigateButton = new Button + { + AutomationId = "PushPage1Button", + Text = "Go to Page 1", + HorizontalOptions = LayoutOptions.Fill + }; + + navigateButton.Clicked += async (s, e) => + { + await Shell.Current.GoToAsync("Issue33351Page1"); + }; + + Content = new VerticalStackLayout + { + Padding = new Thickness(20), + Spacing = 20, + Children = { statusLabel, navigateButton } + }; + } + } + + class Issue33351Page1 : ContentPage + { + public Issue33351Page1() + { + Title = "Page 1"; + + var statusLabel = new Label + { + AutomationId = "TabBarHiddenLabel", + Text = "Tab Bar Hidden" + }; + + var navigateButton = new Button + { + AutomationId = "PushPage2Button", + Text = "Go to Page 2", + HorizontalOptions = LayoutOptions.Fill + }; + + navigateButton.Clicked += async (s, e) => + { + await Shell.Current.GoToAsync("Issue33351Page2"); + }; + + Content = new VerticalStackLayout + { + Padding = new Thickness(20), + Spacing = 20, + Children = { statusLabel, navigateButton } + }; + } + } + + class Issue33351Page2 : ContentPage + { + public Issue33351Page2() + { + Title = "Page 2"; + AutomationId = "Page2"; + + var statusLabel = new Label + { + AutomationId = "TabBarHiddenLabel2", + Text = "Tab Bar Hidden" + }; + + var popToRootButton = new Button + { + AutomationId = "PopToRootButton", + Text = "Go Back (../..)", + HorizontalOptions = LayoutOptions.Fill + }; + + popToRootButton.Clicked += async (s, e) => + { + await Shell.Current.GoToAsync("../.."); + }; + + Content = new VerticalStackLayout + { + Padding = new Thickness(20), + Spacing = 20, + Children = { statusLabel, popToRootButton } + }; + } + } + + class Issue33351SecondTabPage : ContentPage + { + public Issue33351SecondTabPage() + { + Title = "Tab 2"; + Content = new Label { Text = "Tab 2" }; + } + } +} diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/TabBarVisibilityAfterMultiLevelPopToRoot.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/TabBarVisibilityAfterMultiLevelPopToRoot.png new file mode 100644 index 000000000000..cc5e93dbd5a4 Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/TabBarVisibilityAfterMultiLevelPopToRoot.png differ diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33351.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33351.cs new file mode 100644 index 000000000000..89196830f37a --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33351.cs @@ -0,0 +1,34 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue33351 : _IssuesUITest +{ + public Issue33351(TestDevice testDevice) : base(testDevice) + { + } + + public override string Issue => "Changing Shell Tab Visibility when navigating back multiple pages ignores Shell Tab Visibility"; + + [Test] + [Category(UITestCategories.Shell)] + public void TabBarVisibilityAfterMultiLevelPopToRoot() + { + App.WaitForElement("Tab 1"); + App.Tap("Tab 1"); + + App.WaitForElement("PushPage1Button"); + App.Tap("PushPage1Button"); + + App.WaitForElement("PushPage2Button"); + App.Tap("PushPage2Button"); + + App.WaitForElement("PopToRootButton"); + App.Tap("PopToRootButton"); + App.WaitForElement("TabBarVisibleLabel"); + + VerifyScreenshot(); + } +} diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/TabBarVisibilityAfterMultiLevelPopToRoot.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/TabBarVisibilityAfterMultiLevelPopToRoot.png new file mode 100644 index 000000000000..0dd0900859eb Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/TabBarVisibilityAfterMultiLevelPopToRoot.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/TabBarVisibilityAfterMultiLevelPopToRoot.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/TabBarVisibilityAfterMultiLevelPopToRoot.png new file mode 100644 index 000000000000..9395c36ba223 Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/TabBarVisibilityAfterMultiLevelPopToRoot.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/TabBarVisibilityAfterMultiLevelPopToRoot.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/TabBarVisibilityAfterMultiLevelPopToRoot.png new file mode 100644 index 000000000000..93f501b8ecf0 Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/TabBarVisibilityAfterMultiLevelPopToRoot.png differ