feat(fabric, text): support native text selection when selectable={true}#2864
Merged
Saadnajmi merged 7 commits intomicrosoft:mainfrom Mar 26, 2026
Merged
Conversation
When the `selectable` prop is set on a `<Text>` component in Fabric,
swap the content view from a lightweight RCTUIView (RCTParagraphTextView)
to a platform-native text view (NSTextView on macOS, UITextView on iOS)
that handles both rendering and selection natively.
Key design decisions:
- Lazy swap: the native text view is only created when selectable={true},
keeping the default non-selectable path (99% of text) zero-overhead
- No RCTSurfaceTouchHandler changes: touch cancellation uses a helper that
walks the view hierarchy to find and toggle the gesture recognizer
- No old-arch RCTTouchHandler dependency: purely Fabric-side solution
- Cross-platform: ungates getTextStorageForAttributedString in
RCTTextLayoutManager so both iOS and macOS can sync text storage
- Proper recycling: selectable text view is torn down in prepareForRecycle
macOS mouse event handling (hitTest, mouseDown, rightMouseDown) is ported
from the old architecture's RCTTextView.mm with the same peek-ahead
drag detection and responder chain management.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update all #if TARGET_OS_OSX / #if !TARGET_OS_OSX blocks and inline comments to follow the macOS tag format documented in docsite/docs/contributing/diffs-with-upstream.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e of truth Since _textView.state is always kept in sync in updateState:, there's no need to duplicate state onto _selectableTextView or use ternary expressions to pick between them. This removes unnecessary modifications to attributedText, accessibilityElements, and touchEventEmitterAtPoint:, keeping those methods identical to upstream. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove hitTest:withEvent: override on iOS in RCTParagraphSelectableTextView so UITextView receives touches and handles selection natively. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
This was referenced Mar 21, 2026
Saadnajmi
commented
Mar 21, 2026
packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm
Outdated
Show resolved
Hide resolved
Saadnajmi
commented
Mar 21, 2026
packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm
Outdated
Show resolved
Hide resolved
Saadnajmi
commented
Mar 21, 2026
packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm
Outdated
Show resolved
Hide resolved
Saadnajmi
commented
Mar 21, 2026
packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm
Outdated
Show resolved
Hide resolved
- Move macOS diff tag inline with #if TARGET_OS_OSX macro - Move comment below macro, above function definition - Rename `gr` to `gestureHandler` (no abbreviations) - Add macOS diff tags to new #pragma mark lines Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Saadnajmi
commented
Mar 21, 2026
packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm
Outdated
Show resolved
Hide resolved
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Saadnajmi
commented
Mar 21, 2026
...derer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h
Show resolved
Hide resolved
Saadnajmi
commented
Mar 21, 2026
...erer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
Show resolved
Hide resolved
Saadnajmi
commented
Mar 21, 2026
packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm
Show resolved
Hide resolved
Saadnajmi
commented
Mar 21, 2026
packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm
Outdated
Show resolved
Hide resolved
- Add [macOS iOS] diff tags to getTextStorageForAttributedString in .h and .mm - Add comment in updateProps explaining enableContextMenu → _enableSelection rename - Rename _syncSelectableTextStorage → updateSelectableTextStorage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
vmoroz
approved these changes
Mar 23, 2026
mganandraj
approved these changes
Mar 26, 2026
Saadnajmi
added a commit
to Saadnajmi/react-native-macos
that referenced
this pull request
Mar 27, 2026
…ue} (microsoft#2864) Followup from microsoft#2845 ## Summary - Implements native text selection support for `<Text selectable={true}>` in the Fabric (new architecture) renderer - On **macOS**, swaps the content view to an `NSTextView` subclass that handles click-drag, double-click (word), and triple-click (line) selection, plus right-click context menus - On **iOS**, swaps to a `UITextView` subclass that leverages built-in gesture recognizers for long-press-to-select - The selectable text view is created lazily — only when `selectable={true}` — so there is zero overhead for non-selectable text (the common case) - Ungates `RCTTextLayoutManager.getTextStorageForAttributedString:` so both platforms can sync Fabric's attributed string into the native text view | iOS | macOS | | ----------- | ----------- | | <img width="506" height="960" alt="Screenshot 2026-03-20 at 11 30 06 PM" src="https://github.com/user-attachments/assets/ac7ce231-0969-4003-a3ca-55fae4e35515" /> | <img width="805" height="868" alt="Screenshot 2026-03-20 at 11 36 36 PM" src="https://github.com/user-attachments/assets/9cc0e951-8bff-41a5-92ff-1f3589ab7279" /> | ## Approach Rather than adding selection logic to the existing `RCTParagraphTextView`, this introduces a separate `RCTParagraphSelectableTextView` (platform-native text view) that is swapped in as the content view when the `selectable` prop is set. This keeps the non-selectable path untouched and avoids runtime cost for the default case. On macOS, mouse events are intercepted at the `RCTParagraphComponentView` level to distinguish single clicks (forwarded to JS for `onPress`) from drag/double-click/triple-click gestures (forwarded to the `NSTextView` for native selection). Touch cancellation walks the view hierarchy to toggle `RCTSurfaceTouchHandler` without modifying that class. On iOS, `UITextView` handles selection natively through its built-in gesture recognizers — no custom hit-testing needed. ## Test plan Add the following to RNTesterPlayground and verify on both macOS and iOS: ```jsx import {Alert, StyleSheet, Text, View} from 'react-native'; function Playground() { return ( <View style={styles.container}> <Text style={styles.heading}>Text Selection Test</Text> <Text style={styles.label}>Selectable text (try click-drag, double-click, right-click):</Text> <Text selectable={true} style={styles.selectableText}> This text should be selectable. Try clicking and dragging to select a range of text. Double-click to select a word. Triple-click to select a line. Right-click to see the context menu. </Text> <Text style={styles.label}>Non-selectable text (default):</Text> <Text style={styles.nonSelectableText}> This text should NOT be selectable. Clicking and dragging should not create a text selection. This is the default behavior. </Text> <Text style={styles.label}>Selectable with nested styles:</Text> <Text selectable={true} style={styles.selectableText}> This has <Text style={styles.bold}>bold text</Text> and{' '} <Text style={styles.italic}>italic text</Text> and{' '} <Text style={styles.colored}>colored text</Text> inside it. Selection should work across all styled ranges. </Text> <Text style={styles.label}>Selectable with onPress (should not conflict):</Text> <Text selectable={true} onPress={() => Alert.alert('Text pressed!')} style={styles.pressableText}> This text is both selectable and pressable. A single click should trigger onPress. Click-drag should start a selection instead. </Text> </View> ); } ``` - [ ] Verify `<Text selectable={true}>` enables click-drag selection on macOS - [ ] Verify double-click selects a word, triple-click selects a line on macOS - [ ] Verify right-click shows native context menu with Copy on macOS - [ ] Verify `<Text selectable={true}>` enables long-press selection on iOS - [ ] Verify non-selectable text (default) is unchanged on both platforms - [ ] Verify `onPress` still fires for single clicks on selectable text - [ ] Verify nested styled text renders correctly when selectable - [ ] Verify selection is cleared when text loses focus 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Kyle Essenmacher <15271436+kessenma@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Saadnajmi
added a commit
that referenced
this pull request
Mar 28, 2026
…selection, CVEs) (#2876) ## Summary Backport of 10 commits from `main` to `0.81-stable`: - **fix(spm):** fix pre-existing macOS porting bugs (#2869) - **fix(transforms):** enable transforms on new arch and fix hit testing on both arches (#2866) - **chore(deps-dev):** bump undici from 5.29.0 to 6.24.0 (#2871) - **fix(fabric):** colors not respecting dark mode appearance, implement platform color (#2867) - **fix:** adjust content inset behavior for macOS (#2806) - **fix:** reduce dependency CVE overrides and refresh vulnerable transitive deps (#2874) - **feat(fabric, text):** support native text selection when selectable={true} (#2864) - **fix:** macOS ScrollView resize and content inset behavior (#2732) - **docs:** add backporting guide and automation (#2863) - **fix(fabric, textinput):** support `enableFocusRing` ### Conflict resolutions - `RCTUIView.h/m` → On 0.81-stable, `RCTUIView` lives in `React/Base/RCTUIKit.h` and `React/Base/macOS/RCTUIKit.m` (the RCTUIKit module refactor hasn't landed on 0.81). Transform changes were manually applied to these files. - `RCTUIKitCompat.h/m` → `NSColor (RCTAppearanceResolving)` category was added to `RCTUIKit.h/m` instead. - Lockfile conflicts resolved by accepting incoming CVE fixes. ## Test Plan - Same as the original PRs - Verify transforms render correctly on macOS (old + new arch) - Verify dark mode color resolution works in Fabric - Verify ScrollView resize and content inset behavior - Verify text selection with selectable={true} - Verify enableFocusRing on TextInput in Fabric 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: eadron <eadron@users.noreply.github.com> Co-authored-by: Kyle Essenmacher <15271436+kessenma@users.noreply.github.com>
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.
Followup from #2845
Summary
<Text selectable={true}>in the Fabric (new architecture) rendererNSTextViewsubclass that handles click-drag, double-click (word), and triple-click (line) selection, plus right-click context menusUITextViewsubclass that leverages built-in gesture recognizers for long-press-to-selectselectable={true}— so there is zero overhead for non-selectable text (the common case)RCTTextLayoutManager.getTextStorageForAttributedString:so both platforms can sync Fabric's attributed string into the native text viewApproach
Rather than adding selection logic to the existing
RCTParagraphTextView, this introduces a separateRCTParagraphSelectableTextView(platform-native text view) that is swapped in as the content view when theselectableprop is set. This keeps the non-selectable path untouched and avoids runtime cost for the default case.On macOS, mouse events are intercepted at the
RCTParagraphComponentViewlevel to distinguish single clicks (forwarded to JS foronPress) from drag/double-click/triple-click gestures (forwarded to theNSTextViewfor native selection). Touch cancellation walks the view hierarchy to toggleRCTSurfaceTouchHandlerwithout modifying that class.On iOS,
UITextViewhandles selection natively through its built-in gesture recognizers — no custom hit-testing needed.Test plan
Add the following to RNTesterPlayground and verify on both macOS and iOS:
<Text selectable={true}>enables click-drag selection on macOS<Text selectable={true}>enables long-press selection on iOSonPressstill fires for single clicks on selectable text🤖 Generated with Claude Code