You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
213 lines
9.2 KiB
213 lines
9.2 KiB
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow strict-local
|
|
* @format
|
|
*/
|
|
'use strict';
|
|
|
|
type ClickEvent = any;
|
|
type KeyboardEvent = any;
|
|
type ResponderEvent = any;
|
|
export type PressResponderConfig = $ReadOnly<{|
|
|
// The gesture can be interrupted by a parent gesture, e.g., scroll.
|
|
// Defaults to true.
|
|
cancelable?: ?boolean,
|
|
// Whether to disable initialization of the press gesture.
|
|
disabled?: ?boolean,
|
|
// Duration (in addition to `delayPressStart`) after which a press gesture is
|
|
// considered a long press gesture. Defaults to 500 (milliseconds).
|
|
delayLongPress?: ?number,
|
|
// Duration to wait after press down before calling `onPressStart`.
|
|
delayPressStart?: ?number,
|
|
// Duration to wait after letting up before calling `onPressEnd`.
|
|
delayPressEnd?: ?number,
|
|
// Called when a long press gesture has been triggered.
|
|
onLongPress?: ?(event: ResponderEvent) => void,
|
|
// Called when a press gestute has been triggered.
|
|
onPress?: ?(event: ClickEvent) => void,
|
|
// Called when the press is activated to provide visual feedback.
|
|
onPressChange?: ?(event: ResponderEvent) => void,
|
|
// Called when the press is activated to provide visual feedback.
|
|
onPressStart?: ?(event: ResponderEvent) => void,
|
|
// Called when the press location moves. (This should rarely be used.)
|
|
onPressMove?: ?(event: ResponderEvent) => void,
|
|
// Called when the press is deactivated to undo visual feedback.
|
|
onPressEnd?: ?(event: ResponderEvent) => void,
|
|
|}>;
|
|
export type EventHandlers = $ReadOnly<{|
|
|
onClick: (event: ClickEvent) => void,
|
|
onContextMenu: (event: ClickEvent) => void,
|
|
onKeyDown: (event: KeyboardEvent) => void,
|
|
onResponderGrant: (event: ResponderEvent) => void,
|
|
onResponderMove: (event: ResponderEvent) => void,
|
|
onResponderRelease: (event: ResponderEvent) => void,
|
|
onResponderTerminate: (event: ResponderEvent) => void,
|
|
onResponderTerminationRequest: (event: ResponderEvent) => boolean,
|
|
onStartShouldSetResponder: (event: ResponderEvent) => boolean,
|
|
|}>;
|
|
type TouchState = 'NOT_RESPONDER' | 'RESPONDER_INACTIVE_PRESS_START' | 'RESPONDER_ACTIVE_PRESS_START' | 'RESPONDER_ACTIVE_LONG_PRESS_START' | 'ERROR';
|
|
type TouchSignal = 'DELAY' | 'RESPONDER_GRANT' | 'RESPONDER_RELEASE' | 'RESPONDER_TERMINATED' | 'LONG_PRESS_DETECTED';
|
|
const DELAY = 'DELAY';
|
|
const ERROR = 'ERROR';
|
|
const LONG_PRESS_DETECTED = 'LONG_PRESS_DETECTED';
|
|
const NOT_RESPONDER = 'NOT_RESPONDER';
|
|
const RESPONDER_ACTIVE_LONG_PRESS_START = 'RESPONDER_ACTIVE_LONG_PRESS_START';
|
|
const RESPONDER_ACTIVE_PRESS_START = 'RESPONDER_ACTIVE_PRESS_START';
|
|
const RESPONDER_INACTIVE_PRESS_START = 'RESPONDER_INACTIVE_PRESS_START';
|
|
const RESPONDER_GRANT = 'RESPONDER_GRANT';
|
|
const RESPONDER_RELEASE = 'RESPONDER_RELEASE';
|
|
const RESPONDER_TERMINATED = 'RESPONDER_TERMINATED';
|
|
const 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
|
|
}
|
|
});
|
|
declare var isActiveSignal: (signal: any) => any;
|
|
declare var isButtonRole: (element: any) => any;
|
|
declare var isPressStartSignal: (signal: any) => any;
|
|
declare var isTerminalSignal: (signal: any) => any;
|
|
declare var isValidKeyPress: (event: any) => any;
|
|
const DEFAULT_LONG_PRESS_DELAY_MS = 450; // 500 - 50
|
|
|
|
const 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.
|
|
*/
|
|
|
|
declare export default class PressResponder {
|
|
_config: PressResponderConfig,
|
|
_eventHandlers: ?EventHandlers,
|
|
_isPointerTouch: ?boolean,
|
|
_longPressDelayTimeout: ?TimeoutID,
|
|
_longPressDispatched: ?boolean,
|
|
_pressDelayTimeout: ?TimeoutID,
|
|
_pressOutDelayTimeout: ?TimeoutID,
|
|
_selectionTerminated: ?boolean,
|
|
_touchActivatePosition: ?$ReadOnly<{|
|
|
pageX: number,
|
|
pageY: number,
|
|
|}>,
|
|
_touchState: TouchState,
|
|
constructor(config: PressResponderConfig): any,
|
|
configure(config: PressResponderConfig): void,
|
|
reset(): void,
|
|
getEventHandlers(): EventHandlers,
|
|
_createEventHandlers(): EventHandlers,
|
|
_receiveSignal(signal: TouchSignal, event: ResponderEvent): void,
|
|
_performTransitionSideEffects(prevState: TouchState, nextState: TouchState, signal: TouchSignal, event: ResponderEvent): void,
|
|
_activate(event: ResponderEvent): void,
|
|
_deactivate(event: ResponderEvent): void,
|
|
_handleLongPress(event: ResponderEvent): void,
|
|
_cancelLongPressDelayTimeout(): void,
|
|
_cancelPressDelayTimeout(): void,
|
|
_cancelPressOutDelayTimeout(): void,
|
|
}
|
|
declare function normalizeDelay(delay: ?number, min: any, fallback: any): number;
|
|
declare function getTouchFromResponderEvent(event: ResponderEvent): any; |