From dce50ddd2af82e7862cb46dd916d0ae83caa9403 Mon Sep 17 00:00:00 2001 From: ShinYoung-Kim Date: Sat, 24 Oct 2026 14:06:39 +0900 Subject: [PATCH] fix(ios): re-lay out a misplaced first text fragment in RCTUITextView on iOS 27 On iOS 27, TextKit 2 can leave the first layout fragment of a UITextView pushed down by its own height after an edit that replaces text inside the first paragraph while later paragraphs exist, e.g. Korean syllable recomposition. RCTUITextView is sized with frames, so nothing lays the fragment out again and the whole text stays shifted until the next edit (FB25281535). After each text change, check the first layout fragment and, when it is misplaced, invalidate and re-lay out only that fragment's range. Fixes #59386 --- .../Text/TextInput/Multiline/RCTUITextView.mm | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm index eef87f87f632..4eebc85eaada 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm @@ -152,6 +152,35 @@ - (void)textDidChange { _textWasPasted = NO; [self _invalidatePlaceholderVisibility]; + [self _relayoutFirstTextLayoutFragmentIfMisplaced]; +} + +// On iOS 27, TextKit 2 can leave the first layout fragment pushed down by its own height (origin.y == height) after +// an edit that replaces text inside the first paragraph while later paragraphs exist, e.g. Korean syllable +// recomposition in a non-scrolling text view. UIKit hides this when Auto Layout re-measures the view, but views +// here are sized with frames, so the whole text stays shifted until the next edit (FB25281535). +// Re-laying out just that fragment fixes it. Nothing is done while the fragment is in place. +- (void)_relayoutFirstTextLayoutFragmentIfMisplaced +{ + if (@available(iOS 16.0, tvOS 16.0, *)) { + NSTextLayoutManager *textLayoutManager = self.textLayoutManager; + if (textLayoutManager == nil) { + return; + } + __block NSTextLayoutFragment *firstFragment = nil; + [textLayoutManager enumerateTextLayoutFragmentsFromLocation:textLayoutManager.documentRange.location + options:NSTextLayoutFragmentEnumerationOptionsNone + usingBlock:^BOOL(NSTextLayoutFragment *fragment) { + firstFragment = fragment; + return NO; + }]; + if (firstFragment == nil || firstFragment.layoutFragmentFrame.origin.y <= 0) { + return; + } + NSTextRange *range = firstFragment.rangeInElement; + [textLayoutManager invalidateLayoutForRange:range]; + [textLayoutManager ensureLayoutForRange:range]; + } } - (void)setDisableKeyboardShortcuts:(BOOL)disableKeyboardShortcuts