/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * * @format */ 'use strict'; exports.__esModule = true; exports.default = void 0; var DELAY = 'DELAY'; var ERROR = 'ERROR'; var LONG_PRESS_DETECTED = 'LONG_PRESS_DETECTED'; var NOT_RESPONDER = 'NOT_RESPONDER'; var RESPONDER_ACTIVE_LONG_PRESS_START = 'RESPONDER_ACTIVE_LONG_PRESS_START'; var RESPONDER_ACTIVE_PRESS_START = 'RESPONDER_ACTIVE_PRESS_START'; var RESPONDER_INACTIVE_PRESS_START = 'RESPONDER_INACTIVE_PRESS_START'; var RESPONDER_GRANT = 'RESPONDER_GRANT'; var RESPONDER_RELEASE = 'RESPONDER_RELEASE'; var RESPONDER_TERMINATED = 'RESPONDER_TERMINATED'; var Transitions = Object.freeze({ NOT_RESPONDER: { DELAY: ERROR, RESPONDER_GRANT: RESPONDER_INACTIVE_PRESS_START, RESPONDER_RELEASE: ERROR, RESPONDER_TERMINATED: ERROR, LONG_PRESS_DETECTED: ERROR }, RESPONDER_INACTIVE_PRESS_START: { DELAY: RESPONDER_ACTIVE_PRESS_START, RESPONDER_GRANT: ERROR, RESPONDER_RELEASE: NOT_RESPONDER, RESPONDER_TERMINATED: NOT_RESPONDER, LONG_PRESS_DETECTED: ERROR }, RESPONDER_ACTIVE_PRESS_START: { DELAY: ERROR, RESPONDER_GRANT: ERROR, RESPONDER_RELEASE: NOT_RESPONDER, RESPONDER_TERMINATED: NOT_RESPONDER, LONG_PRESS_DETECTED: RESPONDER_ACTIVE_LONG_PRESS_START }, RESPONDER_ACTIVE_LONG_PRESS_START: { DELAY: ERROR, RESPONDER_GRANT: ERROR, RESPONDER_RELEASE: NOT_RESPONDER, RESPONDER_TERMINATED: NOT_RESPONDER, LONG_PRESS_DETECTED: RESPONDER_ACTIVE_LONG_PRESS_START }, ERROR: { DELAY: NOT_RESPONDER, RESPONDER_GRANT: RESPONDER_INACTIVE_PRESS_START, RESPONDER_RELEASE: NOT_RESPONDER, RESPONDER_TERMINATED: NOT_RESPONDER, LONG_PRESS_DETECTED: NOT_RESPONDER } }); var isActiveSignal = function isActiveSignal(signal) { return signal === RESPONDER_ACTIVE_PRESS_START || signal === RESPONDER_ACTIVE_LONG_PRESS_START; }; var isPressStartSignal = function isPressStartSignal(signal) { return signal === RESPONDER_INACTIVE_PRESS_START || signal === RESPONDER_ACTIVE_PRESS_START || signal === RESPONDER_ACTIVE_LONG_PRESS_START; }; var isTerminalSignal = function isTerminalSignal(signal) { return signal === RESPONDER_TERMINATED || signal === RESPONDER_RELEASE; }; var isValidKeyPress = function isValidKeyPress(event) { var key = event.key; var target = event.currentTarget; var role = target.getAttribute('role'); var isSpacebar = key === ' ' || key === 'Spacebar'; return !event.repeat && (key === 'Enter' || isSpacebar && (role === 'button' || role === 'menuitem')); }; var DEFAULT_LONG_PRESS_DELAY_MS = 450; // 500 - 50 var DEFAULT_PRESS_DELAY_MS = 50; /** * =========================== PressResponder Tutorial =========================== * * The `PressResponder` class helps you create press interactions by analyzing the * geometry of elements and observing when another responder (e.g. ScrollView) * has stolen the touch lock. It offers hooks for your component to provide * interaction feedback to the user: * * - When a press has activated (e.g. highlight an element) * - When a press has deactivated (e.g. un-highlight an element) * - When a press sould trigger an action, meaning it activated and deactivated * while within the geometry of the element without the lock being stolen. * * A high quality interaction isn't as simple as you might think. There should * be a slight delay before activation. Moving your finger beyond an element's * bounds should trigger deactivation, but moving the same finger back within an * element's bounds should trigger reactivation. * * In order to use `PressResponder`, do the following: * * const pressResponder = new PressResponder(config); * * 2. Choose the rendered component who should collect the press events. On that * element, spread `pressability.getEventHandlers()` into its props. * * return ( * * ); * * 3. Reset `PressResponder` when your component unmounts. * * componentWillUnmount() { * this.state.pressResponder.reset(); * } * * ==================== Implementation Details ==================== * * `PressResponder` only assumes that there exists a `HitRect` node. The `PressRect` * is an abstract box that is extended beyond the `HitRect`. * * # Geometry * * ┌────────────────────────┐ * │ ┌──────────────────┐ │ - Presses start anywhere within `HitRect`. * │ │ ┌────────────┐ │ │ * │ │ │ VisualRect │ │ │ * │ │ └────────────┘ │ │ - When pressed down for sufficient amount of time * │ │ HitRect │ │ before letting up, `VisualRect` activates. * │ └──────────────────┘ │ * │ Out Region o │ * └────────────────────│───┘ * └────── When the press is released outside the `HitRect`, * the responder is NOT eligible for a "press". * * # State Machine * * ┌───────────────┐ ◀──── RESPONDER_RELEASE * │ NOT_RESPONDER │ * └───┬───────────┘ ◀──── RESPONDER_TERMINATED * │ * │ RESPONDER_GRANT (HitRect) * │ * ▼ * ┌─────────────────────┐ ┌───────────────────┐ ┌───────────────────┐ * │ RESPONDER_INACTIVE_ │ DELAY │ RESPONDER_ACTIVE_ │ T + DELAY │ RESPONDER_ACTIVE_ │ * │ PRESS_START ├────────▶ │ PRESS_START ├────────────▶ │ LONG_PRESS_START │ * └─────────────────────┘ └───────────────────┘ └───────────────────┘ * * T + DELAY => LONG_PRESS_DELAY + DELAY * * Not drawn are the side effects of each transition. The most important side * effect is the invocation of `onLongPress`. Only when the browser produces a * `click` event is `onPress` invoked. */ var PressResponder = /*#__PURE__*/ function () { function PressResponder(config) { this._eventHandlers = null; this._isPointerTouch = false; this._longPressDelayTimeout = null; this._longPressDispatched = false; this._pressDelayTimeout = null; this._pressOutDelayTimeout = null; this._touchState = NOT_RESPONDER; this.configure(config); } var _proto = PressResponder.prototype; _proto.configure = function configure(config) { this._config = config; } /** * Resets any pending timers. This should be called on unmount. */ ; _proto.reset = function reset() { this._cancelLongPressDelayTimeout(); this._cancelPressDelayTimeout(); this._cancelPressOutDelayTimeout(); } /** * Returns a set of props to spread into the interactive element. */ ; _proto.getEventHandlers = function getEventHandlers() { if (this._eventHandlers == null) { this._eventHandlers = this._createEventHandlers(); } return this._eventHandlers; }; _proto._createEventHandlers = function _createEventHandlers() { var _this = this; var start = function start(event, shouldDelay) { event.persist(); _this._cancelPressOutDelayTimeout(); _this._longPressDispatched = false; _this._responder = event.currentTarget; _this._selectionTerminated = false; _this._touchState = NOT_RESPONDER; _this._isPointerTouch = event.nativeEvent.type === 'touchstart'; _this._receiveSignal(RESPONDER_GRANT, event); var delayPressStart = normalizeDelay(_this._config.delayPressStart, 0, DEFAULT_PRESS_DELAY_MS); if (shouldDelay !== false && delayPressStart > 0) { _this._pressDelayTimeout = setTimeout(function () { _this._receiveSignal(DELAY, event); }, delayPressStart); } else { _this._receiveSignal(DELAY, event); } var delayLongPress = normalizeDelay(_this._config.delayLongPress, 10, DEFAULT_LONG_PRESS_DELAY_MS); _this._longPressDelayTimeout = setTimeout(function () { _this._handleLongPress(event); }, delayLongPress + delayPressStart); }; var end = function end(event) { _this._receiveSignal(RESPONDER_RELEASE, event); }; var keyupHandler = function keyupHandler(event) { if (_this._touchState !== NOT_RESPONDER) { end(event); document.removeEventListener('keyup', keyupHandler); } }; return { onStartShouldSetResponder: function onStartShouldSetResponder() { var disabled = _this._config.disabled; if (disabled == null) { return true; } return !disabled; }, onKeyDown: function onKeyDown(event) { if (isValidKeyPress(event)) { if (_this._touchState === NOT_RESPONDER) { start(event, false); // Listen to 'keyup' on document to account for situations where // focus is moved to another element during 'keydown'. document.addEventListener('keyup', keyupHandler); } event.stopPropagation(); } }, onResponderGrant: function onResponderGrant(event) { return start(event); }, onResponderMove: function onResponderMove(event) { if (_this._config.onPressMove != null) { _this._config.onPressMove(event); } var touch = getTouchFromResponderEvent(event); if (_this._touchActivatePosition != null) { var deltaX = _this._touchActivatePosition.pageX - touch.pageX; var deltaY = _this._touchActivatePosition.pageY - touch.pageY; if (Math.hypot(deltaX, deltaY) > 10) { _this._cancelLongPressDelayTimeout(); } } }, onResponderRelease: function onResponderRelease(event) { return end(event); }, onResponderTerminate: function onResponderTerminate(event) { if (event.nativeEvent.type === 'selectionchange') { _this._selectionTerminated = true; } _this._receiveSignal(RESPONDER_TERMINATED, event); }, onResponderTerminationRequest: function onResponderTerminationRequest(event) { var _this$_config = _this._config, cancelable = _this$_config.cancelable, disabled = _this$_config.disabled, onLongPress = _this$_config.onLongPress; // If `onLongPress` is provided, don't terminate on `contextmenu` as default // behavior will be prevented for non-mouse pointers. if (!disabled && onLongPress != null && _this._isPointerTouch && event.nativeEvent.type === 'contextmenu') { return false; } if (cancelable == null) { return true; } return cancelable; }, // NOTE: this diverges from react-native in 3 significant ways: // * The `onPress` callback is not connected to the responder system (the native // `click` event must be used but is dispatched in many scenarios where no pointers // are on the screen.) Therefore, it's possible for `onPress` to be called without // `onPress{Start,End}` being called first. // * The `onPress` callback is only be called on the first ancestor of the native // `click` target that is using the PressResponder. // * The event's `nativeEvent` is a `MouseEvent` not a `TouchEvent`. onClick: function onClick(event) { var _this$_config2 = _this._config, disabled = _this$_config2.disabled, onPress = _this$_config2.onPress; if (!disabled) { // If long press dispatched, cancel default click behavior. // If the responder terminated because text was selected during the gesture, // cancel the default click behavior. if (_this._longPressDispatched || _this._selectionTerminated) { event.preventDefault(); } else if (onPress != null && event.ctrlKey === false && event.altKey === false) { onPress(event); } } event.stopPropagation(); }, // If `onLongPress` is provided and a touch pointer is being used, prevent the // default context menu from opening. onContextMenu: function onContextMenu(event) { var _this$_config3 = _this._config, disabled = _this$_config3.disabled, onLongPress = _this$_config3.onLongPress; if (!disabled && onLongPress != null && _this._isPointerTouch && !event.defaultPrevented) { event.preventDefault(); } event.stopPropagation(); } }; } /** * Receives a state machine signal, performs side effects of the transition * and stores the new state. Validates the transition as well. */ ; _proto._receiveSignal = function _receiveSignal(signal, event) { var prevState = this._touchState; var nextState = null; if (Transitions[prevState] != null) { nextState = Transitions[prevState][signal]; } if (this._responder == null && signal === RESPONDER_RELEASE) { return; } if (nextState == null || nextState === ERROR) { console.error("PressResponder: Invalid signal " + signal + " for state " + prevState + " on responder"); } else if (prevState !== nextState) { this._performTransitionSideEffects(prevState, nextState, signal, event); this._touchState = nextState; } } /** * Performs a transition between touchable states and identify any activations * or deactivations (and callback invocations). */ ; _proto._performTransitionSideEffects = function _performTransitionSideEffects(prevState, nextState, signal, event) { if (isTerminalSignal(signal)) { this._isPointerTouch = false; this._touchActivatePosition = null; this._cancelLongPressDelayTimeout(); } if (isPressStartSignal(prevState) && signal === LONG_PRESS_DETECTED) { var onLongPress = this._config.onLongPress; // Long press is not supported for keyboards because 'click' can be dispatched // immediately (and multiple times) after 'keydown'. if (onLongPress != null && event.nativeEvent.key == null) { onLongPress(event); this._longPressDispatched = true; } } var isPrevActive = isActiveSignal(prevState); var isNextActive = isActiveSignal(nextState); if (!isPrevActive && isNextActive) { this._activate(event); } else if (isPrevActive && !isNextActive) { this._deactivate(event); } if (isPressStartSignal(prevState) && signal === RESPONDER_RELEASE) { var _this$_config4 = this._config, _onLongPress = _this$_config4.onLongPress, onPress = _this$_config4.onPress; if (onPress != null) { var isPressCanceledByLongPress = _onLongPress != null && prevState === RESPONDER_ACTIVE_LONG_PRESS_START; if (!isPressCanceledByLongPress) { // If we never activated (due to delays), activate and deactivate now. if (!isNextActive && !isPrevActive) { this._activate(event); this._deactivate(event); } } } } this._cancelPressDelayTimeout(); }; _proto._activate = function _activate(event) { var _this$_config5 = this._config, onPressChange = _this$_config5.onPressChange, onPressStart = _this$_config5.onPressStart; var touch = getTouchFromResponderEvent(event); this._touchActivatePosition = { pageX: touch.pageX, pageY: touch.pageY }; if (onPressStart != null) { onPressStart(event); } if (onPressChange != null) { onPressChange(true); } }; _proto._deactivate = function _deactivate(event) { var _this$_config6 = this._config, onPressChange = _this$_config6.onPressChange, onPressEnd = _this$_config6.onPressEnd; function end() { if (onPressEnd != null) { onPressEnd(event); } if (onPressChange != null) { onPressChange(false); } } var delayPressEnd = normalizeDelay(this._config.delayPressEnd); if (delayPressEnd > 0) { this._pressOutDelayTimeout = setTimeout(function () { end(); }, delayPressEnd); } else { end(); } }; _proto._handleLongPress = function _handleLongPress(event) { if (this._touchState === RESPONDER_ACTIVE_PRESS_START || this._touchState === RESPONDER_ACTIVE_LONG_PRESS_START) { this._receiveSignal(LONG_PRESS_DETECTED, event); } }; _proto._cancelLongPressDelayTimeout = function _cancelLongPressDelayTimeout() { if (this._longPressDelayTimeout != null) { clearTimeout(this._longPressDelayTimeout); this._longPressDelayTimeout = null; } }; _proto._cancelPressDelayTimeout = function _cancelPressDelayTimeout() { if (this._pressDelayTimeout != null) { clearTimeout(this._pressDelayTimeout); this._pressDelayTimeout = null; } }; _proto._cancelPressOutDelayTimeout = function _cancelPressOutDelayTimeout() { if (this._pressOutDelayTimeout != null) { clearTimeout(this._pressOutDelayTimeout); this._pressOutDelayTimeout = null; } }; return PressResponder; }(); exports.default = PressResponder; function normalizeDelay(delay, min, fallback) { if (min === void 0) { min = 0; } if (fallback === void 0) { fallback = 0; } return Math.max(min, delay !== null && delay !== void 0 ? delay : fallback); } function getTouchFromResponderEvent(event) { var _event$nativeEvent = event.nativeEvent, changedTouches = _event$nativeEvent.changedTouches, touches = _event$nativeEvent.touches; if (touches != null && touches.length > 0) { return touches[0]; } if (changedTouches != null && changedTouches.length > 0) { return changedTouches[0]; } return event.nativeEvent; } module.exports = exports.default;