Skip to content

Commit

Permalink
Support acceptsFirstMouse prop (facebook#531) (facebook#653)
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
appden committed Nov 19, 2020
1 parent 881c7d2 commit 6963c6e
Show file tree
Hide file tree
Showing 15 changed files with 88 additions and 3 deletions.
23 changes: 22 additions & 1 deletion Libraries/Components/Pressable/Pressable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<React.ElementConfig<typeof View>, 'style'>;
Expand Down Expand Up @@ -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)
|}>;

/**
Expand All @@ -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,
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

exports[`<Pressable /> should render as expected: should deep render when mocked (please verify output manually) 1`] = `
<View
acceptsFirstMouse={true}
accessible={true}
enableFocusRing={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
Expand All @@ -20,7 +22,9 @@ exports[`<Pressable /> should render as expected: should deep render when mocked

exports[`<Pressable /> should render as expected: should deep render when not mocked (please verify output manually) 1`] = `
<View
acceptsFirstMouse={true}
accessible={true}
enableFocusRing={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

exports[`TextInput tests should render as expected: should deep render when mocked (please verify output manually) 1`] = `
<RCTSinglelineTextInputView
acceptsFirstMouse={true}
accessible={true}
allowFontScaling={true}
enableFocusRing={true}
Expand Down Expand Up @@ -30,6 +31,7 @@ exports[`TextInput tests should render as expected: should deep render when mock

exports[`TextInput tests should render as expected: should deep render when not mocked (please verify output manually) 1`] = `
<RCTSinglelineTextInputView
acceptsFirstMouse={true}
accessible={true}
allowFontScaling={true}
enableFocusRing={true}
Expand Down
3 changes: 3 additions & 0 deletions Libraries/Components/Touchable/TouchableBounce.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ class TouchableBounce extends React.Component<Props, State> {
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) &&
Expand Down
3 changes: 3 additions & 0 deletions Libraries/Components/Touchable/TouchableHighlight.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@ class TouchableHighlight extends React.Component<Props, State> {
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) &&
Expand Down
3 changes: 3 additions & 0 deletions Libraries/Components/Touchable/TouchableOpacity.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ class TouchableOpacity extends React.Component<Props, State> {
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) &&
Expand Down
5 changes: 4 additions & 1 deletion Libraries/Components/Touchable/TouchableWithoutFeedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -147,6 +148,8 @@ class TouchableWithoutFeedback extends React.Component<Props, State> {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

exports[`TouchableHighlight renders correctly 1`] = `
<View
acceptsFirstMouse={true}
accessible={true}
enableFocusRing={true}
focusable={false}
Expand Down
1 change: 1 addition & 0 deletions Libraries/Components/View/ReactNativeViewAttributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const UIView = {
accessibilityState: true,
accessibilityValue: true,
accessibilityHint: true,
acceptsFirstMouse: true, // TODO(macOS ISS#2323203)
acceptsKeyboardFocus: true, // TODO(macOS ISS#2323203)
enableFocusRing: true, // TODO(macOS ISS#2323203)
importantForAccessibility: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const ReactNativeViewViewConfigMacOS = {
},
},
validAttributes: {
acceptsFirstMouse: true,
acceptsKeyboardFocus: true,
accessibilityTraits: true,
draggedTypes: true,
Expand Down
8 changes: 8 additions & 0 deletions Libraries/Components/View/ViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,14 @@ export type ViewProps = $ReadOnly<{|
*/
tooltip?: ?string, // TODO(macOS ISS#2323203)

/**
* Specifies whether the view should receive the mouse down event when the
* containing window is in the background.
*
* @platform macos
*/
acceptsFirstMouse?: ?boolean, // TODO(macOS ISS#2323203)

/**
* Specifies whether the view participates in the key view loop as user tabs
* through different controls.
Expand Down
9 changes: 8 additions & 1 deletion React/Base/RCTTouchHandler.m
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,14 @@ - (void)touchesCancelled:(NSSet<UITouch *> *)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];
Expand Down
5 changes: 5 additions & 0 deletions React/Base/RCTUIKit.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions React/Base/macOS/RCTUIKit.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
6 changes: 6 additions & 0 deletions React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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:)]) {
Expand Down

0 comments on commit 6963c6e

Please sign in to comment.