Fix iOS TextInput IME composition issues for CJK languages#56082
Open
kdwkr wants to merge 14 commits intofacebook:mainfrom
Open
Fix iOS TextInput IME composition issues for CJK languages#56082kdwkr wants to merge 14 commits intofacebook:mainfrom
kdwkr wants to merge 14 commits intofacebook:mainfrom
Conversation
CJK (Chinese/Japanese/Korean) IME composition on Fabric was broken in multiple ways - the composition underline disappeared and composition state was destroyed. This was caused by four independent issues: 1. `updateEventEmitter:` reapplied `defaultTextAttributes` every render, which destroyed the composition underline. Now deferred during active `markedTextRange` and applied after composition ends. 2. `_setAttributedString:` overwrote `attributedText` during state round-trips, resetting `markedTextRange`. Now skipped when `markedTextRange` is active. 3. `textInputShouldChangeText:inRange:` enforced `maxLength` during composition, blocking or truncating intermediate IME input. Now deferred until composition commits, with post-composition truncation. 4. `textInputDidChangeSelection` used `isEqual:` on attributed strings, which always failed during composition due to system underline attributes. Changed to bare text comparison via `isEqualToString:`. These issues only affected the New Architecture (Fabric); Paper was unaffected due to its asynchronous bridge timing. Fixes: facebook#48497 Fixes: facebook#55257 Fixes: facebook#55059
- Guard defaultTextAttributes in updateProps and traitCollectionDidChange to prevent breaking IME composition during JS-driven re-renders - Use grapheme-cluster-safe truncation in post-composition maxLength enforcement to avoid splitting emoji and composed characters - Skip JS-driven text updates entirely during IME composition in setTextAndSelection to prevent unnecessary state ping-pong
- Prevent double textInputDidChange/onChange when post-composition maxLength truncation triggers _setAttributedString in multiline mode. The recursive call path was: _setAttributedString → setSelectedTextRange → textInputDidChangeSelection → textInputDidChange (recursive). Fixed by suppressing delegate callbacks during internal truncation. - Fix stale event emitter when updateEventEmitter and updateProps both defer during composition. updateProps now reads the event emitter from the pending dict if updateEventEmitter already deferred one, instead of reading the stale value from the view. - Improve test quality: testDefaultTextAttributesSkippedDuringMarkedText now actually verifies deferral behavior instead of only testing mock setup. Add documentation for maxLength test coverage limitations.
- Guard multiline textInputDidChangeSelection against triggering textInputDidChange during composition (markedTextRange check) - Change _ignoreNextTextInputCall guard from isEqual: to bare string comparison to prevent double onChange during composition in multiline - Preserve event emitter key in traitCollectionDidChange deferred path to prevent loss when traitCollectionDidChange fires between updateEventEmitter and updateProps during composition - Handle maxLength=0 edge case in post-composition truncation - Add test file to RNTesterUnitTests Xcode target (pbxproj)
Test controlled/uncontrolled components with CJK composition event patterns (Korean ㅎ→하→한, Japanese か→漢字), multiline composition, maxLength interaction, and value prop updates during composition. All 7 tests pass. Existing 26 TextInput tests unaffected.
- Japanese romaji→hiragana (kanji), romaji→katakana (東京), candidate re-selection (橋→箸→端→箸) - Chinese Pinyin (zhongguo→中国), Wubi stroke (ggtt→王), Zhuyin/Bopomofo (ㄓㄨㄥ→中) - Korean multi-syllable (감사합니다) - Mixed Latin↔CJK switching mid-sentence - Continuous sentence composition (私は学生です, 我爱中国)
Replace individual guard-condition tests with full composition lifecycle tests that call UIKit delegate methods in the exact order they fire during real IME input. Remove Maestro E2E test (inputText bypasses IME). Lifecycle tests added: - Korean: ㅎ→하→한 (single syllable), 감→감ㅅ→감사 (multi-syllable) - Japanese: k→か→かん→漢 (romaji), はし→橋→箸→端→箸 (candidate re-selection) - Chinese: z→zh→zhong→中 (Pinyin), ㄓㄨㄥ→中 (Zhuyin/Bopomofo) - Mixed: "Hello" + Japanese IME → "Hello世界" Guard tests retained: - _setAttributedString blocked/allowed - Deferred defaultTextAttributes preserved/applied - maxLength bypassed during composition - Multiline bare text comparison - JS-driven update blocked during composition 16 tests, all passing.
- Add 3 tests verifying composition underline (NSUnderlineStyleAttributeName) is preserved during state round-trips, deferred attribute updates, and correctly removed after commit - Fix Prettier formatting in TextInput-ime-test.js (CI lint failure) - Remove Maestro textinput-ime.yml (inputText bypasses IME) 19 native tests, 18 JS tests — all passing.
- Skip _updateState in textInputDidChange during composition to prevent Fabric round-trips that interfere with UIKit's marked text rendering - Guard setTextAndSelection selection updates during composition to prevent setSelectedTextRange: from clearing markedTextRange - Remove debug logging, fix Flow type errors in JS test file
…ix IME composition underline UIKit uses typingAttributes/defaultTextAttributes as the base for IME composition marked text rendering. When these contain no-op NSShadow (empty shadow with zero offset/blur) or transparent NSBackgroundColor, UIKit fails to render the composition underline on marked text. These no-op values are added by UIKit as defaults and propagated through React Native's defaultTextAttributes flow. This commit strips them (along with the React-internal EventEmitter attribute) before passing to UIKit, while preserving user-specified shadow/background values. Changes: - RCTUITextView.setDefaultTextAttributes: strip from typingAttributes - RCTUITextField.setDefaultTextAttributes: strip before [super set...] - _updateTypingAttributes: strip when reading from attributed text - Add native tests for stripping behavior and value preservation
Contributor
|
This is a very delicate set of changes. Can you please attach videos of RNTester working fine with english, japanese, korean and chinese characters, please? This will increase our confidence that everything is working correctly. |
d115278 to
5940f15
Compare
Contributor
Author
|
@cipolleschi I have attached a video demonstrating the tests in English, Chinese, Japanese, and Korean. Please review this PR carefully so that the issues with the CJK IME can be resolved as soon as possible. |
…ventEmitter key in traitCollectionDidChange - updateEventEmitter: use _pendingDefaultTextAttributes as base when it exists, so deferred text-style changes from updateProps are not lost - traitCollectionDidChange: preserve EventEmitter key in the non-composing branch (was already preserved in the composing branch) - RCTUITextView/RCTUITextField: replace @"EventEmitter" string literal with local constant kRCTEventEmitterAttributeKey for consistency - Remove duplicate test file from React/Tests/TextInput/ (canonical copy is in rn-tester/RNTesterUnitTests/)
cb95b3f to
1b69da3
Compare
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.
Summary:
CJK (Chinese/Japanese/Korean) IME composition on Fabric is broken — the composition state is destroyed during input, and the composition underline never renders. This affects Japanese, Chinese, and Korean users on the New Architecture.
Root causes and fixes:
defaultTextAttributesreapplied during composition destroys marked text —updateEventEmitter:,updateProps:, andtraitCollectionDidChange:all setdefaultTextAttributesdirectly, which calls[super setDefaultTextAttributes:]and reapplies attributes to the entire text, interfering with the composition state. Fix: defer during activemarkedTextRangeby storing in_pendingDefaultTextAttributes, apply after composition ends intextInputDidChange. WhenupdatePropsdefers, it preserves the event emitter key from any prior pendingupdateEventEmitterupdate. WhentraitCollectionDidChangedefers, it carries forward the existing event emitter key._setAttributedString:overwrites text during state round-trips —updateState:round-trips call_setAttributedString:which replacesattributedText, resettingmarkedTextRange. Fix: skip whenmarkedTextRangeis active; text syncs viatextInputDidChange→_updateStateafter composition.maxLengthenforcement blocks IME mid-composition —textInputShouldChangeText:inRange:enforcesmaxLengthwithout checkingmarkedTextRange, truncating intermediate CJK input (e.g., Korean ㅎ→하→한). Fix: defermaxLengthduring composition, enforce via post-composition truncation intextInputDidChangewith grapheme-cluster-safe truncation (usingrangeOfComposedCharacterSequenceAtIndex:to avoid splitting emoji and composed characters). Truncation uses_comingFromJSwrapper to prevent recursivetextInputDidChangefromtextInputDidChangeSelection.textInputDidChangeSelectiontriggers spurious updates in multiline —isEqual:onNSAttributedStringfails during composition due to system underline attributes, causing unnecessarytextInputDidChangecalls. Fix: use.string isEqualToString:for bare text comparison in bothtextInputDidChangeSelectionand_ignoreNextTextInputCallguard. Also skip the earlytextInputDidChangecall fromtextInputDidChangeSelectionduring composition (!markedTextRangeguard).setTextAndSelectioninterferes with composition — JS-driven text and selection updates viasetTextAndSelectioncan destroy the composition state. Fix: skip text update,_updateState, AND selection update (setSelectedTextRange:) entirely whenmarkedTextRangeis active. JS re-asserts its controlled value after composition ends via theonChange→setTextAndSelectioncycle._updateStateFabric round-trips during composition —_updateStateintextInputDidChangepushes state to Fabric during composition, causing round-trips that interfere with UIKit's marked text rendering. Fix: defer_updateStateuntil composition commits (markedTextRangebecomes nil).No-op
NSShadowandNSBackgroundColorindefaultTextAttributes/typingAttributesprevent IME composition underline rendering — UIKit usestypingAttributesas the base for marked text rendering. When these contain no-opNSShadow(empty shadow with zero offset/blur) or transparentNSBackgroundColor(added by UIKit as defaults and propagated through React Native's attribute flow), UIKit fails to render the composition underline. This is undocumented UIKit behavior. Fix: strip these no-op attributes (along with the React-internalEventEmitterattribute) fromtypingAttributes/defaultTextAttributesbefore passing to UIKit, while preserving user-specified shadow/background values. Applied in three locations:RCTUITextView.setDefaultTextAttributes:(multiline),RCTUITextField.setDefaultTextAttributes:(single-line, stripped before[super setDefaultTextAttributes:]), and_updateTypingAttributes(when reading from Fabric-synced attributed text).Paper (old architecture) is unaffected; these issues are Fabric-only due to synchronous
updateEventEmitter:/updateState:calls.Fixes #48497
Fixes #55257
Changelog:
[IOS] [FIXED] - Fix CJK IME composition state being destroyed and composition underline not rendering in Fabric TextInput
Test Plan:
Native XCTest (22 tests, all passing):
_setAttributedStringblocked during composition, deferreddefaultTextAttributespreserved/applied,maxLengthbypassed during composition, multiline bare text comparison, JS-driven update blockedJS tests (18 tests, all passing):
Manual verification (iOS Simulator):
valueprop works correctly during CJK inputTest Video
Chinese, Japanese, Korean
ime.testing.video.mp4
English
english.test.mp4