Skip to content

Commit 3ab5b85

Browse files
Nick LefeverSaadnajmi
authored andcommitted
[fabric] Support selection of Paragraph component text content
Summary: Override the hitTest and mouseDown handler in `RCTParagraphComponentView` to forward mouse drag events to the underlying NSTextView to get text selection support in Fabric with correct rendering. Test Plan: - Run Zeratul with Fabric enabled. - Select text https://pxl.cl/3q3b3 Reviewers: shawndempsey, chpurrer, #rn-desktop Reviewed By: shawndempsey Differential Revision: https://phabricator.intern.facebook.com/D49465174 Tasks: T163838519
1 parent 7b738c1 commit 3ab5b85

File tree

1 file changed

+86
-6
lines changed

1 file changed

+86
-6
lines changed

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

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
#if !TARGET_OS_OSX // [macOS]
1212
#import <MobileCoreServices/UTCoreTypes.h>
13-
#endif // [macOS]
13+
#else // [macOS
14+
#import <React/RCTSurfaceTouchHandler.h>
15+
#endif // macOS]
1416

1517
#import <react/renderer/components/text/ParagraphComponentDescriptor.h>
1618
#import <react/renderer/components/text/ParagraphProps.h>
@@ -43,10 +45,10 @@ @interface RCTParagraphTextView : NSTextView // [macOS]
4345
@property (nonatomic) ParagraphAttributes paragraphAttributes;
4446
@property (nonatomic) LayoutMetrics layoutMetrics;
4547

46-
#if TARGET_OS_OSX // [macOS]
48+
#if TARGET_OS_OSX // [macOS
4749
/// UIKit compatibility shim that simply calls `[self setNeedsDisplay:YES]`
4850
- (void)setNeedsDisplay;
49-
#endif
51+
#endif // macOS]
5052

5153
@end
5254

@@ -55,6 +57,9 @@ @interface RCTParagraphComponentView () <UIEditMenuInteractionDelegate>
5557

5658
@property (nonatomic, nullable) UIEditMenuInteraction *editMenuInteraction API_AVAILABLE(ios(16.0));
5759

60+
@end
61+
#else // [macOS
62+
@interface RCTParagraphComponentView () <NSTextViewDelegate>
5863
@end
5964
#endif // [macOS]
6065

@@ -83,9 +88,8 @@ - (instancetype)initWithFrame:(CGRect)frame
8388
self.accessibilityRole = NSAccessibilityStaticTextRole;
8489
// Fix blurry text on non-retina displays.
8590
self.canDrawSubviewsIntoLayer = YES;
86-
// The NSTextView is responsible for drawing text and managing selection.
8791
_textView = [[RCTParagraphTextView alloc] initWithFrame:self.bounds];
88-
// The RCTParagraphComponentUnfocusableTextView is only used for rendering and should not appear in the a11y hierarchy.
92+
_textView.delegate = self;
8993
_textView.accessibilityElement = NO;
9094
_textView.usesFontPanel = NO;
9195
_textView.drawsBackground = NO;
@@ -325,14 +329,89 @@ - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
325329
[menuController showMenuFromView:self rect:self.bounds];
326330
}
327331
}
328-
#endif // [macOS]
332+
#else // [macOS
333+
- (NSView *)hitTest:(NSPoint)point
334+
{
335+
// We will forward mouse click events to the NSTextView ourselves to prevent NSTextView from swallowing events that may be handled in JS (e.g. long press).
336+
NSView *hitView = [super hitTest:point];
337+
338+
NSEventType eventType = NSApp.currentEvent.type;
339+
BOOL isMouseClickEvent = NSEvent.pressedMouseButtons > 0;
340+
BOOL isMouseMoveEventType = eventType == NSEventTypeMouseMoved || eventType == NSEventTypeMouseEntered || eventType == NSEventTypeMouseExited || eventType == NSEventTypeCursorUpdate;
341+
BOOL isMouseMoveEvent = !isMouseClickEvent && isMouseMoveEventType;
342+
BOOL isTextViewClick = (hitView && hitView == _textView) && !isMouseMoveEvent;
343+
344+
return isTextViewClick ? self : hitView;
345+
}
346+
347+
- (void)mouseDown:(NSEvent *)event
348+
{
349+
if (!_textView.selectable) {
350+
[super mouseDown:event];
351+
return;
352+
}
353+
354+
// Double/triple-clicks should be forwarded to the NSTextView.
355+
BOOL shouldForward = event.clickCount > 1;
356+
357+
if (!shouldForward) {
358+
// Peek at next event to know if a selection should begin.
359+
NSEvent *nextEvent = [self.window nextEventMatchingMask:NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged
360+
untilDate:[NSDate distantFuture]
361+
inMode:NSEventTrackingRunLoopMode
362+
dequeue:NO];
363+
shouldForward = nextEvent.type == NSEventTypeLeftMouseDragged;
364+
}
365+
366+
if (shouldForward) {
367+
NSView *contentView = self.window.contentView;
368+
// -[NSView hitTest:] takes coordinates in a view's superview coordinate system.
369+
NSPoint point = [contentView.superview convertPoint:event.locationInWindow fromView:nil];
370+
371+
// Start selection if we're still selectable and hit-testable.
372+
if (_textView.selectable && [contentView hitTest:point] == self) {
373+
[[RCTSurfaceTouchHandler surfaceTouchHandlerForView:self] cancelTouchWithEvent:event];
374+
[self.window makeFirstResponder:_textView];
375+
[_textView mouseDown:event];
376+
}
377+
} else {
378+
// Clear selection for single clicks.
379+
_textView.selectedRange = NSMakeRange(NSNotFound, 0);
380+
}
381+
}
329382

383+
#pragma mark - Selection
384+
385+
- (void)textDidEndEditing:(NSNotification *)notification
386+
{
387+
_textView.selectedRange = NSMakeRange(NSNotFound, 0);
388+
}
389+
390+
#endif // macOS]
391+
392+
#if !TARGET_OS_OSX // [macOS]
330393
- (BOOL)canBecomeFirstResponder
331394
{
332395
const auto &paragraphProps = static_cast<const ParagraphProps &>(*_props);
333396
return paragraphProps.isSelectable;
334397
}
398+
#else
399+
- (BOOL)becomeFirstResponder
400+
{
401+
if (![super becomeFirstResponder]) {
402+
return NO;
403+
}
404+
405+
return YES;
406+
}
335407

408+
- (BOOL)canBecomeFirstResponder
409+
{
410+
return self.focusable;
411+
}
412+
#endif // macOS]
413+
414+
#if !TARGET_OS_OSX // [macOS]
336415
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
337416
{
338417
const auto &paragraphProps = static_cast<const ParagraphProps &>(*_props);
@@ -347,6 +426,7 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
347426
return NO;
348427
#endif // macOS]
349428
}
429+
#endif // [macOS]
350430

351431
- (void)copy:(id)sender
352432
{

0 commit comments

Comments
 (0)