Skip to content

fix: support Text selectable prop on macOS with Fabric#2845

Closed
kessenma wants to merge 1 commit intomicrosoft:mainfrom
kessenma:fix/fabric-text-selectable-macos
Closed

fix: support Text selectable prop on macOS with Fabric#2845
kessenma wants to merge 1 commit intomicrosoft:mainfrom
kessenma:fix/fabric-text-selectable-macos

Conversation

@kessenma
Copy link
Copy Markdown

@kessenma kessenma commented Mar 7, 2026

Summary

Fixes #2844

The selectable prop on <Text> was completely ignored on macOS when using Fabric (New Architecture). The isSelectable handling in RCTParagraphComponentView.updateProps: was gated behind #if !TARGET_OS_OSX, so on macOS no selection support was ever enabled.

This adds macOS text selection support by using an NSTextView for both rendering and selection (matching the pattern used by the old architecture in RCTTextView.mm):

  • No double rendering: When selectable, RCTParagraphTextView skips its drawRect: and the NSTextView handles all text rendering via its own NSLayoutManager
  • Native selection: Click-drag, double-click (word), triple-click (line), and right-click context menu all work natively
  • Zero impact when disabled: When selectable={false} (default), no NSTextView is created and behavior is completely unchanged
  • Properly recycles: The NSTextView is torn down in prepareForRecycle

Key implementation details

  • RCTParagraphSelectionTextView (NSTextView subclass with canBecomeKeyView = NO) prevents double-focus
  • Text storage is synced from RCTTextLayoutManager.getTextStorageForAttributedString: (existing macOS-only API)
  • Mouse event handling (hitTest:, mouseDown:, rightMouseDown:) ported from old arch's RCTTextView.mm
  • resignFirstResponder prevents losing focus during active selection tracking

Test plan

  • macOS build succeeds
  • iOS build succeeds (all changes are #if TARGET_OS_OSX guarded)
  • Tested in RNTester macOS app:
    • "Selectable" text example: click-drag selects text
    • "Focusable" section: selectable + focusable text works
    • Double-click selects word
    • Right-click context menu works
    • Non-selectable text unaffected

🤖 Generated with Claude Code

…ure)

The `selectable` prop on `<Text>` was completely ignored on macOS when
using Fabric. The `isSelectable` handling in `updateProps` was gated
behind `#if !TARGET_OS_OSX`, so on macOS no selection support was enabled.

This adds macOS text selection support to `RCTParagraphComponentView` by
using an `NSTextView` overlay (similar to the old architecture's approach
in `RCTTextView.mm`). When `selectable={true}`:

- An `NSTextView` is created and added as a subview for selection handling
- The `NSTextView`'s text storage is synced from `RCTTextLayoutManager`
- `RCTParagraphTextView` skips its own drawing to avoid double-rendering
- Mouse events (click-drag, double/triple-click, right-click) are
  forwarded to the `NSTextView` for native selection behavior

When `selectable={false}` (default), behavior is unchanged — no
`NSTextView` is created and rendering works exactly as before.

Fixes microsoft#2844

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kessenma kessenma requested a review from a team as a code owner March 7, 2026 18:27
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 7, 2026

⚠️ No Changeset found

Latest commit: 95868d6

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

@kessenma
Copy link
Copy Markdown
Author

kessenma commented Mar 7, 2026

@microsoft-github-policy-service agree

@Saadnajmi
Copy link
Copy Markdown
Collaborator

Curious, did you intend this to be a copy of #2673 ? Did you do any testing to make sure it works?

@kessenma
Copy link
Copy Markdown
Author

@Saadnajmi

I made this PR because I noticed I was not able to copy text in the latest version of the repo on my personal project and yes I did test. I put this in the PR description

Tested in RNTester macOS app:
"Selectable" text example: click-drag selects text
"Focusable" section: selectable + focusable text works

@Saadnajmi
Copy link
Copy Markdown
Collaborator

Hello! I like the changes in this branch, but there are some things I want to do slightly differently. Namely, adding iOS as well so this can maybe get pushed upstream to React Native too. I made #2864 based on both my old and your changes. If that merges, I'll close this PR. Sound good?

@Saadnajmi
Copy link
Copy Markdown
Collaborator

Closing in favor of #2864 . I'll try to add a coauthor note before merging so credit stays there :)

@Saadnajmi Saadnajmi closed this Mar 26, 2026
Saadnajmi added a commit that referenced this pull request Mar 26, 2026
…ue} (#2864)

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 |
| ----------- | ----------- |
| <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 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>
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.

Text selectable prop is ignored on macOS with Fabric (New Architecture)

2 participants