Uh oh!
There was an error while loading. Please reload this page.
feat(fabric, text): support native text selection when selectable={true} - #2864
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>
|
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- 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>
Uh oh!
There was an error while loading. Please reload this page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Saad Najmi (Saadnajmi)
left a comment
There was a problem hiding this comment.
Some comments
- 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>
Uh oh!
There was an error while loading. Please reload this page.
…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>
…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>
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