Visitar URL original
fix(wasm): Keep the TextBox in sync with Android soft keyboard input by mikernet · Pull Request #24502 · unoplatform/uno · GitHub
Skip to content

fix(wasm): Keep the TextBox in sync with Android soft keyboard input - #24502

Closed
mikernet wants to merge 7 commits into
unoplatform:masterfrom
mikernet:dev/mikernet/wasm-android-soft-keyboard
Closed

mikernet wants to merge 7 commits into
unoplatform:masterfrom
mikernet:dev/mikernet/wasm-android-soft-keyboard

Conversation

@mikernet

@mikernet mikernet commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

GitHub Issue: closes #22230

PR Type:

🐞 Bugfix

What changed? 🚀

On Skia WebAssembly, soft keyboards on Android browsers type through IME composition on the hidden native input and report key events without a code. Several things went wrong with that:

  1. The composition path inserted the preedit itself and skipped the next input event after compositionend, assuming the browser fires the committed preedit's input event after compositionend (Safari's order). Chrome fires it before, so the skipped event was the space or punctuation that committed the word. The TextBox and the hidden input drifted apart: spaces disappeared, backspace looked dead, and when Gboard re-opened the composition on the whole token the preedit was spliced at the stale caret, duplicating text.

  2. Keys were mapped from event.code only, and soft keyboards send an empty code, so Backspace mapped to VirtualKey.None after the keydown had already been prevented. Backspace never worked in a PasswordBox (no composition there) and stopped working in a TextBox outside a composition. Such keys are now left to the browser and synced from the input event instead.

  3. Managed text and caret changes were ignored while a composition was open, and the keyboard keeps one open for the whole word being typed: tapping elsewhere did not move the caret the keyboard inserts at, and clearing the text brought it back on the next keystroke. Replacing the input's value also restored the caret on a later selectionchange, which could land under the keyboard's next composition.

This PR makes the hidden input's value the source of truth for the text: every input event syncs it to the TextBox (through the TextAlreadyApplied mode the Android extension already uses), and composition events only report the preedit and where it sits in the value. ImeCompositionEventArgs gains a StartIndex so platforms can report where a composition begins when it covers existing text. Soft keyboard keys are left to the browser and synced from the input event. Taps, the delete button and Text set from code reach the hidden input even during a composition, ending it as a native app would, and the caret is restored synchronously when the value is replaced. RichEditBox, which shares the hidden input, gets its composition text through the same input events and, like TextBox, follows the preedit's position in the input and raises TextCompositionChanged once that text is in place.

WebKit applies a commit through input events of its own, with no compositionupdate for the committed text, and delivers compositionend either after them (iOS, verified on a device) or before them. The completion now follows the input event that puts the committed text in place, so TextCompositionChanged and the text change precede TextCompositionEnded and its handlers see the committed text, as they do on Chrome and as WinUI documents the order. A commit the browser never applies ends the composition with the text as-is at the next keystroke. TextCompositionEnded reports its range clamped to the text as it is when a composition ends without its commit, so a coercion or Text set from a handler cannot leave it out of bounds.

Validated with new Skia WASM runtime tests that replay Chrome's and WebKit's composition orderings (a commit equal to the preedit, a candidate differing from it and a cancel, with compositionend delivered before or after the input events that apply them), a composition re-opened on existing text, the caret moved and the text cleared while composing, and Backspace without a code in a TextBox and a PasswordBox. The existing Given_TextBox IME tests pass on WASM and Skia Desktop, and the fix was verified on an Android phone with Gboard.

I did extensive manual testing (iOS Safari and Android Chrome) with all sorts of input sequences, clears, cuts/pastes, bindings, BeforeTextChanging, and multiline inputs, and it appears to be working correctly as far as I can tell.

PR Checklist ✅

Copilot AI lite review requested due to automatic review settings September 11, 2026 11:06
@github-actions github-actions Bot added the platform/wasm 🌐 Categorizes an issue or PR as relevant to the WebAssembly platform label Sep 11, 2026
@mikernet
mikernet force-pushed the dev/mikernet/wasm-android-soft-keyboard branch from b4cc152 to 28d7b06 Compare September 11, 2026 11:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved IME ordering, synchronization, and read-only input issues remain.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Fixes Android soft-keyboard IME synchronization for Skia WebAssembly TextBoxes.

Changes:

  • Synchronizes hidden-input text, caret, and composition state.
  • Adds composition start-index reporting and key-name fallback mapping.
  • Adds runtime coverage for composition ordering and Backspace behavior.
File summaries
File Summary
src/Uno.UI/UI/Xaml/Controls/TextBoxCore/TextBoxCore.IME.cs Tracks composition indices and platform-applied text.
src/Uno.UI/UI/Xaml/Controls/TextBox/Extensions/ImeCompositionEventArgs.cs Adds StartIndex.
src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TextBox.BrowserSoftKeyboard.cs Adds soft-keyboard runtime tests.
src/Uno.UI.Runtime.Skia.WebAssembly.Browser/UI/Xaml/Controls/TextBox/WasmImeTextBoxExtension.cs Bridges browser composition lifecycle events.
src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserInvisibleTextBoxViewExtension.ts Synchronizes browser input, selection, and composition state.
src/Uno.UI.Runtime.Skia.WebAssembly.Browser/Devices/Input/BrowserKeyboardInputSource.cs Falls back from key codes to key names.
Review details

Suppressed comments (5)

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/Devices/Input/BrowserKeyboardInputSource.cs:55

  • The new FromKey fallback is not exercised by the added soft-keyboard tests: those events are stopped at the hidden input before the document-level BrowserKeyboardInputSource callback runs. Add a direct OnNativeKeyboardEvent regression case such as code="", key="Backspace" and assert VirtualKey.Back; otherwise this mapping can regress without a test.
		var virtualKey = BrowserVirtualKeyHelper.FromCode(code);
		if (virtualKey is VirtualKey.None)
		{
			// Soft keyboards report an empty code, so the key name is all there is to go on.
			virtualKey = BrowserVirtualKeyHelper.FromKey(key);
		}

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserInvisibleTextBoxViewExtension.ts:167

  • Unlike OnCompositionUpdated, syncTextFromInput does not honor core.IsReadOnly; ProcessTextInput accepts the value. Because the hidden element is never marked read-only, a focused read-only TextBox can be mutated by a composing soft-keyboard input that this new unconditional sync forwards. Reject native input for read-only cores (or configure the hidden input accordingly) before syncing it.
					BrowserInvisibleTextBoxViewExtension.syncTextFromInput(input);

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserInvisibleTextBoxViewExtension.ts:167

  • This raises TextCompositionChanged before syncTextFromInput updates the managed TextBox. On the first preedit, handlers can therefore observe a composition range that is outside Text (the existing IME sample reads that range from Text), and the event ordering differs from the normal IME path, which applies text before raising the event. Update the managed value first while preserving the platform-apply guard, then raise the composition event.
						BrowserInvisibleTextBoxViewExtension._imeExports.OnCompositionUpdated(
							BrowserInvisibleTextBoxViewExtension.compositionText,
							BrowserInvisibleTextBoxViewExtension.findCompositionStart(input));
					}
					BrowserInvisibleTextBoxViewExtension.syncTextFromInput(input);

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserInvisibleTextBoxViewExtension.ts:216

  • With Safari's ordering, the committed input event arrives after compositionend. Clearing isComposing and completing here means the later oninput skips findCompositionStart; when an IME has reopened over existing text, the managed session still has the caret captured at compositionstart, so TextCompositionEndedEventArgs.StartIndex reports the wrong range. Defer completion until the following input, or otherwise carry that input's computed range into completion.
				BrowserInvisibleTextBoxViewExtension.isComposing = false;
				if (ev.data.length > 0) {
					BrowserInvisibleTextBoxViewExtension._imeExports.OnCompositionCompleted(ev.data);

src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TextBox.BrowserSoftKeyboard.cs:58

  • This test does not actually model the Safari ordering described in its comment: ComposePreedit already dispatches an input event before compositionend, so ab has been synced before completion and the post-end input is only a duplicate. It therefore cannot catch a completion callback observing stale managed text; construct the sequence with the first committed input after compositionend and assert the composition event state as well as the final value.
		ComposePreedit("ab", "ab", caret: 2);
		DispatchComposition("compositionend", "ab");
		DispatchInput("insertCompositionText", "ab", isComposing: false);
		SetHiddenInputValue("ab ", caret: 3);
		DispatchInput("insertText", " ", isComposing: false);
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Copilot AI review requested due to automatic review settings September 11, 2026 11:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

One or more issues must be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (5)

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/Devices/Input/BrowserKeyboardInputSource.cs:55

  • The new FromKey fallback is not exercised by the added soft-keyboard tests: those dispatch events on the hidden input, where the TypeScript handler stops propagation before OnNativeKeyboardEvent is called. The existing browser keyboard tests cover only non-empty event.code, so a regression in mapping an empty-code Backspace/Enter to its key name would pass; add a direct OnNativeKeyboardEvent test for the fallback.
		var virtualKey = BrowserVirtualKeyHelper.FromCode(code);
		if (virtualKey is VirtualKey.None)
		{
			// Soft keyboards report an empty code, so the key name is all there is to go on.
			virtualKey = BrowserVirtualKeyHelper.FromKey(key);
		}

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/UI/Xaml/Controls/TextBox/WasmImeTextBoxExtension.cs:71

  • In Safari's ordering, compositionend arrives before the committed input event. This raises TextCompositionEnded with TextAlreadyApplied before the hidden input's value has been synchronized, so managed handlers observe the old Text (the IME sample computes the committed substring from that property); only the later input event fixes the final value. Defer completion/ended notification until after the trailing input sync, or synchronize managed text before raising these events.
		Instance._isComposing = false;
		Instance.CompositionCompleted?.Invoke(Instance, new ImeCompositionEventArgs(text, textAlreadyApplied: true));
		Instance.CompositionEnded?.Invoke(Instance, EventArgs.Empty);

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserInvisibleTextBoxViewExtension.ts:481

  • This guard suppresses every setText call during an input callback, including the writeback from TextBoxView.UpdateTextFromNative when BeforeTextChanging or text coercion changes/rejects the DOM value. In that case the hidden input keeps the rejected value while managed Text has another value; composition completion is marked already applied, so no later path necessarily resynchronizes them. Suppress only the exact echo, or restore the managed value and end the composition when the input was rejected.
				if (BrowserInvisibleTextBoxViewExtension.isComposing && BrowserInvisibleTextBoxViewExtension.isSyncingFromInput) {
					return;

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserInvisibleTextBoxViewExtension.ts:216

  • Safari can deliver the committed input event after compositionend, but this invokes OnCompositionCompleted immediately and marks the managed composition complete before that input syncs TextBox.Text. TextCompositionEnded handlers therefore see stale text and a range that may not exist; the existing IME sample's substring lookup will report the committed preedit as out of range in this ordering. Buffer the completion notification until the value has been synchronized, or otherwise apply the committed value before raising the managed event.
				BrowserInvisibleTextBoxViewExtension.isComposing = false;
				if (ev.data.length > 0) {
					BrowserInvisibleTextBoxViewExtension._imeExports.OnCompositionCompleted(ev.data);

src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TextBox.BrowserSoftKeyboard.cs:58

  • This test does not actually replay the Safari ordering described in its comment: ComposePreedit dispatches the final input before compositionend, and the post-end input carries the same already-synchronized value. Consequently it cannot catch handlers observing stale TextCompositionEnded state in Safari order. Model the final preedit with compositionupdate only, then set the value and dispatch the committed input after compositionend.
		// Safari order: the input event carrying the committed preedit follows compositionend.
		DispatchComposition("compositionstart", "");
		ComposePreedit("ab", "ab", caret: 2);
		DispatchComposition("compositionend", "ab");
		DispatchInput("insertCompositionText", "ab", isComposing: false);
		SetHiddenInputValue("ab ", caret: 3);
		DispatchInput("insertText", " ", isComposing: false);
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 11, 2026 11:37
@mikernet
mikernet force-pushed the dev/mikernet/wasm-android-soft-keyboard branch from 28d7b06 to 75f5172 Compare September 11, 2026 11:37
@mikernet
mikernet force-pushed the dev/mikernet/wasm-android-soft-keyboard branch from 75f5172 to 483e25f Compare September 11, 2026 11:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Two moderate issues remain unresolved in keyboard fallback and composition synchronization.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserInvisibleTextBoxViewExtension.ts:50

  • The described key-name fallback is not implemented here: for an empty or unmapped event.code, this branch only stops propagation, while BrowserKeyboardInputSource.OnNativeKeyboardEvent still calls BrowserVirtualKeyHelper.FromCode(code) and never uses KeyboardEvent.key. Consequently such events still become VirtualKey.None wherever they are routed to managed input; add that fallback in the keyboard source or remove the claim from the PR description.
		private static isSoftKeyboardKey(ev: KeyboardEvent): boolean {
			return ev.keyCode === BrowserInvisibleTextBoxViewExtension.ANDROID_IME_KEYCODE || ev.code === "";
		}
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 11, 2026 11:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

One or more issues must be addressed before approval.

Review details

Suppressed comments (4)

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserInvisibleTextBoxViewExtension.ts:50

  • This only recognizes an empty code (or keyCode 229) as a soft-keyboard event. The document path still maps only FromCode(code); for a key such as { key: 'Backspace', code: 'Unidentified' }, this predicate is false, the handler's later preventDefault() runs, and managed code receives VirtualKey.None, so deletion still fails. Add the key-name fallback or classify these unmapped events as native before they are prevented.
		private static isSoftKeyboardKey(ev: KeyboardEvent): boolean {
			return ev.keyCode === BrowserInvisibleTextBoxViewExtension.ANDROID_IME_KEYCODE || ev.code === "";
		}

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserInvisibleTextBoxViewExtension.ts:69

  • The active-element check still admits only HTMLInputElement, while createInput creates an HTMLTextAreaElement for multiline (acceptsReturn) TextBoxes. Native selection changes in those controls therefore never reach OnSelectionChanged, leaving the managed caret stale when the IME or native keyboard moves it during composition. Include HTMLTextAreaElement in this guard.
						if (input.selectionDirection == "backward") {
							BrowserInvisibleTextBoxViewExtension._exports.OnSelectionChanged(input.selectionEnd, input.selectionStart - input.selectionEnd);
						} else {
							BrowserInvisibleTextBoxViewExtension._exports.OnSelectionChanged(input.selectionStart, input.selectionEnd - input.selectionStart);

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserInvisibleTextBoxViewExtension.ts:267

  • An empty compositionupdate is valid when an IME clears or cancels its preedit, but value.startsWith("", editEnd) is always true here. This returns the end of the value (or the suffix boundary) instead of the caret captured at compositionstart, so the managed StartIndex and ended range can jump to the end of existing text. Preserve the composition-start caret for the empty-preedit case before using the diff.
			// The preedit ends where the edited region ends, or starts where it starts.
			const editEnd = value.length - suffix;
			if (editEnd >= text.length && value.startsWith(text, editEnd - text.length)) {
				return editEnd - text.length;

src/Uno.UI/UI/Xaml/Controls/TextBoxCore/TextBoxCore.IME.cs:156

  • syncFromInput invokes OnCompositionUpdated before syncing the input value into managed text. For an empty preedit, IsCompositionTextApplied("") is always true because the zero-length comparison succeeds, so this branch raises TextCompositionChanged while Text still has the old value during delete/cancel updates. Keep that update pending until the input value has been applied, then flush it after the sync so handlers never observe a stale composition range.
		if (textAlreadyApplied && !IsCompositionTextApplied(compositionText))
		{
			_compositionChangedPending = true;
		}
		else
		{
			_host.RaiseTextCompositionChanged(new TextCompositionChangedEventArgs(_compositionStartIndex, _compositionLength));
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 11, 2026 12:27
@mikernet
mikernet force-pushed the dev/mikernet/wasm-android-soft-keyboard branch from 483e25f to 10d0f4d Compare September 11, 2026 12:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Two critical review findings remain unresolved.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserInvisibleTextBoxViewExtension.ts:50

  • The PR description says unmapped event.code values fall back to the key name, but this change only classifies empty/Unidentified codes as native soft-keyboard input. The keyboard bridge still passes non-empty codes directly to BrowserVirtualKeyHelper.FromCode, whose unknown-code path returns VirtualKey.None, so that promised fallback is not implemented for other unmapped codes. Please add the fallback in the bridge (with coverage) or remove that claim from the PR description.
		private static isSoftKeyboardKey(ev: KeyboardEvent): boolean {
			return ev.keyCode === BrowserInvisibleTextBoxViewExtension.ANDROID_IME_KEYCODE || ev.code === "" || ev.code === "Unidentified";
		}
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 11, 2026 12:57
@mikernet
mikernet force-pushed the dev/mikernet/wasm-android-soft-keyboard branch from 10d0f4d to 1f0a23f Compare September 11, 2026 12:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Two moderate unresolved synchronization issues remain in the browser TypeScript implementation.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

src/Uno.UI.Runtime.Skia.WebAssembly.Browser/ts/Runtime/BrowserInvisibleTextBoxViewExtension.ts:161

  • The hidden <textarea> normalizes CR/CRLF to LF, but this records the pre-normalized managed string. On a multiline TextBox, compositionend therefore sees a phantom value change even before the user edits anything. Initialize the synchronization marker from the actual DOM value.
			BrowserInvisibleTextBoxViewExtension.lastSyncedValue = text;
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 11, 2026 13:24
@mikernet
mikernet force-pushed the dev/mikernet/wasm-android-soft-keyboard branch from 1f0a23f to 6558e9c Compare September 11, 2026 13:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The changes require final human review because they are too complex or risky for automated approval.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 11, 2026 13:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

A critical composition-commit path can leave the managed composition open when no follow-up input event arrives.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)

@MartinZikmund

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-24502/wasm-skia-net9/index.html

@nventive-devops

Copy link
Copy Markdown
Contributor

The build 235197 found UI Test snapshots differences: skia-linux-screenshots: 57, skia-windows-screenshots: 2310, wasm: 11

Details
  • skia-linux-screenshots: 57 changed over 2348

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • ButtonClippingTestsControl.png-dark
    • ButtonClippingTestsControl.png
    • Buttons.png-dark
    • Buttons.png
    • CalendarView_Theming.png-dark
    • CalendarView_Theming.png
    • ClipboardTests.png-dark
    • Focus_FocusVisual_Properties.png-dark
    • DropDownButtonPage.png-dark
    • DropDownButtonPage.png
    • Examples.png-dark
    • Examples.png
    • ExpanderColorValidationPage.png-dark
    • ExpanderColorValidationPage.png
    • ContextRequested.png-dark
    • ContextRequested.png
    • ElementLevelTheme.png-dark
    • ElementLevelTheme.png
    • ImageIconPage.png-dark
    • DisplayInformation.png-dark
  • skia-windows-screenshots: 2310 changed over 2440

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • Accessibility_ScreenReader.png-dark
    • Accessibility_ScreenReader.png
    • AndroidWindowInsets.png
    • AnimatedIconPage.png-dark
    • AppBarButtonWithIconTest.png-dark
    • AppBarToggleButtonTest.png-dark
    • ApplicationViewMode.png-dark
    • ApplicationViewSpanningRectsPage.png-dark
    • AppWindowFeatures.png-dark
    • AppWindowFeatures.png
    • Arrange_Performance01.png-dark
    • Arrange_Performance01.png
    • AutoBorderStretchwithbottommargin.png-dark
    • AutoBorderStretchwithbottommargin.png
    • AutoSizedPathCentered.png-dark
    • AutoSizedPathCentered.png
    • AutoSuggestBox_Reason.png-dark
    • AutoSuggestBox_Reason.png
    • BasicAutoSuggestBox.png
    • BasicEnability.png-dark
  • wasm: 11 changed over 1076

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • UITests.Microsoft_UI_Xaml_Controls.WebView2Tests.WebView2_NavigationProperties
    • SamplesApp.Microsoft_UI_Xaml_Controls.WebView2Tests.WebView2_EnableDevTools
    • SamplesApp.Windows_UI_Xaml_Controls.ListView.ListViewSelectedItems
    • SamplesApp.Wasm.Windows_UI_Xaml_Controls.ListView.ListView_IsSelected
    • UITests.Shared.Microsoft_UI_Xaml_Controls.ExpanderTests.WinUIExpanderPage
    • UITests.Uno_Web.Http.CookieManagerTests
    • UITests.Windows_UI_Xaml_Media_Animation.ColorAnimation_Background
    • UITests.Shared.Windows_UI_Xaml_Controls.MediaPlayerElement.MediaPlayerElement_Minimal
    • UITests.Shared.Windows_UI_Xaml_Controls.MediaPlayerElement.MediaPlayerElement_Ogg_Extension
    • UITests.Windows_UI_Xaml_Controls.CalendarView.CalendarView_Theming
    • Uno.UI.Samples.Content.UITests.WebView.WebView_AnchorNavigation

@mergify

mergify Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

This pull request is closed, so it cannot be queued. Reopen it to get the box back.

@mikernet

mikernet commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

For a servicing/6.8 backport: ported onto the 6.8.0-dev.212 commit at Singulink@90746b8dd2 (branch Singulink:custom/6.8). The TypeScript and the browser extension applied verbatim; the TextBoxCore IME changes were hand-ported into 6.8's TextBox.IME.skia.cs and TextBox.skia.cs. The WASM-only test file is carried over and compiles; the desktop Given_TextBox suite shows no regression.

…ndroid-soft-keyboard

# Conflicts:
#	src/Uno.UI.Runtime.Skia.WebAssembly.Browser/UI/Xaml/Controls/TextBox/BrowserInvisibleTextBoxViewExtension.cs
#	src/Uno.UI.Runtime.WebAssembly.Browser/UI/Xaml/Controls/TextBox/WasmImeTextBoxExtension.cs
#	src/Uno.UI.Runtime.WebAssembly.Browser/ts/Runtime/BrowserInvisibleTextBoxViewExtension.ts
#	src/Uno.UI.RuntimeTests/Microsoft/UI/Xaml/Controls/Given_TextBox.BrowserSoftKeyboard.cs
#	src/Uno.UI/UI/Xaml/Controls/TextBoxCore/TextBoxCore.IME.cs
The hidden input now reports a composition update right before the
input event that applies its text, and where the preedit sits in the
value. RichEditBox raised TextCompositionChanged, with its Composition
and ConversionTargetChanged automation events, while the document still
held the previous text, and kept the composition at the caret where it
started, so a composition the keyboard re-opened on an existing word
was placed wrong.

Take the reported start, and hold the change until the text sync has
landed and TextChanging has been raised, the order TextBox and WinUI
use.
Copilot AI lite review requested due to automatic review settings October 16, 2026 13:11
@mikernet

Copy link
Copy Markdown
Contributor Author

Merged master to resolve the conflicts. The RichEditBox port (#23924) landed in the meantime and shares the hidden input, so:

  • On the hidden input, this PR's model (the input's value is the source of truth) replaces the composition handling feat(richeditbox): implement Skia support #23924 added: the per-composition generation counter, the cursor offset from the composition start, the input suppression after an invalidated composition and the separate cancel callback. Stragglers are dropped by isComposingOn, and invalidateComposition now resets this PR's composition state. restartComposition and the PasswordBox autocomplete hint are kept.
  • RichEditBox on WASM now gets its composition text from the input events, through the TextAlreadyApplied path it already has for Android. 990f9d3 makes it follow the preedit's position in the input and hold TextCompositionChanged (with its automation events) until that text is in the document and TextChanging has been raised, as TextBox does, with a test.
  • A canceled composition now closes the RichEditBox undo group rather than discarding it, so it can leave an empty undo step: a composition here can cover existing text, and discarding the group would drop those edits.
  • When_Wasm_Composition_Commit_And_Cancel_Follow_Browser_Events set the value before compositionupdate and never fired the input event; it now dispatches them in Chromium's order.

@github-actions github-actions Bot added the area/skia ✏️ Categorizes an issue or PR as relevant to Skia label Oct 16, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical composition-range and moderate read-only synchronization issues remain.

2 open findings
1 resolved since last review

🧠 Review effort: Lite

A cancel the platform reports with its text already applied raised
TextCompositionEnded with the start captured before the text shrank,
out of range for handlers; it now goes through the same clamp as the
other endings.

The hidden input's read-only state was only taken from a TextBox, so a
focused RichEditBox made read-only kept an editable input. It now
follows whichever host owns the input, on focus and when properties
change.
Copilot AI lite review requested due to automatic review settings October 16, 2026 13:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

One or more issues must be addressed before approval.

1 open finding
2 resolved since last review

🧠 Review effort: Lite

@mikernet

Copy link
Copy Markdown
Contributor Author

@morning4coffe-dev @ajpinedam #25804 merged on Nov 3 and reworks the same Android soft keyboard path with a different design, so this PR now conflicts with master in 10 files. To see what's still worth keeping, I ran this PR's soft keyboard tests (Given_TextBox.BrowserSoftKeyboard.cs) against master on WebAssembly: 9 of 35 pass. Most of the 26 failures are WebKit/iOS event-ordering details where master still ends with the right text. About 12 are bugs a user or an app can hit on master:

  1. TextCompositionChanged is raised before its text is in Text. On every Android Chrome composition, in TextBox and RichEditBox, the event's start and length point into text that isn't there yet. A handler that reads Text.Substring(e.StartIndex, e.Length) throws, and a handler that moves focus loses the typed character or carries it into the next TextBox. This PR raised the event once the text was applied.
  2. A character BeforeTextChanging rejects during a composition stays in the hidden input until the composition ends. The TextBox and the input then disagree, and the next character can be lost (e.g. typing a, a rejected x, then b leaves Text at a).
  3. The composition never ends when the hidden input is replaced or removed (the same TextBox refocused mid-word, or the input detached): IsComposing stays true and TextCompositionEnded is never raised.
  4. A read-only TextBox still opens the soft keyboard and lets the user type until the word is reverted. I know fix(textbox): Fix IME and touch editing on Android browsers #25804 keeps the input editable on purpose (When_Browser_Input_Edits_Read_Only_TextBox), so this one is a question rather than a fix.

How would you like to handle this? I can close this PR and open a smaller one on top of #25804's design that fixes 1-3 (roughly 150-250 lines plus about 10 of these tests), with 4 as a separate commit if you want it. Or tell me if you'd rather go another way.

@mikernet

Copy link
Copy Markdown
Contributor Author

Superseded by #26434, and #26435 for the WebKit/iOS commit ordering. They rebuild this PR's fixes on #25804's design, as I proposed in my earlier comment. Closing this one.

@mikernet mikernet closed this Nov 13, 2026
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 mikernet/uno that referenced this pull request Nov 14, 2026
Safari and other WebKit browsers commit a composition through input
events of their own, with no compositionupdate for the committed text.
Current WebKit (Editor::setComposition) dispatches them synchronously,
in one call: a commit is deleteCompositionText, insertFromComposition,
then compositionend with the committed text (the order seen on an
iPhone); an emptied preedit is deleteCompositionText, then
compositionend(""); a cancel is compositionend with the preedit alone,
keeping the text.

TextBox and RichEditBox synced the removal as a composition step of its
own: a TextCompositionChanged with the preedit gone (Text briefly
without it) or a range outside the text, then another one when the
commit came back. The removal is now reported with the insertion that
follows it, and a preedit put back unchanged isn't reported again.

Orders where compositionend comes before the input event that applies
the commit, with the preedit still selected for it or just removed,
aren't sent by current WebKit; they were asked to be covered when unoplatform#24502
was reviewed, and older WebKit versions may send them. On master, TextCompositionEnded then came while Text still
held the preedit. Such a composition now ends with the input event that
applies the commit, after the text change and TextCompositionChanged,
the order WinUI documents. Only these orders wait, so only they need
what follows: when no such event comes in the same task (Blink cancels
a composition whose text is selected with a compositionend alone), the
composition ends after the task as it would have at compositionend,
cancelled if the preedit was removed meanwhile; a next keystroke or
composition in the same task ends it first. The next composition input
event completes the composition whether or not it applied the
announced commit: no recorded order sends one that doesn't, so this
doesn't check that the commit is in place as unoplatform#24502 did.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/skia ✏️ Categorizes an issue or PR as relevant to Skia platform/wasm 🌐 Categorizes an issue or PR as relevant to the WebAssembly platform

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[WASM Skia] Backspace key does not work (TextBox, PasswordBox)

6 participants