Repository navigation
Conversation
mikernet
force-pushed
the
dev/mikernet/textbox-paste-surrogates
branch
from
November 3, 2026 23:05
9c43b3f to
ac50ccf
Compare
mikernet
marked this pull request as ready for review
November 3, 2026 23:34
Contributor
|
🤖 Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-26006/docs/index.html |
Contributor
|
Tick the box to add this pull request to the merge queue (same as
|
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 #25998
Builds on #26005: it uses that PR's rule for when an edit counts as applied, its line-break helper, and its helper that applies input while keeping the undo history. Until that PR is merged, this one also shows its three commits.
PR Type:
🐞 Bugfix
What changed? 🚀
Pasting into a
TextBox, and cutting from it, now behave the way they do on WinUI in these cases. Each fix is its own commit:fix(textbox): Keep surrogate pairs whole in paste: when a paste doesn't fit underMaxLength, the shortened text no longer ends in half an emoji (a surrogate pair). A pair that doesn't fit is left out whole. Before, half of it was inserted: on 6.8.0-dev.222 that crashed text layout, and on master it left a broken character in the text. When the only room left is half a pair, nothing is inserted; with nothing selected, the text events don't fire either. As on WinUI, the text is shortened by code point, so a letter can still lose a combining mark that doesn't fit.fix(textbox): Keep text after a single-line paste: pasting several lines into a single-lineTextBoxinserts the pasted text's first line and keeps the text after the caret. Before, everything after the first line break was dropped, including the text that was already after the caret ("XY" + "a↵b" at the caret gave "Xa"; it now gives "XaY").fix(textbox): Count pasted line breaks as stored: in a multi-lineTextBox, a pasted\r\ncounts as one character againstMaxLength, because that's how theTextBoxstores it. Before, it counted as two, so less text fit. The paste uses the same line-break helper as theTextcoercion.fix(wasm): Ignore browser pastes without text: a browser paste with no text on the clipboard, such as an image, leaves aTextBox's text and selection alone, from the hidden input and, with accessibility on, from the semantic text element. Before, both deleted the selection. Such a paste no longer raisesPasteon aTextBox, where WinUI does (see "Not changed" below); aRichEditBoxstill gets itsPasteevent. Both browser listeners read the text through one helper.fix(textbox): Make cuts undoable: a cut is an undo step, and Undo puts the text back with its selection. Before, the cut setTextthe way code does, which cleared the undo history, so a cut couldn't be undone at all.fix(textbox): Record each applied paste and cut: a paste or a cut is its own undo step, also right after typing (before, the first Undo removed the typing and the paste together). It is recorded once it is applied, with theMaxLengthPR's rule, so a paste or cut thatMaxLengthor aBeforeTextChanginghandler refuses, or that changes nothing (no room left underMaxLength, nothing to paste and nothing selected), adds no undo step and keeps Redo. A paste that replaces a selection with the same text is still an undo step, as on WinUI. A cut with nothing selected only ends the typing run, as on WinUI.fix(textbox): Set caret after redone paste or cut: after Redo, the caret is after the pasted text, or where the cut text was. Before, it went to the start of the paste, or to where the cut selection had ended.Cost: for each paste, one line-break normalization of the pasted text; nothing when typing.
Not changed here:
Pasteevent is still raised after the paste, andHandleddoesn't cancel it (a separate issue). With this PR, a browser paste without text raises noPasteon aTextBox, like the other paste paths when the clipboard has no text; WinUI raises it.MaxLength: what a paste does there (delete the selection and insert nothing) comes from theMaxLengthPR.BeforeTextChanging,TextChangingandTextChangedwhen a paste replaces a selection with the same text, so a handler there can refuse it. Uno raises none, because theTextproperty doesn't change, and a handler can't refuse it.Validation:
MaxLengthbeing refused during a composition.TextBoxthrough its own APIs, called the WinUI recording app in the tests. It set the clipboard and calledPasteFromClipboard(), typed through key messages posted to its own window, and calledUndo,RedoandCutSelectionToClipboard. Tests say where a value wasn't recorded on WinUI.Given_TextBox_Pasteruntime tests. They paste through the internal paste-with-text method and cut through a new internalCutSelection(), which does the cut without the copy, so they don't touch the clipboard.CutSelection()has the same guards as the real cut.Undo()run fromTextChangingduring a cut), and 4 of 4 Redo cases. Before the cut commit, the tests'CutSelection()is a temporary stand-in that cuts the old way without the copy. All 47 desktop cases pass at the end; the browser cases run in WebAssembly only. The per-commit runs predate the rebase on 09b5f7a, which changed nothing in these commits' code. With theMaxLengthPR underneath,Given_TextBox*,Given_PasswordBox,Given_ImeSessionCoordinator*,Given_AccessibleTextBox,Given_RichEditBox*andGiven_BrowserKeyboardInputSourcepass too: 1382 cases, 0 failures, without the clipboard tests.RichEditBoxstill raisesPaste. With both PRs, the paste,MaxLength, IME, browser-input and semantic-input tests pass in the browser: 272 cases, 1 skipped (mobile only).When_Touch_LongPress_Drag_Release_Does_Not_Open_Flyout_iOSfails, the same way as on master without these changes. Among them areWhen_Cut_Paste_Breaks_Typing,When_Cut_Paste,When_Paste_History_Remains_Intact,When_Paste_The_Same_Text,When_Paste_Does_Not_Change_TextandWhen_MaxLine_Paste, which all pass.PR Checklist ✅
Screenshots Compare Test Runresults.