Commit 5ffc39f
feat(fabric, text): support native text selection when selectable={true} (#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>1 parent 13835e9 commit 5ffc39f
File tree
3 files changed
+311
-13
lines changed- packages/react-native
- ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager
- React/Fabric/Mounting/ComponentViews/Text
3 files changed
+311
-13
lines changed
0 commit comments