From cab4da728814bf9d3c0cc7c9921e982bfc090730 Mon Sep 17 00:00:00 2001 From: Igor Klemenski Date: Fri, 8 Jan 2021 13:57:10 -0800 Subject: [PATCH] Add onFocus and onBlur to Pressable. (#30405) Summary: Starting to upstream keyboard-related features from React Native Windows - this is the Android implementation. Exposing onFocus and onBlur callbacks on Pressable; they're already declared for View in ViewPropTypes.js, which Pressable wraps. Registering event listeners in ReactViewManager to listen for native focus events. ## Changelog [Android] [Added] - Add onFocus/onBlur for Pressable on Android. Pull Request resolved: https://github.com/facebook/react-native/pull/30405 Test Plan: Tested on v63-stable, since building master on Windows is now broken. Screenshots from RNTester running on the emulator: ![image](https://user-images.githubusercontent.com/12816515/99320373-59777e80-2820-11eb-91a8-704fff4aa13d.png) ![image](https://user-images.githubusercontent.com/12816515/99320412-6f853f00-2820-11eb-98f2-f9cd29e8aa0d.png) Reviewed By: mdvacca Differential Revision: D25444566 Pulled By: appden fbshipit-source-id: ce0efd3e3b199a508df0ba1ce484b4de17471432 --- Libraries/Components/Pressable/Pressable.js | 23 +++++++++++- Libraries/Components/View/View.js | 36 +++++++++++++++++-- .../java/com/facebook/react/views/common/BUCK | 5 ++- .../ReactViewBlurEvent.java} | 8 ++--- .../ReactViewFocusEvent.java} | 8 ++--- .../com/facebook/react/views/textinput/BUCK | 1 + .../textinput/ReactTextInputManager.java | 6 ++-- .../react/views/view/ReactViewManager.java | 26 ++++++++++++-- .../js/examples/Pressable/PressableExample.js | 34 ++++++++++++++++++ 9 files changed, 131 insertions(+), 16 deletions(-) rename ReactAndroid/src/main/java/com/facebook/react/views/{textinput/ReactTextInputBlurEvent.java => common/ReactViewBlurEvent.java} (79%) rename ReactAndroid/src/main/java/com/facebook/react/views/{textinput/ReactTextInputFocusEvent.java => common/ReactViewFocusEvent.java} (79%) diff --git a/Libraries/Components/Pressable/Pressable.js b/Libraries/Components/Pressable/Pressable.js index a6692c92bd425f..7b52378a8a009e 100644 --- a/Libraries/Components/Pressable/Pressable.js +++ b/Libraries/Components/Pressable/Pressable.js @@ -25,7 +25,12 @@ import type { import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import usePressability from '../../Pressability/usePressability'; import {normalizeRect, type RectOrSize} from '../../StyleSheet/Rect'; -import type {LayoutEvent, PressEvent} from '../../Types/CoreEventTypes'; +import type { + LayoutEvent, + PressEvent, + BlurEvent, + FocusEvent, +} from '../../Types/CoreEventTypes'; import View from '../View/View'; type ViewStyleProp = $ElementType, 'style'>; @@ -105,6 +110,16 @@ type Props = $ReadOnly<{| */ onPressOut?: ?(event: PressEvent) => void, + /** + * Called after the element loses focus. + */ + onBlur?: ?(event: BlurEvent) => mixed, + + /** + * Called after the element is focused. + */ + onFocus?: ?(event: FocusEvent) => mixed, + /** * Either view styles or a function that receives a boolean reflecting whether * the component is currently pressed and returns view styles. @@ -154,6 +169,8 @@ function Pressable(props: Props, forwardedRef): React.Node { onPress, onPressIn, onPressOut, + onBlur, + onFocus, pressRetentionOffset, style, testOnly_pressed, @@ -207,6 +224,8 @@ function Pressable(props: Props, forwardedRef): React.Node { onPressOut(event); } }, + onBlur, + onFocus, }), [ android_disableSound, @@ -218,6 +237,8 @@ function Pressable(props: Props, forwardedRef): React.Node { onPress, onPressIn, onPressOut, + onBlur, + onFocus, pressRetentionOffset, setPressed, unstable_pressDelay, diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index 0fd0d938676524..5e2c460fe9713c 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -11,10 +11,14 @@ 'use strict'; import type {ViewProps} from './ViewPropTypes'; - +import type {BlurEvent, FocusEvent} from '../../Types/CoreEventTypes'; const React = require('react'); import ViewNativeComponent from './ViewNativeComponent'; const TextAncestor = require('../../Text/TextAncestor'); +const TextInputState = require('../TextInput/TextInputState'); +import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; +const setAndForwardRef = require('../../Utilities/setAndForwardRef'); +const {useRef} = React; export type Props = ViewProps; @@ -29,9 +33,37 @@ const View: React.AbstractComponent< ViewProps, React.ElementRef, > = React.forwardRef((props: ViewProps, forwardedRef) => { + const viewRef = useRef>>(null); + + const _setNativeRef = setAndForwardRef({ + getForwardedRef: () => forwardedRef, + setLocalRef: ref => { + viewRef.current = ref; + }, + }); + + const _onBlur = (event: BlurEvent) => { + TextInputState.blurInput(viewRef.current); + if (props.onBlur) { + props.onBlur(event); + } + }; + + const _onFocus = (event: FocusEvent) => { + TextInputState.focusInput(viewRef.current); + if (props.onFocus) { + props.onFocus(event); + } + }; + return ( - + ); }); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/common/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/common/BUCK index 720c0618c0f9a8..9d637c3c628f84 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/common/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/views/common/BUCK @@ -1,4 +1,4 @@ -load("//tools/build_defs/oss:rn_defs.bzl", "react_native_dep", "rn_android_library") +load("//tools/build_defs/oss:rn_defs.bzl", "react_native_dep", "react_native_target", "rn_android_library") rn_android_library( name = "common", @@ -18,5 +18,8 @@ rn_android_library( "PUBLIC", ], deps = [ + react_native_target("java/com/facebook/react/uimanager:uimanager"), + react_native_target("java/com/facebook/react/uimanager/annotations:annotations"), + react_native_target("java/com/facebook/react/bridge:bridge"), ], ) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/common/ReactViewBlurEvent.java similarity index 79% rename from ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.java rename to ReactAndroid/src/main/java/com/facebook/react/views/common/ReactViewBlurEvent.java index 5a05bc4440742a..a9b1f36f109c66 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/common/ReactViewBlurEvent.java @@ -5,19 +5,19 @@ * LICENSE file in the root directory of this source tree. */ -package com.facebook.react.views.textinput; +package com.facebook.react.views.common; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; import com.facebook.react.uimanager.events.Event; import com.facebook.react.uimanager.events.RCTEventEmitter; -/** Event emitted by EditText native view when it loses focus. */ -/* package */ class ReactTextInputBlurEvent extends Event { +/** Event emitted by a native view when it loses focus. */ +public class ReactViewBlurEvent extends Event { private static final String EVENT_NAME = "topBlur"; - public ReactTextInputBlurEvent(int viewId) { + public ReactViewBlurEvent(int viewId) { super(viewId); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/common/ReactViewFocusEvent.java similarity index 79% rename from ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.java rename to ReactAndroid/src/main/java/com/facebook/react/views/common/ReactViewFocusEvent.java index 0bdaa6c19926f1..a7dcab6978bff8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/common/ReactViewFocusEvent.java @@ -5,19 +5,19 @@ * LICENSE file in the root directory of this source tree. */ -package com.facebook.react.views.textinput; +package com.facebook.react.views.common; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; import com.facebook.react.uimanager.events.Event; import com.facebook.react.uimanager.events.RCTEventEmitter; -/** Event emitted by EditText native view when it receives focus. */ -/* package */ class ReactTextInputFocusEvent extends Event { +/** Event emitted by a native view when it receives focus. */ +public class ReactViewFocusEvent extends Event { private static final String EVENT_NAME = "topFocus"; - public ReactTextInputFocusEvent(int viewId) { + public ReactViewFocusEvent(int viewId) { super(viewId); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK index ae5cec1338544f..7fa46e712bf159 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK @@ -21,6 +21,7 @@ rn_android_library( react_native_target("java/com/facebook/react/modules/core:core"), react_native_target("java/com/facebook/react/uimanager:uimanager"), react_native_target("java/com/facebook/react/uimanager/annotations:annotations"), + react_native_target("java/com/facebook/react/views/common:common"), react_native_target("java/com/facebook/react/views/imagehelper:imagehelper"), react_native_target("java/com/facebook/react/views/scroll:scroll"), react_native_target("java/com/facebook/react/views/text:text"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index 6d9ab0d39b6d4c..c252deb5978390 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -59,6 +59,8 @@ import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.annotations.ReactPropGroup; import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.common.ReactViewBlurEvent; +import com.facebook.react.views.common.ReactViewFocusEvent; import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper; import com.facebook.react.views.scroll.ScrollEvent; import com.facebook.react.views.scroll.ScrollEventType; @@ -974,9 +976,9 @@ protected void addEventEmitters( public void onFocusChange(View v, boolean hasFocus) { EventDispatcher eventDispatcher = getEventDispatcher(reactContext, editText); if (hasFocus) { - eventDispatcher.dispatchEvent(new ReactTextInputFocusEvent(editText.getId())); + eventDispatcher.dispatchEvent(new ReactViewFocusEvent(editText.getId())); } else { - eventDispatcher.dispatchEvent(new ReactTextInputBlurEvent(editText.getId())); + eventDispatcher.dispatchEvent(new ReactViewBlurEvent(editText.getId())); eventDispatcher.dispatchEvent( new ReactTextInputEndEditingEvent( diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java index 66aacfcfd157d6..1dacac6de3a9f2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -29,6 +29,8 @@ import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.annotations.ReactPropGroup; import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.common.ReactViewBlurEvent; +import com.facebook.react.views.common.ReactViewFocusEvent; import com.facebook.yoga.YogaConstants; import java.util.Locale; import java.util.Map; @@ -234,8 +236,7 @@ public void setFocusable(final ReactViewGroup view, boolean focusable) { @Override public void onClick(View v) { final EventDispatcher mEventDispatcher = - UIManagerHelper.getEventDispatcherForReactTag( - (ReactContext) view.getContext(), view.getId()); + getEventDispatcher((ReactContext) view.getContext(), view); if (mEventDispatcher == null) { return; } @@ -254,6 +255,27 @@ public void onClick(View v) { } } + private static EventDispatcher getEventDispatcher( + ReactContext reactContext, ReactViewGroup view) { + return UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.getId()); + } + + @Override + protected void addEventEmitters( + final ThemedReactContext reactContext, final ReactViewGroup view) { + view.setOnFocusChangeListener( + new View.OnFocusChangeListener() { + public void onFocusChange(View v, boolean hasFocus) { + EventDispatcher eventDispatcher = getEventDispatcher(reactContext, view); + if (hasFocus) { + eventDispatcher.dispatchEvent(new ReactViewFocusEvent(view.getId())); + } else { + eventDispatcher.dispatchEvent(new ReactViewBlurEvent(view.getId())); + } + } + }); + } + @ReactProp(name = ViewProps.OVERFLOW) public void setOverflow(ReactViewGroup view, String overflow) { view.setOverflow(overflow); diff --git a/packages/rn-tester/js/examples/Pressable/PressableExample.js b/packages/rn-tester/js/examples/Pressable/PressableExample.js index f7a1e4dc33dd2c..d9097cc5602b4d 100644 --- a/packages/rn-tester/js/examples/Pressable/PressableExample.js +++ b/packages/rn-tester/js/examples/Pressable/PressableExample.js @@ -250,6 +250,32 @@ function PressableDisabled() { ); } +function PressableFocusBlurEvents() { + const [lastEvent, setLastEvent] = useState(''); + + return ( + + + { + console.log('Focused!'); + setLastEvent('Received focus event'); + }} + onBlur={() => { + console.log('Blurred!'); + setLastEvent('Received blur event'); + }} + testID="pressable_focus_blur_button"> + Use keyboard to move focus to me + + + + {lastEvent} + + + ); +} + const styles = StyleSheet.create({ row: { justifyContent: 'center', @@ -478,4 +504,12 @@ exports.examples = [ return ; }, }, + { + title: 'Pressable onFocus/onBlur', + description: (' components can receive focus/blur events.': string), + platform: 'android', + render: function(): React.Node { + return ; + }, + }, ];