From 6f3e29f6309698c7aec4a867fc47c04c73862b0c Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Sat, 8 Jul 2017 11:52:03 -0700 Subject: [PATCH] [change] better Touchable support for keyboards Problem: Although 'Touchable' supports basic keyboard usage, it doesn't support delays or interaction via the Space key. Solution: Extend the 'Touchable' mixin to better support keyboard interactions. All touchable callbacks and delays are now supported when interacted with via a keyboard's Enter and Space keys (as would be expected of native 'button' elements). However, events are not normalized to mimic touch events. Minor upstream changes to the Touchables in React Native are also included. --- .../Touchable/TouchableHighlightDocs.js | 18 ++- .../Touchable/TouchableOpacityDocs.js | 17 ++- .../Touchable/TouchableWithoutFeedbackDocs.js | 24 +++- .../Touchable/examples/DelayEvents.js | 101 ++++++++++++++ .../Touchable/examples/FeedbackEvents.js | 95 ++++++++++++++ .../Touchable/examples/PropDisabled.js | 38 ++++-- .../1-components/Touchable/examples/legacy.js | 114 ---------------- src/components/TextInput/index.js | 5 - src/components/Touchable/Touchable.js | 117 +++++++++++++---- .../Touchable/TouchableHighlight.js | 124 ++++++++---------- .../Touchable/TouchableNativeFeedback.js | 10 ++ src/components/Touchable/TouchableOpacity.js | 57 ++++---- .../Touchable/TouchableWithoutFeedback.js | 89 ++++++------- 13 files changed, 504 insertions(+), 305 deletions(-) create mode 100644 docs/storybook/1-components/Touchable/examples/DelayEvents.js create mode 100644 docs/storybook/1-components/Touchable/examples/FeedbackEvents.js diff --git a/docs/storybook/1-components/Touchable/TouchableHighlightDocs.js b/docs/storybook/1-components/Touchable/TouchableHighlightDocs.js index 8ed915db..ef540d26 100644 --- a/docs/storybook/1-components/Touchable/TouchableHighlightDocs.js +++ b/docs/storybook/1-components/Touchable/TouchableHighlightDocs.js @@ -5,6 +5,8 @@ */ import CustomStyleOverrides from './examples/CustomStyleOverrides'; +import DelayEvents from './examples/DelayEvents'; +import FeedbackEvents from './examples/FeedbackEvents'; import React from 'react'; import { storiesOf } from '@kadira/storybook'; import { TouchableHighlightDisabled } from './examples/PropDisabled'; @@ -45,13 +47,27 @@ const sections = [ title: 'More examples', entries: [ }} />, + + }} + />, + + + }} + />, + }} + />, + + + }} + />, + + + }} /> ] } diff --git a/docs/storybook/1-components/Touchable/TouchableWithoutFeedbackDocs.js b/docs/storybook/1-components/Touchable/TouchableWithoutFeedbackDocs.js index c4ec9417..e79acedd 100644 --- a/docs/storybook/1-components/Touchable/TouchableWithoutFeedbackDocs.js +++ b/docs/storybook/1-components/Touchable/TouchableWithoutFeedbackDocs.js @@ -4,9 +4,12 @@ * @flow */ +import DelayEvents from './examples/DelayEvents'; +import FeedbackEvents from './examples/FeedbackEvents'; import React from 'react'; import PropHitSlop from './examples/PropHitSlop'; import { storiesOf } from '@kadira/storybook'; +import { TouchableWithoutFeedbackDisabled } from './examples/PropDisabled'; import UIExplorer, { AppText, Code, DocItem } from '../../ui-explorer'; const sections = [ @@ -53,6 +56,9 @@ const sections = [ If true, disable all interactions for this component. } + example={{ + render: () => + }} />, , @@ -83,7 +89,23 @@ constant to reduce memory allocations.`} { title: 'More examples', - entries: [ }} />] + entries: [ + + }} + />, + + + }} + />, + + }} /> + ] } ]; diff --git a/docs/storybook/1-components/Touchable/examples/DelayEvents.js b/docs/storybook/1-components/Touchable/examples/DelayEvents.js new file mode 100644 index 00000000..61a3c9ad --- /dev/null +++ b/docs/storybook/1-components/Touchable/examples/DelayEvents.js @@ -0,0 +1,101 @@ +/** + * @flow + */ + +import { oneOf } from 'prop-types'; +import React, { PureComponent } from 'react'; +import { + StyleSheet, + Text, + TouchableHighlight, + TouchableOpacity, + TouchableWithoutFeedback, + View +} from 'react-native'; + +const Touchables = { + highlight: TouchableHighlight, + opacity: TouchableOpacity, + withoutFeedback: TouchableWithoutFeedback +}; + +export default class TouchableDelayEvents extends PureComponent { + static propTypes = { + touchable: oneOf(['highlight', 'opacity', 'withoutFeedback']) + }; + + static defaultProps = { + touchable: 'highlight' + }; + + state = { eventLog: [] }; + + render() { + const Touchable = Touchables[this.props.touchable]; + const { displayName } = Touchable; + return ( + + + + + {displayName} + + + + + {this.state.eventLog.map((e, ii) => + + {e} + + )} + + + ); + } + + _createPressHandler = eventName => { + return () => { + const limit = 6; + this.setState(state => { + const eventLog = state.eventLog.slice(0, limit - 1); + eventLog.unshift(eventName); + return { eventLog }; + }); + }; + }; +} + +const styles = StyleSheet.create({ + touchableText: { + borderRadius: 8, + padding: 5, + borderWidth: 1, + borderColor: 'black', + color: '#007AFF', + borderStyle: 'solid', + textAlign: 'center' + }, + logBox: { + padding: 20, + margin: 10, + borderWidth: StyleSheet.hairlineWidth, + borderColor: '#f0f0f0', + backgroundColor: '#f9f9f9' + }, + eventLogBox: { + padding: 10, + margin: 10, + height: 120, + borderWidth: StyleSheet.hairlineWidth, + borderColor: '#f0f0f0', + backgroundColor: '#f9f9f9' + } +}); diff --git a/docs/storybook/1-components/Touchable/examples/FeedbackEvents.js b/docs/storybook/1-components/Touchable/examples/FeedbackEvents.js new file mode 100644 index 00000000..46f98dbc --- /dev/null +++ b/docs/storybook/1-components/Touchable/examples/FeedbackEvents.js @@ -0,0 +1,95 @@ +/** + * @flow + */ + +import { oneOf } from 'prop-types'; +import React, { PureComponent } from 'react'; +import { + StyleSheet, + Text, + TouchableHighlight, + TouchableOpacity, + TouchableWithoutFeedback, + View +} from 'react-native'; + +const Touchables = { + highlight: TouchableHighlight, + opacity: TouchableOpacity, + withoutFeedback: TouchableWithoutFeedback +}; + +export default class TouchableFeedbackEvents extends PureComponent { + static propTypes = { + touchable: oneOf(['highlight', 'opacity', 'withoutFeedback']) + }; + + static defaultProps = { + touchable: 'highlight' + }; + + state = { eventLog: [] }; + + render() { + const Touchable = Touchables[this.props.touchable]; + return ( + + + + Press Me + + + + {this.state.eventLog.map((e, ii) => + + {e} + + )} + + + ); + } + + _createPressHandler = eventName => { + return () => { + const limit = 6; + this.setState(state => { + const eventLog = state.eventLog.slice(0, limit - 1); + eventLog.unshift(eventName); + return { eventLog }; + }); + }; + }; +} + +const styles = StyleSheet.create({ + touchableText: { + borderRadius: 8, + padding: 5, + borderWidth: 1, + borderColor: 'black', + color: '#007AFF', + borderStyle: 'solid', + textAlign: 'center' + }, + logBox: { + padding: 20, + margin: 10, + borderWidth: StyleSheet.hairlineWidth, + borderColor: '#f0f0f0', + backgroundColor: '#f9f9f9' + }, + eventLogBox: { + padding: 10, + margin: 10, + height: 120, + borderWidth: StyleSheet.hairlineWidth, + borderColor: '#f0f0f0', + backgroundColor: '#f9f9f9' + } +}); diff --git a/docs/storybook/1-components/Touchable/examples/PropDisabled.js b/docs/storybook/1-components/Touchable/examples/PropDisabled.js index 12e3ce52..8f3c0a87 100644 --- a/docs/storybook/1-components/Touchable/examples/PropDisabled.js +++ b/docs/storybook/1-components/Touchable/examples/PropDisabled.js @@ -4,7 +4,14 @@ import React from 'react'; import { action } from '@kadira/storybook'; -import { StyleSheet, View, Text, TouchableHighlight, TouchableOpacity } from 'react-native'; +import { + StyleSheet, + View, + Text, + TouchableHighlight, + TouchableOpacity, + TouchableWithoutFeedback +} from 'react-native'; class TouchableHighlightDisabled extends React.Component { render() { @@ -57,7 +64,27 @@ class TouchableOpacityDisabled extends React.Component { } } -export { TouchableHighlightDisabled, TouchableOpacityDisabled }; +class TouchableWithoutFeedbackDisabled extends React.Component { + render() { + return ( + + + + Disabled TouchableWithoutFeedback + + + + + + Enabled TouchableWithoutFeedback + + + + ); + } +} + +export { TouchableHighlightDisabled, TouchableOpacityDisabled, TouchableWithoutFeedbackDisabled }; const styles = StyleSheet.create({ row: { @@ -66,12 +93,5 @@ const styles = StyleSheet.create({ }, block: { padding: 10 - }, - button: { - color: '#007AFF' - }, - disabledButton: { - color: '#007AFF', - opacity: 0.5 } }); diff --git a/docs/storybook/1-components/Touchable/examples/legacy.js b/docs/storybook/1-components/Touchable/examples/legacy.js index 85bace26..759b35af 100644 --- a/docs/storybook/1-components/Touchable/examples/legacy.js +++ b/docs/storybook/1-components/Touchable/examples/legacy.js @@ -17,92 +17,6 @@ import { View } from 'react-native'; -class TouchableFeedbackEvents extends React.Component { - constructor(props) { - super(props); - this.state = { eventLog: [] }; - } - - render() { - return ( - - - - - Press Me - - - - - {this.state.eventLog.map((e, ii) => {e})} - - - ); - } - - _createPressHandler = eventName => { - return () => { - const limit = 6; - const eventLog = this.state.eventLog.slice(0, limit - 1); - eventLog.unshift(eventName); - this.setState({ eventLog }); - }; - }; -} - -class TouchableDelayEvents extends React.Component { - constructor(props) { - super(props); - this.state = { eventLog: [] }; - } - - render() { - return ( - - - - - Press Me - - - - - {this.state.eventLog.map((e, ii) => {e})} - - - ); - } - - _createPressHandler = eventName => { - return () => { - const limit = 6; - const eventLog = this.state.eventLog.slice(0, limit - 1); - eventLog.unshift(eventName); - this.setState({ eventLog }); - }; - }; -} - const heartImage = { uri: 'https://pbs.twimg.com/media/BlXBfT3CQAA6cVZ.png:small' }; const styles = StyleSheet.create({ @@ -214,33 +128,5 @@ const examples = [ ); } }, - { - title: 'Touchable feedback events', - description: - ' components accept onPress, onPressIn, ' + - 'onPressOut, and onLongPress as props.', - render() { - return ; - } - }, - { - title: 'Touchable delay for events', - description: - ' components also accept delayPressIn, ' + - 'delayPressOut, and delayLongPress as props. These props impact the ' + - 'timing of feedback events.', - render() { - return ; - } - }, - { - title: 'Disabled Touchable*', - description: - ' components accept disabled prop which prevents ' + - 'any interaction with component', - render() { - return ; - } - } ]; */ diff --git a/src/components/TextInput/index.js b/src/components/TextInput/index.js index 44beeef2..95e7943d 100644 --- a/src/components/TextInput/index.js +++ b/src/components/TextInput/index.js @@ -14,7 +14,6 @@ import applyLayout from '../../modules/applyLayout'; import applyNativeMethods from '../../modules/applyNativeMethods'; import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; import { Component } from 'react'; -import NativeMethodsMixin from '../../modules/NativeMethodsMixin'; import createDOMElement from '../../modules/createDOMElement'; import findNodeHandle from '../../modules/findNodeHandle'; import StyleSheet from '../../apis/StyleSheet'; @@ -145,10 +144,6 @@ class TextInput extends Component { return TextInputState.currentlyFocusedField() === this._node; } - setNativeProps(props) { - NativeMethodsMixin.setNativeProps.call(this, props); - } - componentDidMount() { setSelection(this._node, this.props.selection); } diff --git a/src/components/Touchable/Touchable.js b/src/components/Touchable/Touchable.js index f975de84..90af32ef 100644 --- a/src/components/Touchable/Touchable.js +++ b/src/components/Touchable/Touchable.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable react/prop-types */ /** * Copyright (c) 2016-present, Nicolas Gallagher. @@ -9,18 +9,19 @@ * LICENSE file in the root directory of this source tree. * * @providesModule Touchable - * @noflow + * @flow */ -/* @edit start */ import BoundingDimensions from './BoundingDimensions'; +import findNodeHandle from '../../modules/findNodeHandle'; import normalizeColor from 'normalize-css-color'; import Position from './Position'; import React from 'react'; import TouchEventUtils from 'fbjs/lib/TouchEventUtils'; import UIManager from '../../apis/UIManager'; import View from '../../components/View'; -/* @edit end */ + +type Event = Object; /** * `Touchable`: Taps done right. @@ -314,10 +315,27 @@ const LONG_PRESS_ALLOWED_MOVEMENT = 10; * @lends Touchable.prototype */ const TouchableMixin = { + componentDidMount: function() { + this._touchableNode = findNodeHandle(this); + this._touchableBlurListener = e => { + if (this._isTouchableKeyboardActive) { + if ( + this.state.touchable.touchState && + this.state.touchable.touchState !== States.NOT_RESPONDER + ) { + this.touchableHandleResponderTerminate({ nativeEvent: e }); + } + this._isTouchableKeyboardActive = false; + } + }; + this._touchableNode.addEventListener('blur', this._touchableBlurListener); + }, + /** * Clear all timeouts on unmount */ componentWillUnmount: function() { + this._touchableNode.removeEventListener('blur', this._touchableBlurListener); this.touchableDelayTimeout && clearTimeout(this.touchableDelayTimeout); this.longPressDelayTimeout && clearTimeout(this.longPressDelayTimeout); this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout); @@ -360,16 +378,13 @@ const TouchableMixin = { /** * Place as callback for a DOM element's `onResponderGrant` event. - * @param {SyntheticEvent} e Synthetic event from event system. - * */ - touchableHandleResponderGrant: function(e) { + touchableHandleResponderGrant: function(e: Event) { 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; @@ -401,7 +416,7 @@ const TouchableMixin = { /** * Place as callback for a DOM element's `onResponderRelease` event. */ - touchableHandleResponderRelease: function(e) { + touchableHandleResponderRelease: function(e: Event) { this._receiveSignal(Signals.RESPONDER_RELEASE, e); // Browsers fire mouse events after touch events. This causes the // 'onResponderRelease' handler to be called twice for Touchables. @@ -415,14 +430,14 @@ const TouchableMixin = { /** * Place as callback for a DOM element's `onResponderTerminate` event. */ - touchableHandleResponderTerminate: function(e) { + touchableHandleResponderTerminate: function(e: Event) { this._receiveSignal(Signals.RESPONDER_TERMINATED, e); }, /** * Place as callback for a DOM element's `onResponderMove` event. */ - touchableHandleResponderMove: function(e) { + 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) { @@ -578,21 +593,34 @@ const TouchableMixin = { UIManager.measure(tag, this._handleQueryLayout); }, - _handleQueryLayout: function(l, t, w, h, globalX, globalY) { + _handleQueryLayout: function( + x: number, + y: number, + width: number, + height: number, + globalX: number, + globalY: number + ) { + // don't do anything if UIManager failed to measure node + if (!x && !y && !width && !height && !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); - this.state.touchable.dimensionsOnActivate = BoundingDimensions.getPooled(w, h); + // $FlowFixMe + this.state.touchable.dimensionsOnActivate = BoundingDimensions.getPooled(width, height); }, - _handleDelay: function(e) { + _handleDelay: function(e: Event) { this.touchableDelayTimeout = null; this._receiveSignal(Signals.DELAY, e); }, - _handleLongDelay: function(e) { + _handleLongDelay: function(e: Event) { this.longPressDelayTimeout = null; const curState = this.state.touchable.touchState; if ( @@ -620,7 +648,7 @@ const TouchableMixin = { * @throws Error if invalid state transition or unrecognized signal. * @sideeffects */ - _receiveSignal: function(signal, e) { + _receiveSignal: function(signal: string, e: Event) { const responderID = this.state.touchable.responderID; const curState = this.state.touchable.touchState; const nextState = Transitions[curState] && Transitions[curState][signal]; @@ -660,13 +688,13 @@ const TouchableMixin = { this.longPressDelayTimeout = null; }, - _isHighlight: function(state) { + _isHighlight: function(state: string) { return ( state === States.RESPONDER_ACTIVE_PRESS_IN || state === States.RESPONDER_ACTIVE_LONG_PRESS_IN ); }, - _savePressInLocation: function(e) { + _savePressInLocation: function(e: Event) { const touch = TouchEventUtils.extractSingleTouch(e.nativeEvent); const pageX = touch && touch.pageX; const pageY = touch && touch.pageY; @@ -675,7 +703,7 @@ const TouchableMixin = { this.pressInLocation = { pageX, pageY, locationX, locationY }; }, - _getDistanceBetweenPoints: function(aX, aY, bX, bY) { + _getDistanceBetweenPoints: function(aX: number, aY: number, bX: number, bY: number) { const deltaX = aX - bX; const deltaY = aY - bY; return Math.sqrt(deltaX * deltaX + deltaY * deltaY); @@ -692,7 +720,12 @@ const TouchableMixin = { * @param {Event} e Native event. * @sideeffects */ - _performSideEffectsForTransition: function(curState, nextState, signal, e) { + _performSideEffectsForTransition: function( + curState: string, + nextState: string, + signal: string, + e: Event + ) { const curIsHighlight = this._isHighlight(curState); const newIsHighlight = this._isHighlight(nextState); @@ -739,12 +772,12 @@ const TouchableMixin = { this.touchableDelayTimeout = null; }, - _startHighlight: function(e) { + _startHighlight: function(e: Event) { this._savePressInLocation(e); this.touchableHandleActivePressIn && this.touchableHandleActivePressIn(e); }, - _endHighlight: function(e) { + _endHighlight: function(e: Event) { if (this.touchableHandleActivePressOut) { if (this.touchableGetPressOutDelayMS && this.touchableGetPressOutDelayMS()) { this.pressOutDelayTimeout = setTimeout(() => { @@ -754,29 +787,59 @@ const TouchableMixin = { this.touchableHandleActivePressOut(e); } } + }, + + // HACK: 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) { + if (type === 'keydown') { + if (!this._isTouchableKeyboardActive) { + if ( + !this.state.touchable.touchState || + this.state.touchable.touchState === States.NOT_RESPONDER + ) { + this.touchableHandleResponderGrant(e); + this._isTouchableKeyboardActive = true; + } + } + } else if (type === 'keyup') { + if (this._isTouchableKeyboardActive) { + if ( + this.state.touchable.touchState && + this.state.touchable.touchState !== States.NOT_RESPONDER + ) { + this.touchableHandleResponderRelease(e); + this._isTouchableKeyboardActive = false; + } + } + } + e.stopPropagation(); + e.preventDefault(); + } } }; -var Touchable = { +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 }) => { + 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 ( * * * ); * }, * ``` - * > **NOTE**: TouchableHighlight must have one child (not zero or more than one) - * > - * > If you wish to have several child components, wrap them in a View. */ +/* eslint-disable react/prefer-es6-class */ const TouchableHighlight = createReactClass({ + displayName: 'TouchableHighlight', propTypes: { ...TouchableWithoutFeedback.propTypes, /** @@ -72,37 +73,36 @@ const TouchableHighlight = createReactClass({ */ activeOpacity: number, /** - * The color of the underlay that will show through when the touch is - * active. + * Called immediately after the underlay is hidden */ - underlayColor: ColorPropType, - style: StyleSheetPropType(ViewStylePropTypes), + onHideUnderlay: func, /** * Called immediately after the underlay is shown */ onShowUnderlay: func, + style: ViewPropTypes.style, /** - * Called immediately after the underlay is hidden + * The color of the underlay that will show through when the touch is + * active. */ - onHideUnderlay: func + underlayColor: ColorPropType }, - mixins: [NativeMethodsMixin, TimerMixin, Touchable.Mixin], + mixins: [TimerMixin, Touchable.Mixin], getDefaultProps: () => DEFAULT_PROPS, // Performance optimization to avoid constantly re-generating these objects. - computeSyntheticState: props => { - const { activeOpacity, style, underlayColor } = props; + _computeSyntheticState: function(props) { return { activeProps: { style: { - opacity: activeOpacity + opacity: props.activeOpacity } }, activeUnderlayProps: { style: { - backgroundColor: underlayColor + backgroundColor: props.underlayColor } }, underlayStyle: [INACTIVE_UNDERLAY_PROPS.style, props.style] @@ -110,20 +110,25 @@ const TouchableHighlight = createReactClass({ }, getInitialState: function() { + this._isMounted = false; return { ...this.touchableGetInitialState(), - ...this.computeSyntheticState(this.props) + ...this._computeSyntheticState(this.props) }; }, componentDidMount: function() { - ensurePositiveDelayProps(this.props); - ensureComponentIsNative(this.refs[CHILD_REF]); this._isMounted = true; + ensurePositiveDelayProps(this.props); + ensureComponentIsNative(this._childRef); + }, + + componentWillUnmount: function() { + this._isMounted = false; }, componentDidUpdate: function() { - ensureComponentIsNative(this.refs[CHILD_REF]); + ensureComponentIsNative(this._childRef); }, componentWillReceiveProps: function(nextProps) { @@ -133,19 +138,10 @@ const TouchableHighlight = createReactClass({ nextProps.underlayColor !== this.props.underlayColor || nextProps.style !== this.props.style ) { - this.setState(this.computeSyntheticState(nextProps)); + this.setState(this._computeSyntheticState(nextProps)); } }, - componentWillUnmount: function() { - this._isMounted = false; - }, - - // viewConfig: { - // uiViewClassName: 'RCTView', - // validAttributes: ReactNativeViewAttributes.RCTView - // }, - /** * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. @@ -200,17 +196,17 @@ const TouchableHighlight = createReactClass({ return; } - this.refs[UNDERLAY_REF].setNativeProps(this.state.activeUnderlayProps); - this.refs[CHILD_REF].setNativeProps(this.state.activeProps); + this._underlayRef.setNativeProps(this.state.activeUnderlayProps); + this._childRef.setNativeProps(this.state.activeProps); this.props.onShowUnderlay && this.props.onShowUnderlay(); }, _hideUnderlay: function() { this.clearTimeout(this._hideTimeout); this._hideTimeout = null; - if (this._hasPressHandler() && this.refs[UNDERLAY_REF]) { - this.refs[CHILD_REF].setNativeProps(INACTIVE_CHILD_PROPS); - this.refs[UNDERLAY_REF].setNativeProps({ + if (this._hasPressHandler() && this._underlayRef) { + this._childRef.setNativeProps(INACTIVE_CHILD_PROPS); + this._underlayRef.setNativeProps({ ...INACTIVE_UNDERLAY_PROPS, style: this.state.underlayStyle }); @@ -227,12 +223,12 @@ const TouchableHighlight = createReactClass({ ); }, - _onKeyEnter(e, callback) { - const ENTER = 13; - if ((e.type === 'keypress' ? e.charCode : e.keyCode) === ENTER) { - callback && callback(e); - e.stopPropagation(); - } + _setChildRef(node) { + this._childRef = node; + }, + + _setUnderlayRef(node) { + this._underlayRef = node; }, render: function() { @@ -258,26 +254,19 @@ const TouchableHighlight = createReactClass({ { - this._onKeyEnter(e, this.touchableHandleActivePressIn); - }} - onKeyPress={e => { - this._onKeyEnter(e, this.touchableHandlePress); - }} - onKeyUp={e => { - this._onKeyEnter(e, this.touchableHandleActivePressOut); - }} - onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} - onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest} + onKeyDown={this.touchableHandleKeyEvent} + onKeyUp={this.touchableHandleKeyEvent} onResponderGrant={this.touchableHandleResponderGrant} onResponderMove={this.touchableHandleResponderMove} onResponderRelease={this.touchableHandleResponderRelease} onResponderTerminate={this.touchableHandleResponderTerminate} - ref={UNDERLAY_REF} + onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest} + onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} + ref={this._setUnderlayRef} style={[styles.root, this.props.disabled && styles.disabled, this.state.underlayStyle]} > {React.cloneElement(React.Children.only(this.props.children), { - ref: CHILD_REF + ref: this._setChildRef })} {Touchable.renderDebugView({ color: 'green', hitSlop: this.props.hitSlop })} @@ -285,17 +274,14 @@ const TouchableHighlight = createReactClass({ } }); -var CHILD_REF = 'childRef'; -var UNDERLAY_REF = 'underlayRef'; - -var INACTIVE_CHILD_PROPS = { +const INACTIVE_CHILD_PROPS = { style: StyleSheet.create({ x: { opacity: 1.0 } }).x }; -var INACTIVE_UNDERLAY_PROPS = { +const INACTIVE_UNDERLAY_PROPS = { style: StyleSheet.create({ x: { backgroundColor: 'transparent' } }).x }; -var styles = StyleSheet.create({ +const styles = StyleSheet.create({ root: { cursor: 'pointer', userSelect: 'none' @@ -305,4 +291,4 @@ var styles = StyleSheet.create({ } }); -export default TouchableHighlight; +export default applyNativeMethods(TouchableHighlight); diff --git a/src/components/Touchable/TouchableNativeFeedback.js b/src/components/Touchable/TouchableNativeFeedback.js index 0a818b1a..ff1004e2 100644 --- a/src/components/Touchable/TouchableNativeFeedback.js +++ b/src/components/Touchable/TouchableNativeFeedback.js @@ -1,2 +1,12 @@ +/** + * Copyright 2017-present, Nicolas Gallagher + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + import UnimplementedView from '../UnimplementedView'; export default UnimplementedView; diff --git a/src/components/Touchable/TouchableOpacity.js b/src/components/Touchable/TouchableOpacity.js index 82f33a02..70f77165 100644 --- a/src/components/Touchable/TouchableOpacity.js +++ b/src/components/Touchable/TouchableOpacity.js @@ -1,5 +1,3 @@ -/* eslint-disable */ - /** * Copyright (c) 2016-present, Nicolas Gallagher. * Copyright (c) 2015-present, Facebook, Inc. @@ -12,13 +10,12 @@ * @noflow */ +import applyNativeMethods from '../../modules/applyNativeMethods'; import createReactClass from 'create-react-class'; import ensurePositiveDelayProps from './ensurePositiveDelayProps'; -import NativeMethodsMixin from '../../modules/NativeMethodsMixin'; import { number } from 'prop-types'; import React from 'react'; import StyleSheet from '../../apis/StyleSheet'; -import TimerMixin from 'react-timer-mixin'; import Touchable from './Touchable'; import TouchableWithoutFeedback from './TouchableWithoutFeedback'; import View from '../View'; @@ -32,8 +29,9 @@ const PRESS_RETENTION_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 }; /** * A wrapper for making views respond properly to touches. * On press down, the opacity of the wrapped view is decreased, dimming it. - * This is done without actually changing the view hierarchy, and in general is - * easy to add to an app without weird side-effects. + * + * Opacity is controlled by wrapping the children in a View, which is + * added to the view hiearchy. Be aware that this can affect layout. * * Example: * @@ -43,15 +41,18 @@ const PRESS_RETENTION_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 }; * * * * ); * }, * ``` */ + +/* eslint-disable react/prefer-es6-class */ const TouchableOpacity = createReactClass({ - mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin], + displayName: 'TouchableOpacity', + mixins: [Touchable.Mixin], propTypes: { ...TouchableWithoutFeedback.propTypes, @@ -82,11 +83,14 @@ const TouchableOpacity = createReactClass({ ensurePositiveDelayProps(nextProps); }, - setOpacityTo: function(value: number, duration: number) { + /** + * Animate the touchable to a new opacity. + */ + setOpacityTo: function(value: number, duration: ?number) { this.setNativeProps({ style: { opacity: value, - transitionDuration: `${duration / 1000}s` + transitionDuration: duration ? `${duration / 1000}s` : '0s' } }); }, @@ -142,20 +146,16 @@ const TouchableOpacity = createReactClass({ }, _opacityInactive: function(duration: number) { - const childStyle = flattenStyle(this.props.style) || {}; - this.setOpacityTo(childStyle.opacity === undefined ? 1 : childStyle.opacity, duration); + this.setOpacityTo(this._getChildStyleOpacityWithDefault(), duration); }, _opacityFocused: function() { this.setOpacityTo(this.props.focusedOpacity); }, - _onKeyEnter(e, callback) { - const ENTER = 13; - if ((e.type === 'keypress' ? e.charCode : e.keyCode) === ENTER) { - callback && callback(e); - e.stopPropagation(); - } + _getChildStyleOpacityWithDefault: function() { + const childStyle = flattenStyle(this.props.style) || {}; + return childStyle.opacity === undefined ? 1 : childStyle.opacity; }, render: function() { @@ -179,22 +179,15 @@ const TouchableOpacity = createReactClass({ { - this._onKeyEnter(e, this.touchableHandleActivePressIn); - }} - onKeyPress={e => { - this._onKeyEnter(e, this.touchableHandlePress); - }} - onKeyUp={e => { - this._onKeyEnter(e, this.touchableHandleActivePressOut); - }} - onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} - onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest} + onKeyDown={this.touchableHandleKeyEvent} + onKeyUp={this.touchableHandleKeyEvent} onResponderGrant={this.touchableHandleResponderGrant} onResponderMove={this.touchableHandleResponderMove} onResponderRelease={this.touchableHandleResponderRelease} onResponderTerminate={this.touchableHandleResponderTerminate} + onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest} + onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} + style={[styles.root, this.props.disabled && styles.disabled, this.props.style]} > {this.props.children} {Touchable.renderDebugView({ color: 'blue', hitSlop: this.props.hitSlop })} @@ -203,7 +196,7 @@ const TouchableOpacity = createReactClass({ } }); -var styles = StyleSheet.create({ +const styles = StyleSheet.create({ root: { cursor: 'pointer', transitionProperty: 'opacity', @@ -215,4 +208,4 @@ var styles = StyleSheet.create({ } }); -export default TouchableOpacity; +export default applyNativeMethods(TouchableOpacity); diff --git a/src/components/Touchable/TouchableWithoutFeedback.js b/src/components/Touchable/TouchableWithoutFeedback.js index 873acfae..88e45d2f 100644 --- a/src/components/Touchable/TouchableWithoutFeedback.js +++ b/src/components/Touchable/TouchableWithoutFeedback.js @@ -1,5 +1,3 @@ -/* eslint-disable */ - /** * Copyright (c) 2016-present, Nicolas Gallagher. * Copyright (c) 2015-present, Facebook, Inc. @@ -9,9 +7,10 @@ * LICENSE file in the root directory of this source tree. * * @providesModule TouchableWithoutFeedback - * @noflow + * @flow */ +import BaseComponentPropTypes from '../../propTypes/BaseComponentPropTypes'; import createReactClass from 'create-react-class'; import EdgeInsetsPropType from '../../propTypes/EdgeInsetsPropType'; import ensurePositiveDelayProps from './ensurePositiveDelayProps'; @@ -27,42 +26,29 @@ type Event = Object; const PRESS_RETENTION_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 }; /** - * Do not use unless you have a very good reason. All the elements that - * respond to press should have a visual feedback when touched. This is - * one of the primary reasons a "web" app doesn't feel "native". + * Do not use unless you have a very good reason. All elements that + * respond to press should have a visual feedback when touched. * - * > **NOTE**: TouchableWithoutFeedback supports only one child - * > - * > If you wish to have several child components, wrap them in a View. + * TouchableWithoutFeedback supports only one child. + * If you wish to have several child components, wrap them in a View. */ + +/* eslint-disable react/prefer-es6-class */ const TouchableWithoutFeedback = createReactClass({ + displayName: 'TouchableWithoutFeedback', mixins: [TimerMixin, Touchable.Mixin], propTypes: { - accessible: bool, + accessibilityComponentType: BaseComponentPropTypes.accessibilityComponentType, accessibilityLabel: string, - accessibilityRole: string, + accessibilityRole: BaseComponentPropTypes.accessibilityRole, + accessibilityTraits: BaseComponentPropTypes.accessibilityTraits, + accessible: bool, children: element, /** - * If true, disable all interactions for this component. + * Delay in ms, from onPressIn, before onLongPress is called. */ - disabled: bool, - /** - * Called when the touch is released, but not if cancelled (e.g. by a scroll - * that steals the responder lock). - */ - onPress: func, - onPressIn: func, - onPressOut: func, - /** - * Invoked on mount and layout changes with - * - * `{nativeEvent: {layout: {x, y, width, height}}}` - */ - onLayout: func, - - onLongPress: func, - + delayLongPress: number, /** * Delay in ms, from the start of the touch, before onPressIn is called. */ @@ -72,9 +58,29 @@ const TouchableWithoutFeedback = createReactClass({ */ delayPressOut: number, /** - * Delay in ms, from onPressIn, before onLongPress is called. + * If true, disable all interactions for this component. */ - delayLongPress: number, + disabled: bool, + /** + * This defines how far your touch can start away from the button. This is + * added to `pressRetentionOffset` when moving off of the button. + */ + // $FlowFixMe(>=0.41.0) + hitSlop: EdgeInsetsPropType, + /** + * Invoked on mount and layout changes with + * + * `{nativeEvent: {layout: {x, y, width, height}}}` + */ + onLayout: func, + onLongPress: func, + /** + * Called when the touch is released, but not if cancelled (e.g. by a scroll + * that steals the responder lock). + */ + onPress: func, + onPressIn: func, + onPressOut: func, /** * 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,16 +90,7 @@ const TouchableWithoutFeedback = createReactClass({ */ // $FlowFixMe pressRetentionOffset: EdgeInsetsPropType, - /** - * 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. - */ - // $FlowFixMe - hitSlop: EdgeInsetsPropType + testID: string }, getInitialState: function() { @@ -187,19 +184,19 @@ const TouchableWithoutFeedback = createReactClass({ return (React: any).cloneElement(child, { ...other, accessible: this.props.accessible !== false, - onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, - onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, + children, onResponderGrant: this.touchableHandleResponderGrant, onResponderMove: this.touchableHandleResponderMove, onResponderRelease: this.touchableHandleResponderRelease, onResponderTerminate: this.touchableHandleResponderTerminate, - style, - children + onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, + onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, + style }); } }); -var styles = StyleSheet.create({ +const styles = StyleSheet.create({ root: { cursor: 'pointer' },