diff --git a/.eslintrc b/.eslintrc index 7de99efb..08b899e7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -34,6 +34,7 @@ "window": false, // Flow global types, "$Enum": false, + "$ReadOnly": false, "CSSStyleSheet": false, "HTMLInputElement": false, "ReactClass": false, diff --git a/.flowconfig b/.flowconfig index e5a4d0ab..d7022150 100644 --- a/.flowconfig +++ b/.flowconfig @@ -5,6 +5,7 @@ /.*/__tests__/.* /packages/.*/dist/.* /packages/examples/.* +/packages/react-native-web/src/vendor/.* /packages/website/.* .*/node_modules/babel-plugin-transform-react-remove-prop-types/* diff --git a/packages/react-native-web/src/exports/Touchable/index.js b/packages/react-native-web/src/exports/Touchable/index.js index a9b6cd05..d974595b 100644 --- a/packages/react-native-web/src/exports/Touchable/index.js +++ b/packages/react-native-web/src/exports/Touchable/index.js @@ -1,26 +1,42 @@ /* eslint-disable react/prop-types */ /** - * Copyright (c) Nicolas Gallagher. * 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 + * @format */ +'use strict'; + import AccessibilityUtil from '../../modules/AccessibilityUtil'; import BoundingDimensions from './BoundingDimensions'; import findNodeHandle from '../findNodeHandle'; import normalizeColor from 'normalize-css-color'; import Position from './Position'; import React from 'react'; -import TouchEventUtils from 'fbjs/lib/TouchEventUtils'; import UIManager from '../UIManager'; import View from '../View'; type Event = Object; +type PressEvent = Object; +type EdgeInsetsProp = Object; + +const extractSingleTouch = nativeEvent => { + const touches = nativeEvent.touches; + const changedTouches = nativeEvent.changedTouches; + const hasTouches = touches && touches.length > 0; + const hasChangedTouches = changedTouches && changedTouches.length > 0; + + return !hasTouches && hasChangedTouches + ? changedTouches[0] + : hasTouches + ? touches[0] + : nativeEvent; +}; /** * `Touchable`: Taps done right. @@ -110,6 +126,7 @@ type Event = Object; /** * Touchable states. */ + const States = { NOT_RESPONDER: 'NOT_RESPONDER', // Not the responder RESPONDER_INACTIVE_PRESS_IN: 'RESPONDER_INACTIVE_PRESS_IN', // Responder, inactive, in the `PressRect` @@ -121,10 +138,33 @@ const States = { ERROR: 'ERROR' }; -/** +type State = + | typeof States.NOT_RESPONDER + | typeof States.RESPONDER_INACTIVE_PRESS_IN + | typeof States.RESPONDER_INACTIVE_PRESS_OUT + | typeof States.RESPONDER_ACTIVE_PRESS_IN + | typeof States.RESPONDER_ACTIVE_PRESS_OUT + | typeof States.RESPONDER_ACTIVE_LONG_PRESS_IN + | typeof States.RESPONDER_ACTIVE_LONG_PRESS_OUT + | typeof States.ERROR; + +/* * Quick lookup map for states that are considered to be "active" */ + +const baseStatesConditions = { + NOT_RESPONDER: false, + RESPONDER_INACTIVE_PRESS_IN: false, + RESPONDER_INACTIVE_PRESS_OUT: false, + RESPONDER_ACTIVE_PRESS_IN: false, + RESPONDER_ACTIVE_PRESS_OUT: false, + RESPONDER_ACTIVE_LONG_PRESS_IN: false, + RESPONDER_ACTIVE_LONG_PRESS_OUT: false, + ERROR: false +}; + const IsActive = { + ...baseStatesConditions, RESPONDER_ACTIVE_PRESS_OUT: true, RESPONDER_ACTIVE_PRESS_IN: true }; @@ -134,12 +174,14 @@ const IsActive = { * therefore eligible to result in a "selection" if the press stops. */ const IsPressingIn = { + ...baseStatesConditions, RESPONDER_INACTIVE_PRESS_IN: true, RESPONDER_ACTIVE_PRESS_IN: true, RESPONDER_ACTIVE_LONG_PRESS_IN: true }; const IsLongPressingIn = { + ...baseStatesConditions, RESPONDER_ACTIVE_LONG_PRESS_IN: true }; @@ -156,6 +198,15 @@ const Signals = { LONG_PRESS_DETECTED: 'LONG_PRESS_DETECTED' }; +type Signal = + | typeof Signals.DELAY + | typeof Signals.RESPONDER_GRANT + | typeof Signals.RESPONDER_RELEASE + | typeof Signals.RESPONDER_TERMINATED + | typeof Signals.ENTER_PRESS_RECT + | typeof Signals.LEAVE_PRESS_RECT + | typeof Signals.LONG_PRESS_DETECTED; + /** * Mapping from States x Signals => States */ @@ -382,13 +433,16 @@ const TouchableMixin = { /** * Place as callback for a DOM element's `onResponderGrant` event. + * @param {SyntheticEvent} e Synthetic event from event system. + * */ - touchableHandleResponderGrant: function(e: Event) { + touchableHandleResponderGrant: function(e: PressEvent) { const dispatchID = e.currentTarget; // Since e is used in a callback invoked on another event loop // (as in setTimeout etc), we need to call e.persist() on the // event to make sure it doesn't get reused in the event object pool. e.persist(); + this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout); this.pressOutDelayTimeout = null; @@ -403,7 +457,6 @@ const TouchableMixin = { if (delayMS !== 0) { this.touchableDelayTimeout = setTimeout(this._handleDelay.bind(this, e), delayMS); } else { - this.state.touchable.positionOnActivate = null; this._handleDelay(e); } @@ -421,27 +474,23 @@ const TouchableMixin = { /** * Place as callback for a DOM element's `onResponderRelease` event. */ - touchableHandleResponderRelease: function(e: Event) { + touchableHandleResponderRelease: function(e: PressEvent) { + this.pressInLocation = null; this._receiveSignal(Signals.RESPONDER_RELEASE, e); }, /** * Place as callback for a DOM element's `onResponderTerminate` event. */ - touchableHandleResponderTerminate: function(e: Event) { + touchableHandleResponderTerminate: function(e: PressEvent) { + this.pressInLocation = null; this._receiveSignal(Signals.RESPONDER_TERMINATED, e); }, /** * Place as callback for a DOM element's `onResponderMove` event. */ - touchableHandleResponderMove: function(e: Event) { - // Not enough time elapsed yet, wait for highlight - - // this is just a perf optimization. - if (this.state.touchable.touchState === States.RESPONDER_INACTIVE_PRESS_IN) { - return; - } - + touchableHandleResponderMove: function(e: PressEvent) { // Measurement may not have returned yet. if (!this.state.touchable.positionOnActivate) { return; @@ -466,13 +515,13 @@ const TouchableMixin = { const hitSlop = this.touchableGetHitSlop ? this.touchableGetHitSlop() : null; if (hitSlop) { - pressExpandLeft += hitSlop.left; - pressExpandTop += hitSlop.top; - pressExpandRight += hitSlop.right; - pressExpandBottom += hitSlop.bottom; + pressExpandLeft += hitSlop.left || 0; + pressExpandTop += hitSlop.top || 0; + pressExpandRight += hitSlop.right || 0; + pressExpandBottom += hitSlop.bottom || 0; } - const touch = TouchEventUtils.extractSingleTouch(e.nativeEvent); + const touch = extractSingleTouch(e.nativeEvent); const pageX = touch && touch.pageX; const pageY = touch && touch.pageY; @@ -494,9 +543,13 @@ const TouchableMixin = { pageX < positionOnActivate.left + dimensionsOnActivate.width + pressExpandRight && pageY < positionOnActivate.top + dimensionsOnActivate.height + pressExpandBottom; if (isTouchWithinActive) { + const prevState = this.state.touchable.touchState; this._receiveSignal(Signals.ENTER_PRESS_RECT, e); const curState = this.state.touchable.touchState; - if (curState === States.RESPONDER_INACTIVE_PRESS_IN) { + if ( + curState === States.RESPONDER_INACTIVE_PRESS_IN && + prevState !== States.RESPONDER_INACTIVE_PRESS_IN + ) { // fix for t7967420 this._cancelLongPressDelayTimeout(); } @@ -506,6 +559,30 @@ const TouchableMixin = { } }, + /** + * Invoked when the item receives focus. Mixers might override this to + * visually distinguish the `VisualRect` so that the user knows that it + * currently has the focus. Most platforms only support a single element being + * focused at a time, in which case there may have been a previously focused + * element that was blurred just prior to this. This can be overridden when + * using `Touchable.Mixin.withoutDefaultFocusAndBlur`. + */ + touchableHandleFocus: function(e: Event) { + this.props.onFocus && this.props.onFocus(e); + }, + + /** + * Invoked when the item loses focus. Mixers might override this to + * visually distinguish the `VisualRect` so that the user knows that it + * no longer has focus. Most platforms only support a single element being + * focused at a time, in which case the focus may have moved to another. + * This can be overridden when using + * `Touchable.Mixin.withoutDefaultFocusAndBlur`. + */ + touchableHandleBlur: function(e: Event) { + this.props.onBlur && this.props.onBlur(e); + }, + // ==== Abstract Application Callbacks ==== /** @@ -592,33 +669,31 @@ const TouchableMixin = { }, _handleQueryLayout: function( - x: number, - y: number, - width: number, - height: number, + l: number, + t: number, + w: number, + h: number, globalX: number, globalY: number ) { - // don't do anything if UIManager failed to measure node - if (!x && !y && !width && !height && !globalX && !globalY) { + //don't do anything UIManager failed to measure node + if (!l && !t && !w && !h && !globalX && !globalY) { return; } this.state.touchable.positionOnActivate && Position.release(this.state.touchable.positionOnActivate); this.state.touchable.dimensionsOnActivate && - // $FlowFixMe BoundingDimensions.release(this.state.touchable.dimensionsOnActivate); this.state.touchable.positionOnActivate = Position.getPooled(globalX, globalY); - // $FlowFixMe - this.state.touchable.dimensionsOnActivate = BoundingDimensions.getPooled(width, height); + this.state.touchable.dimensionsOnActivate = BoundingDimensions.getPooled(w, h); }, - _handleDelay: function(e: Event) { + _handleDelay: function(e: PressEvent) { this.touchableDelayTimeout = null; this._receiveSignal(Signals.DELAY, e); }, - _handleLongDelay: function(e: Event) { + _handleLongDelay: function(e: PressEvent) { this.longPressDelayTimeout = null; const curState = this.state.touchable.touchState; if ( @@ -646,7 +721,7 @@ const TouchableMixin = { * @throws Error if invalid state transition or unrecognized signal. * @sideeffects */ - _receiveSignal: function(signal: string, e: Event) { + _receiveSignal: function(signal: Signal, e: PressEvent) { const responderID = this.state.touchable.responderID; const curState = this.state.touchable.touchState; const nextState = Transitions[curState] && Transitions[curState][signal]; @@ -686,26 +761,19 @@ const TouchableMixin = { this.longPressDelayTimeout = null; }, - _isHighlight: function(state: string) { + _isHighlight: function(state: State) { return ( state === States.RESPONDER_ACTIVE_PRESS_IN || state === States.RESPONDER_ACTIVE_LONG_PRESS_IN ); }, - _savePressInLocation: function(e: Event) { - const touch = TouchEventUtils.extractSingleTouch(e.nativeEvent); + _savePressInLocation: function(e: PressEvent) { + const touch = extractSingleTouch(e.nativeEvent); const pageX = touch && touch.pageX; const pageY = touch && touch.pageY; - this.pressInLocation = { - pageX, - pageY, - get locationX() { - return touch && touch.locationX; - }, - get locationY() { - return touch && touch.locationY; - } - }; + const locationX = touch && touch.locationX; + const locationY = touch && touch.locationY; + this.pressInLocation = { pageX, pageY, locationX, locationY }; }, _getDistanceBetweenPoints: function(aX: number, aY: number, bX: number, bY: number) { @@ -726,10 +794,10 @@ const TouchableMixin = { * @sideeffects */ _performSideEffectsForTransition: function( - curState: string, - nextState: string, - signal: string, - e: Event + curState: State, + nextState: State, + signal: Signal, + e: PressEvent ) { const curIsHighlight = this._isHighlight(curState); const newIsHighlight = this._isHighlight(nextState); @@ -741,7 +809,11 @@ const TouchableMixin = { this._cancelLongPressDelayTimeout(); } - if (!IsActive[curState] && IsActive[nextState]) { + const isInitialTransition = + curState === States.NOT_RESPONDER && nextState === States.RESPONDER_INACTIVE_PRESS_IN; + + const isActiveTransition = !IsActive[curState] && IsActive[nextState]; + if (isInitialTransition || isActiveTransition) { this._remeasureMetricsOnActivation(); } @@ -758,9 +830,8 @@ const TouchableMixin = { if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) { const hasLongPressHandler = !!this.props.onLongPress; const pressIsLongButStillCallOnPress = - IsLongPressingIn[curState] && // We *are* long pressing.. - (!hasLongPressHandler || // But either has no long handler - !this.touchableLongPressCancelsPress()); // or we're told to ignore it. + IsLongPressingIn[curState] && // We *are* long pressing.. // But either has no long handler + (!hasLongPressHandler || !this.touchableLongPressCancelsPress()); // or we're told to ignore it. const shouldInvokePress = !IsLongPressingIn[curState] || pressIsLongButStillCallOnPress; if (shouldInvokePress && this.touchableHandlePress) { @@ -777,12 +848,16 @@ const TouchableMixin = { this.touchableDelayTimeout = null; }, - _startHighlight: function(e: Event) { + _playTouchSound: function() { + UIManager.playTouchSound(); + }, + + _startHighlight: function(e: PressEvent) { this._savePressInLocation(e); this.touchableHandleActivePressIn && this.touchableHandleActivePressIn(e); }, - _endHighlight: function(e: Event) { + _endHighlight: function(e: PressEvent) { if (this.touchableHandleActivePressOut) { if (this.touchableGetPressOutDelayMS && this.touchableGetPressOutDelayMS()) { this.pressOutDelayTimeout = setTimeout(() => { @@ -797,10 +872,8 @@ const TouchableMixin = { // HACK (part 2): basic support for touchable interactions using a keyboard (including // delays and longPress) touchableHandleKeyEvent: function(e: Event) { - const ENTER = 13; - const SPACE = 32; - const { type, which } = e; - if (which === ENTER || which === SPACE) { + const { type, key } = e; + if (key === 'Enter' || key === ' ') { if (type === 'keydown') { if (!this._isTouchableKeyboardActive) { if ( @@ -825,44 +898,66 @@ const TouchableMixin = { e.stopPropagation(); // prevent the default behaviour unless the Touchable functions as a link // and Enter is pressed - if (!(which === ENTER && AccessibilityUtil.propsToAriaRole(this.props) === 'link')) { + if (!(key === 'Enter' && AccessibilityUtil.propsToAriaRole(this.props) === 'link')) { e.preventDefault(); } } - } + }, + + withoutDefaultFocusAndBlur: {} }; +/** + * Provide an optional version of the mixin where `touchableHandleFocus` and + * `touchableHandleBlur` can be overridden. This allows appropriate defaults to + * be set on TV platforms, without breaking existing implementations of + * `Touchable`. + */ +const { + // eslint-disable-next-line no-unused-vars + touchableHandleFocus, + // eslint-disable-next-line no-unused-vars + touchableHandleBlur, + ...TouchableMixinWithoutDefaultFocusAndBlur +} = TouchableMixin; +TouchableMixin.withoutDefaultFocusAndBlur = TouchableMixinWithoutDefaultFocusAndBlur; + const Touchable = { Mixin: TouchableMixin, TOUCH_TARGET_DEBUG: false, // Highlights all touchable targets. Toggle with Inspector. /** * Renders a debugging overlay to visualize touch target with hitSlop (might not work on Android). */ - renderDebugView: ({ color, hitSlop }: Object) => { - if (process.env.NODE_ENV !== 'production') { - if (!Touchable.TOUCH_TARGET_DEBUG) { - return null; - } - const debugHitSlopStyle = {}; - hitSlop = hitSlop || { top: 0, bottom: 0, left: 0, right: 0 }; - for (const key in hitSlop) { - debugHitSlopStyle[key] = -hitSlop[key]; - } - const hexColor = '#' + ('00000000' + normalizeColor(color).toString(16)).substr(-8); - return ( - - ); + renderDebugView: ({ color, hitSlop }: { color: string | number, hitSlop: EdgeInsetsProp }) => { + if (!Touchable.TOUCH_TARGET_DEBUG) { + return null; } + if (process.env.NODE_ENV !== 'production') { + throw Error('Touchable.TOUCH_TARGET_DEBUG should not be enabled in prod!'); + } + const debugHitSlopStyle = {}; + hitSlop = hitSlop || { top: 0, bottom: 0, left: 0, right: 0 }; + for (const key in hitSlop) { + debugHitSlopStyle[key] = -hitSlop[key]; + } + const normalizedColor = normalizeColor(color); + if (typeof normalizedColor !== 'number') { + return null; + } + const hexColor = '#' + ('00000000' + normalizedColor.toString(16)).substr(-8); + return ( + + ); } }; diff --git a/packages/react-native-web/src/exports/TouchableHighlight/index.js b/packages/react-native-web/src/exports/TouchableHighlight/index.js index ad33cdd0..23a1cd89 100644 --- a/packages/react-native-web/src/exports/TouchableHighlight/index.js +++ b/packages/react-native-web/src/exports/TouchableHighlight/index.js @@ -1,36 +1,50 @@ /** - * Copyright (c) Nicolas Gallagher. * 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. * - * @noflow + * @flow + * @format */ +'use strict'; import applyNativeMethods from '../../modules/applyNativeMethods'; -import ColorPropType from '../ColorPropType'; +import DeprecatedColorPropType from '../ColorPropType'; import createReactClass from 'create-react-class'; -import ensureComponentIsNative from '../../modules/ensureComponentIsNative'; import ensurePositiveDelayProps from '../Touchable/ensurePositiveDelayProps'; -import React from 'react'; +import * as React from 'react'; import StyleSheet from '../StyleSheet'; -import TimerMixin from 'react-timer-mixin'; import Touchable from '../Touchable'; -import TouchableWithoutFeedback from '../TouchableWithoutFeedback'; +import TouchableWithoutFeedback, { + type Props as TouchableWithoutFeedbackProps +} from '../TouchableWithoutFeedback'; import View from '../View'; -import ViewPropTypes from '../ViewPropTypes'; -import { func, number } from 'prop-types'; +import DeprecatedViewPropTypes from '../ViewPropTypes'; +import PropTypes from 'prop-types'; type Event = Object; +type PressEvent = Object; const DEFAULT_PROPS = { activeOpacity: 0.85, + delayPressOut: 100, underlayColor: 'black' }; const PRESS_RETENTION_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 }; +type Props = $ReadOnly<{| + ...TouchableWithoutFeedbackProps, + + activeOpacity?: ?number, + underlayColor?: ?any, + style?: ?any, + onShowUnderlay?: ?() => void, + onHideUnderlay?: ?() => void, + testOnly_pressed?: ?boolean +|}>; + /** * A wrapper for making views respond properly to touches. * On press down, the opacity of the wrapped view is decreased, which allows @@ -58,114 +72,244 @@ const PRESS_RETENTION_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 }; * ); * }, * ``` + * + * + * ### Example + * + * ```ReactNativeWebPlayer + * import React, { Component } from 'react' + * import { + * AppRegistry, + * StyleSheet, + * TouchableHighlight, + * Text, + * View, + * } from 'react-native' + * + * class App extends Component { + * constructor(props) { + * super(props) + * this.state = { count: 0 } + * } + * + * onPress = () => { + * this.setState({ + * count: this.state.count+1 + * }) + * } + * + * render() { + * return ( + * + * + * Touch Here + * + * + * + * { this.state.count !== 0 ? this.state.count: null} + * + * + * + * ) + * } + * } + * + * const styles = StyleSheet.create({ + * container: { + * flex: 1, + * justifyContent: 'center', + * paddingHorizontal: 10 + * }, + * button: { + * alignItems: 'center', + * backgroundColor: '#DDDDDD', + * padding: 10 + * }, + * countContainer: { + * alignItems: 'center', + * padding: 10 + * }, + * countText: { + * color: '#FF00FF' + * } + * }) + * + * AppRegistry.registerComponent('App', () => App) + * ``` + * */ -/* eslint-disable react/prefer-es6-class */ -const TouchableHighlight = createReactClass({ +// eslint-disable-next-line react/prefer-es6-class +const TouchableHighlight = ((createReactClass({ displayName: 'TouchableHighlight', propTypes: { + /* $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 this + * comment and run Flow. */ ...TouchableWithoutFeedback.propTypes, /** * Determines what the opacity of the wrapped view should be when touch is * active. */ - activeOpacity: number, - /** - * Called immediately after the underlay is hidden - */ - onHideUnderlay: func, - /** - * Called immediately after the underlay is shown - */ - onShowUnderlay: func, - style: ViewPropTypes.style, + activeOpacity: PropTypes.number, /** * The color of the underlay that will show through when the touch is * active. */ - underlayColor: ColorPropType + hasTVPreferredFocus: PropTypes.bool, + /** + * Style to apply to the container/underlay. Most commonly used to make sure + * rounded corners match the wrapped component. + */ + nextFocusDown: PropTypes.number, + /** + * Called immediately after the underlay is shown + */ + nextFocusForward: PropTypes.number, + /** + * Called immediately after the underlay is hidden + */ + nextFocusLeft: PropTypes.number, + /** + * *(Apple TV only)* TV preferred focus (see documentation for the View component). + * + * @platform ios + */ + nextFocusRight: PropTypes.number, + /** + * TV next focus down (see documentation for the View component). + * + * @platform android + */ + nextFocusUp: PropTypes.number, + /** + * TV next focus forward (see documentation for the View component). + * + * @platform android + */ + onHideUnderlay: PropTypes.func, + /** + * TV next focus left (see documentation for the View component). + * + * @platform android + */ + onShowUnderlay: PropTypes.func, + /** + * TV next focus right (see documentation for the View component). + * + * @platform android + */ + style: DeprecatedViewPropTypes.style, + /** + * TV next focus up (see documentation for the View component). + * + * @platform android + */ + testOnly_pressed: PropTypes.bool, + /** + * *(Apple TV only)* Object with properties to control Apple TV parallax effects. + * + * enabled: If true, parallax effects are enabled. Defaults to true. + * shiftDistanceX: Defaults to 2.0. + * shiftDistanceY: Defaults to 2.0. + * tiltAngle: Defaults to 0.05. + * magnification: Defaults to 1.0. + * pressMagnification: Defaults to 1.0. + * pressDuration: Defaults to 0.3. + * pressDelay: Defaults to 0.0. + * + * @platform ios + */ + tvParallaxProperties: PropTypes.object, + /** + * Handy for snapshot tests. + */ + underlayColor: DeprecatedColorPropType }, - mixins: [TimerMixin, Touchable.Mixin], + mixins: [Touchable.Mixin.withoutDefaultFocusAndBlur], getDefaultProps: () => DEFAULT_PROPS, - // Performance optimization to avoid constantly re-generating these objects. - _computeSyntheticState: function(props) { - return { - activeProps: { - style: { - opacity: props.activeOpacity - } - }, - activeUnderlayProps: { - style: { - backgroundColor: props.underlayColor - } - }, - underlayStyle: [INACTIVE_UNDERLAY_PROPS.style, props.style] - }; - }, - getInitialState: function() { this._isMounted = false; - return { - ...this.touchableGetInitialState(), - ...this._computeSyntheticState(this.props) - }; + if (this.props.testOnly_pressed) { + return { + ...this.touchableGetInitialState(), + extraChildStyle: { + opacity: this.props.activeOpacity + }, + extraUnderlayStyle: { + backgroundColor: this.props.underlayColor + } + }; + } else { + return { + ...this.touchableGetInitialState(), + extraChildStyle: null, + extraUnderlayStyle: null + }; + } }, componentDidMount: function() { this._isMounted = true; ensurePositiveDelayProps(this.props); - ensureComponentIsNative(this._childRef); }, componentWillUnmount: function() { this._isMounted = false; + clearTimeout(this._hideTimeout); }, - componentDidUpdate: function() { - ensureComponentIsNative(this._childRef); - }, - - componentWillReceiveProps: function(nextProps) { + UNSAFE_componentWillReceiveProps: function(nextProps) { ensurePositiveDelayProps(nextProps); - if ( - nextProps.activeOpacity !== this.props.activeOpacity || - nextProps.underlayColor !== this.props.underlayColor || - nextProps.style !== this.props.style - ) { - this.setState(this._computeSyntheticState(nextProps)); - } }, + /* + viewConfig: { + uiViewClassName: 'RCTView', + validAttributes: ReactNativeViewAttributes.RCTView, + }, + */ + /** * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ - touchableHandleActivePressIn: function(e: Event) { - this.clearTimeout(this._hideTimeout); + touchableHandleActivePressIn: function(e: PressEvent) { + clearTimeout(this._hideTimeout); this._hideTimeout = null; this._showUnderlay(); this.props.onPressIn && this.props.onPressIn(e); }, - touchableHandleActivePressOut: function(e: Event) { + touchableHandleActivePressOut: function(e: PressEvent) { if (!this._hideTimeout) { this._hideUnderlay(); } this.props.onPressOut && this.props.onPressOut(e); }, - touchableHandlePress: function(e: Event) { - this.clearTimeout(this._hideTimeout); + touchableHandleFocus: function(e: Event) { + this.props.onFocus && this.props.onFocus(e); + }, + + touchableHandleBlur: function(e: Event) { + this.props.onBlur && this.props.onBlur(e); + }, + + touchableHandlePress: function(e: PressEvent) { + clearTimeout(this._hideTimeout); this._showUnderlay(); - this._hideTimeout = this.setTimeout(this._hideUnderlay, this.props.delayPressOut || 100); + this._hideTimeout = setTimeout(this._hideUnderlay, this.props.delayPressOut); this.props.onPress && this.props.onPress(e); }, - touchableHandleLongPress: function(e: Event) { + touchableHandleLongPress: function(e: PressEvent) { this.props.onLongPress && this.props.onLongPress(e); }, @@ -193,20 +337,27 @@ const TouchableHighlight = createReactClass({ if (!this._isMounted || !this._hasPressHandler()) { return; } - - this._underlayRef.setNativeProps(this.state.activeUnderlayProps); - this._childRef.setNativeProps(this.state.activeProps); + this.setState({ + extraChildStyle: { + opacity: this.props.activeOpacity + }, + extraUnderlayStyle: { + backgroundColor: this.props.underlayColor + } + }); this.props.onShowUnderlay && this.props.onShowUnderlay(); }, _hideUnderlay: function() { - this.clearTimeout(this._hideTimeout); + clearTimeout(this._hideTimeout); this._hideTimeout = null; - if (this._hasPressHandler() && this._underlayRef) { - this._childRef.setNativeProps(INACTIVE_CHILD_PROPS); - this._underlayRef.setNativeProps({ - ...INACTIVE_UNDERLAY_PROPS, - style: this.state.underlayStyle + if (this.props.testOnly_pressed) { + return; + } + if (this._hasPressHandler()) { + this.setState({ + extraChildStyle: null, + extraUnderlayStyle: null }); this.props.onHideUnderlay && this.props.onHideUnderlay(); } @@ -221,63 +372,61 @@ const TouchableHighlight = createReactClass({ ); }, - _setChildRef(node) { - this._childRef = node; - }, - - _setUnderlayRef(node) { - this._underlayRef = node; - }, - render: function() { - const { - /* eslint-disable */ - activeOpacity, - onHideUnderlay, - onShowUnderlay, - underlayColor, - delayLongPress, - delayPressIn, - delayPressOut, - onLongPress, - onPress, - onPressIn, - onPressOut, - pressRetentionOffset, - /* eslint-enable */ - ...other - } = this.props; - + const child = React.Children.only(this.props.children); return ( - {React.cloneElement(React.Children.only(this.props.children), { - ref: this._setChildRef + {React.cloneElement(child, { + style: StyleSheet.compose( + child.props.style, + this.state.extraChildStyle + ) + })} + {Touchable.renderDebugView({ + color: 'green', + hitSlop: this.props.hitSlop })} - {Touchable.renderDebugView({ color: 'green', hitSlop: this.props.hitSlop })} ); } -}); - -const INACTIVE_CHILD_PROPS = { - style: StyleSheet.create({ x: { opacity: 1.0 } }).x -}; -const INACTIVE_UNDERLAY_PROPS = { - style: StyleSheet.create({ x: { backgroundColor: 'transparent' } }).x -}; +}): any): React.ComponentType); const styles = StyleSheet.create({ root: { diff --git a/packages/react-native-web/src/exports/TouchableOpacity/index.js b/packages/react-native-web/src/exports/TouchableOpacity/index.js index b19357b7..1bd1824f 100644 --- a/packages/react-native-web/src/exports/TouchableOpacity/index.js +++ b/packages/react-native-web/src/exports/TouchableOpacity/index.js @@ -1,35 +1,46 @@ /** - * Copyright (c) Nicolas Gallagher. * 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. * - * @noflow + * @format + * @flow */ +'use strict'; + import applyNativeMethods from '../../modules/applyNativeMethods'; import createReactClass from 'create-react-class'; import ensurePositiveDelayProps from '../Touchable/ensurePositiveDelayProps'; -import { number } from 'prop-types'; -import React from 'react'; +import PropTypes from 'prop-types'; +import * as React from 'react'; import StyleSheet from '../StyleSheet'; import Touchable from '../Touchable'; -import TouchableWithoutFeedback from '../TouchableWithoutFeedback'; +import TouchableWithoutFeedback, { + type Props as TouchableWithoutFeedbackProps +} from '../TouchableWithoutFeedback'; import View from '../View'; const flattenStyle = StyleSheet.flatten; type Event = Object; +type PressEvent = Object; const PRESS_RETENTION_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 }; +type Props = $ReadOnly<{| + ...TouchableWithoutFeedbackProps, + activeOpacity?: ?number, + style?: ?any +|}>; + /** * A wrapper for making views respond properly to touches. * On press down, the opacity of the wrapped view is decreased, dimming it. * - * Opacity is controlled by wrapping the children in a View, which is - * added to the view hiearchy. Be aware that this can affect layout. + * Opacity is controlled by wrapping the children in an Animated.View, which is + * added to the view hiearchy. Be aware that this can affect layout. * * Example: * @@ -45,46 +56,160 @@ const PRESS_RETENTION_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 }; * ); * }, * ``` + * ### Example + * + * ```ReactNativeWebPlayer + * import React, { Component } from 'react' + * import { + * AppRegistry, + * StyleSheet, + * TouchableOpacity, + * Text, + * View, + * } from 'react-native' + * + * class App extends Component { + * constructor(props) { + * super(props) + * this.state = { count: 0 } + * } + * + * onPress = () => { + * this.setState({ + * count: this.state.count+1 + * }) + * } + * + * render() { + * return ( + * + * + * Touch Here + * + * + * + * { this.state.count !== 0 ? this.state.count: null} + * + * + * + * ) + * } + * } + * + * const styles = StyleSheet.create({ + * container: { + * flex: 1, + * justifyContent: 'center', + * paddingHorizontal: 10 + * }, + * button: { + * alignItems: 'center', + * backgroundColor: '#DDDDDD', + * padding: 10 + * }, + * countContainer: { + * alignItems: 'center', + * padding: 10 + * }, + * countText: { + * color: '#FF00FF' + * } + * }) + * + * AppRegistry.registerComponent('App', () => App) + * ``` + * */ -/* eslint-disable react/prefer-es6-class */ -const TouchableOpacity = createReactClass({ +// eslint-disable-next-line react/prefer-es6-class +const TouchableOpacity = ((createReactClass({ displayName: 'TouchableOpacity', - mixins: [Touchable.Mixin], + mixins: [Touchable.Mixin.withoutDefaultFocusAndBlur], propTypes: { + /* $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 this + * comment and run Flow. */ ...TouchableWithoutFeedback.propTypes, /** * Determines what the opacity of the wrapped view should be when touch is - * active. + * active. Defaults to 0.2. */ - activeOpacity: number, - focusedOpacity: number + activeOpacity: PropTypes.number, + /** + * TV preferred focus (see documentation for the View component). + */ + hasTVPreferredFocus: PropTypes.bool, + /** + * TV next focus down (see documentation for the View component). + * + * @platform android + */ + nextFocusDown: PropTypes.number, + /** + * TV next focus forward (see documentation for the View component). + * + * @platform android + */ + nextFocusForward: PropTypes.number, + /** + * TV next focus left (see documentation for the View component). + * + * @platform android + */ + nextFocusLeft: PropTypes.number, + /** + * TV next focus right (see documentation for the View component). + * + * @platform android + */ + nextFocusRight: PropTypes.number, + /** + * TV next focus up (see documentation for the View component). + * + * @platform android + */ + nextFocusUp: PropTypes.number, + /** + * Apple TV parallax effects + */ + tvParallaxProperties: PropTypes.object }, getDefaultProps: function() { return { - activeOpacity: 0.2, - focusedOpacity: 0.7 + activeOpacity: 0.2 }; }, getInitialState: function() { - return this.touchableGetInitialState(); + return { + ...this.touchableGetInitialState(), + anim: this._getChildStyleOpacityWithDefault() + }; }, componentDidMount: function() { ensurePositiveDelayProps(this.props); }, - componentWillReceiveProps: function(nextProps) { + UNSAFE_componentWillReceiveProps: function(nextProps) { ensurePositiveDelayProps(nextProps); }, + componentDidUpdate: function(prevProps, prevState) { + if (this.props.disabled !== prevProps.disabled) { + this._opacityInactive(250); + } + }, + /** * Animate the touchable to a new opacity. */ - setOpacityTo: function(value: number, duration: ?number) { + setOpacityTo: function(value: number, duration: number) { this.setNativeProps({ style: { opacity: value, @@ -97,7 +222,7 @@ const TouchableOpacity = createReactClass({ * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ - touchableHandleActivePressIn: function(e: Event) { + touchableHandleActivePressIn: function(e: PressEvent) { if (e.dispatchConfig.registrationName === 'onResponderGrant') { this._opacityActive(0); } else { @@ -106,16 +231,30 @@ const TouchableOpacity = createReactClass({ this.props.onPressIn && this.props.onPressIn(e); }, - touchableHandleActivePressOut: function(e: Event) { + touchableHandleActivePressOut: function(e: PressEvent) { this._opacityInactive(250); this.props.onPressOut && this.props.onPressOut(e); }, - touchableHandlePress: function(e: Event) { + touchableHandleFocus: function(e: Event) { + //if (Platform.isTV) { + // this._opacityActive(150); + //} + this.props.onFocus && this.props.onFocus(e); + }, + + touchableHandleBlur: function(e: Event) { + //if (Platform.isTV) { + // this._opacityInactive(250); + //} + this.props.onBlur && this.props.onBlur(e); + }, + + touchableHandlePress: function(e: PressEvent) { this.props.onPress && this.props.onPress(e); }, - touchableHandleLongPress: function(e: Event) { + touchableHandleLongPress: function(e: PressEvent) { this.props.onLongPress && this.props.onLongPress(e); }, @@ -147,52 +286,60 @@ const TouchableOpacity = createReactClass({ this.setOpacityTo(this._getChildStyleOpacityWithDefault(), duration); }, - _opacityFocused: function() { - this.setOpacityTo(this.props.focusedOpacity); - }, - _getChildStyleOpacityWithDefault: function() { const childStyle = flattenStyle(this.props.style) || {}; - return childStyle.opacity === undefined ? 1 : childStyle.opacity; + return childStyle.opacity == null ? 1 : childStyle.opacity; }, render: function() { - const { - /* eslint-disable */ - activeOpacity, - focusedOpacity, - delayLongPress, - delayPressIn, - delayPressOut, - onLongPress, - onPress, - onPressIn, - onPressOut, - pressRetentionOffset, - /* eslint-enable */ - ...other - } = this.props; - return ( {this.props.children} - {Touchable.renderDebugView({ color: 'blue', hitSlop: this.props.hitSlop })} + {Touchable.renderDebugView({ + color: 'cyan', + hitSlop: this.props.hitSlop + })} ); } -}); +}): any): React.ComponentType); const styles = StyleSheet.create({ root: { diff --git a/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js b/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js index bb0d3554..da6c834f 100644 --- a/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js +++ b/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js @@ -1,28 +1,72 @@ /** - * Copyright (c) 2016-present, Nicolas Gallagher. - * Copyright (c) 2015-present, Facebook, Inc. + * 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 * @flow */ -import createReactClass from 'create-react-class'; -import EdgeInsetsPropType from '../EdgeInsetsPropType'; -import ensurePositiveDelayProps from '../Touchable/ensurePositiveDelayProps'; -import React, { type Element } from 'react'; -import StyleSheet from '../StyleSheet'; -import TimerMixin from 'react-timer-mixin'; -import Touchable from '../Touchable'; -import ViewPropTypes from '../ViewPropTypes'; -import warning from 'fbjs/lib/warning'; -import { any, bool, func, number, string } from 'prop-types'; +'use strict'; -type Event = Object; +import createReactClass from 'create-react-class'; +import DeprecatedEdgeInsetsPropType from '../EdgeInsetsPropType'; +import ensurePositiveDelayProps from '../Touchable/ensurePositiveDelayProps'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import Touchable from '../Touchable'; +import View from '../View'; + +type BlurEvent = Object; +type FocusEvent = Object; +type PressEvent = Object; +type LayoutEvent = Object; +type EdgeInsetsProp = Object; const PRESS_RETENTION_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 }; +const OVERRIDE_PROPS = [ + 'accessibilityLabel', + 'accessibilityHint', + 'accessibilityIgnoresInvertColors', + 'accessibilityRole', + 'accessibilityState', + 'hitSlop', + 'nativeID', + 'onBlur', + 'onFocus', + 'onLayout', + 'testID' +]; + +export type Props = $ReadOnly<{| + accessible?: ?boolean, + accessibilityLabel?: ?string, + accessibilityHint?: ?string, + accessibilityIgnoresInvertColors?: ?boolean, + accessibilityRole?: ?string, + accessibilityState?: ?Object, + children?: ?React.Node, + delayLongPress?: ?number, + delayPressIn?: ?number, + delayPressOut?: ?number, + disabled?: ?boolean, + hitSlop?: ?EdgeInsetsProp, + nativeID?: ?string, + touchSoundDisabled?: ?boolean, + onBlur?: ?(e: BlurEvent) => void, + onFocus?: ?(e: FocusEvent) => void, + onLayout?: ?(event: LayoutEvent) => mixed, + onLongPress?: ?(event: PressEvent) => mixed, + onPress?: ?(event: PressEvent) => mixed, + onPressIn?: ?(event: PressEvent) => mixed, + onPressOut?: ?(event: PressEvent) => mixed, + pressRetentionOffset?: ?EdgeInsetsProp, + rejectResponderTermination?: ?boolean, + testID?: ?string +|}>; + /** * Do not use unless you have a very good reason. All elements that * respond to press should have a visual feedback when touched. @@ -30,53 +74,76 @@ const PRESS_RETENTION_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 }; * TouchableWithoutFeedback supports only one child. * If you wish to have several child components, wrap them in a View. */ - -/* eslint-disable react/prefer-es6-class, react/prop-types */ -const TouchableWithoutFeedback = createReactClass({ +// eslint-disable-next-line react/prefer-es6-class +const TouchableWithoutFeedback = ((createReactClass({ displayName: 'TouchableWithoutFeedback', - mixins: [TimerMixin, Touchable.Mixin], + mixins: [Touchable.Mixin], propTypes: { - accessibilityLabel: string, - accessibilityRole: ViewPropTypes.accessibilityRole, - accessible: bool, - children: any, + accessibilityHint: PropTypes.string, + accessibilityIgnoresInvertColors: PropTypes.bool, + accessibilityLabel: PropTypes.node, + accessibilityRole: PropTypes.string, + accessibilityState: PropTypes.object, + accessible: PropTypes.bool, /** - * Delay in ms, from onPressIn, before onLongPress is called. + * When `accessible` is true (which is the default) this may be called when + * the OS-specific concept of "focus" occurs. Some platforms may not have + * the concept of focus. */ - delayLongPress: number, + delayLongPress: PropTypes.number, /** - * Delay in ms, from the start of the touch, before onPressIn is called. + * When `accessible` is true (which is the default) this may be called when + * the OS-specific concept of "blur" occurs, meaning the element lost focus. + * Some platforms may not have the concept of blur. */ - delayPressIn: number, - /** - * Delay in ms, from the release of the touch, before onPressOut is called. - */ - delayPressOut: number, + delayPressIn: PropTypes.number, /** * If true, disable all interactions for this component. */ - disabled: bool, + delayPressOut: PropTypes.number, /** - * This defines how far your touch can start away from the button. This is - * added to `pressRetentionOffset` when moving off of the button. + * Called when the touch is released, but not if cancelled (e.g. by a scroll + * that steals the responder lock). */ - // $FlowFixMe(>=0.41.0) - hitSlop: EdgeInsetsPropType, + disabled: PropTypes.bool, + /** + * Called as soon as the touchable element is pressed and invoked even before onPress. + * This can be useful when making network requests. + */ + hitSlop: DeprecatedEdgeInsetsPropType, + /** + * Called as soon as the touch is released even before onPress. + */ + nativeID: PropTypes.string, /** * Invoked on mount and layout changes with * * `{nativeEvent: {layout: {x, y, width, height}}}` */ - onLayout: func, - onLongPress: func, + onBlur: PropTypes.func, /** - * Called when the touch is released, but not if cancelled (e.g. by a scroll - * that steals the responder lock). + * If true, doesn't play system sound on touch (Android Only) + **/ + onFocus: PropTypes.func, + + onLayout: PropTypes.func, + + onLongPress: PropTypes.func, + onPress: PropTypes.func, + + /** + * Delay in ms, from the start of the touch, before onPressIn is called. */ - onPress: func, - onPressIn: func, - onPressOut: func, + onPressIn: PropTypes.func, + /** + * Delay in ms, from the release of the touch, before onPressOut is called. + */ + onPressOut: PropTypes.func, + /** + * Delay in ms, from onPressIn, before onLongPress is called. + */ + pressRetentionOffset: DeprecatedEdgeInsetsPropType, /** * When the scroll view is disabled, this defines how far your touch may * move off of the button, before deactivating the button. Once deactivated, @@ -84,9 +151,16 @@ const TouchableWithoutFeedback = createReactClass({ * reactivated! Move it back and forth several times while the scroll view * is disabled. Ensure you pass in a constant to reduce memory allocations. */ - // $FlowFixMe - pressRetentionOffset: EdgeInsetsPropType, - testID: string + testID: PropTypes.string, + /** + * This defines how far your touch can start away from the button. This is + * added to `pressRetentionOffset` when moving off of the button. + * ** NOTE ** + * The touch area never extends past the parent view bounds and the Z-index + * of sibling views always takes precedence if a touch hits two overlapping + * views. + */ + touchSoundDisabled: PropTypes.bool }, getInitialState: function() { @@ -97,7 +171,7 @@ const TouchableWithoutFeedback = createReactClass({ ensurePositiveDelayProps(this.props); }, - componentWillReceiveProps: function(nextProps: Object) { + UNSAFE_componentWillReceiveProps: function(nextProps: Object) { ensurePositiveDelayProps(nextProps); }, @@ -105,23 +179,24 @@ const TouchableWithoutFeedback = createReactClass({ * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ - touchableHandlePress: function(e: Event) { + touchableHandlePress: function(e: PressEvent) { this.props.onPress && this.props.onPress(e); }, - touchableHandleActivePressIn: function(e: Event) { + touchableHandleActivePressIn: function(e: PressEvent) { this.props.onPressIn && this.props.onPressIn(e); }, - touchableHandleActivePressOut: function(e: Event) { + touchableHandleActivePressOut: function(e: PressEvent) { this.props.onPressOut && this.props.onPressOut(e); }, - touchableHandleLongPress: function(e: Event) { + touchableHandleLongPress: function(e: PressEvent) { this.props.onLongPress && this.props.onLongPress(e); }, touchableGetPressRectOffset: function(): typeof PRESS_RETENTION_OFFSET { + // $FlowFixMe Invalid prop usage return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET; }, @@ -141,65 +216,41 @@ const TouchableWithoutFeedback = createReactClass({ return this.props.delayPressOut || 0; }, - render: function(): Element { - const { - /* eslint-disable */ - delayLongPress, - delayPressIn, - delayPressOut, - onLongPress, - onPress, - onPressIn, - onPressOut, - pressRetentionOffset, - /* eslint-enable */ - ...other - } = this.props; - + render: function(): React.Element { // Note(avik): remove dynamic typecast once Flow has been upgraded - // $FlowFixMe + // $FlowFixMe(>=0.41.0) + // eslint-disable-next-line react/prop-types const child = React.Children.only(this.props.children); let children = child.props.children; - warning( - !child.type || child.type.displayName !== 'Text', - 'TouchableWithoutFeedback does not work well with Text children. Wrap children in a View instead. See ' + - ((child._owner && child._owner.getName && child._owner.getName()) || '') - ); - if ( - process.env.NODE_ENV !== 'production' && - Touchable.TOUCH_TARGET_DEBUG && - child.type && - child.type.displayName === 'View' - ) { + if (Touchable.TOUCH_TARGET_DEBUG && child.type === View) { children = React.Children.toArray(children); children.push(Touchable.renderDebugView({ color: 'red', hitSlop: this.props.hitSlop })); } - const style = - Touchable.TOUCH_TARGET_DEBUG && child.type && child.type.displayName === 'Text' - ? [!this.props.disabled && styles.actionable, child.props.style, { color: 'red' }] - : [!this.props.disabled && styles.actionable, child.props.style]; + + const overrides = {}; + for (const prop of OVERRIDE_PROPS) { + if (this.props[prop] !== undefined) { + overrides[prop] = this.props[prop]; + } + } + return (React: any).cloneElement(child, { - ...other, + ...overrides, accessible: this.props.accessible !== false, - children, + //clickable: + // this.props.clickable !== false && this.props.onPress !== undefined, + //onClick: this.touchableHandlePress, onKeyDown: this.touchableHandleKeyEvent, onKeyUp: this.touchableHandleKeyEvent, + onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, + onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, onResponderGrant: this.touchableHandleResponderGrant, onResponderMove: this.touchableHandleResponderMove, onResponderRelease: this.touchableHandleResponderRelease, onResponderTerminate: this.touchableHandleResponderTerminate, - onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, - onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, - style + children }); } -}); - -const styles = StyleSheet.create({ - actionable: { - cursor: 'pointer', - touchAction: 'manipulation' - } -}); +}): any): React.ComponentType); export default TouchableWithoutFeedback;