diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js deleted file mode 100644 index ff54f40993..0000000000 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js +++ /dev/null @@ -1,403 +0,0 @@ -/** - * 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. - * - * @flow strict-local - * @format - */ - -'use strict'; - -import TouchableInjection from './TouchableInjection'; - -const Platform = require('../../Utilities/Platform'); -const PropTypes = require('prop-types'); -const React = require('react'); -const ReactNative = require('../../Renderer/shims/ReactNative'); -const Touchable = require('./Touchable'); -const TouchableWithoutFeedback = require('./TouchableWithoutFeedback'); -const View = require('../View/View'); -import {Commands} from '../View/ViewNativeComponent'; - -const createReactClass = require('create-react-class'); -const ensurePositiveDelayProps = require('./ensurePositiveDelayProps'); -const processColor = require('../../StyleSheet/processColor'); - -import type {Props as TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback'; -import type {PressEvent} from '../../Types/CoreEventTypes'; - -const rippleBackgroundPropType = PropTypes.shape({ - type: PropTypes.oneOf(['RippleAndroid']), - color: PropTypes.number, - borderless: PropTypes.bool, -}); - -const themeAttributeBackgroundPropType = PropTypes.shape({ - type: PropTypes.oneOf(['ThemeAttrAndroid']), - attribute: PropTypes.string.isRequired, -}); - -const backgroundPropType = PropTypes.oneOfType([ - rippleBackgroundPropType, - themeAttributeBackgroundPropType, -]); - -const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; - -export type Props = $ReadOnly<{| - ...TouchableWithoutFeedbackProps, - - background?: ?( - | $ReadOnly<{| - type: 'ThemeAttrAndroid', - attribute: - | 'selectableItemBackground' - | 'selectableItemBackgroundBorderless', - |}> - | $ReadOnly<{| - type: 'RippleAndroid', - color: ?number, - borderless: boolean, - |}> - ), - hasTVPreferredFocus?: ?boolean, - nextFocusDown?: ?number, - nextFocusForward?: ?number, - nextFocusLeft?: ?number, - nextFocusRight?: ?number, - nextFocusUp?: ?number, - useForeground?: ?boolean, -|}>; - -/** - * A wrapper for making views respond properly to touches (Android only). - * On Android this component uses native state drawable to display touch - * feedback. - * - * At the moment it only supports having a single View instance as a child - * node, as it's implemented by replacing that View with another instance of - * RCTView node with some additional properties set. - * - * Background drawable of native feedback touchable can be customized with - * `background` property. - * - * Example: - * - * ``` - * renderButton: function() { - * return ( - * - * - * Button - * - * - * ); - * }, - * ``` - */ - -const TouchableNativeFeedbackImpl = createReactClass({ - displayName: 'TouchableNativeFeedback', - propTypes: { - /* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment - * suppresses an error found when Flow v0.89 was deployed. To see the - * error, delete this comment and run Flow. */ - ...TouchableWithoutFeedback.propTypes, - - /** - * Determines the type of background drawable that's going to be used to - * display feedback. It takes an object with `type` property and extra data - * depending on the `type`. It's recommended to use one of the static - * methods to generate that dictionary. - */ - background: backgroundPropType, - - /** - * TV preferred focus (see documentation for the View component). - */ - hasTVPreferredFocus: PropTypes.bool, - - /** - * TV next focus down (see documentation for the View component). - */ - nextFocusDown: PropTypes.number, - - /** - * TV next focus forward (see documentation for the View component). - */ - nextFocusForward: PropTypes.number, - - /** - * TV next focus left (see documentation for the View component). - */ - nextFocusLeft: PropTypes.number, - - /** - * TV next focus right (see documentation for the View component). - */ - nextFocusRight: PropTypes.number, - - /** - * TV next focus up (see documentation for the View component). - */ - nextFocusUp: PropTypes.number, - - /** - * Set to true to add the ripple effect to the foreground of the view, instead of the - * background. This is useful if one of your child views has a background of its own, or you're - * e.g. displaying images, and you don't want the ripple to be covered by them. - * - * Check TouchableNativeFeedback.canUseNativeForeground() first, as this is only available on - * Android 6.0 and above. If you try to use this on older versions you will get a warning and - * fallback to background. - */ - useForeground: PropTypes.bool, - }, - - statics: { - /** - * Creates an object that represents android theme's default background for - * selectable elements (?android:attr/selectableItemBackground). - */ - SelectableBackground: function(): { - type: 'ThemeAttrAndroid', - attribute: 'selectableItemBackground', - ... - } { - return {type: 'ThemeAttrAndroid', attribute: 'selectableItemBackground'}; - }, - /** - * Creates an object that represent android theme's default background for borderless - * selectable elements (?android:attr/selectableItemBackgroundBorderless). - * Available on android API level 21+. - */ - SelectableBackgroundBorderless: function(): { - type: 'ThemeAttrAndroid', - attribute: 'selectableItemBackgroundBorderless', - ... - } { - return { - type: 'ThemeAttrAndroid', - attribute: 'selectableItemBackgroundBorderless', - }; - }, - /** - * Creates an object that represents ripple drawable with specified color (as a - * string). If property `borderless` evaluates to true the ripple will - * render outside of the view bounds (see native actionbar buttons as an - * example of that behavior). This background type is available on Android - * API level 21+. - * - * @param color The ripple color - * @param borderless If the ripple can render outside it's bounds - */ - Ripple: function( - color: string, - borderless: boolean, - ): { - type: 'RippleAndroid', - color: ?number, - borderless: boolean, - ... - } { - return { - type: 'RippleAndroid', - color: processColor(color), - borderless: borderless, - }; - }, - - canUseNativeForeground: function(): boolean { - return Platform.OS === 'android' && Platform.Version >= 23; - }, - }, - - mixins: [Touchable.Mixin], - - getDefaultProps: function() { - return { - background: this.SelectableBackground(), - }; - }, - - getInitialState: function() { - return this.touchableGetInitialState(); - }, - - componentDidMount: function() { - ensurePositiveDelayProps(this.props); - }, - - UNSAFE_componentWillReceiveProps: function(nextProps) { - ensurePositiveDelayProps(nextProps); - }, - - /** - * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are - * defined on your component. - */ - touchableHandleActivePressIn: function(e: PressEvent) { - this.props.onPressIn && this.props.onPressIn(e); - this._dispatchPressedStateChange(true); - if (this.pressInLocation) { - this._dispatchHotspotUpdate( - this.pressInLocation.locationX, - this.pressInLocation.locationY, - ); - } - }, - - touchableHandleActivePressOut: function(e: PressEvent) { - this.props.onPressOut && this.props.onPressOut(e); - this._dispatchPressedStateChange(false); - }, - - touchableHandlePress: function(e: PressEvent) { - this.props.onPress && this.props.onPress(e); - }, - - touchableHandleLongPress: function(e: PressEvent) { - this.props.onLongPress && this.props.onLongPress(e); - }, - - touchableGetPressRectOffset: function() { - // Always make sure to predeclare a constant! - return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET; - }, - - touchableGetHitSlop: function() { - return this.props.hitSlop; - }, - - touchableGetHighlightDelayMS: function() { - return this.props.delayPressIn; - }, - - touchableGetLongPressDelayMS: function() { - return this.props.delayLongPress; - }, - - touchableGetPressOutDelayMS: function() { - return this.props.delayPressOut; - }, - - _handleResponderMove: function(e) { - this.touchableHandleResponderMove(e); - this._dispatchHotspotUpdate( - e.nativeEvent.locationX, - e.nativeEvent.locationY, - ); - }, - - _dispatchHotspotUpdate: function(destX, destY) { - const hostComponentRef = ReactNative.findHostInstance_DEPRECATED(this); - - if (hostComponentRef == null) { - console.warn( - 'Touchable: Unable to find HostComponent instance. ' + - 'Has your Touchable component been unmounted?', - ); - } else { - Commands.hotspotUpdate(hostComponentRef, destX || 0, destY || 0); - } - }, - - _dispatchPressedStateChange: function(pressed) { - const hostComponentRef = ReactNative.findHostInstance_DEPRECATED(this); - if (hostComponentRef == null) { - console.warn( - 'Touchable: Unable to find HostComponent instance. ' + - 'Has your Touchable component been unmounted?', - ); - } else { - Commands.setPressed(hostComponentRef, pressed); - } - }, - - render: function() { - const child = React.Children.only(this.props.children); - let children = child.props.children; - if (Touchable.TOUCH_TARGET_DEBUG && child.type === View) { - if (!Array.isArray(children)) { - children = [children]; - } - children.push( - Touchable.renderDebugView({ - color: 'brown', - hitSlop: this.props.hitSlop, - }), - ); - } - if ( - this.props.useForeground && - !TouchableNativeFeedbackImpl.canUseNativeForeground() - ) { - console.warn( - 'Requested foreground ripple, but it is not available on this version of Android. ' + - 'Consider calling TouchableNativeFeedback.canUseNativeForeground() and using a different ' + - 'Touchable if the result is false.', - ); - } - const drawableProp = - this.props.useForeground && - TouchableNativeFeedbackImpl.canUseNativeForeground() - ? 'nativeForegroundAndroid' - : 'nativeBackgroundAndroid'; - const childProps = { - ...child.props, - /* $FlowFixMe(>=0.111.0 site=react_native_android_fb) This comment - * suppresses an error found when Flow v0.111 was deployed. To see the - * error, delete this comment and run Flow. */ - [drawableProp]: this.props.background, - accessible: this.props.accessible !== false, - accessibilityLabel: this.props.accessibilityLabel, - accessibilityRole: this.props.accessibilityRole, - accessibilityState: this.props.accessibilityState, - accessibilityActions: this.props.accessibilityActions, - onAccessibilityAction: this.props.onAccessibilityAction, - accessibilityValue: this.props.accessibilityValue, - importantForAccessibility: this.props.importantForAccessibility, - accessibilityLiveRegion: this.props.accessibilityLiveRegion, - accessibilityViewIsModal: this.props.accessibilityViewIsModal, - accessibilityElementsHidden: this.props.accessibilityElementsHidden, - children, - testID: this.props.testID, - onLayout: this.props.onLayout, - hitSlop: this.props.hitSlop, - nextFocusDown: this.props.nextFocusDown, - nextFocusForward: this.props.nextFocusForward, - nextFocusLeft: this.props.nextFocusLeft, - nextFocusRight: this.props.nextFocusRight, - nextFocusUp: this.props.nextFocusUp, - hasTVPreferredFocus: this.props.hasTVPreferredFocus, - focusable: - this.props.focusable !== false && - this.props.onPress !== undefined && - !this.props.disabled, - onClick: this.touchableHandlePress, - onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, - onResponderTerminationRequest: this - .touchableHandleResponderTerminationRequest, - onResponderGrant: this.touchableHandleResponderGrant, - onResponderMove: this._handleResponderMove, - onResponderRelease: this.touchableHandleResponderRelease, - onResponderTerminate: this.touchableHandleResponderTerminate, - }; - - // We need to clone the actual element so that the ripple background drawable - // can be applied directly to the background of this element rather than to - // a wrapper view as done in other Touchable* - return React.cloneElement(child, childProps); - }, -}); - -const TouchableNativeFeedback: React.ComponentType = - TouchableInjection.unstable_TouchableNativeFeedback == null - ? TouchableNativeFeedbackImpl - : TouchableInjection.unstable_TouchableNativeFeedback; - -module.exports = (TouchableNativeFeedback: $FlowFixMe); diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.ios.js b/Libraries/Components/Touchable/TouchableNativeFeedback.ios.js deleted file mode 100644 index 8efacfa365..0000000000 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.ios.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * 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. - * - * @format - */ - -'use strict'; - -const React = require('react'); -const StyleSheet = require('../../StyleSheet/StyleSheet'); -const Text = require('../../Text/Text'); -const View = require('../View/View'); - -class DummyTouchableNativeFeedback extends React.Component { - static SelectableBackground = () => ({}); - static SelectableBackgroundBorderless = () => ({}); - static Ripple = () => ({}); - static canUseNativeForeground = () => false; - - render() { - return ( - - - TouchableNativeFeedback is not supported on this platform! - - - ); - } -} - -const styles = StyleSheet.create({ - container: { - height: 100, - width: 300, - backgroundColor: '#ffbcbc', - borderWidth: 1, - borderColor: 'red', - alignItems: 'center', - justifyContent: 'center', - margin: 10, - }, - info: { - color: '#333333', - margin: 20, - }, -}); - -module.exports = DummyTouchableNativeFeedback; diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.js b/Libraries/Components/Touchable/TouchableNativeFeedback.js new file mode 100644 index 0000000000..2b54b95c07 --- /dev/null +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.js @@ -0,0 +1,331 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import Pressability from '../../Pressability/Pressability.js'; +import {PressabilityDebugView} from '../../Pressability/PressabilityDebug.js'; +import TVTouchable from './TVTouchable.js'; +import type {Props as TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback.js'; +import {Commands} from 'react-native/Libraries/Components/View/ViewNativeComponent'; +import ReactNative from 'react-native/Libraries/Renderer/shims/ReactNative'; +import type {PressEvent} from 'react-native/Libraries/Types/CoreEventTypes'; +import {Platform, View, processColor} from 'react-native'; +import * as React from 'react'; + +type Props = $ReadOnly<{| + ...TouchableWithoutFeedbackProps, + + /** + * Determines the type of background drawable that's going to be used to + * display feedback. It takes an object with `type` property and extra data + * depending on the `type`. It's recommended to use one of the static + * methods to generate that dictionary. + */ + background?: ?( + | $ReadOnly<{| + type: 'ThemeAttrAndroid', + attribute: + | 'selectableItemBackground' + | 'selectableItemBackgroundBorderless', + |}> + | $ReadOnly<{| + type: 'RippleAndroid', + color: ?number, + borderless: boolean, + |}> + ), + + /** + * TV preferred focus (see documentation for the View component). + */ + hasTVPreferredFocus?: ?boolean, + + /** + * TV next focus down (see documentation for the View component). + */ + nextFocusDown?: ?number, + + /** + * TV next focus forward (see documentation for the View component). + */ + nextFocusForward?: ?number, + + /** + * TV next focus left (see documentation for the View component). + */ + nextFocusLeft?: ?number, + + /** + * TV next focus right (see documentation for the View component). + */ + nextFocusRight?: ?number, + + /** + * TV next focus up (see documentation for the View component). + */ + nextFocusUp?: ?number, + + /** + * Set to true to add the ripple effect to the foreground of the view, instead + * of the background. This is useful if one of your child views has a + * background of its own, or you're e.g. displaying images, and you don't want + * the ripple to be covered by them. + * + * Check TouchableNativeFeedback.canUseNativeForeground() first, as this is + * only available on Android 6.0 and above. If you try to use this on older + * versions, this will fallback to background. + */ + useForeground?: ?boolean, +|}>; + +type State = $ReadOnly<{| + pressability: Pressability, +|}>; + +class TouchableNativeFeedback extends React.Component { + /** + * Creates a value for the `background` prop that uses the Android theme's + * default background for selectable elements. + */ + static SelectableBackground: () => $ReadOnly<{| + attribute: 'selectableItemBackground', + type: 'ThemeAttrAndroid', + |}> = () => ({ + type: 'ThemeAttrAndroid', + attribute: 'selectableItemBackground', + }); + + /** + * Creates a value for the `background` prop that uses the Android theme's + * default background for borderless selectable elements. Requires API 21+. + */ + static SelectableBackgroundBorderless: () => $ReadOnly<{| + attribute: 'selectableItemBackgroundBorderless', + type: 'ThemeAttrAndroid', + |}> = () => ({ + type: 'ThemeAttrAndroid', + attribute: 'selectableItemBackgroundBorderless', + }); + + /** + * Creates a value for the `background` prop that uses the Android ripple with + * the supplied color. If `borderless` is true, the ripple will render outside + * of the view bounds. Requires API 21+. + */ + static Ripple: ( + color: string, + borderless: boolean, + ) => $ReadOnly<{| + borderless: boolean, + color: ?number, + type: 'RippleAndroid', + |}> = (color: string, borderless: boolean) => ({ + type: 'RippleAndroid', + color: processColor(color), + borderless, + }); + + /** + * Whether `useForeground` is supported. + */ + static canUseNativeForeground: () => boolean = () => + Platform.OS === 'android' && Platform.Version >= 23; + + _tvTouchable: ?TVTouchable; + + state: State = { + pressability: new Pressability({ + getHitSlop: () => this.props.hitSlop, + getLongPressDelayMS: () => { + if (this.props.delayLongPress != null) { + const maybeNumber = this.props.delayLongPress; + if (typeof maybeNumber === 'number') { + return maybeNumber; + } + } + return 500; + }, + getPressDelayMS: () => this.props.delayPressIn, + getPressOutDelayMS: () => this.props.delayPressOut, + getPressRectOffset: () => this.props.pressRetentionOffset, + getTouchSoundDisabled: () => this.props.touchSoundDisabled, + onLongPress: event => { + if (this.props.onLongPress != null) { + this.props.onLongPress(event); + } + }, + onPress: event => { + if (this.props.onPress != null) { + this.props.onPress(event); + } + }, + onPressIn: event => { + if (Platform.OS === 'android') { + this._dispatchPressedStateChange(true); + this._dispatchHotspotUpdate(event); + } + if (this.props.onPressIn != null) { + this.props.onPressIn(event); + } + }, + onPressMove: event => { + if (Platform.OS === 'android') { + this._dispatchHotspotUpdate(event); + } + }, + onPressOut: event => { + if (Platform.OS === 'android') { + this._dispatchPressedStateChange(false); + } + if (this.props.onPressOut != null) { + this.props.onPressOut(event); + } + }, + onResponderTerminationRequest: () => + !this.props.rejectResponderTermination, + onStartShouldSetResponder: () => !this.props.disabled, + }), + }; + + _dispatchPressedStateChange(pressed: boolean): void { + if (Platform.OS === 'android') { + const hostComponentRef = ReactNative.findHostInstance_DEPRECATED(this); + if (hostComponentRef == null) { + console.warn( + 'Touchable: Unable to find HostComponent instance. ' + + 'Has your Touchable component been unmounted?', + ); + } else { + Commands.setPressed(hostComponentRef, pressed); + } + } + } + + _dispatchHotspotUpdate(event: PressEvent): void { + if (Platform.OS === 'android') { + const {locationX, locationY} = event.nativeEvent; + const hostComponentRef = ReactNative.findHostInstance_DEPRECATED(this); + if (hostComponentRef == null) { + console.warn( + 'Touchable: Unable to find HostComponent instance. ' + + 'Has your Touchable component been unmounted?', + ); + } else { + Commands.hotspotUpdate( + hostComponentRef, + locationX ?? 0, + locationY ?? 0, + ); + } + } + } + + render(): React.Node { + const element = React.Children.only(this.props.children); + const children = [element.props.children]; + if (__DEV__) { + if (element.type === View) { + children.push( + , + ); + } + } + + // BACKWARD-COMPATIBILITY: Focus and blur events were never supported before + // adopting `Pressability`, so preserve that behavior. + const { + onBlur, + onFocus, + ...eventHandlersWithoutBlurAndFocus + } = this.state.pressability.getEventHandlers(); + + return React.cloneElement( + element, + { + ...eventHandlersWithoutBlurAndFocus, + ...getBackgroundProp( + this.props.background === undefined + ? TouchableNativeFeedback.SelectableBackground() + : this.props.background, + this.props.useForeground === true, + ), + accessible: this.props.accessible !== false, + accessibilityLabel: this.props.accessibilityLabel, + accessibilityRole: this.props.accessibilityRole, + accessibilityState: this.props.accessibilityState, + accessibilityActions: this.props.accessibilityActions, + onAccessibilityAction: this.props.onAccessibilityAction, + accessibilityValue: this.props.accessibilityValue, + importantForAccessibility: this.props.importantForAccessibility, + accessibilityLiveRegion: this.props.accessibilityLiveRegion, + accessibilityViewIsModal: this.props.accessibilityViewIsModal, + accessibilityElementsHidden: this.props.accessibilityElementsHidden, + hasTVPreferredFocus: this.props.hasTVPreferredFocus, + hitSlop: this.props.hitSlop, + focusable: + this.props.focusable !== false && + this.props.onPress !== undefined && + !this.props.disabled, + nativeID: this.props.nativeID, + nextFocusDown: this.props.nextFocusDown, + nextFocusForward: this.props.nextFocusForward, + nextFocusLeft: this.props.nextFocusLeft, + nextFocusRight: this.props.nextFocusRight, + nextFocusUp: this.props.nextFocusUp, + onLayout: this.props.onLayout, + testID: this.props.testID, + }, + ...children, + ); + } + + componentDidMount(): void { + if (Platform.isTV) { + this._tvTouchable = new TVTouchable(this, { + getDisabled: () => this.props.disabled === true, + onBlur: event => { + if (this.props.onBlur != null) { + this.props.onBlur(event); + } + }, + onFocus: event => { + if (this.props.onFocus != null) { + this.props.onFocus(event); + } + }, + onPress: event => { + if (this.props.onPress != null) { + this.props.onPress(event); + } + }, + }); + } + } + + componentWillUnmount(): void { + if (Platform.isTV) { + if (this._tvTouchable != null) { + this._tvTouchable.destroy(); + } + } + this.state.pressability.reset(); + } +} + +const getBackgroundProp = + Platform.OS === 'android' + ? (background, useForeground) => + useForeground && TouchableNativeFeedback.canUseNativeForeground() + ? {nativeForegroundAndroid: background} + : {nativeBackgroundAndroid: background} + : (background, useForeground) => null; + +module.exports = TouchableNativeFeedback; diff --git a/RNTester/js/examples/Touchable/TouchableExample.js b/RNTester/js/examples/Touchable/TouchableExample.js index a687f13f1f..3fbecac3a1 100644 --- a/RNTester/js/examples/Touchable/TouchableExample.js +++ b/RNTester/js/examples/Touchable/TouchableExample.js @@ -402,10 +402,9 @@ class TouchableDisabled extends React.Component<{...}> { {Platform.OS === 'android' && ( console.log('custom TNF has been clicked')} background={TouchableNativeFeedback.SelectableBackground()}> - + Enabled TouchableNativeFeedback @@ -416,10 +415,9 @@ class TouchableDisabled extends React.Component<{...}> { {Platform.OS === 'android' && ( console.log('custom TNF has been clicked')} background={TouchableNativeFeedback.SelectableBackground()}> - + Disabled TouchableNativeFeedback