Skip to content

Commit a035363

Browse files
delektameta-codesync[bot]
authored andcommitted
Fix: [iOS][Fabric] Correctly mark inline attachments as clipped when in truncated text range (#55518)
Summary: ### Title [iOS] Inline attachments in truncated Text still visible (Fabric) --- ### Description On iOS with the New Architecture (Fabric), inline attachments in a `<Text>` that lie beyond the `numberOfLines` limit are still being rendered instead of being clipped. As a result, the attachment that should be clipped appears just above its wrapper. See screenshots below. --- ### MANDATORY Reproducer https://snack.expo.dev/kamildelekta/truncatedtextattachmentvisiblebug --- ### Screenshots of issue Only `maxLines` changes between these two screenshots. You can check it yourself in the expo snack above. | Text | Truncated Text | | --------- | ------- | |<img width="250" height="1027" alt="maxLines4" src="https://github.com/user-attachments/assets/bc93a03a-7819-4ec6-84ab-dea2bd7cbe6c" /> | <img width="250" height="1029" alt="maxLines3" src="https://github.com/user-attachments/assets/25eee68d-ef10-40d2-a657-b9dc9e158c1b" /> | ### React Native Version ``` "react-native": "0.83.0" ``` --- ### Affected Platforms only IOS New Arch --- Pull Request resolved: #55518 Test Plan: **Visible cases** Tested multiple layouts where the attachment is in range; the attachment is shown and laid out correctly. **Clipped cases** Tested both clipping paths: (1) attachment after the last visible line → `isOutsideVisibleRange`; (2) attachment in the truncated "..." range → `isInTruncatedRange`. In both cases the attachment is correctly clipped. **Context** Before this change we only had `isInTruncatedRange`. Handling for `isOutsideVisibleRange` was added. The example below shows that these two clipping cases are mutually exclusive; in the app, both cases correctly clip the attachments. Testing code: ``` <View style={{ flex: 1, justifyContent: 'center', padding: 16, alignItems: 'stretch' }}> <Text style={{ fontSize: 20, fontWeight: 'bold', marginBottom: 8, color: '#333' }}> Correctly clipped </Text> {/* Case 1: isOutsideVisibleRange: 1, isInTruncatedRange: 0 */} <Text numberOfLines={3} style={{ fontSize: 18, borderWidth: 1, marginBottom: 16, textAlign: 'justify' }}> Line 1{'\n'}Line 2{'\n'}Line 3{'\n'} <View style={{ width: 20, height: 20, backgroundColor: 'red' }} /> </Text> {/* Case 2: isOutsideVisibleRange: 0, isInTruncatedRange: 1 */} <Text numberOfLines={1} style={{ fontSize: 18, width: 200, borderWidth: 1, marginBottom: 8 }}> Long text that truncates with ellipsis <View style={{ width: 20, height: 20, backgroundColor: 'blue' }} /> </Text> <Text style={{ fontSize: 20, fontWeight: 'bold', marginTop: 16, marginBottom: 8, color: '#333' }}> Visible </Text> <Text style={{ fontSize: 12, color: '#666', marginBottom: 4, marginTop: 8 }}>First char</Text> <Text numberOfLines={2} style={{ fontSize: 18, borderWidth: 1, marginBottom: 16, textAlign: 'justify' }}> <View style={{ width: 20, height: 20, backgroundColor: 'green' }} /> text after attachment </Text> <Text style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>Middle of line</Text> <Text numberOfLines={3} style={{ fontSize: 18, borderWidth: 1, marginBottom: 16, textAlign: 'justify' }}> Line 1{'\n'}Line 2 with <View style={{ width: 20, height: 20, backgroundColor: 'green' }} /> {'\n'}Line 3 </Text> <Text style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>End of line 1</Text> <Text numberOfLines={3} style={{ fontSize: 18, borderWidth: 1, marginBottom: 16, textAlign: 'justify' }}> Line 1 <View style={{ width: 20, height: 20, backgroundColor: 'orange' }} /> {'\n'}Line 2{'\n'}Line 3 </Text> <Text style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>Short text, end</Text> <Text style={{ fontSize: 18, borderWidth: 1, marginBottom: 16, textAlign: 'justify' }}> Short text <View style={{ width: 20, height: 20, backgroundColor: 'orange' }} /> </Text> <Text style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>Only attachment on line</Text> <Text numberOfLines={2} style={{ fontSize: 18, borderWidth: 1, textAlign: 'justify' }}> <View style={{ width: 20, height: 20, backgroundColor: 'purple' }} /> {'\n'}Second line </Text> </View> ``` and here some examples: | oldArch | newArch | newArch fixed | --------- | ------- | ------- | |<img width="250" height="2622" alt="oldArch" src="https://github.com/user-attachments/assets/aaffac4c-f3da-43ff-9560-7eb6e5258eb3" /> | <img width="250" height="2622" alt="newArch" src="https://github.com/user-attachments/assets/8905bb4e-8431-4f4d-ae46-770705520a61" /> | <img width="250" height="2622" alt="newArchFixed" src="https://github.com/user-attachments/assets/f786178e-438d-4a90-8ccf-9802181b5d8c" />| --- ## Changelog: [iOS] [Fixed] - Inline attachments after the last visible line in truncated text are now correctly marked as clipped. --- ### Output of `npx react-native-community/cli info` ``` System: OS: macOS 26.2 CPU: (12) arm64 Apple M4 Pro Memory: 982.39 MB / 48.00 GB Shell: version: "5.9" path: /bin/zsh Binaries: Node: version: 22.14.0 path: /nix/store/04fc23dsflkxl4s9p6lkigia1hq3vjp2-nodejs-22.14.0/bin/node Yarn: version: 1.22.19 path: /Users/kamil/.nix-profile/bin/yarn npm: version: 10.9.2 path: /nix/store/04fc23dsflkxl4s9p6lkigia1hq3vjp2-nodejs-22.14.0/bin/npm Watchman: version: 2024.03.11.00 path: /Users/kamil/.nix-profile/bin/watchman Managers: CocoaPods: version: 1.15.2 path: /Users/kamil/.nix-profile/bin/pod SDKs: iOS SDK: Platforms: - DriverKit 25.2 - iOS 26.2 - macOS 26.2 - tvOS 26.2 - visionOS 26.2 - watchOS 26.2 Android SDK: Not Found IDEs: Android Studio: Not Found Xcode: version: 26.2/17C52 path: /usr/bin/xcodebuild Languages: Java: Not Found Ruby: version: 2.6.10 path: /usr/bin/ruby npmPackages: "react-native-community/cli": installed: 20.0.0 wanted: 20.0.0 react: installed: 19.2.0 wanted: 19.2.0 react-native: installed: 0.83.0 wanted: 0.83.0 react-native-macos: Not Found npmGlobalPackages: "*react-native*": Not Found Android: hermesEnabled: true newArchEnabled: true iOS: hermesEnabled: true newArchEnabled: true info React Native v0.83.2 is now available (your project is running on v0.83.0). ``` --- Reviewed By: NickGerleman Differential Revision: D92991293 Pulled By: CalixTang fbshipit-source-id: ebd097c4311a18e0f628fe5dd3eec353582ce423
1 parent 4ef7ac3 commit a035363

1 file changed

Lines changed: 11 additions & 1 deletion

File tree

  • packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager

packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,8 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage
391391
size = (CGSize){ceil(size.width * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
392392
ceil(size.height * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};
393393

394+
NSRange visibleGlyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
395+
394396
__block auto attachments = TextMeasurement::Attachments{};
395397

396398
[textStorage
@@ -406,7 +408,15 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage
406408
actualCharacterRange:NULL];
407409
NSRange truncatedRange =
408410
[layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:attachmentGlyphRange.location];
409-
if (truncatedRange.location != NSNotFound && attachmentGlyphRange.location >= truncatedRange.location) {
411+
412+
// Attachment on a line that did not fit (e.g. on the 4th line when the container is limited to 3 lines)
413+
BOOL isOutsideVisibleRange = !NSLocationInRange(attachmentGlyphRange.location, visibleGlyphRange);
414+
// Attachment in the ellipsis range of the last visible line (line truncated with "..." and the
415+
// attachment falls in that portion)
416+
BOOL isInTruncatedRange =
417+
truncatedRange.location != NSNotFound && attachmentGlyphRange.location >= truncatedRange.location;
418+
419+
if (isOutsideVisibleRange || isInTruncatedRange) {
410420
attachments.push_back(TextMeasurement::Attachment{.isClipped = true});
411421
} else {
412422
CGSize attachmentSize = attachment.bounds.size;

0 commit comments

Comments
 (0)