Skip to content

Commit 01a8159

Browse files
shwantonShawn DempseySaadnajmi
authored
Only forward click events to NSTextview (#1515)
* Forward mouse clicks to NSTextView to prevent swallowing of events Change HitTest to forward events correctly Add MouseDown & forward events correctly Add touch handler static methods * Show the NSTextView contextMenu on right click * No need to forward copy to nstextview since it will have first responder upon click * Fix Event mask deprecation warnings * Fix missing macOS tag & extra comment Co-authored-by: Shawn Dempsey <shawndempsey@fb.com> Co-authored-by: Saad Najmi <sanajmi@microsoft.com>
1 parent 1b4706f commit 01a8159

File tree

4 files changed

+96
-25
lines changed

4 files changed

+96
-25
lines changed

Libraries/Text/Text/RCTTextView.m

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#import <React/RCTFocusChangeEvent.h> // [macOS]
1818

1919
#import <React/RCTTextShadowView.h>
20+
#import <React/RCTTouchHandler.h>
2021

2122
#import <QuartzCore/QuartzCore.h>
2223

@@ -208,7 +209,6 @@ - (void)drawRect:(CGRect)rect
208209
return;
209210
}
210211

211-
212212
NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject;
213213
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
214214

@@ -407,36 +407,65 @@ - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
407407
}
408408
#else // [macOS
409409

410+
- (NSView *)hitTest:(NSPoint)point
411+
{
412+
// 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).
413+
NSView *hitView = [super hitTest:point];
414+
415+
NSEventType eventType = NSApp.currentEvent.type;
416+
BOOL isMouseClickEvent = NSEvent.pressedMouseButtons > 0;
417+
BOOL isMouseMoveEventType = eventType == NSEventTypeMouseMoved || eventType == NSEventTypeMouseEntered || eventType == NSEventTypeMouseExited || eventType == NSEventTypeCursorUpdate;
418+
BOOL isMouseMoveEvent = !isMouseClickEvent && isMouseMoveEventType;
419+
BOOL isTextViewClick = (hitView && hitView == _textView) && !isMouseMoveEvent;
420+
421+
return isTextViewClick ? self : hitView;
422+
}
423+
410424
- (void)rightMouseDown:(NSEvent *)event
411425
{
412-
if (_selectable == NO) {
426+
427+
if (self.selectable == NO) {
413428
[super rightMouseDown:event];
414429
return;
415430
}
416-
NSText *fieldEditor = [self.window fieldEditor:YES forObject:self];
417-
NSMenu *fieldEditorMenu = [fieldEditor menuForEvent:event];
418431

419-
RCTAssert(fieldEditorMenu, @"Unable to obtain fieldEditor's context menu");
420-
421-
if (fieldEditorMenu) {
422-
NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
432+
[[RCTTouchHandler touchHandlerForView:self] cancelTouchWithEvent:event];
433+
[_textView rightMouseDown:event];
434+
}
423435

424-
for (NSMenuItem *fieldEditorMenuItem in fieldEditorMenu.itemArray) {
425-
if (fieldEditorMenuItem.action == @selector(copy:)) {
426-
NSMenuItem *item = [fieldEditorMenuItem copy];
436+
- (void)mouseDown:(NSEvent *)event
437+
{
438+
if (!self.selectable) {
439+
[super mouseDown:event];
440+
return;
441+
}
427442

428-
item.target = self;
429-
[menu addItem:item];
443+
// Double/triple-clicks should be forwarded to the NSTextView.
444+
BOOL shouldForward = event.clickCount > 1;
430445

431-
break;
432-
}
433-
}
446+
if (!shouldForward) {
447+
// Peek at next event to know if a selection should begin.
448+
NSEvent *nextEvent = [self.window nextEventMatchingMask:NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged
449+
untilDate:[NSDate distantFuture]
450+
inMode:NSEventTrackingRunLoopMode
451+
dequeue:NO];
452+
shouldForward = nextEvent.type == NSEventTypeLeftMouseDragged;
453+
}
434454

435-
RCTAssert(menu.numberOfItems > 0, @"Unable to create context menu with \"Copy\" item");
455+
if (shouldForward) {
456+
NSView *contentView = self.window.contentView;
457+
// -[NSView hitTest:] takes coordinates in a view's superview coordinate system.
458+
NSPoint point = [contentView.superview convertPoint:event.locationInWindow fromView:nil];
436459

437-
if (menu.numberOfItems > 0) {
438-
[NSMenu popUpContextMenu:menu withEvent:event forView:self];
460+
// Start selection if we're still selectable and hit-testable.
461+
if (self.selectable && [contentView hitTest:point] == self) {
462+
[[RCTTouchHandler touchHandlerForView:self] cancelTouchWithEvent:event];
463+
[self.window makeFirstResponder:_textView];
464+
[_textView mouseDown:event];
439465
}
466+
} else {
467+
// Clear selection for single clicks.
468+
_textView.selectedRange = NSMakeRange(NSNotFound, 0);
440469
}
441470
}
442471
#endif // macOS]
@@ -533,8 +562,6 @@ - (void)copy:(id)sender
533562
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
534563
pasteboard.items = @[item];
535564
#else // [macOS
536-
[_textView copy:sender];
537-
538565
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
539566
[pasteboard clearContents];
540567
[pasteboard setData:rtf forType:NSPasteboardTypeRTFD];

Libraries/Text/TextInput/Singleline/RCTUITextField.m

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
#import <React/RCTBackedTextInputDelegateAdapter.h>
1313
#import <React/RCTBackedTextInputDelegate.h> // [macOS]
1414
#import <React/RCTTextAttributes.h>
15-
15+
#import <React/RCTTouchHandler.h> // [TODO(macOS GH#774)
1616

1717
#if TARGET_OS_OSX // [macOS
1818

@@ -436,7 +436,7 @@ - (CGRect)editingRectForBounds:(CGRect)bounds
436436

437437
#else // [macOS
438438

439-
#pragma mark - NSTextViewDelegate methods
439+
#pragma mark - NSTextFieldDelegate methods
440440

441441
- (void)textDidChange:(NSNotification *)notification
442442
{
@@ -473,6 +473,15 @@ - (BOOL)textView:(NSTextView *)aTextView shouldChangeTextInRange:(NSRange)aRange
473473
return NO;
474474
}
475475

476+
- (NSMenu *)textView:(NSTextView *)view menu:(NSMenu *)menu forEvent:(NSEvent *)event atIndex:(NSUInteger)charIndex
477+
{
478+
if (menu) {
479+
[[RCTTouchHandler touchHandlerForView:self] willShowMenuWithEvent:event];
480+
}
481+
482+
return menu;
483+
}
484+
476485
#endif // macOS]
477486

478487
#pragma mark - Overrides

React/Base/RCTTouchHandler.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,13 @@
1919
- (void)detachFromView:(RCTUIView *)view; // [macOS]
2020

2121
- (void)cancel;
22+
2223
#if TARGET_OS_OSX // [macOS
23-
- (void)willShowMenuWithEvent:(NSEvent*)event;
24+
+ (instancetype)touchHandlerForEvent:(NSEvent *)event;
25+
+ (instancetype)touchHandlerForView:(NSView *)view;
26+
27+
- (void)willShowMenuWithEvent:(NSEvent *)event;
28+
- (void)cancelTouchWithEvent:(NSEvent *)event;
2429
#endif // macOS]
2530

2631
@end

React/Base/RCTTouchHandler.m

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,12 +577,42 @@ - (void)cancel
577577
}
578578

579579
#if TARGET_OS_OSX // [macOS
580-
- (void)willShowMenuWithEvent:(NSEvent*)event
580+
+ (instancetype)touchHandlerForEvent:(NSEvent *)event {
581+
// The window's frame view must be used for hit testing against `locationInWindow`
582+
NSView *hitView = [event.window.contentView.superview hitTest:event.locationInWindow];
583+
return [self touchHandlerForView:hitView];
584+
}
585+
586+
+ (instancetype)touchHandlerForView:(NSView *)view {
587+
if ([view isKindOfClass:[RCTRootView class]]) {
588+
// The RCTTouchHandler is attached to the contentView.
589+
view = ((RCTRootView *)view).contentView;
590+
}
591+
592+
while (view) {
593+
for (NSGestureRecognizer *gestureRecognizer in view.gestureRecognizers) {
594+
if ([gestureRecognizer isKindOfClass:[self class]]) {
595+
return (RCTTouchHandler *)gestureRecognizer;
596+
}
597+
}
598+
599+
view = view.superview;
600+
}
601+
602+
return nil;
603+
}
604+
605+
- (void)willShowMenuWithEvent:(NSEvent *)event
581606
{
582607
if (event.type == NSEventTypeRightMouseDown) {
583608
[self interactionsEnded:[NSSet setWithObject:event] withEvent:event];
584609
}
585610
}
611+
612+
- (void)cancelTouchWithEvent:(NSEvent *)event
613+
{
614+
[self interactionsCancelled:[NSSet setWithObject:event] withEvent:event];
615+
}
586616
#endif // macOS]
587617

588618
#pragma mark - UIGestureRecognizerDelegate

0 commit comments

Comments
 (0)