From 4ec683f092a98fe6c91d8930de93fde169559a6a Mon Sep 17 00:00:00 2001 From: Kevin Gozali Date: Wed, 29 Apr 2026 19:24:24 -0700 Subject: [PATCH] Fix controlled TextInput jumbling characters during autocorrect on Fabric MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: On iOS Fabric, a controlled `` could silently drop characters when iOS autocorrect/IME composition was in flight, regressing the controlled-input "jumbled text" behavior that the `TextInput-dynamicWidth-e2e.js` regression test was designed to catch. The test had been left disabled on iOS Fabric configs because of this, masking the real product regression. Root cause: `RCTTextInputComponentView` snapshots the backing field's `typingAttributes` once at view init time and uses that snapshot as the "insensitive attributes" set for `RCTIsAttributedStringEffectivelySame` — i.e. the set of UIKit-injected attributes the comparison should ignore so it doesn't trigger a destructive `setAttributedText:` on every keystroke. UIKit, however, injects additional typing attributes lazily after the field becomes first responder. Concretely, the init snapshot has `{NSColor, NSParagraphStyle, NSFont}`, but after focus the live `typingAttributes` also contains `NSBackgroundColor` (used for the marked-text highlight during composition/autocorrect) and the React Native event-emitter wrapping key. When iOS performs a multi-step text replacement (e.g. autocorrect substituting `Thos` -> `This`), characters in the backing string transiently carry these extra attributes. The comparison then incorrectly returns "different", we overwrite the backing field mid-replacement, and the in-flight text mutation is corrupted — exactly the original failure mode this code path was meant to prevent. Fix: introduce `_insensitiveTypingAttributes`, which unions the init snapshot with the live `typingAttributes` at compare time. The init snapshot is preserved as a floor (in case UIKit later removes attributes it had during normal typing), and any attribute UIKit silently injects after focus is now correctly ignored by the comparison. The fix is small, scoped to `RCTTextInputComponentView.mm`, and preserves the contract of `RCTIsAttributedStringEffectivelySame`: we only broaden the insensitive set, never narrow it. Changelog: [iOS][Fixed] - Fix controlled TextInput jumbling characters during autocorrect/IME composition on Fabric Differential Revision: D103123618 --- .../TextInput/RCTTextInputComponentView.mm | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index c0e56acd2a0f..3ac270aa39fb 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -879,8 +879,30 @@ - (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldTe return [newText.string isEqualToString:oldText.string]; } else { return RCTIsAttributedStringEffectivelySame( - newText, oldText, _originalTypingAttributes, static_cast(*_props).textAttributes); - } + newText, + oldText, + [self _insensitiveTypingAttributes], + static_cast(*_props).textAttributes); + } +} + +// UIKit injects typing attributes lazily — e.g. NSBackgroundColor while marked text is +// in flight during autocorrect/IME composition. The set captured at init only contains +// pre-focus attributes (NSColor, NSParagraphStyle, NSFont), so attributes added after +// focus would be treated as JS-imposed differences and trigger a destructive +// `setAttributedText:` mid-composition (the controlled-input "jumbled text" bug). Union +// the init snapshot with the live typing attributes so the comparison stays insensitive +// to anything UIKit silently adds. +- (NSDictionary *)_insensitiveTypingAttributes +{ + NSDictionary *liveTypingAttributes = _backedTextInputView.typingAttributes; + if (liveTypingAttributes.count == 0) { + return _originalTypingAttributes; + } + NSMutableDictionary *merged = + [NSMutableDictionary dictionaryWithDictionary:_originalTypingAttributes]; + [merged addEntriesFromDictionary:liveTypingAttributes]; + return merged; } - (SubmitBehavior)getSubmitBehavior