GT2/Ejectable/node_modules/react-native-web/dist/cjs/hooks/usePressEvents/PressResponder.js

542 lines
18 KiB
JavaScript

/**
* 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 (
* <View {...this.state.pressResponder.getEventHandlers()} />
* );
*
* 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;