Repository navigation
Conversation
2 of 5 tasks
Contributor
|
🤖 Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-26005/docs/index.html |
mikernet
force-pushed
the
dev/mikernet/textbox-maxlength-programmatic
branch
from
November 3, 2026 23:05
ba9bbb6 to
be85699
Compare
Contributor
|
🤖 Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-26005/docs/index.html |
Contributor
|
Tick the box to add this pull request to the merge queue (same as
|
mikernet
added a commit
to Singulink/uno
that referenced
this pull request
Nov 14, 2026
Takes Uno's servicing/6.8 from 6951c2c up to 5254ace (42 pull requests). Conflicts, by area: IME and TextBox (unoplatform#25984, backport of unoplatform#25804; unoplatform#25952, backport of unoplatform#25619; unoplatform#25976, backport of unoplatform#25818) - Takes upstream's composition design (unoplatform#25804): the browser's input is the source of truth for the text, compositions included. This drops the composition parts of the unoplatform#24502 port (90746b8): its TypeScript composition handling, the StartIndex/TextChangePending event arguments, the IsReadOnly forwarding to the hidden input, and its Given_TextBox.BrowserSoftKeyboard tests. - Keeps the non-IME browser fixes on top of it, in the form their master pull requests have on unoplatform#25804: Enter raised once per press and tracked by key code (unoplatform#25174, with Given_TextBox.BrowserEnterKey from master in place of the Enter tests that lived in the dropped file), the iOS refocus on tap (unoplatform#25185), pastes without text and isIOS from BrowserInputHelper. - Moves the unoplatform#26005 MaxLength fixes onto unoplatform#25804's native input path, as master has them: a refused browser edit keeps the TextBox's selection, TextBoxView.UpdateTextFromNative reports whether the edit went in, and the IME keeps its range on the applied text (master be85699, which d226897 had ported onto the unoplatform#24502 IME). Adds that commit's browser and platform-applied IME tests. - TextBox.pointers.skia.cs: upstream's file (unoplatform#25619 iOS touch editing, unoplatform#25818, unoplatform#25804), with the unoplatform#25126 port (the hold acts when it starts, a touch right-tap keeps the caret) in the form master's unoplatform#25126 branch has it on unoplatform#25619, including focusing before the iOS hold timer acts and its iOS not-focusable test. Pointers (unoplatform#25976, backport of unoplatform#25818) - Tapped and RightTapped are raised once the PointerReleased bubbled (unoplatform#25818), with the unoplatform#25732 port's window coordinates (2-argument routed event args, RawPosition for ContextRequested), as master's unoplatform#25732 branch has them. - The release wrapper of unoplatform#25818 keeps the unoplatform#25126 port's StopContextMenuTimer at the start of the release. DependencyObject (unoplatform#26142, backport of unoplatform#26123) - Takes upstream, then the code of unoplatform#25839 (97a2f17), which replaces this branch's unoplatform#25102 ports (800247f, be41da6, fd40bf9, 7546379). DependencyObjectStore.Binder.cs, .alc.cs, Application.Alc.cs, ResourceDictionary.alc.cs and their tests now match unoplatform#25839; this drops 800247f's own SingleParentNonFE_Parent_Removes_Value_Then_Next_Parent_Starts_Clean test, which unoplatform#25839 doesn't have. Imaging (unoplatform#25597, unoplatform#25915, unoplatform#25911, unoplatform#25897) - Keeps this branch's side: it has the later forms of these fixes (unoplatform#25124, unoplatform#25559, unoplatform#25633 and unoplatform#26370 ports), where upstream has the 6.8-only unoplatform#25597 design and earlier snapshots. Upstream's shared download, IgnoreImageCache and failed-load fixes are covered by SharedImageDecode. Adds upstream's When_Source_Outlives_Brushes test. ToolTip (unoplatform#26098, backport of unoplatform#25137 and unoplatform#26096) - Keeps this branch's unoplatform#25137 port and adds unoplatform#26096 in master's form: the service releases only a ToolTip it opened, and leaves one the app opened during the show delay to the app. Keeps both sides' tests. Flyout (unoplatform#26062, backport of unoplatform#26060) - This branch had the same commits already; keeps the "Keep non-resizable flyouts in window" port and its tests. Also removes a helper pair in Given_AccessibleTextBox that both sides added.
mikernet
added a commit
to Singulink/uno
that referenced
this pull request
Nov 14, 2026
Port of unoplatform#26434 (for unoplatform#26432), commits faba3ed, 096daef, 8e2644f, b8d2189, edce6fa and 6939ef4, onto the unoplatform#25804 IME design this branch takes from servicing/6.8 (unoplatform#25984). - TextCompositionChanged for text the platform applied (WebAssembly, Skia Android) waits for the platform's text sync, and isn't raised for an update the TextBox refused. The Changed and no-commit Ended ranges are clamped to the text. Skia Android reports where its composing region starts. - A composition ends when the hidden input is replaced or detached. - An edit a BeforeTextChanging handler or a read-only TextBox refuses is written back to the input at once, ending the composition. Unlike master, this branch does the same for an edit MaxLength refuses or a TextChanging handler changes (see below). - The hidden input is read-only with the TextBox. - The caret goes back at once after a refused edit. 6.8 differences: - Master's TextBoxCore.IME.cs, .Input.cs and .cs changes go to TextBox.IME.skia.cs, TextBox.skia.cs and TextBox.cs; events are raised on the TextBox itself. There is no RichEditBox IME, partial commit or CompositionCanceled on 6.8, so those parts are left out. - Skia Android syncs through a new TextBox.UpdateTextFromNative, the 6.8 counterpart of master's IImeSessionHost.UpdateTextFromNative. It raises the held TextCompositionChanged, and compares the editable's \n line breaks as the TextBox's \r, as master's IsNativeEchoOfCurrentText does. - The read-only input follows TextBox.IsReadOnly through OnIsReadonlyChangedPartial; UpdateProperties, which only sets the enter key hint on 6.8, now also sets readOnly or re-enters the input. With this branch's port of unoplatform#26005 (be85699): - A refused platform sync puts the composition range back on the text that is there (unoplatform#26005), and raises TextCompositionChanged for that range only when it isn't the last range raised. The refused update's own range is never raised (unoplatform#26434). - While a composition holds text the TextBox refused past MaxLength (reconciled when it ends), the hidden input's selectionchange isn't reported: its offsets are in that text, not in the TextBox's. Without this, the input's caret replaced the one edce6fa restores at once. - ApplyNativeInput keeps unoplatform#26005's handling of a refused edit's pending selection. Like Android's own LengthFilter (and this branch before the catch-up), a composition edit the TextBox doesn't keep as the keyboard applied it (MaxLength, a TextChanging filter, as well as BeforeTextChanging and read-only) writes the TextBox's text back at once and ends the composition, on WebAssembly and on Skia Android. Master keeps a MaxLength or TextChanging refusal in the input until the composition ends, so a CJK conversion can bring it back within the limit. Gboard composes every Latin word, though, so the refused letters stayed in the input for the whole word: its suggestions were built on them, and Backspace deleted them before it reached the TextBox's text. A conversion past the limit is lost here, as with Android's LengthFilter. When_Browser_Ime_Composes_With_TextChanging_Filter (unoplatform#25804) and the two conversion tests of unoplatform#26434 now expect that, and When_Browser_Ime_Composes_Word_Past_MaxLength_Then_Input_Keeps_The_Limit covers the Gboard case. Adds tests for a refused Skia Android sync, \n line breaks in its sync, and a refused composition followed by another before the dispatcher runs.
mikernet
added a commit
to Singulink/uno
that referenced
this pull request
Nov 14, 2026
Port of the review round of unoplatform#26434 (commits 0e475cdb9e and 73279a2cf1 there) onto this branch's write-back. - Narrower rule: the input or the native editable takes the TextBox's text back at once, ending the composition, only when the TextBox refused the edit or kept text of another length (MaxLength, a length limit). A change that keeps the length (an upper-case TextChanging filter) waits for the end of the composition again, as unoplatform#25804 made it, so an IME can still convert it: When_Browser_Ime_Composes_With_TextChanging_Filter is back to servicing/6.8's version. TextBox.MustReturnTextToNative holds the rule, and compares without allocating. - Skia Android's EndBatchEdit no longer reads ActiveTextBox again after the sync, which runs app handlers that may move focus or clear it (a null reference in the IME callback): it writes back only to the TextBox it synced, and only after the IME changed the text. The write-back is one batch for the listeners, so a TextCompositionEnded handler that sets the text again is written after it. - The comments no longer describe this as what Android's LengthFilter does, and the remaining ones about the end-of-composition reconciliation say which edits it still covers. - When_Browser_Ime_Composes_Past_MaxLength and When_Browser_Ime_Composition_Coerced_Shorter_Than_Its_Start check the input right after the update, and that the browser's late compositionend changes nothing. TextBox.UpdateTextFromNative already kept the TextBox's caret on a refused sync (the applied check of unoplatform#26005), unlike master's; When_IME_Platform_Sync_Refused_Then_Caret_Stays covers it here too. 6.8 has no RichEditBox IME, so the RichEditBox part doesn't apply.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
GitHub Issue: closes #25997
PR Type:
🐞 Bugfix
What changed? 🚀
Before: a
TextBoxwithMaxLengthrefused any newTextlonger thanMaxLength, even one set from code, aBindingorx:Bind, and kept its previous text. An editor that reuses oneTextBoxfor several items could show the previous item's text for an item with a long value, and the next edit wrote that text into the current item. Once the text was longer thanMaxLength, the user also couldn't delete characters one by one, and Undo couldn't put the text back.PasswordBox.Passwordbehaved the same way.After:
MaxLengthlimits only user input, as on WinUI. The PR has three commits:1.
fix(textbox): Limit only user input to MaxLengthText, bindings,SelectedText,PasswordBox.Password, and text an app sets inside aTextChangingorSelectionChangedhandler while the user types.MaxLengthis refused, with noBeforeTextChanging, as before. A paste still inserts only what fits.MaxLength. Backspace from the browser or the Android keyboard in multi-line text works too.TextBoxalready held, as on WinUI.TextBoxorPasswordBoxautomation peer refuses a value longer thanMaxLengthand keeps the text, as WinUI's UI Automation does; a value that fits is applied like text set from code. On the Win32 host, the error reaches the client asUIA_E_INVALIDOPERATION, where WinUI returnsE_FAIL.MaxLengthapplies to it and it's reported as user input (anAutoSuggestBoxseesUserInput). When theTextBoxrefuses such input, the semantic element shows theTextBox's text again. A read-onlyTextBoxused to throw there, and a disabled one accepted the input; both now refuse it.setAccessibilityValue) is like UI Automation's set-value, so a value longer thanMaxLengthis refused. Replacing the selected text (setAccessibilitySelectedText, which dictation uses) is user input, like typing. When theTextBoxrefuses either, the native element shows its text again; aPasswordBoxgets no text there.MaxLength, as Android's own text fields do, and goes in as user input. A read-only or disabled box ignores it.TextBoxrefuses their text, it keeps its own caret instead of taking the native one. In the browser, the input path from fix(textbox): Fix IME and touch editing on Android browsers #25804 now selects only when the edit is applied, so the selection doesn't go to the refused caret and back, and raises noSelectionChanged.PasswordBoxwhose password is longer thanMaxLengthkeeps it when it gets focus: the native field puts its text back then, and that is no longer refused.2.
fix(textbox): Record undo steps only once appliedMaxLengthor aBeforeTextChanginghandler refuses no longer leaves an undo step. Before, Redo could then apply a refused suggestion pastMaxLength.MaxLengthor a handler all refuse no longer become an empty undo step that clears Redo, as on WinUI.ArgumentOutOfRangeException. When there is something to redo, the run is recorded first, which ends what Redo could put back, as typing does on WinUI. With nothing to redo, Redo leaves the typing run open, as before.Undo()fromKeyDown, no longer leaves a step behind for the next edit.Undo()that aTextChanginghandler runs in the middle of a deletion or a paste no longer makes that edit clear the history.3.
fix(textbox): Keep IME ranges on the applied textMaxLengthor aBeforeTextChanginghandler refuses an IME composition update, the composition keeps its range. Before a composition has put any text in, it has no text of its own: emptying, cancelling or committing an empty composition no longer deletes the selection it was going to replace, and the composition events report no length. Before, a refused first update followed by a cancel deleted the selection, also at exactlyMaxLength.TextCompositionChangedreports it a second time, andTextCompositionEndedreports that range instead of the length of the refused text.Which input counts as user input: typing and deleting, paste, input methods, spelling suggestions, native text views (the browser's hidden input, the Android and iOS keyboards), the WebAssembly semantic text box, macOS selected-text replacement, and autofill. UI Automation's set-value and macOS's set-value aren't user input: WinUI refuses an over-long value there, and Uno now does too. The WebAssembly semantic text box differs from them on purpose: it receives the browser's input events, which are the user's own keystrokes, IME and dictation, so it follows the typing rule. The WebAssembly, macOS and iOS changes belong here because these paths used to rely on
Textrefusing long values: without them, phone-keyboard input on WebAssembly would go pastMaxLength, and iOS would block Backspace in text that can now be longer thanMaxLength.The check now runs on the user-input path instead of in the
Textcoercion, so the coercion no longer needs to tell input from code. One line-break helper serves both, so the check compares the text as the coercion will store it.Cost: for text within
MaxLength, or with noMaxLength, the check returns at once. Only when a user edit would leave the text longer thanMaxLengthdoes it normalize the new text's line breaks and compare it with the old text once. Multi-line text with no\nnow skips the line-break pass of the coercion, which used to copy it every time.Not changed here:
CanRedois false as soon as typing starts after an Undo; Uno still reportstrueuntil the typing run ends.Intentional difference from WinUI: a cancelled edit keeps the undo history
When a
BeforeTextChanginghandler cancels an edit, WinUI clears the whole undo history, both Undo and Redo. I measured this for a cancelled Backspace, typing, paste and cut. Uno keeps the history, as it does today; the cancelled edit just adds no undo step. AMaxLengthrefusal keeps the history on both.I left this as it is because:
BeforeTextChangingis the usual way to filter input. In a digits-only box, a user who types12345and hits a letter by accident could no longer undo any of it.BeforeTextChangingis raised, so WinUI sets the old text back and clears the undo stack; a comment inTextBox.cppdescribes this. Uno raises the event before changing anything, so it has nothing to set back.WinUI does come out ahead in one edge case: a handler that refuses an Undo or Redo. Uno moves its place in the history before it applies the step and doesn't move back when the step is cancelled, so the step is used up and the next Undo goes back two steps. Nothing is corrupted, because each step restores its full recorded text. This PR doesn't change that. From its source, WinUI clears the history here too, but I didn't measure this case. Moving the place only once the step is applied would fix it without clearing anything.
If you'd prefer to match WinUI and clear the history on a cancelled edit, I can do that in a follow-up PR.
Validation:
MaxLengthbeing refused during a composition.TextBoxthrough its own APIs, called the WinUI recording app in the tests. It set text from code, typed through key messages posted to its own window, and set values through UI Automation from a second process. Values that can't be recorded on WinUI are marked in the tests: the native-view, input-method, autofill, accessibility-input, browser and WebAssembly semantic-input cases, and Shift and Ctrl key combinations.Given_TextBox_MaxLength, IME and browser-input tests inGiven_TextBox, and semantic-input tests inGiven_AccessibleTextBox.ReplaceWithSuggestionwas made internal so the tests could call it. All 51Given_TextBox_MaxLengthdesktop cases pass after it.Given_TextBox*,Given_PasswordBox,Given_ImeSessionCoordinator*,Given_AccessibleTextBox,Given_RichEditBox*andGiven_BrowserKeyboardInputSource, 1382 cases, 0 failures, without the clipboard tests.When_Touch_LongPress_Drag_Release_Does_Not_Open_Flyout_iOSfails, the same way as on master without these changes.TextBox, and deleting from over-long text and keeping the selection in the hidden input (the tests that call methods the old code doesn't have were left out). Against commit 2, the 2 browser tests of commit 3's composition ranges fail. At the head of this PR, theMaxLength, IME, browser-input andGiven_AccessibleTextBoxtests pass, with fix(textbox): Fix IME and touch editing on Android browsers #25804'sGiven_RichEditBox,Given_ImeSessionCoordinator*andGiven_BrowserKeyboardInputSourcetests: 220 cases, 1 skipped (mobile only). With the paste PR on top, 272 cases pass, the paste tests included. The composition tests replay Chromium's event order, as fix(textbox): Fix IME and touch editing on Android browsers #25804's do; no real IME or phone keyboard was run.MaxLengthis refused, and aPasswordBoxkeeps an over-long password when it gets focus.PR Checklist ✅
Screenshots Compare Test Runresults.Behavior change (described in the new migration entry): a value set from code that's longer than
MaxLengthis now kept whole, as on WinUI. Apps that relied onMaxLengthto refuse such values should check the length themselves before setting them. UI Automation now refuses an over-long value with an error instead of ignoring it.