diff --git a/change/react-native-windows-2020-05-17-10-26-45-pressability-kbd.json b/change/react-native-windows-2020-05-17-10-26-45-pressability-kbd.json
new file mode 100644
index 00000000000..24845722c81
--- /dev/null
+++ b/change/react-native-windows-2020-05-17-10-26-45-pressability-kbd.json
@@ -0,0 +1,8 @@
+{
+ "type": "prerelease",
+ "comment": "Implement Keybpoarding/Gamepad Activation for Pressability",
+ "packageName": "react-native-windows",
+ "email": "ngerlem@microsoft.com",
+ "dependentChangeType": "patch",
+ "date": "2020-05-17T17:26:45.850Z"
+}
diff --git a/vnext/src/Libraries/Pressability/Pressability.windows.js b/vnext/src/Libraries/Pressability/Pressability.windows.js
new file mode 100644
index 00000000000..31883d6d352
--- /dev/null
+++ b/vnext/src/Libraries/Pressability/Pressability.windows.js
@@ -0,0 +1,874 @@
+/**
+ * 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.
+ *
+ * @flow strict-local
+ * @format
+ */
+
+'use strict';
+
+import {isHoverEnabled} from './HoverState.js';
+import invariant from 'invariant';
+import SoundManager from '../Components/Sound/SoundManager.js';
+import type {EdgeInsetsProp} from '../StyleSheet/EdgeInsetsPropType.js';
+import type {
+ BlurEvent,
+ FocusEvent,
+ PressEvent,
+ MouseEvent,
+ KeyEvent, // [Windows]
+} from '../Types/CoreEventTypes.js';
+import Platform from '../Utilities/Platform';
+import UIManager from '../ReactNative/UIManager';
+import type {HostComponent} from '../Renderer/shims/ReactNativeTypes';
+import * as React from 'react';
+
+export type PressabilityConfig = $ReadOnly<{|
+ /**
+ * Returns the amount to extend the `VisualRect` by to create `HitRect`.
+ */
+ getHitSlop?: ?() => ?EdgeInsetsProp,
+
+ /**
+ * Returns the duration to wait after hover in before activation.
+ */
+ getHoverInDelayMS?: ?() => ?number,
+
+ /**
+ * Returns the duration to wait after hover out before deactivation.
+ */
+ getHoverOutDelayMS?: ?() => ?number,
+
+ /**
+ * Returns the duration (in addition to the value from `getPressDelayMS`)
+ * after which a press gesture becomes a long press gesture.
+ */
+ getLongPressDelayMS?: ?() => ?number,
+
+ /**
+ * Returns the duration to wait after press down before activation.
+ */
+ getPressDelayMS?: ?() => ?number,
+
+ /**
+ * Returns the duration to wait after letting up before deactivation.
+ */
+ getPressOutDelayMS?: ?() => ?number,
+
+ /**
+ * Returns the amount to extend the `HitRect` by to create `PressRect`.
+ */
+ getPressRectOffset?: ?() => ?EdgeInsetsProp,
+
+ /**
+ * Returns true to disable playing system sound on touch (Android Only)
+ **/
+ getTouchSoundDisabled?: ?() => ?boolean,
+
+ /**
+ * Called after the element loses focus.
+ */
+ onBlur?: ?(event: BlurEvent) => void,
+
+ /**
+ * Called after the element is focused.
+ */
+ onFocus?: ?(event: FocusEvent) => void,
+
+ /**
+ * Called when the hover is activated to provide visual feedback.
+ */
+ onHoverIn?: ?(event: MouseEvent) => void,
+
+ /**
+ * Called when the hover is deactivated to undo visual feedback.
+ */
+ onHoverOut?: ?(event: MouseEvent) => void,
+
+ /**
+ * Called when a long press gesture has been triggered.
+ */
+ onLongPress?: ?(event: PressEvent) => void,
+
+ /**
+ * Returns whether a long press gesture should cancel the press gesture.
+ * Defaults to true.
+ */
+ onLongPressShouldCancelPress?: ?() => boolean,
+
+ /**
+ * Called when a press gestute has been triggered.
+ */
+ onPress?: ?(event: PressEvent) => void,
+
+ /**
+ * Called when the press is activated to provide visual feedback.
+ */
+ onPressIn?: ?(event: PressEvent) => void,
+
+ /**
+ * Called when the press location moves. (This should rarely be used.)
+ */
+ onPressMove?: ?(event: PressEvent) => void,
+
+ /**
+ * Called when the press is deactivated to undo visual feedback.
+ */
+ onPressOut?: ?(event: PressEvent) => void,
+
+ /**
+ * Returns whether to yield to a lock termination request (e.g. if a native
+ * scroll gesture attempts to steal the responder lock).
+ */
+ onResponderTerminationRequest?: ?() => boolean,
+
+ /**
+ * Returns whether to start a press gesture.
+ */
+ onStartShouldSetResponder?: ?() => boolean,
+|}>;
+
+type EventHandlers = $ReadOnly<{|
+ onBlur: (event: BlurEvent) => void,
+ onClick: (event: PressEvent) => void,
+ onFocus: (event: FocusEvent) => void,
+ onMouseEnter?: (event: MouseEvent) => void,
+ onMouseLeave?: (event: MouseEvent) => void,
+ onResponderGrant: (event: PressEvent) => void,
+ onResponderMove: (event: PressEvent) => void,
+ onResponderRelease: (event: PressEvent) => void,
+ onResponderTerminate: (event: PressEvent) => void,
+ onResponderTerminationRequest: () => boolean,
+ onStartShouldSetResponder: () => boolean,
+ // [Windows
+ onKeyUp: (event: KeyEvent) => void,
+ onKeyDown: (event: KeyEvent) => void,
+ // Windows]
+|}>;
+
+type TouchState =
+ | 'NOT_RESPONDER'
+ | 'RESPONDER_INACTIVE_PRESS_IN'
+ | 'RESPONDER_INACTIVE_PRESS_OUT'
+ | 'RESPONDER_ACTIVE_PRESS_IN'
+ | 'RESPONDER_ACTIVE_PRESS_OUT'
+ | 'RESPONDER_ACTIVE_LONG_PRESS_IN'
+ | 'RESPONDER_ACTIVE_LONG_PRESS_OUT'
+ | 'ERROR';
+
+type TouchSignal =
+ | 'DELAY'
+ | 'RESPONDER_GRANT'
+ | 'RESPONDER_RELEASE'
+ | 'RESPONDER_TERMINATED'
+ | 'ENTER_PRESS_RECT'
+ | 'LEAVE_PRESS_RECT'
+ | 'LONG_PRESS_DETECTED';
+
+const Transitions = Object.freeze({
+ NOT_RESPONDER: {
+ DELAY: 'ERROR',
+ RESPONDER_GRANT: 'RESPONDER_INACTIVE_PRESS_IN',
+ RESPONDER_RELEASE: 'ERROR',
+ RESPONDER_TERMINATED: 'ERROR',
+ ENTER_PRESS_RECT: 'ERROR',
+ LEAVE_PRESS_RECT: 'ERROR',
+ LONG_PRESS_DETECTED: 'ERROR',
+ },
+ RESPONDER_INACTIVE_PRESS_IN: {
+ DELAY: 'RESPONDER_ACTIVE_PRESS_IN',
+ RESPONDER_GRANT: 'ERROR',
+ RESPONDER_RELEASE: 'NOT_RESPONDER',
+ RESPONDER_TERMINATED: 'NOT_RESPONDER',
+ ENTER_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_IN',
+ LEAVE_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_OUT',
+ LONG_PRESS_DETECTED: 'ERROR',
+ },
+ RESPONDER_INACTIVE_PRESS_OUT: {
+ DELAY: 'RESPONDER_ACTIVE_PRESS_OUT',
+ RESPONDER_GRANT: 'ERROR',
+ RESPONDER_RELEASE: 'NOT_RESPONDER',
+ RESPONDER_TERMINATED: 'NOT_RESPONDER',
+ ENTER_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_IN',
+ LEAVE_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_OUT',
+ LONG_PRESS_DETECTED: 'ERROR',
+ },
+ RESPONDER_ACTIVE_PRESS_IN: {
+ DELAY: 'ERROR',
+ RESPONDER_GRANT: 'ERROR',
+ RESPONDER_RELEASE: 'NOT_RESPONDER',
+ RESPONDER_TERMINATED: 'NOT_RESPONDER',
+ ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_IN',
+ LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_OUT',
+ LONG_PRESS_DETECTED: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
+ },
+ RESPONDER_ACTIVE_PRESS_OUT: {
+ DELAY: 'ERROR',
+ RESPONDER_GRANT: 'ERROR',
+ RESPONDER_RELEASE: 'NOT_RESPONDER',
+ RESPONDER_TERMINATED: 'NOT_RESPONDER',
+ ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_IN',
+ LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_OUT',
+ LONG_PRESS_DETECTED: 'ERROR',
+ },
+ RESPONDER_ACTIVE_LONG_PRESS_IN: {
+ DELAY: 'ERROR',
+ RESPONDER_GRANT: 'ERROR',
+ RESPONDER_RELEASE: 'NOT_RESPONDER',
+ RESPONDER_TERMINATED: 'NOT_RESPONDER',
+ ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
+ LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_OUT',
+ LONG_PRESS_DETECTED: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
+ },
+ RESPONDER_ACTIVE_LONG_PRESS_OUT: {
+ DELAY: 'ERROR',
+ RESPONDER_GRANT: 'ERROR',
+ RESPONDER_RELEASE: 'NOT_RESPONDER',
+ RESPONDER_TERMINATED: 'NOT_RESPONDER',
+ ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
+ LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_OUT',
+ LONG_PRESS_DETECTED: 'ERROR',
+ },
+ ERROR: {
+ DELAY: 'NOT_RESPONDER',
+ RESPONDER_GRANT: 'RESPONDER_INACTIVE_PRESS_IN',
+ RESPONDER_RELEASE: 'NOT_RESPONDER',
+ RESPONDER_TERMINATED: 'NOT_RESPONDER',
+ ENTER_PRESS_RECT: 'NOT_RESPONDER',
+ LEAVE_PRESS_RECT: 'NOT_RESPONDER',
+ LONG_PRESS_DETECTED: 'NOT_RESPONDER',
+ },
+});
+
+const isActiveSignal = signal =>
+ signal === 'RESPONDER_ACTIVE_PRESS_IN' ||
+ signal === 'RESPONDER_ACTIVE_LONG_PRESS_IN';
+
+const isActivationSignal = signal =>
+ signal === 'RESPONDER_ACTIVE_PRESS_OUT' ||
+ signal === 'RESPONDER_ACTIVE_PRESS_IN';
+
+const isPressInSignal = signal =>
+ signal === 'RESPONDER_INACTIVE_PRESS_IN' ||
+ signal === 'RESPONDER_ACTIVE_PRESS_IN' ||
+ signal === 'RESPONDER_ACTIVE_LONG_PRESS_IN';
+
+const isTerminalSignal = signal =>
+ signal === 'RESPONDER_TERMINATED' || signal === 'RESPONDER_RELEASE';
+
+const DEFAULT_LONG_PRESS_DELAY_MS = 500;
+const DEFAULT_PRESS_DELAY_MS = 0;
+const DEFAULT_PRESS_RECT_OFFSETS = {
+ bottom: 30,
+ left: 20,
+ right: 20,
+ top: 20,
+};
+
+/**
+ * Pressability implements press handling capabilities.
+ *
+ * =========================== Pressability Tutorial ===========================
+ *
+ * The `Pressability` 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 `Pressability`, do the following:
+ *
+ * 1. Instantiate `Pressability` and store it on your component's state.
+ *
+ * state = {
+ * pressability: new Pressability({
+ * // ...
+ * }),
+ * };
+ *
+ * 2. Choose the rendered component who should collect the press events. On that
+ * element, spread `pressability.getEventHandlers()` into its props.
+ *
+ * return (
+ *
+ * );
+ *
+ * 3. Reset `Pressability` when your component unmounts.
+ *
+ * componentWillUnmount() {
+ * this.state.pressability.reset();
+ * }
+ *
+ * ==================== Pressability Implementation Details ====================
+ *
+ * `Pressability` 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`, which
+ * │ │ ┌────────────┐ │ │ is expanded via the prop `getHitSlop`.
+ * │ │ │ VisualRect │ │ │
+ * │ │ └────────────┘ │ │ - When pressed down for sufficient amount of time
+ * │ │ HitRect │ │ before letting up, `VisualRect` activates for
+ * │ └──────────────────┘ │ as long as the press stays within `PressRect`.
+ * │ PressRect o │
+ * └────────────────────│───┘
+ * Out Region └────── `PressRect`, which is expanded via the prop
+ * `getPressRectOffset`, allows presses to move
+ * beyond `HitRect` while maintaining activation
+ * and being 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_IN ├────────▶ │ PRESS_IN ├────────────▶ │ LONG_PRESS_IN │
+ * └─┬───────────────────┘ └─┬─────────────────┘ └─┬─────────────────┘
+ * │ ▲ │ ▲ │ ▲
+ * │LEAVE_ │ │LEAVE_ │ │LEAVE_ │
+ * │PRESS_RECT │ENTER_ │PRESS_RECT │ENTER_ │PRESS_RECT │ENTER_
+ * │ │PRESS_RECT │ │PRESS_RECT │ │PRESS_RECT
+ * ▼ │ ▼ │ ▼ │
+ * ┌─────────────┴───────┐ ┌─────────────┴─────┐ ┌─────────────┴─────┐
+ * │ RESPONDER_INACTIVE_ │ DELAY │ RESPONDER_ACTIVE_ │ │ RESPONDER_ACTIVE_ │
+ * │ PRESS_OUT ├────────▶ │ PRESS_OUT │ │ LONG_PRESS_OUT │
+ * └─────────────────────┘ └───────────────────┘ └───────────────────┘
+ *
+ * T + DELAY => LONG_PRESS_DELAY + DELAY
+ *
+ * Not drawn are the side effects of each transition. The most important side
+ * effect is the invocation of `onPress` and `onLongPress` that occur when a
+ * responder is release while in the "press in" states.
+ */
+export default class Pressability {
+ _config: PressabilityConfig;
+ _eventHandlers: ?EventHandlers = null;
+ _hoverInDelayTimeout: ?TimeoutID = null;
+ _hoverOutDelayTimeout: ?TimeoutID = null;
+ _isHovered: boolean = false;
+ _longPressDelayTimeout: ?TimeoutID = null;
+ _pressDelayTimeout: ?TimeoutID = null;
+ _pressOutDelayTimeout: ?TimeoutID = null;
+ _responderID: ?number | React.ElementRef> = null;
+ _responderRegion: ?$ReadOnly<{|
+ bottom: number,
+ left: number,
+ right: number,
+ top: number,
+ |}> = null;
+ _touchActivatePosition: ?$ReadOnly<{|
+ pageX: number,
+ pageY: number,
+ |}>;
+ _touchState: TouchState = 'NOT_RESPONDER';
+
+ constructor(config: PressabilityConfig) {
+ this._config = config;
+ }
+
+ /**
+ * Resets any pending timers. This should be called on unmount.
+ */
+ reset(): void {
+ this._cancelHoverInDelayTimeout();
+ this._cancelHoverOutDelayTimeout();
+ this._cancelLongPressDelayTimeout();
+ this._cancelPressDelayTimeout();
+ this._cancelPressOutDelayTimeout();
+ }
+
+ /**
+ * Returns a set of props to spread into the interactive element.
+ */
+ getEventHandlers(): EventHandlers {
+ if (this._eventHandlers == null) {
+ this._eventHandlers = this._createEventHandlers();
+ }
+ return this._eventHandlers;
+ }
+
+ _createEventHandlers(): EventHandlers {
+ const focusEventHandlers = {
+ onBlur: (event: BlurEvent): void => {
+ const {onBlur} = this._config;
+ if (onBlur != null) {
+ onBlur(event);
+ }
+ },
+ onFocus: (event: FocusEvent): void => {
+ const {onFocus} = this._config;
+ if (onFocus != null) {
+ onFocus(event);
+ }
+ },
+ };
+
+ const responderEventHandlers = {
+ onStartShouldSetResponder: () => {
+ const {onStartShouldSetResponder} = this._config;
+ return onStartShouldSetResponder == null
+ ? true
+ : onStartShouldSetResponder();
+ },
+
+ onResponderGrant: (event: PressEvent): void => {
+ event.persist();
+
+ this._cancelPressOutDelayTimeout();
+
+ this._responderID = event.currentTarget;
+ this._touchState = 'NOT_RESPONDER';
+ this._receiveSignal('RESPONDER_GRANT', event);
+
+ const {getLongPressDelayMS, getPressDelayMS} = this._config;
+
+ const pressDelay = getDelayMS(
+ getPressDelayMS,
+ 0,
+ DEFAULT_PRESS_DELAY_MS,
+ );
+ if (pressDelay > 0) {
+ this._pressDelayTimeout = setTimeout(() => {
+ this._receiveSignal('DELAY', event);
+ }, pressDelay);
+ } else {
+ this._receiveSignal('DELAY', event);
+ }
+
+ const longPressDelay = getDelayMS(
+ getLongPressDelayMS,
+ 10,
+ DEFAULT_LONG_PRESS_DELAY_MS,
+ );
+ this._longPressDelayTimeout = setTimeout(() => {
+ this._handleLongPress(event);
+ }, longPressDelay + pressDelay);
+ },
+
+ onResponderMove: (event: PressEvent): void => {
+ if (this._config.onPressMove != null) {
+ this._config.onPressMove(event);
+ }
+
+ // Region may not have finished being measured, yet.
+ const responderRegion = this._responderRegion;
+ if (responderRegion == null) {
+ return;
+ }
+
+ const touch = getTouchFromPressEvent(event);
+ if (touch == null) {
+ this._cancelLongPressDelayTimeout();
+ this._receiveSignal('LEAVE_PRESS_RECT', event);
+ return;
+ }
+
+ if (this._touchActivatePosition != null) {
+ const deltaX = this._touchActivatePosition.pageX - touch.pageX;
+ const deltaY = this._touchActivatePosition.pageY - touch.pageY;
+ if (Math.hypot(deltaX, deltaY) > 10) {
+ this._cancelLongPressDelayTimeout();
+ }
+ }
+
+ if (this._isTouchWithinResponderRegion(touch, responderRegion)) {
+ this._receiveSignal('ENTER_PRESS_RECT', event);
+ } else {
+ this._cancelLongPressDelayTimeout();
+ this._receiveSignal('LEAVE_PRESS_RECT', event);
+ }
+ },
+
+ onResponderRelease: (event: PressEvent): void => {
+ this._receiveSignal('RESPONDER_RELEASE', event);
+ },
+
+ onResponderTerminate: (event: PressEvent): void => {
+ this._receiveSignal('RESPONDER_TERMINATED', event);
+ },
+
+ onResponderTerminationRequest: (): boolean => {
+ const {onResponderTerminationRequest} = this._config;
+ return onResponderTerminationRequest == null
+ ? true
+ : onResponderTerminationRequest();
+ },
+
+ onClick: (event: PressEvent) => {
+ const {onPress} = this._config;
+ if (onPress != null) {
+ onPress(event);
+ }
+ },
+ };
+
+ const mouseEventHandlers =
+ Platform.OS === 'ios' || Platform.OS === 'android'
+ ? null
+ : {
+ onMouseEnter: (event: MouseEvent): void => {
+ if (isHoverEnabled()) {
+ this._isHovered = true;
+ this._cancelHoverOutDelayTimeout();
+ const {onHoverIn, getHoverInDelayMS} = this._config;
+ if (onHoverIn != null) {
+ const delay = getDelayMS(getHoverInDelayMS);
+ if (delay > 0) {
+ this._hoverInDelayTimeout = setTimeout(() => {
+ onHoverIn(event);
+ }, delay);
+ } else {
+ onHoverIn(event);
+ }
+ }
+ }
+ },
+
+ onMouseLeave: (event: MouseEvent): void => {
+ if (this._isHovered) {
+ this._isHovered = false;
+ this._cancelHoverInDelayTimeout();
+ const {onHoverOut, getHoverOutDelayMS} = this._config;
+ if (onHoverOut != null) {
+ const delay = getDelayMS(getHoverOutDelayMS);
+ if (delay > 0) {
+ this._hoverInDelayTimeout = setTimeout(() => {
+ onHoverOut(event);
+ }, delay);
+ } else {
+ onHoverOut(event);
+ }
+ }
+ }
+ },
+ };
+
+ // [Windows
+ const keyboardEventHandlers = {
+ onKeyUp: (event: KeyEvent): void => {
+ if (
+ event.nativeEvent.code === 'Space' ||
+ event.nativeEvent.code === 'Enter' ||
+ event.nativeEvent.code === 'GamepadA'
+ ) {
+ const {onPressOut, onPress} = this._config;
+
+ // $FlowFixMe: PressEvents don't mesh with keyboarding APIs. Keep legacy behavior of passing KeyEvents instead
+ onPressOut && onPressOut(event);
+ // $FlowFixMe: PressEvents don't mesh with keyboarding APIs. Keep legacy behavior of passing KeyEvents instead
+ onPress && onPress(event);
+ }
+ },
+ onKeyDown: (event: KeyEvent): void => {
+ if (
+ event.nativeEvent.code === 'Space' ||
+ event.nativeEvent.code === 'Enter' ||
+ event.nativeEvent.code === 'GamepadA'
+ ) {
+ const {onPressIn} = this._config;
+
+ // $FlowFixMe: PressEvents don't mesh with keyboarding APIs. Keep legacy behavior of passing KeyEvents instead
+ onPressIn && onPressIn(event);
+ }
+ },
+ };
+ // Windows]
+
+ return {
+ ...focusEventHandlers,
+ ...responderEventHandlers,
+ ...mouseEventHandlers,
+ ...keyboardEventHandlers, // [Windows]
+ };
+ }
+
+ /**
+ * Receives a state machine signal, performs side effects of the transition
+ * and stores the new state. Validates the transition as well.
+ */
+ _receiveSignal(signal: TouchSignal, event: PressEvent): void {
+ const prevState = this._touchState;
+ const nextState = Transitions[prevState]?.[signal];
+ if (this._responderID == null && signal === 'RESPONDER_RELEASE') {
+ return;
+ }
+ invariant(
+ nextState != null && nextState !== 'ERROR',
+ 'Pressability: Invalid signal `%s` for state `%s` on responder: %s',
+ signal,
+ prevState,
+ typeof this._responderID === 'number'
+ ? this._responderID
+ : '<>',
+ );
+ 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).
+ */
+ _performTransitionSideEffects(
+ prevState: TouchState,
+ nextState: TouchState,
+ signal: TouchSignal,
+ event: PressEvent,
+ ): void {
+ if (isTerminalSignal(signal)) {
+ this._touchActivatePosition = null;
+ this._cancelLongPressDelayTimeout();
+ }
+
+ const isInitialTransition =
+ prevState === 'NOT_RESPONDER' &&
+ nextState === 'RESPONDER_INACTIVE_PRESS_IN';
+
+ const isActivationTransiton =
+ !isActivationSignal(prevState) && isActivationSignal(nextState);
+
+ if (isInitialTransition || isActivationTransiton) {
+ this._measureResponderRegion();
+ }
+
+ if (isPressInSignal(prevState) && signal === 'LONG_PRESS_DETECTED') {
+ const {onLongPress} = this._config;
+ if (onLongPress != null) {
+ onLongPress(event);
+ }
+ }
+
+ const isPrevActive = isActiveSignal(prevState);
+ const isNextActive = isActiveSignal(nextState);
+
+ if (!isPrevActive && isNextActive) {
+ this._activate(event);
+ } else if (isPrevActive && !isNextActive) {
+ this._deactivate(event);
+ }
+
+ if (isPressInSignal(prevState) && signal === 'RESPONDER_RELEASE') {
+ const {onLongPress, onPress, getTouchSoundDisabled} = this._config;
+ if (onPress != null) {
+ const isPressCanceledByLongPress =
+ onLongPress != null &&
+ prevState === 'RESPONDER_ACTIVE_LONG_PRESS_IN' &&
+ this._shouldLongPressCancelPress();
+ if (!isPressCanceledByLongPress) {
+ // If we never activated (due to delays), activate and deactivate now.
+ if (!isNextActive && !isPrevActive) {
+ this._activate(event);
+ this._deactivate(event);
+ }
+ const isTouchSoundDisabled =
+ (getTouchSoundDisabled == null ? null : getTouchSoundDisabled()) ??
+ false;
+ if (Platform.OS === 'android' && !isTouchSoundDisabled) {
+ SoundManager.playTouchSound();
+ }
+ onPress(event);
+ }
+ }
+ }
+
+ this._cancelPressDelayTimeout();
+ }
+
+ _activate(event: PressEvent): void {
+ const {onPressIn} = this._config;
+ const touch = getTouchFromPressEvent(event);
+ this._touchActivatePosition = {
+ pageX: touch.pageX,
+ pageY: touch.pageY,
+ };
+ if (onPressIn != null) {
+ onPressIn(event);
+ }
+ }
+
+ _deactivate(event: PressEvent): void {
+ const {onPressOut, getPressOutDelayMS} = this._config;
+ if (onPressOut != null) {
+ const delay = getDelayMS(getPressOutDelayMS);
+ if (delay > 0) {
+ this._pressOutDelayTimeout = setTimeout(() => {
+ onPressOut(event);
+ }, delay);
+ } else {
+ onPressOut(event);
+ }
+ }
+ }
+
+ _measureResponderRegion(): void {
+ if (this._responderID == null) {
+ return;
+ }
+
+ if (typeof this._responderID === 'number') {
+ UIManager.measure(this._responderID, this._measureCallback);
+ } else {
+ this._responderID.measure(this._measureCallback);
+ }
+ }
+
+ _measureCallback = (left, top, width, height, pageX, pageY) => {
+ if (!left && !top && !width && !height && !pageX && !pageY) {
+ return;
+ }
+ this._responderRegion = {
+ bottom: pageY + height,
+ left: pageX,
+ right: pageX + width,
+ top: pageY,
+ };
+ };
+
+ _isTouchWithinResponderRegion(
+ touch: $PropertyType,
+ responderRegion: $ReadOnly<{|
+ bottom: number,
+ left: number,
+ right: number,
+ top: number,
+ |}>,
+ ): boolean {
+ const {getHitSlop, getPressRectOffset} = this._config;
+
+ let regionBottom = responderRegion.bottom;
+ let regionLeft = responderRegion.left;
+ let regionRight = responderRegion.right;
+ let regionTop = responderRegion.top;
+
+ const hitSlop = getHitSlop == null ? null : getHitSlop();
+ if (hitSlop != null) {
+ if (hitSlop.bottom != null) {
+ regionBottom += hitSlop.bottom;
+ }
+ if (hitSlop.left != null) {
+ regionLeft -= hitSlop.left;
+ }
+ if (hitSlop.right != null) {
+ regionRight += hitSlop.right;
+ }
+ if (hitSlop.top != null) {
+ regionTop -= hitSlop.top;
+ }
+ }
+
+ const rectOffset = getPressRectOffset == null ? null : getPressRectOffset();
+ regionBottom +=
+ rectOffset == null || rectOffset.bottom == null
+ ? DEFAULT_PRESS_RECT_OFFSETS.bottom
+ : rectOffset.bottom;
+ regionLeft -=
+ rectOffset == null || rectOffset.left == null
+ ? DEFAULT_PRESS_RECT_OFFSETS.left
+ : rectOffset.left;
+ regionRight +=
+ rectOffset == null || rectOffset.right == null
+ ? DEFAULT_PRESS_RECT_OFFSETS.right
+ : rectOffset.right;
+ regionTop -=
+ rectOffset == null || rectOffset.top == null
+ ? DEFAULT_PRESS_RECT_OFFSETS.top
+ : rectOffset.top;
+
+ return (
+ touch.pageX > regionLeft &&
+ touch.pageX < regionRight &&
+ touch.pageY > regionTop &&
+ touch.pageY < regionBottom
+ );
+ }
+
+ _handleLongPress(event: PressEvent): void {
+ if (
+ this._touchState === 'RESPONDER_ACTIVE_PRESS_IN' ||
+ this._touchState === 'RESPONDER_ACTIVE_LONG_PRESS_IN'
+ ) {
+ this._receiveSignal('LONG_PRESS_DETECTED', event);
+ }
+ }
+
+ _shouldLongPressCancelPress(): boolean {
+ return (
+ this._config.onLongPressShouldCancelPress == null ||
+ this._config.onLongPressShouldCancelPress()
+ );
+ }
+
+ _cancelHoverInDelayTimeout(): void {
+ if (this._hoverInDelayTimeout != null) {
+ clearTimeout(this._hoverInDelayTimeout);
+ this._hoverInDelayTimeout = null;
+ }
+ }
+
+ _cancelHoverOutDelayTimeout(): void {
+ if (this._hoverOutDelayTimeout != null) {
+ clearTimeout(this._hoverOutDelayTimeout);
+ this._hoverOutDelayTimeout = null;
+ }
+ }
+
+ _cancelLongPressDelayTimeout(): void {
+ if (this._longPressDelayTimeout != null) {
+ clearTimeout(this._longPressDelayTimeout);
+ this._longPressDelayTimeout = null;
+ }
+ }
+
+ _cancelPressDelayTimeout(): void {
+ if (this._pressDelayTimeout != null) {
+ clearTimeout(this._pressDelayTimeout);
+ this._pressDelayTimeout = null;
+ }
+ }
+
+ _cancelPressOutDelayTimeout(): void {
+ if (this._pressOutDelayTimeout != null) {
+ clearTimeout(this._pressOutDelayTimeout);
+ this._pressOutDelayTimeout = null;
+ }
+ }
+}
+
+const getDelayMS = (getDelay: ?() => ?number, min = 0, fallback = 0) => {
+ return Math.max(min, (getDelay == null ? null : getDelay()) ?? fallback);
+};
+
+const getTouchFromPressEvent = (event: PressEvent) => {
+ const {changedTouches, touches} = event.nativeEvent;
+
+ if (touches != null && touches.length > 0) {
+ return touches[0];
+ }
+ if (changedTouches != null && changedTouches.length > 0) {
+ return changedTouches[0];
+ }
+ return event.nativeEvent;
+};
diff --git a/vnext/src/overrides.json b/vnext/src/overrides.json
index ff537dfc57b..c02cfcdb585 100644
--- a/vnext/src/overrides.json
+++ b/vnext/src/overrides.json
@@ -588,6 +588,14 @@
"baseHash": "39326801da6c9ce8c350aa8ba971be4a386499bc",
"issue": "LEGACY_FIXME"
},
+ {
+ "type": "patch",
+ "file": "Libraries\\Pressability\\Pressability.windows.js",
+ "baseFile": "Libraries\\Pressability\\Pressability.js",
+ "baseVersion": "0.62.2",
+ "baseHash": "acf1c23e6f39d8fb201a06bca850760efe295057",
+ "issue": 4379
+ },
{
"type": "patch",
"file": "Libraries\\Pressability\\PressabilityDebug.windows.js",