diff --git a/src/Controls/src/Core/Platform/iOS/Extensions/FormattedStringExtensions.cs b/src/Controls/src/Core/Platform/iOS/Extensions/FormattedStringExtensions.cs index 31c75c13dc61..24bc609e4223 100644 --- a/src/Controls/src/Core/Platform/iOS/Extensions/FormattedStringExtensions.cs +++ b/src/Controls/src/Core/Platform/iOS/Extensions/FormattedStringExtensions.cs @@ -177,8 +177,11 @@ internal static void RecalculateSpanPositions(this UILabel control, Label elemen textStorage.AddLayoutManager(layoutManager); layoutManager.AddTextContainer(textContainer); - textContainer.Size = new(control.Bounds.Width, - control.Lines == 0 ? nfloat.MaxValue : control.Bounds.Height); + // On iOS 26+ with NavigationPage, UILabel.Bounds may still be {0,0,0,0} + // during ArrangeOverride. Use finalSize (MAUI's computed size) as fallback. + var containerWidth = control.Bounds.Width > 0 ? control.Bounds.Width : (nfloat)finalSize.Width; + var containerHeight = control.Bounds.Height > 0 ? control.Bounds.Height : (nfloat)finalSize.Height; + textContainer.Size = new(containerWidth, control.Lines == 0 ? nfloat.MaxValue : containerHeight); textStorage.SetString(attributedText); layoutManager.EnsureLayoutForTextContainer(textContainer); diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue34504.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue34504.cs new file mode 100644 index 000000000000..65551972b23c --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue34504.cs @@ -0,0 +1,142 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 34504, "[iOS] Span TapGestureRecognizer does not work on the second line of the span, if the span is wrapped to the next line", PlatformAffected.iOS)] +public class Issue34504 : NavigationPage +{ + public Issue34504() : base(new FirstPage()) { } + + // First page mirrors the sandbox MainPage — has the same span content so + // the layout system is exercised before navigating to the second page. + class FirstPage : ContentPage + { + public FirstPage() + { + void OnSpanTapped(object sender, TappedEventArgs e) + { + // no-op — first page just needs spans to exist + } + + var label = BuildSpanLabel(OnSpanTapped); + + var navigateButton = new Button + { + Text = "Navigate to Test Page", + AutomationId = "NavigateButton", + HorizontalOptions = LayoutOptions.Fill, + }; + navigateButton.Clicked += async (s, e) => + await Navigation.PushAsync(new SecondPage()); + + Content = new VerticalStackLayout + { + Padding = new Thickness(30, 0), + Spacing = 25, + Children = + { + new Label + { + Text = "Click the button below to navigate to the test page with wrapped span gestures.", + FontSize = 14, + TextColor = Colors.Gray, + }, + navigateButton, + new Border + { + StrokeThickness = 2, + Stroke = Colors.Black, + Padding = new Thickness(10), + Content = label, + }, + } + }; + } + } + + // Second page mirrors the sandbox TestPage — this is where the bug manifests on iOS 26+. + public class SecondPage : ContentPage + { + public SecondPage() + { + var statusLabel = new Label + { + AutomationId = "StatusLabel", + Text = "Tap status will appear here", + FontSize = 14, + TextColor = Colors.Black, + }; + + void OnSpanTapped(object sender, TappedEventArgs e) + { + statusLabel.Text = "Success"; + } + + var spanLabel = BuildSpanLabel(OnSpanTapped); + spanLabel.AutomationId = "SpanLabel"; + + Content = new ScrollView + { + Content = new VerticalStackLayout + { + Padding = new Thickness(30, 0), + Spacing = 25, + Children = + { + new Label + { + Text = "iOS TapGesture Issue Demonstration", + FontSize = 18, + FontAttributes = FontAttributes.Bold, + }, + new Label + { + Text = "Tap on the colored text below. On iOS, gestures on wrapped lines may not work.", + FontSize = 12, + TextColor = Colors.Gray, + }, + new Border + { + StrokeThickness = 2, + Stroke = Colors.Black, + Padding = new Thickness(10), + Content = spanLabel, + }, + statusLabel, + } + } + }; + } + } + + static Label BuildSpanLabel(EventHandler onTapped) + { + Span MakeSpan(string text, Color color) + { + var span = new Span + { + Text = text, + TextColor = color, + TextDecorations = TextDecorations.Underline, + }; + var tap = new TapGestureRecognizer(); + tap.Tapped += onTapped; + span.GestureRecognizers.Add(tap); + return span; + } + + var fs = new FormattedString(); + fs.Spans.Add(MakeSpan("Hello,This is a test. Hello,This is a test. Hello,This is a test.", Colors.Blue)); + fs.Spans.Add(MakeSpan("Hello,This is a test1. Hello,This is a test1. Hello,This is a test1.", Colors.Red)); + fs.Spans.Add(MakeSpan("Hello,This is a test2. Hello,This is a test2. Hello,This is a test2.", Colors.Green)); + fs.Spans.Add(MakeSpan("Hello,This is a test4. Hello,This is a test4. Hello,This is a test4.", Colors.Orange)); + fs.Spans.Add(MakeSpan("Hello,This is a test3. Hello,This is a test3. Hello,This is a test3.", Colors.Purple)); + fs.Spans.Add(new Span { Text = " World!", FontAttributes = FontAttributes.Bold }); + + return new Label + { + FormattedText = fs, + BackgroundColor = Colors.Transparent, + LineBreakMode = LineBreakMode.WordWrap, + MaximumWidthRequest = 300, + }; + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34504.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34504.cs new file mode 100644 index 000000000000..c4172fe46a88 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34504.cs @@ -0,0 +1,34 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue34504 : _IssuesUITest +{ + public Issue34504(TestDevice device) : base(device) { } + + public override string Issue => "[iOS] Span TapGestureRecognizer does not work on the second line of the span, if the span is wrapped to the next line"; + + [Test] + [Category(UITestCategories.Label)] + // The bug only manifests on iOS 26+ (not Android / Windows / MacCatalyst), + // but the test is safe to run on all platforms — it will simply pass on unaffected ones. + public void SpanTapGestureOnSecondLineShouldWork() + { + // Navigate to the second page — the bug only reproduces on a pushed page. + App.WaitForElement("NavigateButton"); + App.Tap("NavigateButton"); + + var labelRect = App.WaitForElement("SpanLabel").GetRect(); + + // Ensure the label wrapped to multiple lines; if it's single-line the tap + // cannot exercise the second-line bug and the test would be a false-positive. + Assert.That(labelRect.Height, Is.GreaterThan(40), "SpanLabel must be tall enough to indicate multi-line text before tapping the second line."); + + // Tap near the bottom of the label to hit the second wrapped line of a span. + App.TapCoordinates(labelRect.X + labelRect.Width / 2, labelRect.Y + labelRect.Height * 0.75f); + + Assert.That(App.WaitForElement("StatusLabel").GetText(), Is.EqualTo("Success")); + } +}