Skip to content

feat(fabric, text): support native text selection when selectable={true}#2864

Merged
Saadnajmi merged 7 commits intomicrosoft:mainfrom
Saadnajmi:fabric/text-selection-v2
Mar 26, 2026
Merged

feat(fabric, text): support native text selection when selectable={true}#2864
Saadnajmi merged 7 commits intomicrosoft:mainfrom
Saadnajmi:fabric/text-selection-v2

Conversation

@Saadnajmi
Copy link
Copy Markdown
Collaborator

@Saadnajmi Saadnajmi commented Mar 21, 2026

Followup from #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
Screenshot 2026-03-20 at 11 30 06 PM Screenshot 2026-03-20 at 11 36 36 PM

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:

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

Saadnajmi and others added 4 commits March 20, 2026 22:33
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>
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 21, 2026

⚠️ No Changeset found

Latest commit: 73b644a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

- 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>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator Author

@Saadnajmi Saadnajmi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@Saadnajmi Saadnajmi changed the title feat(fabric, Text): support native text selection when selectable={true} feat(fabric, text): support native text selection when selectable={true} Mar 21, 2026
@Saadnajmi Saadnajmi marked this pull request as ready for review March 26, 2026 03:34
@Saadnajmi Saadnajmi requested a review from a team as a code owner March 26, 2026 03:34
@Saadnajmi Saadnajmi merged commit 5ffc39f into microsoft:main Mar 26, 2026
41 of 42 checks passed
@Saadnajmi Saadnajmi deleted the fabric/text-selection-v2 branch March 26, 2026 19:25
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants