diff --git a/src/Controls/src/Core/Page/Page.cs b/src/Controls/src/Core/Page/Page.cs index 693a744288ff..2fe377b3396d 100644 --- a/src/Controls/src/Core/Page/Page.cs +++ b/src/Controls/src/Core/Page/Page.cs @@ -840,6 +840,7 @@ internal Toolbar Toolbar internal void SendNavigatedTo(NavigatedToEventArgs args) { + // Prevent duplicate OnNavigatedTo during a single navigation burst (fixes #23902). if (HasNavigatedTo) { return; @@ -848,7 +849,21 @@ internal void SendNavigatedTo(NavigatedToEventArgs args) HasNavigatedTo = true; NavigatedTo?.Invoke(this, args); OnNavigatedTo(args); - (this as IPageContainer)?.CurrentPage?.SendNavigatedTo(args); + + // Cascade to child page (e.g. TabbedPage → CurrentPage). + // On Pop, reset the child flag first — a prior tab change while a modal was open + // can leave it true, which would incorrectly block the pop-return (fixes #35756). + // PopToRoot is excluded: SendNavigatedFrom already resets all flags before PopToRoot cascades. + var containerChild = (this as IPageContainer)?.CurrentPage; + if (containerChild is not null) + { + if (args.NavigationType == NavigationType.Pop) + { + containerChild.HasNavigatedTo = false; + } + + containerChild.SendNavigatedTo(args); + } } internal void SendNavigatingFrom(NavigatingFromEventArgs args) diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue35756.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue35756.cs new file mode 100644 index 000000000000..fdc423ec865b --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue35756.cs @@ -0,0 +1,126 @@ +using Microsoft.Maui.Controls; + +namespace Maui.Controls.Sample.Issues +{ + [Issue(IssueTracker.Github, 35756, "OnNavigatedTo does not fire after PopModalAsync when tab was changed from inside the modal", PlatformAffected.All)] + public class Issue35756TabbedPage : TabbedPage + { + public Issue35756TabbedPage() + { + Title = "Issue35756"; + Children.Add(new Issue35756Tab1Page()); + Children.Add(new Issue35756Tab2Page(this)); + } + } + + public class Issue35756Tab1Page : ContentPage + { + readonly Label _navigatedToCountLabel; + int _navigatedToCount; + + public Issue35756Tab1Page() + { + Title = "Tab 1"; + _navigatedToCountLabel = new Label + { + AutomationId = "Tab1NavigatedToCount", + Text = "NavigatedTo count: 0" + }; + Content = new StackLayout + { + Padding = 20, + Children = + { + new Label { Text = "Tab 1", FontSize = 18, AutomationId = "Tab1Content" }, + _navigatedToCountLabel + } + }; + } + + protected override void OnNavigatedTo(NavigatedToEventArgs args) + { + base.OnNavigatedTo(args); + _navigatedToCount++; + _navigatedToCountLabel.Text = $"NavigatedTo count: {_navigatedToCount}"; + } + } + + public class Issue35756Tab2Page : ContentPage + { + readonly TabbedPage _tabbedPage; + + public Issue35756Tab2Page(TabbedPage tabbedPage) + { + Title = "Tab 2"; + _tabbedPage = tabbedPage; + + var pushModalButton = new Button + { + Text = "Push Modal", + AutomationId = "PushModalButton" + }; + pushModalButton.Clicked += OnPushModalClicked; + + Content = new StackLayout + { + Padding = 20, + Children = + { + new Label { Text = "Tab 2", FontSize = 18, AutomationId = "Tab2Content" }, + pushModalButton + } + }; + } + + async void OnPushModalClicked(object sender, EventArgs e) + { + await Navigation.PushModalAsync(new Issue35756ModalPage(_tabbedPage)); + } + } + + public class Issue35756ModalPage : ContentPage + { + readonly TabbedPage _tabbedPage; + + public Issue35756ModalPage(TabbedPage tabbedPage) + { + Title = "Modal"; + _tabbedPage = tabbedPage; + + var switchTabButton = new Button + { + Text = "Switch to Tab 1", + AutomationId = "SwitchToTab1Button" + }; + switchTabButton.Clicked += OnSwitchToTab1Clicked; + + var closeModalButton = new Button + { + Text = "Close Modal", + AutomationId = "CloseModalButton" + }; + closeModalButton.Clicked += OnCloseModalClicked; + + Content = new StackLayout + { + Padding = 20, + Children = + { + new Label { Text = "Modal Page", FontSize = 18, AutomationId = "ModalContent" }, + switchTabButton, + closeModalButton + } + }; + } + + void OnSwitchToTab1Clicked(object sender, EventArgs e) + { + _tabbedPage.CurrentPage = _tabbedPage.Children[0]; + } + + async void OnCloseModalClicked(object sender, EventArgs e) + { + await Navigation.PopModalAsync(); + } + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35756.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35756.cs new file mode 100644 index 000000000000..cb061d681f92 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35756.cs @@ -0,0 +1,32 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue35756 : _IssuesUITest +{ + public Issue35756(TestDevice device) : base(device) { } + + public override string Issue => "OnNavigatedTo does not fire after PopModalAsync when tab was changed from inside the modal"; + + [Test] + [Category(UITestCategories.TabbedPage)] + public void OnNavigatedToFiresAfterPopModalWhenTabChangedFromInsideModal() + { + App.WaitForElement("Tab1Content"); + Assert.That(App.WaitForElement("Tab1NavigatedToCount").GetText(), Is.EqualTo("NavigatedTo count: 1")); + + App.TapTab("Tab 2"); + App.WaitForElement("Tab2Content"); + App.Tap("PushModalButton"); + App.WaitForElement("ModalContent"); + + App.Tap("SwitchToTab1Button"); + + App.Tap("CloseModalButton"); + App.WaitForElement("Tab1Content"); + + Assert.That(App.WaitForElement("Tab1NavigatedToCount").GetText(), Is.EqualTo("NavigatedTo count: 3")); + } +}