[lexical] Bug Fix: Apply targetRange for insertReplacementText so dictation replaces words instead of deleting them#8711
Conversation
…tation replaces words instead of deleting them macOS/iOS dictation, autocorrect and spellcheck Replace revise an already-inserted word with an insertReplacementText beforeinput event whose getTargetRanges() identifies the word to replace and whose dataTransfer carries the replacement text. The pre-switch applyDOMRange only syncs the selection to the OS targetRange when the selection is collapsed, so a stale non-collapsed selection (as dictation leaves behind via composition events) routes the replacement to the wrong text, deleting the misunderstood word and leaving a gap instead of replacing it. The targetRange is authoritative for this input type, so apply it before inserting. Fixes facebook#6940
|
Hi @jorgemanrubia! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
|
Tested dictation in en / ko / ja on iPhone Safari against current main. ko / ja drop / shuffle words far more often than en at completion — especially Korean, where in some attempts almost the entire sentence collapses down to a single fragment (e.g. typing a full two-sentence greeting leaves just Walking the event stream (
Likely root, though I'm not sure: the shrink-then-rebuild on stop looks like webkit's own dictation reinterpret pass. Lexical's wrapper / per-chunk reconcile might amplify it but doesn't look like the trigger. Could be something else. One small thing — the trigger described in this PR's description is This is just what came out of debugging, hope it's useful either way. |
…ns too Dictation can revise an already-inserted word through an insertText beforeinput event (data set, targetRange at the word to revise), not only insertReplacementText. The insertText path never synced the OS targetRange to a stale non-collapsed selection, so the revision was routed to the live selection instead of the OS-targeted word — the same facebook#6940 deletion the insertReplacementText case fixes, just through a different inputType. Apply the authoritative targetRange before inserting, scoped to non-collapsed targetRanges and stale non-collapsed selections so the common collapsed-caret typing path is untouched. Verified on a real iPhone (iOS 17 Safari): a synthetic insertText revision with a stale non-collapsed selection yields "best is a test" without this change and "this is a best" with it.
|
🤖 Thanks @mayrang — great catch, and exactly right. The I reproduced it on a real iPhone (iOS 17 Safari): a synthetic On the dictation-stop collapse-then-rebuild you traced: the rebuild arrives as collapsed |
|
@jorgemanrubia if this is ready and you would like this to be considered for approval and potential merge you'll want to click the "Ready for review" button |
|
Tested this branch (e7584c0) on a real iPhone with iOS Safari dictation — ko / ja / en, same procedure as before (system mic, 2-3 sentences, stop). English dictation works correctly. Korean and Japanese still lose text on dictation stop, same pattern as main: the shrink-to-fragment event fires with a short The stale-selection misroute that this PR fixes is a real improvement for the For the ko/ja dictation-stop loss, the fix direction that came closest in my earlier debugging was skipping Lexical's processing of the shrink event entirely — letting the DOM stay intact so WebKit's word-by-word rebuild pass has the correct state to work against. Concretely: early-return from |
Description
macOS/iOS dictation, autocorrect, and spellcheck "Replace" revise an already-inserted word by firing a
beforeinputevent withinputType: 'insertReplacementText'. The word to replace comes fromevent.getTargetRanges()and the replacement text fromevent.dataTransfer(event.datais typicallynullfor this input type).In
$handleBeforeInput, the OS-providedtargetRangeis only synced to the Lexical selection by the pre-switchapplyDOMRangecall whenselection.isCollapsed(). Dictation drives input through composition events, which can leave a stale, non-collapsed selection in place when the replacement event arrives. In that caseapplyDOMRangeis skipped and the replacement is routed to whatever the selection currently covers instead of the OS-targeted word — so the misunderstood word is effectively deleted (or the wrong text replaced), leaving a gap in the sentence.This is the same failure mode already worked around elsewhere in core: for the iOS 10-key Korean IME (
deleteContentBackwardwith atargetRange) and for cross/same-editor drags inlexical-clipboard(clipboard.ts, which notes that a non-collapsed selection makes the beforeinput handler "skip applyDOMRange and route the insert to the wrong location").For
insertReplacementTextthetargetRangeis authoritative, so this PR applies it before dispatchingCONTROLLED_TEXT_INSERTION_COMMANDwhen the range is non-collapsed. The change is scoped toinsertReplacementTextonly —insertFromYank/insertFromDropkeep their existing selection semantics.Closes #6940
Test plan
New unit suite
LexicalDictationReplacement.test.ts(modeled onLexicalIosKoreanIME.test.ts) simulates theinsertReplacementTextbeforeinput event with atargetRange+text/plainDataTransfer.Before
On
main, with a stale non-collapsed selection present, the replacement lands on the wrong text:"best is a test"instead of"this is a best"After
All three cases pass:
Full
lexicalcore unit suite (706 tests) pluslexical-rich-textandlexical-clipboardsuites pass;eslint/prettier/tscclean.