From 6963c6edefc7a3456b2ed9a0d83b891a0cf5eaf2 Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Thu, 19 Nov 2020 11:35:33 -0800 Subject: [PATCH] Support acceptsFirstMouse prop (#531) (#653) Presently in RN macOS, clickable views (buttons, etc.) require two clicks when that window is not in the foreground. This counter to the typical behavior on macOS where controls will default to accepting the mouse event even when in the background (and simultaneously bring to the foreground unless the command key is held). --- Libraries/Components/Pressable/Pressable.js | 23 ++++++++++++++++++- .../__snapshots__/Pressable-test.js.snap | 4 ++++ .../__snapshots__/TextInput-test.js.snap | 2 ++ .../Components/Touchable/TouchableBounce.js | 3 +++ .../Touchable/TouchableHighlight.js | 3 +++ .../Components/Touchable/TouchableOpacity.js | 3 +++ .../Touchable/TouchableWithoutFeedback.js | 5 +++- .../TouchableHighlight-test.js.snap | 1 + .../View/ReactNativeViewAttributes.js | 1 + .../View/ReactNativeViewViewConfigMacOS.js | 1 + Libraries/Components/View/ViewPropTypes.js | 8 +++++++ React/Base/RCTTouchHandler.m | 9 +++++++- React/Base/RCTUIKit.h | 5 ++++ React/Base/macOS/RCTUIKit.m | 17 ++++++++++++++ React/Views/RCTViewManager.m | 6 +++++ 15 files changed, 88 insertions(+), 3 deletions(-) diff --git a/Libraries/Components/Pressable/Pressable.js b/Libraries/Components/Pressable/Pressable.js index c35c9e3337a6d3..deb90c923a5946 100644 --- a/Libraries/Components/Pressable/Pressable.js +++ b/Libraries/Components/Pressable/Pressable.js @@ -26,7 +26,12 @@ import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import usePressability from '../../Pressability/usePressability'; import {normalizeRect, type RectOrSize} from '../../StyleSheet/Rect'; import type {ColorValue} from '../../StyleSheet/StyleSheetTypes'; -import type {LayoutEvent, PressEvent} from '../../Types/CoreEventTypes'; +import type { + LayoutEvent, + MouseEvent, // TODO(macOS ISS#2323203) + PressEvent, +} from '../../Types/CoreEventTypes'; +import type {DraggedTypesType} from '../View/DraggedType'; // TODO(macOS ISS#2323203) import View from '../View/View'; type ViewStyleProp = $ElementType, 'style'>; @@ -131,6 +136,18 @@ type Props = $ReadOnly<{| * Used only for documentation or testing (e.g. snapshot testing). */ testOnly_pressed?: ?boolean, + + // [TODO(macOS ISS#2323203) + acceptsFirstMouse?: ?boolean, + enableFocusRing?: ?boolean, + tooltip?: ?string, + onMouseEnter?: (event: MouseEvent) => void, + onMouseLeave?: (event: MouseEvent) => void, + onDragEnter?: (event: MouseEvent) => void, + onDragLeave?: (event: MouseEvent) => void, + onDrop?: (event: MouseEvent) => void, + draggedTypes?: ?DraggedTypesType, + // ]TODO(macOS ISS#2323203) |}>; /** @@ -139,6 +156,8 @@ type Props = $ReadOnly<{| */ function Pressable(props: Props, forwardedRef): React.Node { const { + acceptsFirstMouse, // [TODO(macOS ISS#2323203) + enableFocusRing, // ]TODO(macOS ISS#2323203) accessible, android_disableSound, android_ripple, @@ -215,6 +234,8 @@ function Pressable(props: Props, forwardedRef): React.Node { {...restProps} {...eventHandlers} {...android_rippleConfig?.viewProps} + acceptsFirstMouse={acceptsFirstMouse !== false && !disabled} // [TODO(macOS ISS#2323203) + enableFocusRing={enableFocusRing !== false && !disabled} // ]TODO(macOS ISS#2323203) accessible={accessible !== false} focusable={focusable !== false} hitSlop={hitSlop} diff --git a/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap b/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap index 5c82f9ab1c7e67..aada55fc3c066a 100644 --- a/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap +++ b/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap @@ -2,7 +2,9 @@ exports[` should render as expected: should deep render when mocked (please verify output manually) 1`] = ` should render as expected: should deep render when mocked exports[` should render as expected: should deep render when not mocked (please verify output manually) 1`] = ` { accessibilityLiveRegion={this.props.accessibilityLiveRegion} accessibilityViewIsModal={this.props.accessibilityViewIsModal} accessibilityElementsHidden={this.props.accessibilityElementsHidden} + acceptsFirstMouse={ + this.props.acceptsFirstMouse !== false && !this.props.disabled + } // TODO(macOS ISS#2323203) enableFocusRing={ (this.props.enableFocusRing === undefined || this.props.enableFocusRing === true) && diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index 291682f1fd8bff..771c847e3e353c 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -307,6 +307,9 @@ class TouchableHighlight extends React.Component { accessibilityLiveRegion={this.props.accessibilityLiveRegion} accessibilityViewIsModal={this.props.accessibilityViewIsModal} accessibilityElementsHidden={this.props.accessibilityElementsHidden} + acceptsFirstMouse={ + this.props.acceptsFirstMouse !== false && !this.props.disabled + } // TODO(macOS ISS#2323203) enableFocusRing={ (this.props.enableFocusRing === undefined || this.props.enableFocusRing === true) && diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index 1297c1f8d2f886..4381977e2b4193 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -236,6 +236,9 @@ class TouchableOpacity extends React.Component { accessibilityLiveRegion={this.props.accessibilityLiveRegion} accessibilityViewIsModal={this.props.accessibilityViewIsModal} accessibilityElementsHidden={this.props.accessibilityElementsHidden} + acceptsFirstMouse={ + this.props.acceptsFirstMouse !== false && !this.props.disabled + } // TODO(macOS ISS#2323203) enableFocusRing={ (this.props.enableFocusRing === undefined || this.props.enableFocusRing === true) && diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index 3fa81ddff6d299..de448985aac3f9 100755 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -69,7 +69,8 @@ type Props = $ReadOnly<{| onPress?: ?(event: PressEvent) => mixed, onPressIn?: ?(event: PressEvent) => mixed, onPressOut?: ?(event: PressEvent) => mixed, - acceptsKeyboardFocus?: ?boolean, // [TODO(macOS ISS#2323203) + acceptsFirstMouse?: ?boolean, // [TODO(macOS ISS#2323203) + acceptsKeyboardFocus?: ?boolean, enableFocusRing?: ?boolean, tooltip?: ?string, onMouseEnter?: (event: MouseEvent) => void, @@ -147,6 +148,8 @@ class TouchableWithoutFeedback extends React.Component { const elementProps: {[string]: mixed, ...} = { ...eventHandlersWithoutBlurAndFocus, accessible: this.props.accessible !== false, + acceptsFirstMouse: + this.props.acceptsFirstMouse !== false && !this.props.disabled, // [TODO(macOS ISS#2323203) // [macOS #656 We need to reconcile between focusable and acceptsKeyboardFocus // (e.g. if one is explicitly disabled, we shouldn't implicitly enable the // other on the underlying view). Prefer passing acceptsKeyboardFocus if diff --git a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap index ec51b5ccb43f91..7b68b8593fd50a 100644 --- a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap +++ b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap @@ -2,6 +2,7 @@ exports[`TouchableHighlight renders correctly 1`] = ` *)touches withEvent:(UIEvent *)event [self interactionsCancelled:touches withEvent:event]; } #else - + +- (BOOL)acceptsFirstMouse:(NSEvent *)event +{ + // This will only be called if the hit-tested view returns YES for acceptsFirstMouse, + // therefore asking it again would be redundant. + return YES; +} + - (void)mouseDown:(NSEvent *)event { [super mouseDown:event]; diff --git a/React/Base/RCTUIKit.h b/React/Base/RCTUIKit.h index a24a95f1c5986b..ef25f3dd273967 100644 --- a/React/Base/RCTUIKit.h +++ b/React/Base/RCTUIKit.h @@ -389,6 +389,11 @@ CGPathRef UIBezierPathCreateCGPathRef(UIBezierPath *path); @property (nonatomic, readwrite, getter=isOpaque) BOOL opaque; @property (nonatomic) CGAffineTransform transform; +/** + * Specifies whether the view should receive the mouse down event when the + * containing window is in the background. + */ +@property (nonatomic, assign) BOOL acceptsFirstMouse; /** * Specifies whether the view participates in the key view loop as user tabs through different controls * This is equivalent to acceptsFirstResponder on mac OS. diff --git a/React/Base/macOS/RCTUIKit.m b/React/Base/macOS/RCTUIKit.m index 395944a14df823..8766d252e5e404 100644 --- a/React/Base/macOS/RCTUIKit.m +++ b/React/Base/macOS/RCTUIKit.m @@ -251,6 +251,23 @@ - (instancetype)initWithCoder:(NSCoder *)coder return RCTUIViewCommonInit([super initWithCoder:coder]); } +- (BOOL)acceptsFirstMouse:(NSEvent *)event +{ + if (self.acceptsFirstMouse || [super acceptsFirstMouse:event]) { + return YES; + } + + // If any RCTUIView view above has acceptsFirstMouse set, then return YES here. + NSView *view = self; + while ((view = view.superview)) { + if ([view isKindOfClass:[RCTUIView class]] && [(RCTUIView *)view acceptsFirstMouse]) { + return YES; + } + } + + return NO; +} + - (BOOL)acceptsFirstResponder { return [self canBecomeFirstResponder]; diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 5749e5e5534bdd..e1dff7e8c44fc2 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -367,6 +367,12 @@ - (RCTShadowView *)shadowView #if TARGET_OS_OSX // [TODO(macOS ISS#2323203) // macOS properties +RCT_CUSTOM_VIEW_PROPERTY(acceptsFirstMouse, BOOL, RCTView) +{ + if ([view respondsToSelector:@selector(setAcceptsFirstMouse:)]) { + view.acceptsFirstMouse = json ? [RCTConvert BOOL:json] : defaultView.acceptsFirstMouse; + } +} RCT_CUSTOM_VIEW_PROPERTY(acceptsKeyboardFocus, BOOL, RCTView) { if ([view respondsToSelector:@selector(setFocusable:)]) {