diff --git a/src/Controls/src/Core/Editor/Editor.Mapper.cs b/src/Controls/src/Core/Editor/Editor.Mapper.cs index 0258acedebea..e76abb40f447 100644 --- a/src/Controls/src/Core/Editor/Editor.Mapper.cs +++ b/src/Controls/src/Core/Editor/Editor.Mapper.cs @@ -30,6 +30,10 @@ public partial class Editor EditorHandler.Mapper.AppendToMapping(nameof(VisualElement.IsVisible), InputView.MapIsVisible); #endif +#if IOS || MACCATALYST + EditorHandler.Mapper.AppendToMapping(nameof(AutoSize), MapAutoSize); +#endif + #if ANDROID EditorHandler.CommandMapper.PrependToMapping(nameof(IEditor.Focus), InputView.MapFocus); #endif diff --git a/src/Controls/src/Core/Editor/Editor.iOS.cs b/src/Controls/src/Core/Editor/Editor.iOS.cs index 1430b10cb4dc..93b1319df8f3 100644 --- a/src/Controls/src/Core/Editor/Editor.iOS.cs +++ b/src/Controls/src/Core/Editor/Editor.iOS.cs @@ -3,6 +3,15 @@ namespace Microsoft.Maui.Controls { public partial class Editor { + // TODO: Make this public in .NET 11 + internal static void MapAutoSize(IEditorHandler handler, Editor editor) + { + if (handler.PlatformView is Microsoft.Maui.Platform.MauiTextView textView) + { + textView.AllowAutoGrowth = editor.AutoSize == EditorAutoSizeOption.TextChanges; + } + } + public static void MapText(EditorHandler handler, Editor editor) => MapText((IEditorHandler)handler, editor); diff --git a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.iOS.cs index 1a234254f127..a9326b70579e 100644 --- a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.iOS.cs @@ -64,6 +64,56 @@ Task GetPlatformIsVisible(EditorHandler editorHandler) }); } + [Fact] + [Description("Editor with AutoSize=TextChanges should continue to grow after a simulated rotation (width constraint change)")] + public async Task AutoSizeTextChangesEditorGrowsAfterRotation() + { + // Regression test for https://github.com/dotnet/maui/issues/35114 + // Verifies that AllowAutoGrowth remains true after a width constraint change + // (which is what happens internally during portrait↔landscape rotation). + SetupBuilder(); + + var editor = new Editor + { + AutoSize = EditorAutoSizeOption.TextChanges, + Text = "Line1\nLine2\nLine3", + WidthRequest = 200, + }; + + var layout = new VerticalStackLayout + { + WidthRequest = 200, + HorizontalOptions = LayoutOptions.Start, + VerticalOptions = LayoutOptions.Start, + Children = { editor } + }; + + await AttachAndRun(layout, async (_) => + { + var frame = editor.Frame; + await WaitForUIUpdate(frame, editor); + var heightBeforeRotation = editor.Height; + + // Simulate rotation: portrait → landscape (widen) → portrait (narrow) + frame = editor.Frame; + layout.WidthRequest = 400; + await WaitForUIUpdate(frame, editor); + + frame = editor.Frame; + layout.WidthRequest = 200; + await WaitForUIUpdate(frame, editor); + + // After simulated rotation, typing more text should still grow the editor + frame = editor.Frame; + editor.Text += "\nLine4\nLine5\nLine6"; + await WaitForUIUpdate(frame, editor); + + Assert.True(editor.Height > heightBeforeRotation, + $"Editor with AutoSize=TextChanges should still grow after rotation. Before: {heightBeforeRotation}, After: {editor.Height}"); + }); + } + + [Category(TestCategory.Editor)] public class PlaceholderTests : ControlsHandlerTestBase { diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue35114.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue35114.cs new file mode 100644 index 000000000000..cc37199e1f49 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue35114.cs @@ -0,0 +1,38 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 35114, "Editor can not be scrolled after rotating simulator", PlatformAffected.iOS)] +public class Issue35114 : ContentPage +{ + public Issue35114() + { + Title = "Issue 35114"; + + var slider = new Slider + { + AutomationId = "Slider", + Maximum = 300, + Minimum = 0 + }; + + var editor = new Editor + { + AutomationId = "TestEditor", + Text = "testing" + }; + + editor.BindingContext = slider; + editor.SetBinding(Editor.CharacterSpacingProperty, new Binding("Value")); + + Content = new VerticalStackLayout + { + Children = + { + new Label { Text = "1. Drag the slider to the right to increase character spacing on the Editor." }, + new Label { Text = "2. Rotate the device to landscape and back to portrait." }, + new Label { Text = "3. The test fails if the Editor grows to full content height after rotation (it should remain scrollable)." }, + slider, + editor + } + }; + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35114.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35114.cs new file mode 100644 index 000000000000..b225f400ae1a --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35114.cs @@ -0,0 +1,57 @@ +#if IOS || ANDROID // The SetOrientation method is only supported on mobile platforms. +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue35114 : _IssuesUITest +{ + public Issue35114(TestDevice device) : base(device) + { + } + + public override string Issue => "Editor can not be scrolled after rotating simulator"; + + [TearDown] + public void TearDown() + { + App.SetOrientationPortrait(); + } + + [Test] + [Category(UITestCategories.Editor)] + public void EditorCanBeScrolledAfterRotation() + { + // Step 1: Wait for slider and drag it to max + var sliderRect = App.WaitForElement("Slider").GetRect(); + App.DragCoordinates( + sliderRect.X + 5, + sliderRect.Y + sliderRect.Height / 2, + sliderRect.X + sliderRect.Width - 5, + sliderRect.Y + sliderRect.Height / 2); + App.WaitForElement("TestEditor"); + + // Step 2: Get editor height before rotation + var editorRectBefore = App.WaitForElement("TestEditor").GetRect(); + var heightBefore = editorRectBefore.Height; + + // Step 3: Rotate to landscape + App.SetOrientationLandscape(); + App.WaitForElement("TestEditor"); + + // Step 4: Rotate back to portrait + App.SetOrientationPortrait(); + // Allow time for layout to settle after rotation + Task.Delay(2000).Wait(); + App.WaitForElement("TestEditor"); + + // Step 5: Get editor height after rotation — should NOT grow + var editorRectAfter = App.WaitForElement("TestEditor").GetRect(); + var heightAfter = editorRectAfter.Height; + + Assert.That(heightAfter, Is.EqualTo(heightBefore).Within(1), + $"Editor height should not grow after rotation. Before: {heightBefore}, After: {heightAfter}"); + } +} +#endif diff --git a/src/Core/src/Handlers/Editor/EditorHandler.iOS.cs b/src/Core/src/Handlers/Editor/EditorHandler.iOS.cs index aaf1a2ec7eda..3bd314c01e9b 100644 --- a/src/Core/src/Handlers/Editor/EditorHandler.iOS.cs +++ b/src/Core/src/Handlers/Editor/EditorHandler.iOS.cs @@ -69,11 +69,38 @@ public override Size GetDesiredSize(double widthConstraint, double heightConstra if (double.IsInfinity(heightConstraint)) { - heightConstraint = sizeThatFits.Height; + var currentHeight = (double)PlatformView.Bounds.Height; + + // NOTE: Bounds and ContentSize reflect the pre-rotation frame at this point. + // The check is intentionally based on the previous layout state to detect + // scrollable Editors that should not grow after cache invalidation (#35114). + // When content overflows the frame and auto-growth is off, cap the height + // to the current frame height to preserve scrollability after rotation (#35114). + // Skip when AllowAutoGrowth is set (AutoSize=TextChanges) — Editor should grow freely. + if (!PlatformView.AllowAutoGrowth + && currentHeight > 0 + && PlatformView.ContentSize.Height > currentHeight) + { + heightConstraint = currentHeight; + } + else + { + heightConstraint = sizeThatFits.Height; + } } } - return base.GetDesiredSize(widthConstraint, heightConstraint); + var result = base.GetDesiredSize(widthConstraint, heightConstraint); + + // Cap applies even for finite constraints: UITextView.SizeThatFits (UIScrollView subclass) + // ignores the height argument and always returns full content height. + // Capping here ensures GetDesiredSize honours the caller's constraint. + if (result.Height > heightConstraint) + { + return new Size(result.Width, heightConstraint); + } + + return result; } public static void MapText(IEditorHandler handler, IEditor editor) diff --git a/src/Core/src/Platform/iOS/MauiTextView.cs b/src/Core/src/Platform/iOS/MauiTextView.cs index abd5be6475b1..66cba191fd55 100644 --- a/src/Core/src/Platform/iOS/MauiTextView.cs +++ b/src/Core/src/Platform/iOS/MauiTextView.cs @@ -68,6 +68,13 @@ public UIColor? PlaceholderTextColor public TextAlignment VerticalTextAlignment { get; set; } + /// + /// When true, the Editor is allowed to grow beyond its current frame to fit content + /// (AutoSize=TextChanges mode). When false, GetDesiredSize may cap the height to + /// preserve scrollability. + /// + internal bool AllowAutoGrowth { get; set; } + public override string? Text { get => base.Text;