diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue32016.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue32016.cs new file mode 100644 index 000000000000..c47d8556c6fc --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue32016.cs @@ -0,0 +1,15 @@ +namespace Maui.Controls.Sample.Issues +{ + [Issue(IssueTracker.Github, 32016, "iOS 26 MaxLength not enforced on Entry", PlatformAffected.iOS)] + public class Issue32016 : ContentPage + { + public Issue32016() + { + Content = new Entry() + { + AutomationId = "TestEntry", + MaxLength = 10, + }; + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32016.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32016.cs new file mode 100644 index 000000000000..d96b91a80930 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32016.cs @@ -0,0 +1,26 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue32016 : _IssuesUITest +{ + public override string Issue => "iOS 26 MaxLength not enforced on Entry"; + + public Issue32016(TestDevice device) : base(device) { } + + [Test] + [Category(UITestCategories.Entry)] + public void EntryMaxLengthEnforcedOnIOS26() + { + App.WaitForElement("TestEntry"); + + // Type characters up to MaxLength + App.Tap("TestEntry"); + App.EnterText("TestEntry", "1234567890x"); + + var text = App.FindElement("TestEntry").GetText(); + Assert.That(text, Is.EqualTo("1234567890"), "Text should be '1234567890' - the 'x' should be blocked by MaxLength"); + } +} \ No newline at end of file diff --git a/src/Core/src/Handlers/Entry/EntryHandler.iOS.cs b/src/Core/src/Handlers/Entry/EntryHandler.iOS.cs index e6e2030716c8..62ce3cca368c 100644 --- a/src/Core/src/Handlers/Entry/EntryHandler.iOS.cs +++ b/src/Core/src/Handlers/Entry/EntryHandler.iOS.cs @@ -136,7 +136,14 @@ public void Connect(IEntry virtualView, MauiTextField platformView) platformView.EditingChanged += OnEditingChanged; platformView.EditingDidEnd += OnEditingEnded; platformView.TextPropertySet += OnTextPropertySet; - platformView.ShouldChangeCharacters += OnShouldChangeCharacters; + if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)) + { + platformView.ShouldChangeCharactersInRanges += ShouldChangeCharactersInRanges; + } + else + { + platformView.ShouldChangeCharacters += OnShouldChangeCharacters; + } } public void Disconnect(MauiTextField platformView) @@ -148,7 +155,14 @@ public void Disconnect(MauiTextField platformView) platformView.EditingChanged -= OnEditingChanged; platformView.EditingDidEnd -= OnEditingEnded; platformView.TextPropertySet -= OnTextPropertySet; - platformView.ShouldChangeCharacters -= OnShouldChangeCharacters; + if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)) + { + platformView.ShouldChangeCharactersInRanges -= ShouldChangeCharactersInRanges; + } + else + { + platformView.ShouldChangeCharacters -= OnShouldChangeCharacters; + } if (_set) platformView.SelectionChanged -= OnSelectionChanged; @@ -213,6 +227,56 @@ void OnTextPropertySet(object? sender, EventArgs e) } } + bool ShouldChangeCharactersInRanges(UITextField textField, NSValue[] ranges, string replacementString) + { + if (ranges == null || ranges.Length == 0) + return true; + + var maxLength = VirtualView?.MaxLength ?? -1; + if (maxLength < 0) + return true; + + // Handle null replacement string defensively + replacementString ??= string.Empty; + + var currentText = textField.Text ?? string.Empty; + + // Copy and sort ranges (existing code is correct) + var count = ranges.Length; + var rangeArray = new NSRange[count]; + for (int i = 0; i < count; i++) + rangeArray[i] = ranges[i].RangeValue; + + Array.Sort(rangeArray, (a, b) => (int)(b.Location - a.Location)); + + // Simulate all range replacements (existing code is correct) + for (int i = 0; i < count; i++) + { + var range = rangeArray[i]; + var start = (int)range.Location; + var length = (int)range.Length; + + if (start < 0 || length < 0 || start > currentText.Length || start + length > currentText.Length) + return false; + + var before = start > 0 ? currentText.Substring(0, start) : string.Empty; + var afterIndex = start + length; + var after = afterIndex < currentText.Length ? currentText.Substring(afterIndex) : string.Empty; + currentText = before + replacementString + after; + } + + var shouldChange = currentText.Length <= maxLength; + + // Paste truncation feature (matches pre-iOS 26 behavior) + if (VirtualView is not null && !shouldChange && !string.IsNullOrWhiteSpace(replacementString) && + replacementString.Length >= maxLength) + { + VirtualView.Text = replacementString.AsSpan(0, maxLength).ToString(); + } + + return shouldChange; + } + bool OnShouldChangeCharacters(UITextField textField, NSRange range, string replacementString) => VirtualView?.TextWithinMaxLength(textField.Text, range, replacementString) ?? false; @@ -232,4 +296,4 @@ void OnSelectionChanged(object? sender, EventArgs e) } } } -} \ No newline at end of file +}