Skip to content

Commit d974ca1

Browse files
committed
feat: put partial text selection behind the enableIOSPartialTextSelection flag
1 parent ec3503a commit d974ca1

22 files changed

Lines changed: 493 additions & 102 deletions

‎packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm‎

Lines changed: 120 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,21 @@ @interface RCTParagraphTextView : UIView
5050

5151
@end
5252

53+
#if !TARGET_OS_TV
54+
@interface RCTParagraphComponentView () <UIEditMenuInteractionDelegate>
55+
56+
@property (nonatomic, nullable) UIEditMenuInteraction *editMenuInteraction API_AVAILABLE(ios(16.0));
57+
58+
@end
59+
#else
5360
@interface RCTParagraphComponentView ()
5461
@end
62+
#endif
5563

5664
@implementation RCTParagraphComponentView {
5765
ParagraphAttributes _paragraphAttributes;
5866
RCTParagraphComponentAccessibilityProvider *_accessibilityProvider;
67+
UILongPressGestureRecognizer *_longPressGestureRecognizer;
5968
RCTParagraphTextView *_textView;
6069
CGRect _textLayoutFrame;
6170
#if !TARGET_OS_TV
@@ -186,7 +195,12 @@ - (void)prepareForRecycle
186195
_textView.state = nullptr;
187196
_accessibilityProvider = nil;
188197
#if !TARGET_OS_TV
189-
[self disableContextMenu];
198+
// Recycling removes only the selection text view. The long press stays,
199+
// because the view keeps its `_props`: when the next paragraph is selectable
200+
// too, `updateProps:` sees no change and does not add the long press again.
201+
if (ReactNativeFeatureFlags::enableIOSPartialTextSelection()) {
202+
[self disableTextSelection];
203+
}
190204
#endif
191205
}
192206

@@ -217,7 +231,7 @@ - (void)_updateTextViewFrame
217231

218232
#if !TARGET_OS_TV
219233
const auto &paragraphProps = static_cast<const ParagraphProps &>(*_props);
220-
if (paragraphProps.isSelectable) {
234+
if (paragraphProps.isSelectable && ReactNativeFeatureFlags::enableIOSPartialTextSelection()) {
221235
// `drawingFrame` is the frame `RCTParagraphTextView` draws the glyphs into,
222236
// compression adjustment included. The selection must use the same frame,
223237
// or the selection rects sit away from the glyphs they select.
@@ -370,18 +384,120 @@ - (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
370384
#pragma mark - Context Menu
371385

372386
#if !TARGET_OS_TV
387+
- (void)enableContextMenu
388+
{
389+
if (ReactNativeFeatureFlags::enableIOSPartialTextSelection()) {
390+
[self enableTextSelection];
391+
return;
392+
}
393+
394+
_longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self
395+
action:@selector(handleLongPress:)];
396+
397+
if (@available(iOS 16.0, *)) {
398+
_editMenuInteraction = [[UIEditMenuInteraction alloc] initWithDelegate:self];
399+
[self addInteraction:_editMenuInteraction];
400+
}
401+
[self addGestureRecognizer:_longPressGestureRecognizer];
402+
}
403+
404+
- (void)disableContextMenu
405+
{
406+
if (ReactNativeFeatureFlags::enableIOSPartialTextSelection()) {
407+
[self disableTextSelection];
408+
return;
409+
}
410+
411+
[self removeGestureRecognizer:_longPressGestureRecognizer];
412+
if (@available(iOS 16.0, *)) {
413+
[self removeInteraction:_editMenuInteraction];
414+
_editMenuInteraction = nil;
415+
}
416+
_longPressGestureRecognizer = nil;
417+
}
418+
419+
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
420+
{
421+
if (@available(iOS 16.0, macCatalyst 16.0, *)) {
422+
CGPoint location = [gesture locationInView:self];
423+
UIEditMenuConfiguration *config = [UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:location];
424+
if (_editMenuInteraction) {
425+
[_editMenuInteraction presentEditMenuWithConfiguration:config];
426+
}
427+
} else {
428+
UIMenuController *menuController = [UIMenuController sharedMenuController];
429+
430+
if (menuController.isMenuVisible) {
431+
return;
432+
}
433+
434+
[menuController showMenuFromView:self rect:self.bounds];
435+
}
436+
}
437+
438+
- (BOOL)canBecomeFirstResponder
439+
{
440+
// With partial selection, `_selectableTextView` is the responder that owns the selection.
441+
if (ReactNativeFeatureFlags::enableIOSPartialTextSelection()) {
442+
return NO;
443+
}
444+
445+
const auto &paragraphProps = static_cast<const ParagraphProps &>(*_props);
446+
return paragraphProps.isSelectable;
447+
}
448+
449+
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
450+
{
451+
// With partial selection, `_selectableTextView` copies the selected range.
452+
// This class implements `copy:` for the long-press menu, so the inherited
453+
// answer for `copy:` is YES and would make the paragraph copy all its text.
454+
if (ReactNativeFeatureFlags::enableIOSPartialTextSelection()) {
455+
return action != @selector(copy:) && [super canPerformAction:action withSender:sender];
456+
}
457+
458+
const auto &paragraphProps = static_cast<const ParagraphProps &>(*_props);
459+
460+
if (paragraphProps.isSelectable && action == @selector(copy:)) {
461+
return YES;
462+
}
463+
464+
return [self.nextResponder canPerformAction:action withSender:sender];
465+
}
466+
467+
- (void)copy:(id)sender
468+
{
469+
NSAttributedString *attributedText = self.attributedText;
470+
471+
NSMutableDictionary *item = [NSMutableDictionary new];
472+
473+
NSData *rtf = [attributedText dataFromRange:NSMakeRange(0, attributedText.length)
474+
documentAttributes:@{NSDocumentTypeDocumentAttribute : NSRTFDTextDocumentType}
475+
error:nil];
476+
477+
if (rtf) {
478+
[item setObject:rtf forKey:(id)kUTTypeFlatRTFD];
479+
}
480+
481+
[item setObject:attributedText.string forKey:(id)kUTTypeUTF8PlainText];
482+
483+
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
484+
pasteboard.items = @[ item ];
485+
}
486+
487+
#pragma mark - Text Selection
488+
373489
/*
374490
* Selection is provided by a `UITextView` laid out with the paragraph's own
375491
* TextKit stack, which gives the platform behaviour users expect: long press to
376492
* select a word, drag handles to extend the range and an edit menu that copies
377493
* only what is selected.
378494
*/
379-
- (void)enableContextMenu
495+
- (void)enableTextSelection
380496
{
381497
_selectableTextViewNeedsUpdate = YES;
382498
}
383499

384-
- (void)disableContextMenu
500+
- (void)disableTextSelection
385501
{
386502
_selectableTextViewNeedsUpdate = NO;
387503
[self removeSelectableTextView];
@@ -445,13 +561,6 @@ - (void)updateSelectableTextViewWithDrawingFrame:(CGRect)drawingFrame
445561
_selectableTextView.frame = drawingFrame;
446562
_selectableTextView.sourceAttributedText = attributedText;
447563
}
448-
449-
- (BOOL)canBecomeFirstResponder
450-
{
451-
// While selectable, `_selectableTextView` is the responder that owns the selection.
452-
return NO;
453-
}
454-
455564
#else
456565
- (void)enableContextMenu
457566
{

0 commit comments

Comments
 (0)