542 lines
18 KiB
JavaScript
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; |