Skip to content

Commit

Permalink
Fix onFocus/onBlur event bubbling (microsoft#506)
Browse files Browse the repository at this point in the history
* Update scripts to publish react-native-macos-init

* Clean up merge markers

* Restored ios:macos RNTester parity except for InputAccessoryView.

* Revert "Restored ios:macos RNTester parity except for InputAccessoryView."

This reverts commit 5a67ae0.

* Remove unnecessary android builds and tar file upload.

* Fix onFocus/onBlur View events to properly bubble.

* The Text component can also be selectable={true} and needs the same focus/blur event triggering as View.

Co-authored-by: React-Native Bot <53619745+rnbot@users.noreply.github.com>
  • Loading branch information
tom-un and rnbot committed Jul 20, 2020
1 parent 20b896e commit ec1ea19
Show file tree
Hide file tree
Showing 16 changed files with 237 additions and 32 deletions.
Empty file modified .ado/setup_droid_deps.sh
100644 → 100755
Empty file.
17 changes: 17 additions & 0 deletions Libraries/Components/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const View = require('./View/View');
const invariant = require('invariant');

import type {PressEvent} from '../Types/CoreEventTypes';
import type {FocusEvent, BlurEvent} from './TextInput/TextInput'; // TODO(OSS Candidate ISS#2710739)

type ButtonProps = $ReadOnly<{|
/**
Expand Down Expand Up @@ -100,6 +101,18 @@ type ButtonProps = $ReadOnly<{|
* Used to locate this view in end-to-end tests.
*/
testID?: ?string,

// [TODO(OSS Candidate ISS#2710739)
/**
* Handler to be called when the button receives key focus
*/
onBlur?: ?(e: BlurEvent) => void,

/**
* Handler to be called when the button loses key focus
*/
onFocus?: ?(e: FocusEvent) => void,
// ]TODO(OSS Candidate ISS#2710739)
|}>;

/**
Expand Down Expand Up @@ -147,6 +160,8 @@ class Button extends React.Component<ButtonProps> {
nextFocusUp,
disabled,
testID,
onFocus, // TODO(OSS Candidate ISS#2710739)
onBlur, // TODO(OSS Candidate ISS#2710739)
} = this.props;
const buttonStyles = [styles.button];
const textStyles = [styles.text];
Expand Down Expand Up @@ -189,6 +204,8 @@ class Button extends React.Component<ButtonProps> {
testID={testID}
disabled={disabled}
onPress={onPress}
onFocus={onFocus} // TODO(OSS Candidate ISS#2710739)
onBlur={onBlur} // TODO(OSS Candidate ISS#2710739)
touchSoundDisabled={touchSoundDisabled}>
<View style={buttonStyles}>
<Text style={textStyles} disabled={disabled}>
Expand Down
6 changes: 6 additions & 0 deletions Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,8 @@ const TextInput = createReactClass({
<TouchableWithoutFeedback
onLayout={props.onLayout}
onPress={this._onPress}
onFocus={this._onFocus} // TODO(macOS ISS#2323203)
onBlur={this._onBlur} // TODO(macOS ISS#2323203)
rejectResponderTermination={true}
accessible={props.accessible}
accessibilityLabel={props.accessibilityLabel}
Expand Down Expand Up @@ -1196,6 +1198,8 @@ const TextInput = createReactClass({
<TouchableWithoutFeedback
onLayout={props.onLayout}
onPress={this._onPress}
onFocus={this._onFocus} // TODO(macOS ISS#2323203)
onBlur={this._onBlur} // TODO(macOS ISS#2323203)
rejectResponderTermination={props.rejectResponderTermination}
accessible={props.accessible}
accessibilityLabel={props.accessibilityLabel}
Expand Down Expand Up @@ -1254,6 +1258,8 @@ const TextInput = createReactClass({
<TouchableWithoutFeedback
onLayout={props.onLayout}
onPress={this._onPress}
onFocus={this._onFocus} // TODO(macOS ISS#2323203)
onBlur={this._onBlur} // TODO(macOS ISS#2323203)
accessible={this.props.accessible}
accessibilityLabel={this.props.accessibilityLabel}
accessibilityRole={this.props.accessibilityRole}
Expand Down
2 changes: 2 additions & 0 deletions Libraries/Components/Touchable/TouchableBounce.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ const TouchableBounce = ((createReactClass({
onDragEnter={this.props.onDragEnter}
onDragLeave={this.props.onDragLeave}
onDrop={this.props.onDrop}
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
draggedTypes={this.props.draggedTypes} // ]TODO(macOS ISS#2323203)
>
{this.props.children}
Expand Down
2 changes: 2 additions & 0 deletions Libraries/Components/Touchable/TouchableHighlight.js
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,8 @@ const TouchableHighlight = ((createReactClass({
onDragEnter={this.props.onDragEnter}
onDragLeave={this.props.onDragLeave}
onDrop={this.props.onDrop}
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
draggedTypes={this.props.draggedTypes} // ]TODO(macOS/win ISS#2323203)
nativeID={this.props.nativeID}
testID={this.props.testID}>
Expand Down
2 changes: 2 additions & 0 deletions Libraries/Components/Touchable/TouchableOpacity.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,8 @@ const TouchableOpacity = ((createReactClass({
onDragEnter={this.props.onDragEnter}
onDragLeave={this.props.onDragLeave}
onDrop={this.props.onDrop}
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
draggedTypes={this.props.draggedTypes} // ]TODO(macOS ISS#2323203)
/* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete
Expand Down
10 changes: 6 additions & 4 deletions Libraries/Components/Touchable/TouchableWithoutFeedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,10 +340,12 @@ const TouchableWithoutFeedback = ((createReactClass({
tooltip: this.props.tooltip, // TODO(macOS/win ISS#2323203)
onClick: this.touchableHandlePress, // TODO(android ISS)
onMouseEnter: this.props.onMouseEnter, // [TODO(macOS ISS#2323203)
onMouseLeave: this.props.onMouseLeave, // [TODO(macOS ISS#2323203)
onDragEnter: this.props.onDragEnter, // [TODO(macOS ISS#2323203)
onDragLeave: this.props.onDragLeave, // [TODO(macOS ISS#2323203)
onDrop: this.props.onDrop, // [TODO(macOS ISS#2323203)
onMouseLeave: this.props.onMouseLeave,
onDragEnter: this.props.onDragEnter,
onDragLeave: this.props.onDragLeave,
onDrop: this.props.onDrop,
onFocus: this.props.onFocus,
onBlur: this.props.onBlur,
draggedTypes: this.props.draggedTypes, // ]TODO(macOS ISS#2323203)
children,
});
Expand Down
3 changes: 3 additions & 0 deletions Libraries/Text/Text/RCTTextView.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
*/

#import <React/RCTComponent.h>
#import <React/RCTEventDispatcher.h> // TODO(OSS Candidate ISS#2710739)

#import <React/RCTUIKit.h> // TODO(macOS ISS#2323203)

NS_ASSUME_NONNULL_BEGIN

@interface RCTTextView : RCTUIView // TODO(macOS ISS#3536887)

- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher; // TODO(OSS Candidate ISS#2710739)

@property (nonatomic, assign) BOOL selectable;

- (void)setTextStorage:(NSTextStorage *)textStorage
Expand Down
37 changes: 37 additions & 0 deletions Libraries/Text/Text/RCTTextView.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#import <React/RCTAssert.h> // TODO(macOS ISS#2323203)
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
#import <React/RCTFocusChangeEvent.h> // TODO(OSS Candidate ISS#2710739)

#import "RCTTextShadowView.h"

Expand All @@ -41,11 +42,22 @@ @implementation RCTTextView
#endif // TODO(macOS ISS#2323203)

CAShapeLayer *_highlightLayer;
RCTEventDispatcher *_eventDispatcher; // TODO(OSS Candidate ISS#2710739)
NSArray<RCTUIView *> *_Nullable _descendantViews; // TODO(macOS ISS#3536887)
NSTextStorage *_Nullable _textStorage;
CGRect _contentFrame;
}

// [TODO(OSS Candidate ISS#2710739)
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
{
if ((self = [self initWithFrame:CGRectZero])) {
_eventDispatcher = eventDispatcher;
}
return self;
}
// ]TODO(OSS Candidate ISS#2710739)

- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
Expand Down Expand Up @@ -333,6 +345,31 @@ - (void)rightMouseDown:(NSEvent *)event
}
}
}

- (BOOL)becomeFirstResponder
{
if (![super becomeFirstResponder]) {
return NO;
}

// If we've gained focus, notify listeners
[_eventDispatcher sendEvent:[RCTFocusChangeEvent focusEventWithReactTag:self.reactTag]];

return YES;
}

- (BOOL)resignFirstResponder
{
if (![super resignFirstResponder]) {
return NO;
}

// If we've lost focus, notify listeners
[_eventDispatcher sendEvent:[RCTFocusChangeEvent blurEventWithReactTag:self.reactTag]];

return YES;
}

#endif // ]TODO(macOS ISS#2323203)

- (BOOL)canBecomeFirstResponder
Expand Down
2 changes: 1 addition & 1 deletion Libraries/Text/Text/RCTTextViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ - (void)dealloc

- (RCTUIView *)view // TODO(macOS ISS#3536887)
{
return [RCTTextView new];
return [[RCTTextView alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; // TODO(OSS Candidate ISS#2710739)
}

- (RCTShadowView *)shadowView
Expand Down
108 changes: 89 additions & 19 deletions RNTester/js/FocusEventsExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
var React = require('react');
var ReactNative = require('react-native');
import Platform from '../../Libraries/Utilities/Platform';
var {StyleSheet, Text, View, TextInput} = ReactNative;
var {Button, StyleSheet, Text, View, TextInput} = ReactNative;

type State = {
eventStream: string,
Expand Down Expand Up @@ -110,33 +110,103 @@ class FocusEventExample extends React.Component<{}, State> {

{// Only test View on MacOS, since canBecomeFirstResponder is false on all iOS, therefore we can't focus
Platform.OS === 'macos' ? (
<View
onFocus={() => {
this.setState(prevState => ({
eventStream:
prevState.eventStream + '\nNested View Parent Focus',
}));
}}
onBlur={() => {
this.setState(prevState => ({
eventStream:
prevState.eventStream + '\nNested View Parent Blur',
}));
}}>
<View>
<View
onFocus={() => {
this.setState(prevState => ({
eventStream:
prevState.eventStream + '\nDescendent Button Focus',
}));
}}
onBlur={() => {
this.setState(prevState => ({
eventStream:
prevState.eventStream + '\nDescendent Button Blur',
}));
}}>
<View>
<Button
title="Button whose ancestor has onFocus/onBlur"
onPress={() => {}}
/>
</View>
</View>
<View
onFocus={() => {
this.setState(prevState => ({
eventStream:
prevState.eventStream + '\nDescendent Button Focus',
}));
}}
onBlur={() => {
this.setState(prevState => ({
eventStream:
prevState.eventStream + '\nDescendent Button Blur',
}));
}}>
<View>
<Button
title="Button with onFocus/onBlur and ancestor has onFocus/onBlur"
onPress={() => {}}
onFocus={() => {
this.setState(prevState => ({
eventStream: prevState.eventStream + '\nButton Focus',
}));
}}
onBlur={() => {
this.setState(prevState => ({
eventStream: prevState.eventStream + '\nButton Blur',
}));
}}
/>
</View>
</View>
<View
onFocus={() => {
this.setState(prevState => ({
eventStream:
prevState.eventStream + '\nDescendent Text Focus',
}));
}}
onBlur={() => {
this.setState(prevState => ({
eventStream:
prevState.eventStream + '\nDescendent Text Blur',
}));
}}>
<View>
<Text selectable={true}>Selectable text</Text>
</View>
</View>
<View
acceptsKeyboardFocus={true}
enableFocusRing={true}
onFocus={() => {
this.setState(prevState => ({
eventStream: prevState.eventStream + '\nNested View Focus',
eventStream:
prevState.eventStream + '\nNested View Parent Focus',
}));
}}
onBlur={() => {
this.setState(prevState => ({
eventStream: prevState.eventStream + '\nNested View Blur',
eventStream:
prevState.eventStream + '\nNested View Parent Blur',
}));
}}>
<Text>Nested Focusable View</Text>
<View
acceptsKeyboardFocus={true}
enableFocusRing={true}
onFocus={() => {
this.setState(prevState => ({
eventStream:
prevState.eventStream + '\nNested View Focus',
}));
}}
onBlur={() => {
this.setState(prevState => ({
eventStream: prevState.eventStream + '\nNested View Blur',
}));
}}>
<Text>Nested Focusable View</Text>
</View>
</View>
</View>
) : null}
Expand Down
20 changes: 20 additions & 0 deletions React/Base/RCTFocusChangeEvent.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import <Foundation/Foundation.h>

#import <React/RCTComponentEvent.h>

/**
* Represents a focus change event meaning that a view that can become first responder has become or resigned being first responder.
*/
@interface RCTFocusChangeEvent : RCTComponentEvent

+ (instancetype)focusEventWithReactTag:(NSNumber *)reactTag;
+ (instancetype)blurEventWithReactTag:(NSNumber *)reactTag;

@end
30 changes: 30 additions & 0 deletions React/Base/RCTFocusChangeEvent.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import "RCTFocusChangeEvent.h"

#import "RCTAssert.h"

@implementation RCTFocusChangeEvent

+ (instancetype)focusEventWithReactTag:(NSNumber *)reactTag
{
RCTFocusChangeEvent *event = [[self alloc] initWithName:@"focus"
viewTag:reactTag
body:@{}];
return event;
}

+ (instancetype)blurEventWithReactTag:(NSNumber *)reactTag
{
RCTFocusChangeEvent *event = [[self alloc] initWithName:@"blur"
viewTag:reactTag
body:@{}];
return event;
}

@end
Loading

0 comments on commit ec1ea19

Please sign in to comment.