Pressability.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  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 {isHoverEnabled} from './HoverState';
  12. import invariant from 'invariant';
  13. import SoundManager from '../Components/Sound/SoundManager';
  14. import {normalizeRect, type RectOrSize} from '../StyleSheet/Rect';
  15. import type {
  16. BlurEvent,
  17. FocusEvent,
  18. PressEvent,
  19. MouseEvent,
  20. } from '../Types/CoreEventTypes';
  21. import Platform from '../Utilities/Platform';
  22. import UIManager from '../ReactNative/UIManager';
  23. import type {HostComponent} from '../Renderer/shims/ReactNativeTypes';
  24. import * as React from 'react';
  25. export type PressabilityConfig = $ReadOnly<{|
  26. /**
  27. * Whether a press gesture can be interrupted by a parent gesture such as a
  28. * scroll event. Defaults to true.
  29. */
  30. cancelable?: ?boolean,
  31. /**
  32. * Whether to disable initialization of the press gesture.
  33. */
  34. disabled?: ?boolean,
  35. /**
  36. * Amount to extend the `VisualRect` by to create `HitRect`.
  37. */
  38. hitSlop?: ?RectOrSize,
  39. /**
  40. * Amount to extend the `HitRect` by to create `PressRect`.
  41. */
  42. pressRectOffset?: ?RectOrSize,
  43. /**
  44. * Whether to disable the systemm sound when `onPress` fires on Android.
  45. **/
  46. android_disableSound?: ?boolean,
  47. /**
  48. * Duration to wait after hover in before calling `onHoverIn`.
  49. */
  50. delayHoverIn?: ?number,
  51. /**
  52. * Duration to wait after hover out before calling `onHoverOut`.
  53. */
  54. delayHoverOut?: ?number,
  55. /**
  56. * Duration (in addition to `delayPressIn`) after which a press gesture is
  57. * considered a long press gesture. Defaults to 500 (milliseconds).
  58. */
  59. delayLongPress?: ?number,
  60. /**
  61. * Duration to wait after press down before calling `onPressIn`.
  62. */
  63. delayPressIn?: ?number,
  64. /**
  65. * Duration to wait after letting up before calling `onPressOut`.
  66. */
  67. delayPressOut?: ?number,
  68. /**
  69. * Minimum duration to wait between calling `onPressIn` and `onPressOut`.
  70. */
  71. minPressDuration?: ?number,
  72. /**
  73. * Called after the element loses focus.
  74. */
  75. onBlur?: ?(event: BlurEvent) => mixed,
  76. /**
  77. * Called after the element is focused.
  78. */
  79. onFocus?: ?(event: FocusEvent) => mixed,
  80. /**
  81. * Called when the hover is activated to provide visual feedback.
  82. */
  83. onHoverIn?: ?(event: MouseEvent) => mixed,
  84. /**
  85. * Called when the hover is deactivated to undo visual feedback.
  86. */
  87. onHoverOut?: ?(event: MouseEvent) => mixed,
  88. /**
  89. * Called when a long press gesture has been triggered.
  90. */
  91. onLongPress?: ?(event: PressEvent) => mixed,
  92. /**
  93. * Called when a press gestute has been triggered.
  94. */
  95. onPress?: ?(event: PressEvent) => mixed,
  96. /**
  97. * Called when the press is activated to provide visual feedback.
  98. */
  99. onPressIn?: ?(event: PressEvent) => mixed,
  100. /**
  101. * Called when the press location moves. (This should rarely be used.)
  102. */
  103. onPressMove?: ?(event: PressEvent) => mixed,
  104. /**
  105. * Called when the press is deactivated to undo visual feedback.
  106. */
  107. onPressOut?: ?(event: PressEvent) => mixed,
  108. /**
  109. * Returns whether a long press gesture should cancel the press gesture.
  110. * Defaults to true.
  111. */
  112. onLongPressShouldCancelPress_DEPRECATED?: ?() => boolean,
  113. /**
  114. * If `cancelable` is set, this will be ignored.
  115. *
  116. * Returns whether to yield to a lock termination request (e.g. if a native
  117. * scroll gesture attempts to steal the responder lock).
  118. */
  119. onResponderTerminationRequest_DEPRECATED?: ?() => boolean,
  120. /**
  121. * If `disabled` is set, this will be ignored.
  122. *
  123. * Returns whether to start a press gesture.
  124. *
  125. * @deprecated
  126. */
  127. onStartShouldSetResponder_DEPRECATED?: ?() => boolean,
  128. |}>;
  129. export type EventHandlers = $ReadOnly<{|
  130. onBlur: (event: BlurEvent) => void,
  131. onClick: (event: PressEvent) => void,
  132. onFocus: (event: FocusEvent) => void,
  133. onMouseEnter?: (event: MouseEvent) => void,
  134. onMouseLeave?: (event: MouseEvent) => void,
  135. onResponderGrant: (event: PressEvent) => void,
  136. onResponderMove: (event: PressEvent) => void,
  137. onResponderRelease: (event: PressEvent) => void,
  138. onResponderTerminate: (event: PressEvent) => void,
  139. onResponderTerminationRequest: () => boolean,
  140. onStartShouldSetResponder: () => boolean,
  141. |}>;
  142. type TouchState =
  143. | 'NOT_RESPONDER'
  144. | 'RESPONDER_INACTIVE_PRESS_IN'
  145. | 'RESPONDER_INACTIVE_PRESS_OUT'
  146. | 'RESPONDER_ACTIVE_PRESS_IN'
  147. | 'RESPONDER_ACTIVE_PRESS_OUT'
  148. | 'RESPONDER_ACTIVE_LONG_PRESS_IN'
  149. | 'RESPONDER_ACTIVE_LONG_PRESS_OUT'
  150. | 'ERROR';
  151. type TouchSignal =
  152. | 'DELAY'
  153. | 'RESPONDER_GRANT'
  154. | 'RESPONDER_RELEASE'
  155. | 'RESPONDER_TERMINATED'
  156. | 'ENTER_PRESS_RECT'
  157. | 'LEAVE_PRESS_RECT'
  158. | 'LONG_PRESS_DETECTED';
  159. const Transitions = Object.freeze({
  160. NOT_RESPONDER: {
  161. DELAY: 'ERROR',
  162. RESPONDER_GRANT: 'RESPONDER_INACTIVE_PRESS_IN',
  163. RESPONDER_RELEASE: 'ERROR',
  164. RESPONDER_TERMINATED: 'ERROR',
  165. ENTER_PRESS_RECT: 'ERROR',
  166. LEAVE_PRESS_RECT: 'ERROR',
  167. LONG_PRESS_DETECTED: 'ERROR',
  168. },
  169. RESPONDER_INACTIVE_PRESS_IN: {
  170. DELAY: 'RESPONDER_ACTIVE_PRESS_IN',
  171. RESPONDER_GRANT: 'ERROR',
  172. RESPONDER_RELEASE: 'NOT_RESPONDER',
  173. RESPONDER_TERMINATED: 'NOT_RESPONDER',
  174. ENTER_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_IN',
  175. LEAVE_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_OUT',
  176. LONG_PRESS_DETECTED: 'ERROR',
  177. },
  178. RESPONDER_INACTIVE_PRESS_OUT: {
  179. DELAY: 'RESPONDER_ACTIVE_PRESS_OUT',
  180. RESPONDER_GRANT: 'ERROR',
  181. RESPONDER_RELEASE: 'NOT_RESPONDER',
  182. RESPONDER_TERMINATED: 'NOT_RESPONDER',
  183. ENTER_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_IN',
  184. LEAVE_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_OUT',
  185. LONG_PRESS_DETECTED: 'ERROR',
  186. },
  187. RESPONDER_ACTIVE_PRESS_IN: {
  188. DELAY: 'ERROR',
  189. RESPONDER_GRANT: 'ERROR',
  190. RESPONDER_RELEASE: 'NOT_RESPONDER',
  191. RESPONDER_TERMINATED: 'NOT_RESPONDER',
  192. ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_IN',
  193. LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_OUT',
  194. LONG_PRESS_DETECTED: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
  195. },
  196. RESPONDER_ACTIVE_PRESS_OUT: {
  197. DELAY: 'ERROR',
  198. RESPONDER_GRANT: 'ERROR',
  199. RESPONDER_RELEASE: 'NOT_RESPONDER',
  200. RESPONDER_TERMINATED: 'NOT_RESPONDER',
  201. ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_IN',
  202. LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_OUT',
  203. LONG_PRESS_DETECTED: 'ERROR',
  204. },
  205. RESPONDER_ACTIVE_LONG_PRESS_IN: {
  206. DELAY: 'ERROR',
  207. RESPONDER_GRANT: 'ERROR',
  208. RESPONDER_RELEASE: 'NOT_RESPONDER',
  209. RESPONDER_TERMINATED: 'NOT_RESPONDER',
  210. ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
  211. LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_OUT',
  212. LONG_PRESS_DETECTED: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
  213. },
  214. RESPONDER_ACTIVE_LONG_PRESS_OUT: {
  215. DELAY: 'ERROR',
  216. RESPONDER_GRANT: 'ERROR',
  217. RESPONDER_RELEASE: 'NOT_RESPONDER',
  218. RESPONDER_TERMINATED: 'NOT_RESPONDER',
  219. ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
  220. LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_OUT',
  221. LONG_PRESS_DETECTED: 'ERROR',
  222. },
  223. ERROR: {
  224. DELAY: 'NOT_RESPONDER',
  225. RESPONDER_GRANT: 'RESPONDER_INACTIVE_PRESS_IN',
  226. RESPONDER_RELEASE: 'NOT_RESPONDER',
  227. RESPONDER_TERMINATED: 'NOT_RESPONDER',
  228. ENTER_PRESS_RECT: 'NOT_RESPONDER',
  229. LEAVE_PRESS_RECT: 'NOT_RESPONDER',
  230. LONG_PRESS_DETECTED: 'NOT_RESPONDER',
  231. },
  232. });
  233. const isActiveSignal = signal =>
  234. signal === 'RESPONDER_ACTIVE_PRESS_IN' ||
  235. signal === 'RESPONDER_ACTIVE_LONG_PRESS_IN';
  236. const isActivationSignal = signal =>
  237. signal === 'RESPONDER_ACTIVE_PRESS_OUT' ||
  238. signal === 'RESPONDER_ACTIVE_PRESS_IN';
  239. const isPressInSignal = signal =>
  240. signal === 'RESPONDER_INACTIVE_PRESS_IN' ||
  241. signal === 'RESPONDER_ACTIVE_PRESS_IN' ||
  242. signal === 'RESPONDER_ACTIVE_LONG_PRESS_IN';
  243. const isTerminalSignal = signal =>
  244. signal === 'RESPONDER_TERMINATED' || signal === 'RESPONDER_RELEASE';
  245. const DEFAULT_LONG_PRESS_DELAY_MS = 500;
  246. const DEFAULT_PRESS_RECT_OFFSETS = {
  247. bottom: 30,
  248. left: 20,
  249. right: 20,
  250. top: 20,
  251. };
  252. const DEFAULT_MIN_PRESS_DURATION = 130;
  253. /**
  254. * Pressability implements press handling capabilities.
  255. *
  256. * =========================== Pressability Tutorial ===========================
  257. *
  258. * The `Pressability` class helps you create press interactions by analyzing the
  259. * geometry of elements and observing when another responder (e.g. ScrollView)
  260. * has stolen the touch lock. It offers hooks for your component to provide
  261. * interaction feedback to the user:
  262. *
  263. * - When a press has activated (e.g. highlight an element)
  264. * - When a press has deactivated (e.g. un-highlight an element)
  265. * - When a press sould trigger an action, meaning it activated and deactivated
  266. * while within the geometry of the element without the lock being stolen.
  267. *
  268. * A high quality interaction isn't as simple as you might think. There should
  269. * be a slight delay before activation. Moving your finger beyond an element's
  270. * bounds should trigger deactivation, but moving the same finger back within an
  271. * element's bounds should trigger reactivation.
  272. *
  273. * In order to use `Pressability`, do the following:
  274. *
  275. * 1. Instantiate `Pressability` and store it on your component's state.
  276. *
  277. * state = {
  278. * pressability: new Pressability({
  279. * // ...
  280. * }),
  281. * };
  282. *
  283. * 2. Choose the rendered component who should collect the press events. On that
  284. * element, spread `pressability.getEventHandlers()` into its props.
  285. *
  286. * return (
  287. * <View {...this.state.pressability.getEventHandlers()} />
  288. * );
  289. *
  290. * 3. Reset `Pressability` when your component unmounts.
  291. *
  292. * componentWillUnmount() {
  293. * this.state.pressability.reset();
  294. * }
  295. *
  296. * ==================== Pressability Implementation Details ====================
  297. *
  298. * `Pressability` only assumes that there exists a `HitRect` node. The `PressRect`
  299. * is an abstract box that is extended beyond the `HitRect`.
  300. *
  301. * # Geometry
  302. *
  303. * ┌────────────────────────┐
  304. * │ ┌──────────────────┐ │ - Presses start anywhere within `HitRect`, which
  305. * │ │ ┌────────────┐ │ │ is expanded via the prop `hitSlop`.
  306. * │ │ │ VisualRect │ │ │
  307. * │ │ └────────────┘ │ │ - When pressed down for sufficient amount of time
  308. * │ │ HitRect │ │ before letting up, `VisualRect` activates for
  309. * │ └──────────────────┘ │ as long as the press stays within `PressRect`.
  310. * │ PressRect o │
  311. * └────────────────────│───┘
  312. * Out Region └────── `PressRect`, which is expanded via the prop
  313. * `pressRectOffset`, allows presses to move
  314. * beyond `HitRect` while maintaining activation
  315. * and being eligible for a "press".
  316. *
  317. * # State Machine
  318. *
  319. * ┌───────────────┐ ◀──── RESPONDER_RELEASE
  320. * │ NOT_RESPONDER │
  321. * └───┬───────────┘ ◀──── RESPONDER_TERMINATED
  322. * │
  323. * │ RESPONDER_GRANT (HitRect)
  324. * │
  325. * ▼
  326. * ┌─────────────────────┐ ┌───────────────────┐ ┌───────────────────┐
  327. * │ RESPONDER_INACTIVE_ │ DELAY │ RESPONDER_ACTIVE_ │ T + DELAY │ RESPONDER_ACTIVE_ │
  328. * │ PRESS_IN ├────────▶ │ PRESS_IN ├────────────▶ │ LONG_PRESS_IN │
  329. * └─┬───────────────────┘ └─┬─────────────────┘ └─┬─────────────────┘
  330. * │ ▲ │ ▲ │ ▲
  331. * │LEAVE_ │ │LEAVE_ │ │LEAVE_ │
  332. * │PRESS_RECT │ENTER_ │PRESS_RECT │ENTER_ │PRESS_RECT │ENTER_
  333. * │ │PRESS_RECT │ │PRESS_RECT │ │PRESS_RECT
  334. * ▼ │ ▼ │ ▼ │
  335. * ┌─────────────┴───────┐ ┌─────────────┴─────┐ ┌─────────────┴─────┐
  336. * │ RESPONDER_INACTIVE_ │ DELAY │ RESPONDER_ACTIVE_ │ │ RESPONDER_ACTIVE_ │
  337. * │ PRESS_OUT ├────────▶ │ PRESS_OUT │ │ LONG_PRESS_OUT │
  338. * └─────────────────────┘ └───────────────────┘ └───────────────────┘
  339. *
  340. * T + DELAY => LONG_PRESS_DELAY + DELAY
  341. *
  342. * Not drawn are the side effects of each transition. The most important side
  343. * effect is the invocation of `onPress` and `onLongPress` that occur when a
  344. * responder is release while in the "press in" states.
  345. */
  346. export default class Pressability {
  347. _config: PressabilityConfig;
  348. _eventHandlers: ?EventHandlers = null;
  349. _hoverInDelayTimeout: ?TimeoutID = null;
  350. _hoverOutDelayTimeout: ?TimeoutID = null;
  351. _isHovered: boolean = false;
  352. _longPressDelayTimeout: ?TimeoutID = null;
  353. _pressDelayTimeout: ?TimeoutID = null;
  354. _pressOutDelayTimeout: ?TimeoutID = null;
  355. _responderID: ?number | React.ElementRef<HostComponent<mixed>> = null;
  356. _responderRegion: ?$ReadOnly<{|
  357. bottom: number,
  358. left: number,
  359. right: number,
  360. top: number,
  361. |}> = null;
  362. _touchActivatePosition: ?$ReadOnly<{|
  363. pageX: number,
  364. pageY: number,
  365. |}>;
  366. _touchActivateTime: ?number;
  367. _touchState: TouchState = 'NOT_RESPONDER';
  368. constructor(config: PressabilityConfig) {
  369. this.configure(config);
  370. }
  371. configure(config: PressabilityConfig): void {
  372. this._config = config;
  373. }
  374. /**
  375. * Resets any pending timers. This should be called on unmount.
  376. */
  377. reset(): void {
  378. this._cancelHoverInDelayTimeout();
  379. this._cancelHoverOutDelayTimeout();
  380. this._cancelLongPressDelayTimeout();
  381. this._cancelPressDelayTimeout();
  382. this._cancelPressOutDelayTimeout();
  383. }
  384. /**
  385. * Returns a set of props to spread into the interactive element.
  386. */
  387. getEventHandlers(): EventHandlers {
  388. if (this._eventHandlers == null) {
  389. this._eventHandlers = this._createEventHandlers();
  390. }
  391. return this._eventHandlers;
  392. }
  393. _createEventHandlers(): EventHandlers {
  394. const focusEventHandlers = {
  395. onBlur: (event: BlurEvent): void => {
  396. const {onBlur} = this._config;
  397. if (onBlur != null) {
  398. onBlur(event);
  399. }
  400. },
  401. onFocus: (event: FocusEvent): void => {
  402. const {onFocus} = this._config;
  403. if (onFocus != null) {
  404. onFocus(event);
  405. }
  406. },
  407. };
  408. const responderEventHandlers = {
  409. onStartShouldSetResponder: (): boolean => {
  410. const {disabled} = this._config;
  411. if (disabled == null) {
  412. const {onStartShouldSetResponder_DEPRECATED} = this._config;
  413. return onStartShouldSetResponder_DEPRECATED == null
  414. ? true
  415. : onStartShouldSetResponder_DEPRECATED();
  416. }
  417. return !disabled;
  418. },
  419. onResponderGrant: (event: PressEvent): void => {
  420. event.persist();
  421. this._cancelPressOutDelayTimeout();
  422. this._responderID = event.currentTarget;
  423. this._touchState = 'NOT_RESPONDER';
  424. this._receiveSignal('RESPONDER_GRANT', event);
  425. const delayPressIn = normalizeDelay(this._config.delayPressIn);
  426. if (delayPressIn > 0) {
  427. this._pressDelayTimeout = setTimeout(() => {
  428. this._receiveSignal('DELAY', event);
  429. }, delayPressIn);
  430. } else {
  431. this._receiveSignal('DELAY', event);
  432. }
  433. const delayLongPress = normalizeDelay(
  434. this._config.delayLongPress,
  435. 10,
  436. DEFAULT_LONG_PRESS_DELAY_MS - delayPressIn,
  437. );
  438. this._longPressDelayTimeout = setTimeout(() => {
  439. this._handleLongPress(event);
  440. }, delayLongPress + delayPressIn);
  441. },
  442. onResponderMove: (event: PressEvent): void => {
  443. if (this._config.onPressMove != null) {
  444. this._config.onPressMove(event);
  445. }
  446. // Region may not have finished being measured, yet.
  447. const responderRegion = this._responderRegion;
  448. if (responderRegion == null) {
  449. return;
  450. }
  451. const touch = getTouchFromPressEvent(event);
  452. if (touch == null) {
  453. this._cancelLongPressDelayTimeout();
  454. this._receiveSignal('LEAVE_PRESS_RECT', event);
  455. return;
  456. }
  457. if (this._touchActivatePosition != null) {
  458. const deltaX = this._touchActivatePosition.pageX - touch.pageX;
  459. const deltaY = this._touchActivatePosition.pageY - touch.pageY;
  460. if (Math.hypot(deltaX, deltaY) > 10) {
  461. this._cancelLongPressDelayTimeout();
  462. }
  463. }
  464. if (this._isTouchWithinResponderRegion(touch, responderRegion)) {
  465. this._receiveSignal('ENTER_PRESS_RECT', event);
  466. } else {
  467. this._cancelLongPressDelayTimeout();
  468. this._receiveSignal('LEAVE_PRESS_RECT', event);
  469. }
  470. },
  471. onResponderRelease: (event: PressEvent): void => {
  472. this._receiveSignal('RESPONDER_RELEASE', event);
  473. },
  474. onResponderTerminate: (event: PressEvent): void => {
  475. this._receiveSignal('RESPONDER_TERMINATED', event);
  476. },
  477. onResponderTerminationRequest: (): boolean => {
  478. const {cancelable} = this._config;
  479. if (cancelable == null) {
  480. const {onResponderTerminationRequest_DEPRECATED} = this._config;
  481. return onResponderTerminationRequest_DEPRECATED == null
  482. ? true
  483. : onResponderTerminationRequest_DEPRECATED();
  484. }
  485. return cancelable;
  486. },
  487. onClick: (event: PressEvent): void => {
  488. const {onPress} = this._config;
  489. if (onPress != null) {
  490. onPress(event);
  491. }
  492. },
  493. };
  494. if (process.env.NODE_ENV === 'test') {
  495. // We are setting this in order to find this node in ReactNativeTestTools
  496. responderEventHandlers.onStartShouldSetResponder.testOnly_pressabilityConfig = () =>
  497. this._config;
  498. }
  499. const mouseEventHandlers =
  500. Platform.OS === 'ios' || Platform.OS === 'android'
  501. ? null
  502. : {
  503. onMouseEnter: (event: MouseEvent): void => {
  504. if (isHoverEnabled()) {
  505. this._isHovered = true;
  506. this._cancelHoverOutDelayTimeout();
  507. const {onHoverIn} = this._config;
  508. if (onHoverIn != null) {
  509. const delayHoverIn = normalizeDelay(
  510. this._config.delayHoverIn,
  511. );
  512. if (delayHoverIn > 0) {
  513. this._hoverInDelayTimeout = setTimeout(() => {
  514. onHoverIn(event);
  515. }, delayHoverIn);
  516. } else {
  517. onHoverIn(event);
  518. }
  519. }
  520. }
  521. },
  522. onMouseLeave: (event: MouseEvent): void => {
  523. if (this._isHovered) {
  524. this._isHovered = false;
  525. this._cancelHoverInDelayTimeout();
  526. const {onHoverOut} = this._config;
  527. if (onHoverOut != null) {
  528. const delayHoverOut = normalizeDelay(
  529. this._config.delayHoverOut,
  530. );
  531. if (delayHoverOut > 0) {
  532. this._hoverInDelayTimeout = setTimeout(() => {
  533. onHoverOut(event);
  534. }, delayHoverOut);
  535. } else {
  536. onHoverOut(event);
  537. }
  538. }
  539. }
  540. },
  541. };
  542. return {
  543. ...focusEventHandlers,
  544. ...responderEventHandlers,
  545. ...mouseEventHandlers,
  546. };
  547. }
  548. /**
  549. * Receives a state machine signal, performs side effects of the transition
  550. * and stores the new state. Validates the transition as well.
  551. */
  552. _receiveSignal(signal: TouchSignal, event: PressEvent): void {
  553. const prevState = this._touchState;
  554. const nextState = Transitions[prevState]?.[signal];
  555. if (this._responderID == null && signal === 'RESPONDER_RELEASE') {
  556. return;
  557. }
  558. invariant(
  559. nextState != null && nextState !== 'ERROR',
  560. 'Pressability: Invalid signal `%s` for state `%s` on responder: %s',
  561. signal,
  562. prevState,
  563. typeof this._responderID === 'number'
  564. ? this._responderID
  565. : '<<host component>>',
  566. );
  567. if (prevState !== nextState) {
  568. this._performTransitionSideEffects(prevState, nextState, signal, event);
  569. this._touchState = nextState;
  570. }
  571. }
  572. /**
  573. * Performs a transition between touchable states and identify any activations
  574. * or deactivations (and callback invocations).
  575. */
  576. _performTransitionSideEffects(
  577. prevState: TouchState,
  578. nextState: TouchState,
  579. signal: TouchSignal,
  580. event: PressEvent,
  581. ): void {
  582. if (isTerminalSignal(signal)) {
  583. this._touchActivatePosition = null;
  584. this._cancelLongPressDelayTimeout();
  585. }
  586. const isInitialTransition =
  587. prevState === 'NOT_RESPONDER' &&
  588. nextState === 'RESPONDER_INACTIVE_PRESS_IN';
  589. const isActivationTransiton =
  590. !isActivationSignal(prevState) && isActivationSignal(nextState);
  591. if (isInitialTransition || isActivationTransiton) {
  592. this._measureResponderRegion();
  593. }
  594. if (isPressInSignal(prevState) && signal === 'LONG_PRESS_DETECTED') {
  595. const {onLongPress} = this._config;
  596. if (onLongPress != null) {
  597. onLongPress(event);
  598. }
  599. }
  600. const isPrevActive = isActiveSignal(prevState);
  601. const isNextActive = isActiveSignal(nextState);
  602. if (!isPrevActive && isNextActive) {
  603. this._activate(event);
  604. } else if (isPrevActive && !isNextActive) {
  605. this._deactivate(event);
  606. }
  607. if (isPressInSignal(prevState) && signal === 'RESPONDER_RELEASE') {
  608. const {onLongPress, onPress, android_disableSound} = this._config;
  609. if (onPress != null) {
  610. const isPressCanceledByLongPress =
  611. onLongPress != null &&
  612. prevState === 'RESPONDER_ACTIVE_LONG_PRESS_IN' &&
  613. this._shouldLongPressCancelPress();
  614. if (!isPressCanceledByLongPress) {
  615. // If we never activated (due to delays), activate and deactivate now.
  616. if (!isNextActive && !isPrevActive) {
  617. this._activate(event);
  618. this._deactivate(event);
  619. }
  620. if (Platform.OS === 'android' && android_disableSound !== true) {
  621. SoundManager.playTouchSound();
  622. }
  623. onPress(event);
  624. }
  625. }
  626. }
  627. this._cancelPressDelayTimeout();
  628. }
  629. _activate(event: PressEvent): void {
  630. const {onPressIn} = this._config;
  631. const touch = getTouchFromPressEvent(event);
  632. this._touchActivatePosition = {
  633. pageX: touch.pageX,
  634. pageY: touch.pageY,
  635. };
  636. this._touchActivateTime = Date.now();
  637. if (onPressIn != null) {
  638. onPressIn(event);
  639. }
  640. }
  641. _deactivate(event: PressEvent): void {
  642. const {onPressOut} = this._config;
  643. if (onPressOut != null) {
  644. const minPressDuration = normalizeDelay(
  645. this._config.minPressDuration,
  646. 0,
  647. DEFAULT_MIN_PRESS_DURATION,
  648. );
  649. const pressDuration = Date.now() - (this._touchActivateTime ?? 0);
  650. const delayPressOut = Math.max(
  651. minPressDuration - pressDuration,
  652. normalizeDelay(this._config.delayPressOut),
  653. );
  654. if (delayPressOut > 0) {
  655. this._pressOutDelayTimeout = setTimeout(() => {
  656. onPressOut(event);
  657. }, delayPressOut);
  658. } else {
  659. onPressOut(event);
  660. }
  661. }
  662. this._touchActivateTime = null;
  663. }
  664. _measureResponderRegion(): void {
  665. if (this._responderID == null) {
  666. return;
  667. }
  668. if (typeof this._responderID === 'number') {
  669. UIManager.measure(this._responderID, this._measureCallback);
  670. } else {
  671. this._responderID.measure(this._measureCallback);
  672. }
  673. }
  674. _measureCallback = (left, top, width, height, pageX, pageY) => {
  675. if (!left && !top && !width && !height && !pageX && !pageY) {
  676. return;
  677. }
  678. this._responderRegion = {
  679. bottom: pageY + height,
  680. left: pageX,
  681. right: pageX + width,
  682. top: pageY,
  683. };
  684. };
  685. _isTouchWithinResponderRegion(
  686. touch: $PropertyType<PressEvent, 'nativeEvent'>,
  687. responderRegion: $ReadOnly<{|
  688. bottom: number,
  689. left: number,
  690. right: number,
  691. top: number,
  692. |}>,
  693. ): boolean {
  694. const hitSlop = normalizeRect(this._config.hitSlop);
  695. const pressRectOffset = normalizeRect(this._config.pressRectOffset);
  696. let regionBottom = responderRegion.bottom;
  697. let regionLeft = responderRegion.left;
  698. let regionRight = responderRegion.right;
  699. let regionTop = responderRegion.top;
  700. if (hitSlop != null) {
  701. if (hitSlop.bottom != null) {
  702. regionBottom += hitSlop.bottom;
  703. }
  704. if (hitSlop.left != null) {
  705. regionLeft -= hitSlop.left;
  706. }
  707. if (hitSlop.right != null) {
  708. regionRight += hitSlop.right;
  709. }
  710. if (hitSlop.top != null) {
  711. regionTop -= hitSlop.top;
  712. }
  713. }
  714. regionBottom +=
  715. pressRectOffset?.bottom ?? DEFAULT_PRESS_RECT_OFFSETS.bottom;
  716. regionLeft -= pressRectOffset?.left ?? DEFAULT_PRESS_RECT_OFFSETS.left;
  717. regionRight += pressRectOffset?.right ?? DEFAULT_PRESS_RECT_OFFSETS.right;
  718. regionTop -= pressRectOffset?.top ?? DEFAULT_PRESS_RECT_OFFSETS.top;
  719. return (
  720. touch.pageX > regionLeft &&
  721. touch.pageX < regionRight &&
  722. touch.pageY > regionTop &&
  723. touch.pageY < regionBottom
  724. );
  725. }
  726. _handleLongPress(event: PressEvent): void {
  727. if (
  728. this._touchState === 'RESPONDER_ACTIVE_PRESS_IN' ||
  729. this._touchState === 'RESPONDER_ACTIVE_LONG_PRESS_IN'
  730. ) {
  731. this._receiveSignal('LONG_PRESS_DETECTED', event);
  732. }
  733. }
  734. _shouldLongPressCancelPress(): boolean {
  735. return (
  736. this._config.onLongPressShouldCancelPress_DEPRECATED == null ||
  737. this._config.onLongPressShouldCancelPress_DEPRECATED()
  738. );
  739. }
  740. _cancelHoverInDelayTimeout(): void {
  741. if (this._hoverInDelayTimeout != null) {
  742. clearTimeout(this._hoverInDelayTimeout);
  743. this._hoverInDelayTimeout = null;
  744. }
  745. }
  746. _cancelHoverOutDelayTimeout(): void {
  747. if (this._hoverOutDelayTimeout != null) {
  748. clearTimeout(this._hoverOutDelayTimeout);
  749. this._hoverOutDelayTimeout = null;
  750. }
  751. }
  752. _cancelLongPressDelayTimeout(): void {
  753. if (this._longPressDelayTimeout != null) {
  754. clearTimeout(this._longPressDelayTimeout);
  755. this._longPressDelayTimeout = null;
  756. }
  757. }
  758. _cancelPressDelayTimeout(): void {
  759. if (this._pressDelayTimeout != null) {
  760. clearTimeout(this._pressDelayTimeout);
  761. this._pressDelayTimeout = null;
  762. }
  763. }
  764. _cancelPressOutDelayTimeout(): void {
  765. if (this._pressOutDelayTimeout != null) {
  766. clearTimeout(this._pressOutDelayTimeout);
  767. this._pressOutDelayTimeout = null;
  768. }
  769. }
  770. }
  771. function normalizeDelay(delay: ?number, min = 0, fallback = 0): number {
  772. return Math.max(min, delay ?? fallback);
  773. }
  774. const getTouchFromPressEvent = (event: PressEvent) => {
  775. const {changedTouches, touches} = event.nativeEvent;
  776. if (touches != null && touches.length > 0) {
  777. return touches[0];
  778. }
  779. if (changedTouches != null && changedTouches.length > 0) {
  780. return changedTouches[0];
  781. }
  782. return event.nativeEvent;
  783. };