TouchableHighlight.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @flow strict-local
  8. * @format
  9. */
  10. 'use strict';
  11. import Pressability, {
  12. type PressabilityConfig,
  13. } from '../../Pressability/Pressability';
  14. import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
  15. import StyleSheet, {type ViewStyleProp} from '../../StyleSheet/StyleSheet';
  16. import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
  17. import TVTouchable from './TVTouchable';
  18. import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback';
  19. import Platform from '../../Utilities/Platform';
  20. import View from '../../Components/View/View';
  21. import * as React from 'react';
  22. type AndroidProps = $ReadOnly<{|
  23. nextFocusDown?: ?number,
  24. nextFocusForward?: ?number,
  25. nextFocusLeft?: ?number,
  26. nextFocusRight?: ?number,
  27. nextFocusUp?: ?number,
  28. |}>;
  29. type IOSProps = $ReadOnly<{|
  30. hasTVPreferredFocus?: ?boolean,
  31. |}>;
  32. type Props = $ReadOnly<{|
  33. ...React.ElementConfig<TouchableWithoutFeedback>,
  34. ...AndroidProps,
  35. ...IOSProps,
  36. activeOpacity?: ?number,
  37. underlayColor?: ?ColorValue,
  38. style?: ?ViewStyleProp,
  39. onShowUnderlay?: ?() => void,
  40. onHideUnderlay?: ?() => void,
  41. testOnly_pressed?: ?boolean,
  42. hostRef: React.Ref<typeof View>,
  43. |}>;
  44. type ExtraStyles = $ReadOnly<{|
  45. child: ViewStyleProp,
  46. underlay: ViewStyleProp,
  47. |}>;
  48. type State = $ReadOnly<{|
  49. pressability: Pressability,
  50. extraStyles: ?ExtraStyles,
  51. |}>;
  52. /**
  53. * A wrapper for making views respond properly to touches.
  54. * On press down, the opacity of the wrapped view is decreased, which allows
  55. * the underlay color to show through, darkening or tinting the view.
  56. *
  57. * The underlay comes from wrapping the child in a new View, which can affect
  58. * layout, and sometimes cause unwanted visual artifacts if not used correctly,
  59. * for example if the backgroundColor of the wrapped view isn't explicitly set
  60. * to an opaque color.
  61. *
  62. * TouchableHighlight must have one child (not zero or more than one).
  63. * If you wish to have several child components, wrap them in a View.
  64. *
  65. * Example:
  66. *
  67. * ```
  68. * renderButton: function() {
  69. * return (
  70. * <TouchableHighlight onPress={this._onPressButton}>
  71. * <Image
  72. * style={styles.button}
  73. * source={require('./myButton.png')}
  74. * />
  75. * </TouchableHighlight>
  76. * );
  77. * },
  78. * ```
  79. *
  80. *
  81. * ### Example
  82. *
  83. * ```ReactNativeWebPlayer
  84. * import React, { Component } from 'react'
  85. * import {
  86. * AppRegistry,
  87. * StyleSheet,
  88. * TouchableHighlight,
  89. * Text,
  90. * View,
  91. * } from 'react-native'
  92. *
  93. * class App extends Component {
  94. * constructor(props) {
  95. * super(props)
  96. * this.state = { count: 0 }
  97. * }
  98. *
  99. * onPress = () => {
  100. * this.setState({
  101. * count: this.state.count+1
  102. * })
  103. * }
  104. *
  105. * render() {
  106. * return (
  107. * <View style={styles.container}>
  108. * <TouchableHighlight
  109. * style={styles.button}
  110. * onPress={this.onPress}
  111. * >
  112. * <Text> Touch Here </Text>
  113. * </TouchableHighlight>
  114. * <View style={[styles.countContainer]}>
  115. * <Text style={[styles.countText]}>
  116. * { this.state.count !== 0 ? this.state.count: null}
  117. * </Text>
  118. * </View>
  119. * </View>
  120. * )
  121. * }
  122. * }
  123. *
  124. * const styles = StyleSheet.create({
  125. * container: {
  126. * flex: 1,
  127. * justifyContent: 'center',
  128. * paddingHorizontal: 10
  129. * },
  130. * button: {
  131. * alignItems: 'center',
  132. * backgroundColor: '#DDDDDD',
  133. * padding: 10
  134. * },
  135. * countContainer: {
  136. * alignItems: 'center',
  137. * padding: 10
  138. * },
  139. * countText: {
  140. * color: '#FF00FF'
  141. * }
  142. * })
  143. *
  144. * AppRegistry.registerComponent('App', () => App)
  145. * ```
  146. *
  147. */
  148. class TouchableHighlight extends React.Component<Props, State> {
  149. _hideTimeout: ?TimeoutID;
  150. _isMounted: boolean = false;
  151. _tvTouchable: ?TVTouchable;
  152. state: State = {
  153. pressability: new Pressability(this._createPressabilityConfig()),
  154. extraStyles:
  155. this.props.testOnly_pressed === true ? this._createExtraStyles() : null,
  156. };
  157. _createPressabilityConfig(): PressabilityConfig {
  158. return {
  159. cancelable: !this.props.rejectResponderTermination,
  160. disabled: this.props.disabled,
  161. hitSlop: this.props.hitSlop,
  162. delayLongPress: this.props.delayLongPress,
  163. delayPressIn: this.props.delayPressIn,
  164. delayPressOut: this.props.delayPressOut,
  165. minPressDuration: 0,
  166. pressRectOffset: this.props.pressRetentionOffset,
  167. android_disableSound: this.props.touchSoundDisabled,
  168. onBlur: event => {
  169. if (Platform.isTV) {
  170. this._hideUnderlay();
  171. }
  172. if (this.props.onBlur != null) {
  173. this.props.onBlur(event);
  174. }
  175. },
  176. onFocus: event => {
  177. if (Platform.isTV) {
  178. this._showUnderlay();
  179. }
  180. if (this.props.onFocus != null) {
  181. this.props.onFocus(event);
  182. }
  183. },
  184. onLongPress: event => {
  185. if (this.props.onLongPress != null) {
  186. this.props.onLongPress(event);
  187. }
  188. },
  189. onPress: event => {
  190. if (this._hideTimeout != null) {
  191. clearTimeout(this._hideTimeout);
  192. }
  193. if (!Platform.isTV) {
  194. this._showUnderlay();
  195. this._hideTimeout = setTimeout(() => {
  196. this._hideUnderlay();
  197. }, this.props.delayPressOut ?? 0);
  198. }
  199. if (this.props.onPress != null) {
  200. this.props.onPress(event);
  201. }
  202. },
  203. onPressIn: event => {
  204. if (this._hideTimeout != null) {
  205. clearTimeout(this._hideTimeout);
  206. this._hideTimeout = null;
  207. }
  208. this._showUnderlay();
  209. if (this.props.onPressIn != null) {
  210. this.props.onPressIn(event);
  211. }
  212. },
  213. onPressOut: event => {
  214. if (this._hideTimeout == null) {
  215. this._hideUnderlay();
  216. }
  217. if (this.props.onPressOut != null) {
  218. this.props.onPressOut(event);
  219. }
  220. },
  221. };
  222. }
  223. _createExtraStyles(): ExtraStyles {
  224. return {
  225. child: {opacity: this.props.activeOpacity ?? 0.85},
  226. underlay: {
  227. backgroundColor:
  228. this.props.underlayColor === undefined
  229. ? 'black'
  230. : this.props.underlayColor,
  231. },
  232. };
  233. }
  234. _showUnderlay(): void {
  235. if (!this._isMounted || !this._hasPressHandler()) {
  236. return;
  237. }
  238. this.setState({extraStyles: this._createExtraStyles()});
  239. if (this.props.onShowUnderlay != null) {
  240. this.props.onShowUnderlay();
  241. }
  242. }
  243. _hideUnderlay(): void {
  244. if (this._hideTimeout != null) {
  245. clearTimeout(this._hideTimeout);
  246. this._hideTimeout = null;
  247. }
  248. if (this.props.testOnly_pressed === true) {
  249. return;
  250. }
  251. if (this._hasPressHandler()) {
  252. this.setState({extraStyles: null});
  253. if (this.props.onHideUnderlay != null) {
  254. this.props.onHideUnderlay();
  255. }
  256. }
  257. }
  258. _hasPressHandler(): boolean {
  259. return (
  260. this.props.onPress != null ||
  261. this.props.onPressIn != null ||
  262. this.props.onPressOut != null ||
  263. this.props.onLongPress != null
  264. );
  265. }
  266. render(): React.Node {
  267. const child = React.Children.only(this.props.children);
  268. // BACKWARD-COMPATIBILITY: Focus and blur events were never supported before
  269. // adopting `Pressability`, so preserve that behavior.
  270. const {
  271. onBlur,
  272. onFocus,
  273. ...eventHandlersWithoutBlurAndFocus
  274. } = this.state.pressability.getEventHandlers();
  275. return (
  276. <View
  277. accessible={this.props.accessible !== false}
  278. accessibilityLabel={this.props.accessibilityLabel}
  279. accessibilityHint={this.props.accessibilityHint}
  280. accessibilityRole={this.props.accessibilityRole}
  281. accessibilityState={this.props.accessibilityState}
  282. accessibilityValue={this.props.accessibilityValue}
  283. accessibilityActions={this.props.accessibilityActions}
  284. onAccessibilityAction={this.props.onAccessibilityAction}
  285. importantForAccessibility={this.props.importantForAccessibility}
  286. accessibilityLiveRegion={this.props.accessibilityLiveRegion}
  287. accessibilityViewIsModal={this.props.accessibilityViewIsModal}
  288. accessibilityElementsHidden={this.props.accessibilityElementsHidden}
  289. style={StyleSheet.compose(
  290. this.props.style,
  291. this.state.extraStyles?.underlay,
  292. )}
  293. onLayout={this.props.onLayout}
  294. hitSlop={this.props.hitSlop}
  295. hasTVPreferredFocus={this.props.hasTVPreferredFocus}
  296. nextFocusDown={this.props.nextFocusDown}
  297. nextFocusForward={this.props.nextFocusForward}
  298. nextFocusLeft={this.props.nextFocusLeft}
  299. nextFocusRight={this.props.nextFocusRight}
  300. nextFocusUp={this.props.nextFocusUp}
  301. focusable={
  302. this.props.focusable !== false && this.props.onPress !== undefined
  303. }
  304. nativeID={this.props.nativeID}
  305. testID={this.props.testID}
  306. ref={this.props.hostRef}
  307. {...eventHandlersWithoutBlurAndFocus}>
  308. {React.cloneElement(child, {
  309. style: StyleSheet.compose(
  310. child.props.style,
  311. this.state.extraStyles?.child,
  312. ),
  313. })}
  314. {__DEV__ ? (
  315. <PressabilityDebugView color="green" hitSlop={this.props.hitSlop} />
  316. ) : null}
  317. </View>
  318. );
  319. }
  320. componentDidMount(): void {
  321. this._isMounted = true;
  322. if (Platform.isTV) {
  323. this._tvTouchable = new TVTouchable(this, {
  324. getDisabled: () => this.props.disabled === true,
  325. onBlur: event => {
  326. if (this.props.onBlur != null) {
  327. this.props.onBlur(event);
  328. }
  329. },
  330. onFocus: event => {
  331. if (this.props.onFocus != null) {
  332. this.props.onFocus(event);
  333. }
  334. },
  335. onPress: event => {
  336. if (this.props.onPress != null) {
  337. this.props.onPress(event);
  338. }
  339. },
  340. });
  341. }
  342. }
  343. componentDidUpdate(prevProps: Props, prevState: State) {
  344. this.state.pressability.configure(this._createPressabilityConfig());
  345. }
  346. componentWillUnmount(): void {
  347. this._isMounted = false;
  348. if (this._hideTimeout != null) {
  349. clearTimeout(this._hideTimeout);
  350. }
  351. if (Platform.isTV) {
  352. if (this._tvTouchable != null) {
  353. this._tvTouchable.destroy();
  354. }
  355. }
  356. this.state.pressability.reset();
  357. }
  358. }
  359. module.exports = (React.forwardRef((props, hostRef) => (
  360. <TouchableHighlight {...props} hostRef={hostRef} />
  361. )): React.ComponentType<$ReadOnly<$Diff<Props, {|hostRef: mixed|}>>>);