From 74d2aedaa5a9a9cf5b571395cd0f9bded945024e Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Fri, 6 Aug 2021 11:26:18 -0700 Subject: [PATCH 1/5] enable usePressableState with stock Pressable component --- .../src/Pressability/Pressability.ts | 1284 ++++++++--------- .../src/Pressability/Pressability.types.ts | 186 +-- .../interactive-hooks/src/useAsPressable.ts | 28 +- .../src/useAsPressable.types.ts | 15 +- 4 files changed, 785 insertions(+), 728 deletions(-) diff --git a/packages/utils/interactive-hooks/src/Pressability/Pressability.ts b/packages/utils/interactive-hooks/src/Pressability/Pressability.ts index 47ea5e3a85..66a4d3da69 100644 --- a/packages/utils/interactive-hooks/src/Pressability/Pressability.ts +++ b/packages/utils/interactive-hooks/src/Pressability/Pressability.ts @@ -8,645 +8,645 @@ * @format */ -'use strict'; - -import invariant from 'invariant'; -import * as React from 'react'; -import { Platform, UIManager } from 'react-native'; -import { BlurEvent, FocusEvent, MouseEvent, PressEvent } from './CoreEventTypes'; -import { isHoverEnabled } from './HoverState'; -import { HostComponent, normalizeRect, Rect } from './InternalTypes'; -import { PressabilityConfig, PressabilityEventHandlers } from './Pressability.types'; - -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: { [K in TouchState]: { [T in TouchSignal]: TouchState } } = { - 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 = 370; // 500 - 130 -const DEFAULT_PRESS_DELAY_MS = 130; -const DEFAULT_PRESS_RECT_OFFSETS: Rect = { - bottom: 30, - left: 20, - right: 20, - top: 20, -}; - -function normalizeDelay(delay?: number, min: number = 0, fallback: number = 0): number { - return Math.max(min, delay ?? 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; -}; - -/** - * 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 `hitSlop`. - * │ │ │ 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 - * `pressRectOffset`, 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 class Pressability { - private _config: PressabilityConfig; - private _eventHandlers: PressabilityEventHandlers = null; - private _hoverInDelayTimeout: any /* TimeoutID */ = null; - private _hoverOutDelayTimeout: any /* TimeoutID */ = null; - private _isHovered: boolean = false; - private _longPressDelayTimeout: any /* TimeoutID */ = null; - private _pressDelayTimeout: any /* TimeoutID */ = null; - private _pressOutDelayTimeout: any /* TimeoutID */ = null; - private _responderID: number | React.ElementRef> = null; - private _responderRegion: Rect = null; - private _touchActivatePosition: Readonly<{ - pageX: number; - pageY: number; - }>; - private _touchState: TouchState = 'NOT_RESPONDER'; - - constructor(config: PressabilityConfig) { - this.configure(config); - } - - public configure(config: PressabilityConfig): void { - this._config = config; - } - - /** - * Resets any pending timers. This should be called on unmount. - */ - public reset(): void { - this._cancelHoverInDelayTimeout(); - this._cancelHoverOutDelayTimeout(); - this._cancelLongPressDelayTimeout(); - this._cancelPressDelayTimeout(); - this._cancelPressOutDelayTimeout(); - } - - /** - * Returns a set of props to spread into the interactive element. - */ - public getEventHandlers(): PressabilityEventHandlers { - if (this._eventHandlers == null) { - this._eventHandlers = this._createEventHandlers(); - } - return this._eventHandlers; - } - - private _createEventHandlers(): PressabilityEventHandlers { - 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: (): boolean => { - const { disabled } = this._config; - if (disabled == null) { - return true; - } - return !disabled; - }, - - onResponderGrant: (event: PressEvent): void => { - event.persist(); - - this._cancelPressOutDelayTimeout(); - - this._responderID = event.currentTarget; - this._touchState = 'NOT_RESPONDER'; - this._receiveSignal('RESPONDER_GRANT', event); - - const delayPressIn = normalizeDelay(this._config.delayPressIn, 0, DEFAULT_PRESS_DELAY_MS); - - if (delayPressIn > 0) { - this._pressDelayTimeout = setTimeout(() => { - this._receiveSignal('DELAY', event); - }, delayPressIn); - } else { - this._receiveSignal('DELAY', event); - } - - const delayLongPress = normalizeDelay(this._config.delayLongPress, 10, DEFAULT_LONG_PRESS_DELAY_MS); - this._longPressDelayTimeout = setTimeout(() => { - this._handleLongPress(event); - }, delayLongPress + delayPressIn); - }, - - 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 { cancelable } = this._config; - return cancelable || true; - }, - - onClick: (event: PressEvent): void => { - 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 } = this._config; - if (onHoverIn != null) { - const delayHoverIn = normalizeDelay(this._config.delayHoverIn); - if (delayHoverIn > 0) { - this._hoverInDelayTimeout = setTimeout(() => { - onHoverIn(event); - }, delayHoverIn); - } else { - onHoverIn(event); - } - } - } - }, - - onMouseLeave: (event: MouseEvent): void => { - if (this._isHovered) { - this._isHovered = false; - this._cancelHoverInDelayTimeout(); - const { onHoverOut } = this._config; - if (onHoverOut != null) { - const delayHoverOut = normalizeDelay(this._config.delayHoverOut); - if (delayHoverOut > 0) { - this._hoverInDelayTimeout = setTimeout(() => { - onHoverOut(event); - }, delayHoverOut); - } else { - onHoverOut(event); - } - } - } - }, - }; - - return { - ...focusEventHandlers, - ...responderEventHandlers, - ...mouseEventHandlers, - }; - } - - /** - * Receives a state machine signal, performs side effects of the transition - * and stores the new state. Validates the transition as well. - */ - private _receiveSignal(signal: TouchSignal, event: PressEvent): void { - const prevState = this._touchState; - const nextState: TouchState = 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). - */ - private _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 /*, android_disableSound */ } = 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); - } - /* - if (Platform.OS === 'android' && android_disableSound !== true) { - SoundManager.playTouchSound(); - } - */ - onPress(event); - } - } - } - - this._cancelPressDelayTimeout(); - } - - private _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); - } - } - - private _deactivate(event: PressEvent): void { - const { onPressOut } = this._config; - if (onPressOut != null) { - const delayPressOut = normalizeDelay(this._config.delayPressOut); - if (delayPressOut > 0) { - this._pressOutDelayTimeout = setTimeout(() => { - onPressOut(event); - }, delayPressOut); - } else { - onPressOut(event); - } - } - } - - private _measureResponderRegion(): void { - if (this._responderID == null) { - return; - } - - if (typeof this._responderID === 'number') { - UIManager.measure(this._responderID, this._measureCallback); - } else { - const measure = (this as any)?._responderID?.measure; - - if (typeof measure === 'function' && this._measureCallback) { - (this as any)?._responderID?.measure(this._measureCallback); - } - } - } - - private _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, - }; - }; - - private _isTouchWithinResponderRegion(touch: any /* PropertyType */, responderRegion: Rect): boolean { - const hitSlop = normalizeRect(this._config.hitSlop); - const pressRectOffset = normalizeRect(this._config.pressRectOffset); - - let regionBottom = responderRegion.bottom; - let regionLeft = responderRegion.left; - let regionRight = responderRegion.right; - let regionTop = responderRegion.top; - - 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; - } - } - - regionBottom += pressRectOffset?.bottom ?? DEFAULT_PRESS_RECT_OFFSETS.bottom; - regionLeft -= pressRectOffset?.left ?? DEFAULT_PRESS_RECT_OFFSETS.left; - regionRight += pressRectOffset?.right ?? DEFAULT_PRESS_RECT_OFFSETS.right; - regionTop -= pressRectOffset?.top ?? DEFAULT_PRESS_RECT_OFFSETS.top; - - return touch.pageX > regionLeft && touch.pageX < regionRight && touch.pageY > regionTop && touch.pageY < regionBottom; - } - - private _handleLongPress(event: PressEvent): void { - if (this._touchState === 'RESPONDER_ACTIVE_PRESS_IN' || this._touchState === 'RESPONDER_ACTIVE_LONG_PRESS_IN') { - this._receiveSignal('LONG_PRESS_DETECTED', event); - } - } - - private _shouldLongPressCancelPress(): boolean { - return true; - } - - private _cancelHoverInDelayTimeout(): void { - if (this._hoverInDelayTimeout != null) { - clearTimeout(this._hoverInDelayTimeout); - this._hoverInDelayTimeout = null; - } - } - - private _cancelHoverOutDelayTimeout(): void { - if (this._hoverOutDelayTimeout != null) { - clearTimeout(this._hoverOutDelayTimeout); - this._hoverOutDelayTimeout = null; - } - } - - private _cancelLongPressDelayTimeout(): void { - if (this._longPressDelayTimeout != null) { - clearTimeout(this._longPressDelayTimeout); - this._longPressDelayTimeout = null; - } - } - - private _cancelPressDelayTimeout(): void { - if (this._pressDelayTimeout != null) { - clearTimeout(this._pressDelayTimeout); - this._pressDelayTimeout = null; - } - } - - private _cancelPressOutDelayTimeout(): void { - if (this._pressOutDelayTimeout != null) { - clearTimeout(this._pressOutDelayTimeout); - this._pressOutDelayTimeout = null; - } - } -} + 'use strict'; + + import invariant from 'invariant'; + import * as React from 'react'; + import { Platform, UIManager } from 'react-native'; + import { BlurEvent, FocusEvent, MouseEvent, PressEvent } from './CoreEventTypes'; + import { isHoverEnabled } from './HoverState'; + import { HostComponent, normalizeRect, Rect } from './InternalTypes'; + import { PressabilityConfig, PressabilityEventHandlers } from './Pressability.types'; + + 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: { [K in TouchState]: { [T in TouchSignal]: TouchState } } = { + 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 = 370; // 500 - 130 + const DEFAULT_PRESS_DELAY_MS = 130; + const DEFAULT_PRESS_RECT_OFFSETS: Rect = { + bottom: 30, + left: 20, + right: 20, + top: 20, + }; + + function normalizeDelay(delay?: number, min: number = 0, fallback: number = 0): number { + return Math.max(min, delay ?? 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; + }; + + /** + * 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 `hitSlop`. + * │ │ │ 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 + * `pressRectOffset`, 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 class Pressability { + private _config: PressabilityConfig; + private _eventHandlers: PressabilityEventHandlers = null; + private _hoverInDelayTimeout: any /* TimeoutID */ = null; + private _hoverOutDelayTimeout: any /* TimeoutID */ = null; + private _isHovered: boolean = false; + private _longPressDelayTimeout: any /* TimeoutID */ = null; + private _pressDelayTimeout: any /* TimeoutID */ = null; + private _pressOutDelayTimeout: any /* TimeoutID */ = null; + private _responderID: number | React.ElementRef> = null; + private _responderRegion: Rect = null; + private _touchActivatePosition: Readonly<{ + pageX: number; + pageY: number; + }>; + private _touchState: TouchState = 'NOT_RESPONDER'; + + constructor(config: PressabilityConfig) { + this.configure(config); + } + + public configure(config: PressabilityConfig): void { + this._config = config; + } + + /** + * Resets any pending timers. This should be called on unmount. + */ + public reset(): void { + this._cancelHoverInDelayTimeout(); + this._cancelHoverOutDelayTimeout(); + this._cancelLongPressDelayTimeout(); + this._cancelPressDelayTimeout(); + this._cancelPressOutDelayTimeout(); + } + + /** + * Returns a set of props to spread into the interactive element. + */ + public getEventHandlers(): PressabilityEventHandlers { + if (this._eventHandlers == null) { + this._eventHandlers = this._createEventHandlers(); + } + return this._eventHandlers; + } + + private _createEventHandlers(): PressabilityEventHandlers { + 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: (): boolean => { + const { disabled } = this._config; + if (disabled == null) { + return true; + } + return !disabled; + }, + + onResponderGrant: (event: PressEvent): void => { + event.persist(); + + this._cancelPressOutDelayTimeout(); + + this._responderID = event.currentTarget; + this._touchState = 'NOT_RESPONDER'; + this._receiveSignal('RESPONDER_GRANT', event); + + const delayPressIn = normalizeDelay(this._config.delayPressIn, 0, DEFAULT_PRESS_DELAY_MS); + + if (delayPressIn > 0) { + this._pressDelayTimeout = setTimeout(() => { + this._receiveSignal('DELAY', event); + }, delayPressIn); + } else { + this._receiveSignal('DELAY', event); + } + + const delayLongPress = normalizeDelay(this._config.delayLongPress, 10, DEFAULT_LONG_PRESS_DELAY_MS); + this._longPressDelayTimeout = setTimeout(() => { + this._handleLongPress(event); + }, delayLongPress + delayPressIn); + }, + + 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 { cancelable } = this._config; + return cancelable || true; + }, + + onClick: (event): void => { + 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 } = this._config; + if (onHoverIn != null) { + const delayHoverIn = normalizeDelay(this._config.delayHoverIn); + if (delayHoverIn > 0) { + this._hoverInDelayTimeout = setTimeout(() => { + onHoverIn(event); + }, delayHoverIn); + } else { + onHoverIn(event); + } + } + } + }, + + onMouseLeave: (event: MouseEvent): void => { + if (this._isHovered) { + this._isHovered = false; + this._cancelHoverInDelayTimeout(); + const { onHoverOut } = this._config; + if (onHoverOut != null) { + const delayHoverOut = normalizeDelay(this._config.delayHoverOut); + if (delayHoverOut > 0) { + this._hoverInDelayTimeout = setTimeout(() => { + onHoverOut(event); + }, delayHoverOut); + } else { + onHoverOut(event); + } + } + } + }, + }; + + return { + ...focusEventHandlers, + ...responderEventHandlers, + ...mouseEventHandlers, + }; + } + + /** + * Receives a state machine signal, performs side effects of the transition + * and stores the new state. Validates the transition as well. + */ + private _receiveSignal(signal: TouchSignal, event: PressEvent): void { + const prevState = this._touchState; + const nextState: TouchState = 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). + */ + private _performTransitionSideEffects(prevState: TouchState, nextState: TouchState, signal: TouchSignal, event): 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 /*, android_disableSound */ } = 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); + } + /* + if (Platform.OS === 'android' && android_disableSound !== true) { + SoundManager.playTouchSound(); + } + */ + onPress(event); + } + } + } + + this._cancelPressDelayTimeout(); + } + + private _activate(event): void { + const { onPressIn } = this._config; + const touch = getTouchFromPressEvent(event); + this._touchActivatePosition = { + pageX: touch.pageX, + pageY: touch.pageY, + }; + if (onPressIn != null) { + onPressIn(event); + } + } + + private _deactivate(event): void { + const { onPressOut } = this._config; + if (onPressOut != null) { + const delayPressOut = normalizeDelay(this._config.delayPressOut); + if (delayPressOut > 0) { + this._pressOutDelayTimeout = setTimeout(() => { + onPressOut(event); + }, delayPressOut); + } else { + onPressOut(event); + } + } + } + + private _measureResponderRegion(): void { + if (this._responderID == null) { + return; + } + + if (typeof this._responderID === 'number') { + UIManager.measure(this._responderID, this._measureCallback); + } else { + const measure = (this as any)?._responderID?.measure; + + if (typeof measure === 'function' && this._measureCallback) { + (this as any)?._responderID?.measure(this._measureCallback); + } + } + } + + private _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, + }; + }; + + private _isTouchWithinResponderRegion(touch: any /* PropertyType */, responderRegion: Rect): boolean { + const hitSlop = normalizeRect(this._config.hitSlop); + const pressRectOffset = normalizeRect(this._config.pressRectOffset); + + let regionBottom = responderRegion.bottom; + let regionLeft = responderRegion.left; + let regionRight = responderRegion.right; + let regionTop = responderRegion.top; + + 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; + } + } + + regionBottom += pressRectOffset?.bottom ?? DEFAULT_PRESS_RECT_OFFSETS.bottom; + regionLeft -= pressRectOffset?.left ?? DEFAULT_PRESS_RECT_OFFSETS.left; + regionRight += pressRectOffset?.right ?? DEFAULT_PRESS_RECT_OFFSETS.right; + regionTop -= pressRectOffset?.top ?? DEFAULT_PRESS_RECT_OFFSETS.top; + + return touch.pageX > regionLeft && touch.pageX < regionRight && touch.pageY > regionTop && touch.pageY < regionBottom; + } + + private _handleLongPress(event: PressEvent): void { + if (this._touchState === 'RESPONDER_ACTIVE_PRESS_IN' || this._touchState === 'RESPONDER_ACTIVE_LONG_PRESS_IN') { + this._receiveSignal('LONG_PRESS_DETECTED', event); + } + } + + private _shouldLongPressCancelPress(): boolean { + return true; + } + + private _cancelHoverInDelayTimeout(): void { + if (this._hoverInDelayTimeout != null) { + clearTimeout(this._hoverInDelayTimeout); + this._hoverInDelayTimeout = null; + } + } + + private _cancelHoverOutDelayTimeout(): void { + if (this._hoverOutDelayTimeout != null) { + clearTimeout(this._hoverOutDelayTimeout); + this._hoverOutDelayTimeout = null; + } + } + + private _cancelLongPressDelayTimeout(): void { + if (this._longPressDelayTimeout != null) { + clearTimeout(this._longPressDelayTimeout); + this._longPressDelayTimeout = null; + } + } + + private _cancelPressDelayTimeout(): void { + if (this._pressDelayTimeout != null) { + clearTimeout(this._pressDelayTimeout); + this._pressDelayTimeout = null; + } + } + + private _cancelPressOutDelayTimeout(): void { + if (this._pressOutDelayTimeout != null) { + clearTimeout(this._pressOutDelayTimeout); + this._pressOutDelayTimeout = null; + } + } + } diff --git a/packages/utils/interactive-hooks/src/Pressability/Pressability.types.ts b/packages/utils/interactive-hooks/src/Pressability/Pressability.types.ts index 4a945ebff0..3754ee1e22 100644 --- a/packages/utils/interactive-hooks/src/Pressability/Pressability.types.ts +++ b/packages/utils/interactive-hooks/src/Pressability/Pressability.types.ts @@ -1,60 +1,30 @@ import { RectOrSize } from './InternalTypes'; import { BlurEvent, FocusEvent, PressEvent, MouseEvent } from './CoreEventTypes'; -import { ViewProps } from 'react-native'; +import { ViewProps, PressableProps } from 'react-native'; -export type PressabilityConfig = Readonly<{ +export type PressablePressProps = { /** - * Whether a press gesture can be interrupted by a parent gesture such as a - * scroll event. Defaults to true. - */ - cancelable?: boolean; - - /** - * Whether to disable initialization of the press gesture. - */ - disabled?: boolean; - - /** - * Amount to extend the `VisualRect` by to create `HitRect`. - */ - hitSlop?: ViewProps['hitSlop']; - - /** - * Amount to extend the `HitRect` by to create `PressRect`. - */ - pressRectOffset?: RectOrSize; - - /** - * Whether to disable the systemm sound when `onPress` fires on Android. - **/ - android_disableSound?: boolean; - - /** - * Duration to wait after hover in before calling `onHoverIn`. - */ - delayHoverIn?: number; - - /** - * Duration to wait after hover out before calling `onHoverOut`. + * Called when a press gestute has been triggered. */ - delayHoverOut?: number; + onPress?: PressableProps['onPress']; /** - * Duration (in addition to `delayPressIn`) after which a press gesture is - * considered a long press gesture. Defaults to 500 (milliseconds). + * Called when the press is activated to provide visual feedback. */ - delayLongPress?: number; + onPressIn?: PressableProps['onPressIn']; /** - * Duration to wait after press down before calling `onPressIn`. + * Called when the press is deactivated to undo visual feedback. */ - delayPressIn?: number; + onPressOut?: PressableProps['onPressOut']; /** - * Duration to wait after letting up before calling `onPressOut`. + * Called when a long press gesture has been triggered. */ - delayPressOut?: number; + onLongPress?: PressableProps['onLongPress']; +}; +export type PressableFocusProps = { /** * Called after the element loses focus. */ @@ -64,7 +34,9 @@ export type PressabilityConfig = Readonly<{ * Called after the element is focused. */ onFocus?: (event: FocusEvent) => any; +}; +export type PressableHoverProps = { /** * Called when the hover is activated to provide visual feedback. */ @@ -74,43 +46,93 @@ export type PressabilityConfig = Readonly<{ * Called when the hover is deactivated to undo visual feedback. */ onHoverOut?: (event: MouseEvent) => any; - - /** - * Called when a long press gesture has been triggered. - */ - onLongPress?: (event: PressEvent) => any; - - /** - * Called when a press gestute has been triggered. - */ - onPress?: (event: PressEvent) => any; - - /** - * Called when the press is activated to provide visual feedback. - */ - onPressIn?: (event: PressEvent) => any; - - /** - * Called when the press location moves. (This should rarely be used.) - */ - onPressMove?: (event: PressEvent) => any; - - /** - * Called when the press is deactivated to undo visual feedback. - */ - onPressOut?: (event: PressEvent) => any; -}>; - -export type PressabilityEventHandlers = 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; -}>; +}; + +export type PressableHoverEventProps = { + /** + * While the user API is onHoverIn the View event is onMouseEnter + */ + onMouseEnter?: (event: MouseEvent) => any; + + /** + * While the user API is onHoverOut the View event is onMouseLeave + */ + onMouseLeave?: (event: MouseEvent) => any; +}; + +export type PressabilityConfig = Readonly< + PressablePressProps & + PressableFocusProps & + PressableHoverProps & { + /** + * Whether a press gesture can be interrupted by a parent gesture such as a + * scroll event. Defaults to true. + */ + cancelable?: boolean; + + /** + * Whether to disable initialization of the press gesture. + */ + disabled?: boolean; + + /** + * Amount to extend the `VisualRect` by to create `HitRect`. + */ + hitSlop?: ViewProps['hitSlop']; + + /** + * Amount to extend the `HitRect` by to create `PressRect`. + */ + pressRectOffset?: RectOrSize; + + /** + * Whether to disable the systemm sound when `onPress` fires on Android. + **/ + android_disableSound?: boolean; + + /** + * Duration to wait after hover in before calling `onHoverIn`. + */ + delayHoverIn?: number; + + /** + * Duration to wait after hover out before calling `onHoverOut`. + */ + delayHoverOut?: number; + + /** + * Duration (in addition to `delayPressIn`) 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 `onPressIn`. + */ + delayPressIn?: number; + + /** + * Duration to wait after letting up before calling `onPressOut`. + */ + delayPressOut?: number; + + /** + * Called when the press location moves. (This should rarely be used.) + */ + onPressMove?: (event: PressEvent) => any; + } +>; + +export type PressabilityEventHandlers = Readonly< + PressablePressProps & + PressableHoverEventProps & + PressableFocusProps & { + onClick: (event: PressEvent) => void; + onResponderGrant: (event: PressEvent) => void; + onResponderMove: (event: PressEvent) => void; + onResponderRelease: (event: PressEvent) => void; + onResponderTerminate: (event: PressEvent) => void; + onResponderTerminationRequest: () => boolean; + onStartShouldSetResponder: () => boolean; + } +>; diff --git a/packages/utils/interactive-hooks/src/useAsPressable.ts b/packages/utils/interactive-hooks/src/useAsPressable.ts index 539cd85774..8be6d7ca27 100644 --- a/packages/utils/interactive-hooks/src/useAsPressable.ts +++ b/packages/utils/interactive-hooks/src/useAsPressable.ts @@ -1,4 +1,6 @@ import * as React from 'react'; +import { PressableProps, Platform } from 'react-native'; +import { PressableFocusProps, PressableHoverProps, PressablePressProps } from './Pressability/Pressability.types'; import { IPressableHooks, IWithPressableOptions, @@ -6,6 +8,8 @@ import { IHoverState, IFocusState, IWithPressableEvents, + IPressableState, + PressablePropsExtended, } from './useAsPressable.types'; import { usePressability } from './usePressability'; @@ -13,7 +17,7 @@ import { usePressability } from './usePressability'; * hover specific state and callback helper */ // eslint-disable-next-line @typescript-eslint/ban-types -function useHoverHelper(props: IWithPressableOptions): [IWithPressableOptions, IHoverState] { +function useHoverHelper(props: PressableHoverProps): [PressableHoverProps, IHoverState] { const [hoverState, setHoverState] = React.useState({ hovered: false }); const onHoverIn = React.useCallback( (e) => { @@ -41,7 +45,7 @@ function useHoverHelper(props: IWithPressableOptions): [IWi * focus specific state and callback helper */ // eslint-disable-next-line @typescript-eslint/ban-types -function useFocusHelper(props: IWithPressableOptions): [IWithPressableOptions, IFocusState] { +function useFocusHelper(props: PressableFocusProps): [PressableFocusProps, IFocusState] { const [focusState, setFocusState] = React.useState({ focused: false }); const onFocus = React.useCallback( (e) => { @@ -69,7 +73,7 @@ function useFocusHelper(props: IWithPressableOptions): [IWi * press specific state and callback helper */ // eslint-disable-next-line @typescript-eslint/ban-types -function usePressHelper(props: IWithPressableOptions): [IWithPressableOptions, IPressState] { +function usePressHelper(props: PressablePressProps): [PressablePressProps, IPressState] { const [pressState, setPressState] = React.useState({ pressed: false }); const onPressIn = React.useCallback( @@ -146,3 +150,21 @@ export function useHoverState(props: IWithPressableOptions) const [hoverProps, hoverState] = useHoverHelper(props); return [{ ...props, ...usePressability({ ...props, ...hoverProps }) }, hoverState]; } + +/** + * This routine hooks the props to pass to a Pressable component to obtain the current state of the Pressable as well as generating + * state change updates when those props change. This allows a parent component to control the render of the whole component rather than having + * to split the code between a child function or style function. + * + * @param props - props to pass to a Pressable component + * @returns - modified props to pass into the Pressable as well as the current state with regards to hover, focus, and press + */ +export function usePressableState(props: PressablePropsExtended): { props: PressableProps; state: IPressableState } { + const { onPressIn, onPressOut, onHoverIn, onHoverOut, onFocus, onBlur, ...rest } = props; + const [focusProps, focusState] = useFocusHelper({ onFocus, onBlur }); + const [pressProps, pressState] = usePressHelper({ onPressIn, onPressOut }); + const [hoverProps, hoverState] = + Platform.OS !== 'android' && Platform.OS !== 'ios' ? useHoverHelper({ onHoverIn, onHoverOut }) : [{}, {}]; + + return { props: { ...rest, ...focusProps, ...pressProps, ...hoverProps }, state: { ...focusState, ...pressState, ...hoverState } }; +} diff --git a/packages/utils/interactive-hooks/src/useAsPressable.types.ts b/packages/utils/interactive-hooks/src/useAsPressable.types.ts index b820f89259..e4ddf58ed9 100644 --- a/packages/utils/interactive-hooks/src/useAsPressable.types.ts +++ b/packages/utils/interactive-hooks/src/useAsPressable.types.ts @@ -1,4 +1,5 @@ -import { PressabilityConfig, PressabilityEventHandlers } from './Pressability/Pressability.types'; +import { PressableProps } from 'react-native'; +import { PressabilityConfig, PressabilityEventHandlers, PressableFocusProps, PressableHoverProps } from './Pressability/Pressability.types'; export type IPressState = { pressed?: boolean; @@ -29,3 +30,15 @@ export type IPressableHooks = { props: IWithPressableEvents; state: IPressableState; }; + +/** + * The hover props on RN 0.63 don't exist in react-native, only on the desktop SKUs, but we need them for hooking + * mouse events on windows and macos. This adds them to the props type, stripping them from the base pressable type as + * future proofing for when we upgrade to 0.64+. + * + * Similarly the focus methods exist at the View level but are not exposed on pressable props in 0.63. As a result they + * need a similar treatment + */ +export type PressablePropsExtended = Exclude & + PressableHoverProps & + PressableFocusProps; From 8d08a821a2d1eb63b54edc3f88e87f1e8aeec735 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Fri, 6 Aug 2021 11:38:21 -0700 Subject: [PATCH 2/5] Change files --- ...-react-native-7d512093-71b2-40bb-8033-4d0baf1137b0.json | 7 +++++++ ...tive-adapters-90b75eae-223e-4b72-a54a-2369be99ee64.json | 7 +++++++ ...android-theme-b44291fe-369f-4255-8b3b-5fba484d8d2b.json | 7 +++++++ ...e-apple-theme-d60319d4-4d6c-4dc7-8342-7418729aa00f.json | 7 +++++++ ...native-button-8e4544ee-0825-448e-afd4-18559ab5ff0f.json | 7 +++++++ ...ative-callout-71df3dd9-2202-4292-91e2-2d62637dd46e.json | 7 +++++++ ...tive-checkbox-88df73d6-334b-4371-a71b-61efb0ca83ab.json | 7 +++++++ ...mponent-cache-5ec33ba1-15cb-41d9-a462-8f92a0290ad2.json | 7 +++++++ ...e-composition-8e3c27ee-a0ab-4903-84c4-f59b35da4257.json | 7 +++++++ ...ntextual-menu-0da401b3-8398-4e31-aa69-76d982e1158d.json | 7 +++++++ ...default-theme-2297d5f3-2996-4316-be99-697e74e1797d.json | 7 +++++++ ...ity-indicator-ee25437c-9917-4f3f-8daa-656ed70c9146.json | 7 +++++++ ...mental-avatar-8113fffd-d03a-4ac7-8e4c-244a97664e97.json | 7 +++++++ ...mental-button-0d9b68fd-55f3-4861-b7de-01e99bdabf58.json | 7 +++++++ ...native-button-62fe4948-f964-4f62-aa3d-f4507fc007d7.json | 7 +++++++ ...e-date-picker-a5afb9c3-842d-461c-9138-1366eb008d6f.json | 7 +++++++ ...ental-shimmer-61da57dd-baa5-40ba-b20e-0fb7f7935a61.json | 7 +++++++ ...rimental-text-f71ecc30-17f3-4dbf-9ab6-c869faf7636e.json | 7 +++++++ ...cus-trap-zone-e5701e0c-1df0-42ed-9068-48db861cee8c.json | 7 +++++++ ...ve-focus-zone-791e777d-b9d2-4725-8e1c-8aa43f44396c.json | 7 +++++++ ...ive-framework-afb2fbbf-e4d7-4a5f-8626-ff5ed3959c38.json | 7 +++++++ ...t-native-icon-bc67640e-b05b-476d-bf5c-14b5523f69ad.json | 7 +++++++ ...mutable-merge-ced1fad7-3161-49f9-8655-682cc3415d76.json | 7 +++++++ ...ractive-hooks-485acb03-240b-4962-9c87-75095b69a017.json | 7 +++++++ ...t-native-link-856b90c2-1505-4b5d-a9d8-cdba94641e11.json | 7 +++++++ ...ve-memo-cache-9abd4cc2-08af-4f14-8ddf-a8ec00be0888.json | 7 +++++++ ...e-menu-button-194aebff-99ae-4265-9f58-06053c8cfa05.json | 7 +++++++ ...e-merge-props-c02f5227-1238-43d2-81bb-b6b7ff75cc1a.json | 7 +++++++ ...-radio-button-be1bd5f8-09e2-45fd-a53d-ba03e4d6a683.json | 7 +++++++ ...ative-persona-1bd46d85-ae28-4769-b4fc-77e682a11148.json | 7 +++++++ ...-persona-coin-0fb8998b-6e13-4eab-a5f5-d4b96a30c6f5.json | 7 +++++++ ...ive-pressable-b7b6e32d-5084-4536-9563-44147e3457cf.json | 7 +++++++ ...e-radio-group-34215e0c-d5fc-41fb-ab0c-4bc1d04f2cf1.json | 7 +++++++ ...ive-separator-ec248cab-bd88-4f26-95f2-e99cd69f126b.json | 7 +++++++ ...-native-stack-c9a3d82c-b085-41de-b59a-ed59cd890f6a.json | 7 +++++++ ...native-tester-1b1b4d96-2b82-4270-9e62-0ea9348f6e37.json | 7 +++++++ ...-tester-win32-66e279df-0e1a-41b0-8cf6-e3026b9c6cc4.json | 7 +++++++ ...t-native-text-15b0c91a-57a7-48ee-9e6f-1ff24cb42d93.json | 7 +++++++ ...-native-theme-844ff0d1-05fa-43fc-9c06-5135683fcba5.json | 7 +++++++ ...-theme-tokens-52466e00-c918-42a9-b250-7db74a67e085.json | 7 +++++++ ...e-theme-types-6e96d5b1-49af-400d-a794-422a1d03cfbe.json | 7 +++++++ ...theming-utils-b1c21a7c-dbc0-470a-ab3d-f454fddd4786.json | 7 +++++++ ...native-tokens-5154ad0a-3776-4f59-b6cb-7e32c1611720.json | 7 +++++++ ...tive-use-slot-35fb9856-7455-41db-8e94-df7ffadaaffd.json | 7 +++++++ ...ive-use-slots-bbdce544-8a15-4f5f-a7e6-014c5b6d6b02.json | 7 +++++++ ...e-use-styling-ce6b1998-d638-4395-a347-f915ddc97230.json | 7 +++++++ ...ve-use-tokens-c94f2b57-7fa8-4551-9ed6-9c899902061e.json | 7 +++++++ ...e-win32-theme-059dc9ab-1a28-401c-a252-91f2a8130ce1.json | 7 +++++++ ...on-composable-79de053c-262c-4c73-ad4c-9e616a5c822f.json | 7 +++++++ ...ation-compose-a89cf93c-fc39-467c-9a2b-5805ebbf81ac.json | 7 +++++++ ...tion-settings-1f0a4f6f-b386-4951-9b3c-0c80a7efda0e.json | 7 +++++++ ...dation-tokens-94ed7a50-1c42-406d-a30c-7c2fd365aa4c.json | 7 +++++++ ...heme-registry-d1c539a8-6fba-48a7-9825-e806d86b0a85.json | 7 +++++++ ...emed-settings-314ebd11-0795-47d5-b31c-de4baac1da81.json | 7 +++++++ ...-theming-ramp-5114ef9a-2ecf-4dbf-ac9d-b6227bc1141b.json | 7 +++++++ ...-react-native-e9f1dcfe-6dc9-46de-ae28-275a1b6b5bf5.json | 7 +++++++ 56 files changed, 392 insertions(+) create mode 100644 change/@fluentui-react-native-7d512093-71b2-40bb-8033-4d0baf1137b0.json create mode 100644 change/@fluentui-react-native-adapters-90b75eae-223e-4b72-a54a-2369be99ee64.json create mode 100644 change/@fluentui-react-native-android-theme-b44291fe-369f-4255-8b3b-5fba484d8d2b.json create mode 100644 change/@fluentui-react-native-apple-theme-d60319d4-4d6c-4dc7-8342-7418729aa00f.json create mode 100644 change/@fluentui-react-native-button-8e4544ee-0825-448e-afd4-18559ab5ff0f.json create mode 100644 change/@fluentui-react-native-callout-71df3dd9-2202-4292-91e2-2d62637dd46e.json create mode 100644 change/@fluentui-react-native-checkbox-88df73d6-334b-4371-a71b-61efb0ca83ab.json create mode 100644 change/@fluentui-react-native-component-cache-5ec33ba1-15cb-41d9-a462-8f92a0290ad2.json create mode 100644 change/@fluentui-react-native-composition-8e3c27ee-a0ab-4903-84c4-f59b35da4257.json create mode 100644 change/@fluentui-react-native-contextual-menu-0da401b3-8398-4e31-aa69-76d982e1158d.json create mode 100644 change/@fluentui-react-native-default-theme-2297d5f3-2996-4316-be99-697e74e1797d.json create mode 100644 change/@fluentui-react-native-experimental-activity-indicator-ee25437c-9917-4f3f-8daa-656ed70c9146.json create mode 100644 change/@fluentui-react-native-experimental-avatar-8113fffd-d03a-4ac7-8e4c-244a97664e97.json create mode 100644 change/@fluentui-react-native-experimental-button-0d9b68fd-55f3-4861-b7de-01e99bdabf58.json create mode 100644 change/@fluentui-react-native-experimental-native-button-62fe4948-f964-4f62-aa3d-f4507fc007d7.json create mode 100644 change/@fluentui-react-native-experimental-native-date-picker-a5afb9c3-842d-461c-9138-1366eb008d6f.json create mode 100644 change/@fluentui-react-native-experimental-shimmer-61da57dd-baa5-40ba-b20e-0fb7f7935a61.json create mode 100644 change/@fluentui-react-native-experimental-text-f71ecc30-17f3-4dbf-9ab6-c869faf7636e.json create mode 100644 change/@fluentui-react-native-focus-trap-zone-e5701e0c-1df0-42ed-9068-48db861cee8c.json create mode 100644 change/@fluentui-react-native-focus-zone-791e777d-b9d2-4725-8e1c-8aa43f44396c.json create mode 100644 change/@fluentui-react-native-framework-afb2fbbf-e4d7-4a5f-8626-ff5ed3959c38.json create mode 100644 change/@fluentui-react-native-icon-bc67640e-b05b-476d-bf5c-14b5523f69ad.json create mode 100644 change/@fluentui-react-native-immutable-merge-ced1fad7-3161-49f9-8655-682cc3415d76.json create mode 100644 change/@fluentui-react-native-interactive-hooks-485acb03-240b-4962-9c87-75095b69a017.json create mode 100644 change/@fluentui-react-native-link-856b90c2-1505-4b5d-a9d8-cdba94641e11.json create mode 100644 change/@fluentui-react-native-memo-cache-9abd4cc2-08af-4f14-8ddf-a8ec00be0888.json create mode 100644 change/@fluentui-react-native-menu-button-194aebff-99ae-4265-9f58-06053c8cfa05.json create mode 100644 change/@fluentui-react-native-merge-props-c02f5227-1238-43d2-81bb-b6b7ff75cc1a.json create mode 100644 change/@fluentui-react-native-native-radio-button-be1bd5f8-09e2-45fd-a53d-ba03e4d6a683.json create mode 100644 change/@fluentui-react-native-persona-1bd46d85-ae28-4769-b4fc-77e682a11148.json create mode 100644 change/@fluentui-react-native-persona-coin-0fb8998b-6e13-4eab-a5f5-d4b96a30c6f5.json create mode 100644 change/@fluentui-react-native-pressable-b7b6e32d-5084-4536-9563-44147e3457cf.json create mode 100644 change/@fluentui-react-native-radio-group-34215e0c-d5fc-41fb-ab0c-4bc1d04f2cf1.json create mode 100644 change/@fluentui-react-native-separator-ec248cab-bd88-4f26-95f2-e99cd69f126b.json create mode 100644 change/@fluentui-react-native-stack-c9a3d82c-b085-41de-b59a-ed59cd890f6a.json create mode 100644 change/@fluentui-react-native-tester-1b1b4d96-2b82-4270-9e62-0ea9348f6e37.json create mode 100644 change/@fluentui-react-native-tester-win32-66e279df-0e1a-41b0-8cf6-e3026b9c6cc4.json create mode 100644 change/@fluentui-react-native-text-15b0c91a-57a7-48ee-9e6f-1ff24cb42d93.json create mode 100644 change/@fluentui-react-native-theme-844ff0d1-05fa-43fc-9c06-5135683fcba5.json create mode 100644 change/@fluentui-react-native-theme-tokens-52466e00-c918-42a9-b250-7db74a67e085.json create mode 100644 change/@fluentui-react-native-theme-types-6e96d5b1-49af-400d-a794-422a1d03cfbe.json create mode 100644 change/@fluentui-react-native-theming-utils-b1c21a7c-dbc0-470a-ab3d-f454fddd4786.json create mode 100644 change/@fluentui-react-native-tokens-5154ad0a-3776-4f59-b6cb-7e32c1611720.json create mode 100644 change/@fluentui-react-native-use-slot-35fb9856-7455-41db-8e94-df7ffadaaffd.json create mode 100644 change/@fluentui-react-native-use-slots-bbdce544-8a15-4f5f-a7e6-014c5b6d6b02.json create mode 100644 change/@fluentui-react-native-use-styling-ce6b1998-d638-4395-a347-f915ddc97230.json create mode 100644 change/@fluentui-react-native-use-tokens-c94f2b57-7fa8-4551-9ed6-9c899902061e.json create mode 100644 change/@fluentui-react-native-win32-theme-059dc9ab-1a28-401c-a252-91f2a8130ce1.json create mode 100644 change/@uifabricshared-foundation-composable-79de053c-262c-4c73-ad4c-9e616a5c822f.json create mode 100644 change/@uifabricshared-foundation-compose-a89cf93c-fc39-467c-9a2b-5805ebbf81ac.json create mode 100644 change/@uifabricshared-foundation-settings-1f0a4f6f-b386-4951-9b3c-0c80a7efda0e.json create mode 100644 change/@uifabricshared-foundation-tokens-94ed7a50-1c42-406d-a30c-7c2fd365aa4c.json create mode 100644 change/@uifabricshared-theme-registry-d1c539a8-6fba-48a7-9825-e806d86b0a85.json create mode 100644 change/@uifabricshared-themed-settings-314ebd11-0795-47d5-b31c-de4baac1da81.json create mode 100644 change/@uifabricshared-theming-ramp-5114ef9a-2ecf-4dbf-ac9d-b6227bc1141b.json create mode 100644 change/@uifabricshared-theming-react-native-e9f1dcfe-6dc9-46de-ae28-275a1b6b5bf5.json diff --git a/change/@fluentui-react-native-7d512093-71b2-40bb-8033-4d0baf1137b0.json b/change/@fluentui-react-native-7d512093-71b2-40bb-8033-4d0baf1137b0.json new file mode 100644 index 0000000000..5fdd61d076 --- /dev/null +++ b/change/@fluentui-react-native-7d512093-71b2-40bb-8033-4d0baf1137b0.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui/react-native", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-adapters-90b75eae-223e-4b72-a54a-2369be99ee64.json b/change/@fluentui-react-native-adapters-90b75eae-223e-4b72-a54a-2369be99ee64.json new file mode 100644 index 0000000000..3804c4baa1 --- /dev/null +++ b/change/@fluentui-react-native-adapters-90b75eae-223e-4b72-a54a-2369be99ee64.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/adapters", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-android-theme-b44291fe-369f-4255-8b3b-5fba484d8d2b.json b/change/@fluentui-react-native-android-theme-b44291fe-369f-4255-8b3b-5fba484d8d2b.json new file mode 100644 index 0000000000..633c96580a --- /dev/null +++ b/change/@fluentui-react-native-android-theme-b44291fe-369f-4255-8b3b-5fba484d8d2b.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/android-theme", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-apple-theme-d60319d4-4d6c-4dc7-8342-7418729aa00f.json b/change/@fluentui-react-native-apple-theme-d60319d4-4d6c-4dc7-8342-7418729aa00f.json new file mode 100644 index 0000000000..7b0fb08cd8 --- /dev/null +++ b/change/@fluentui-react-native-apple-theme-d60319d4-4d6c-4dc7-8342-7418729aa00f.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/apple-theme", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-button-8e4544ee-0825-448e-afd4-18559ab5ff0f.json b/change/@fluentui-react-native-button-8e4544ee-0825-448e-afd4-18559ab5ff0f.json new file mode 100644 index 0000000000..5d74660420 --- /dev/null +++ b/change/@fluentui-react-native-button-8e4544ee-0825-448e-afd4-18559ab5ff0f.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/button", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-callout-71df3dd9-2202-4292-91e2-2d62637dd46e.json b/change/@fluentui-react-native-callout-71df3dd9-2202-4292-91e2-2d62637dd46e.json new file mode 100644 index 0000000000..27503b66db --- /dev/null +++ b/change/@fluentui-react-native-callout-71df3dd9-2202-4292-91e2-2d62637dd46e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/callout", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-checkbox-88df73d6-334b-4371-a71b-61efb0ca83ab.json b/change/@fluentui-react-native-checkbox-88df73d6-334b-4371-a71b-61efb0ca83ab.json new file mode 100644 index 0000000000..5b2acda0da --- /dev/null +++ b/change/@fluentui-react-native-checkbox-88df73d6-334b-4371-a71b-61efb0ca83ab.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/checkbox", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-component-cache-5ec33ba1-15cb-41d9-a462-8f92a0290ad2.json b/change/@fluentui-react-native-component-cache-5ec33ba1-15cb-41d9-a462-8f92a0290ad2.json new file mode 100644 index 0000000000..ad75ca248d --- /dev/null +++ b/change/@fluentui-react-native-component-cache-5ec33ba1-15cb-41d9-a462-8f92a0290ad2.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/component-cache", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-composition-8e3c27ee-a0ab-4903-84c4-f59b35da4257.json b/change/@fluentui-react-native-composition-8e3c27ee-a0ab-4903-84c4-f59b35da4257.json new file mode 100644 index 0000000000..d773440cdb --- /dev/null +++ b/change/@fluentui-react-native-composition-8e3c27ee-a0ab-4903-84c4-f59b35da4257.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/composition", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-contextual-menu-0da401b3-8398-4e31-aa69-76d982e1158d.json b/change/@fluentui-react-native-contextual-menu-0da401b3-8398-4e31-aa69-76d982e1158d.json new file mode 100644 index 0000000000..ebc8b42613 --- /dev/null +++ b/change/@fluentui-react-native-contextual-menu-0da401b3-8398-4e31-aa69-76d982e1158d.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/contextual-menu", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-default-theme-2297d5f3-2996-4316-be99-697e74e1797d.json b/change/@fluentui-react-native-default-theme-2297d5f3-2996-4316-be99-697e74e1797d.json new file mode 100644 index 0000000000..4413ed2ce4 --- /dev/null +++ b/change/@fluentui-react-native-default-theme-2297d5f3-2996-4316-be99-697e74e1797d.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/default-theme", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-experimental-activity-indicator-ee25437c-9917-4f3f-8daa-656ed70c9146.json b/change/@fluentui-react-native-experimental-activity-indicator-ee25437c-9917-4f3f-8daa-656ed70c9146.json new file mode 100644 index 0000000000..89e2e50278 --- /dev/null +++ b/change/@fluentui-react-native-experimental-activity-indicator-ee25437c-9917-4f3f-8daa-656ed70c9146.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/experimental-activity-indicator", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-experimental-avatar-8113fffd-d03a-4ac7-8e4c-244a97664e97.json b/change/@fluentui-react-native-experimental-avatar-8113fffd-d03a-4ac7-8e4c-244a97664e97.json new file mode 100644 index 0000000000..91963cab0e --- /dev/null +++ b/change/@fluentui-react-native-experimental-avatar-8113fffd-d03a-4ac7-8e4c-244a97664e97.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/experimental-avatar", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-experimental-button-0d9b68fd-55f3-4861-b7de-01e99bdabf58.json b/change/@fluentui-react-native-experimental-button-0d9b68fd-55f3-4861-b7de-01e99bdabf58.json new file mode 100644 index 0000000000..f9f04f2a05 --- /dev/null +++ b/change/@fluentui-react-native-experimental-button-0d9b68fd-55f3-4861-b7de-01e99bdabf58.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/experimental-button", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-experimental-native-button-62fe4948-f964-4f62-aa3d-f4507fc007d7.json b/change/@fluentui-react-native-experimental-native-button-62fe4948-f964-4f62-aa3d-f4507fc007d7.json new file mode 100644 index 0000000000..e37df433d2 --- /dev/null +++ b/change/@fluentui-react-native-experimental-native-button-62fe4948-f964-4f62-aa3d-f4507fc007d7.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/experimental-native-button", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-experimental-native-date-picker-a5afb9c3-842d-461c-9138-1366eb008d6f.json b/change/@fluentui-react-native-experimental-native-date-picker-a5afb9c3-842d-461c-9138-1366eb008d6f.json new file mode 100644 index 0000000000..8b16889c52 --- /dev/null +++ b/change/@fluentui-react-native-experimental-native-date-picker-a5afb9c3-842d-461c-9138-1366eb008d6f.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/experimental-native-date-picker", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-experimental-shimmer-61da57dd-baa5-40ba-b20e-0fb7f7935a61.json b/change/@fluentui-react-native-experimental-shimmer-61da57dd-baa5-40ba-b20e-0fb7f7935a61.json new file mode 100644 index 0000000000..1c3b9cf740 --- /dev/null +++ b/change/@fluentui-react-native-experimental-shimmer-61da57dd-baa5-40ba-b20e-0fb7f7935a61.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/experimental-shimmer", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-experimental-text-f71ecc30-17f3-4dbf-9ab6-c869faf7636e.json b/change/@fluentui-react-native-experimental-text-f71ecc30-17f3-4dbf-9ab6-c869faf7636e.json new file mode 100644 index 0000000000..cd4b93097a --- /dev/null +++ b/change/@fluentui-react-native-experimental-text-f71ecc30-17f3-4dbf-9ab6-c869faf7636e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/experimental-text", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-focus-trap-zone-e5701e0c-1df0-42ed-9068-48db861cee8c.json b/change/@fluentui-react-native-focus-trap-zone-e5701e0c-1df0-42ed-9068-48db861cee8c.json new file mode 100644 index 0000000000..d865eabd4b --- /dev/null +++ b/change/@fluentui-react-native-focus-trap-zone-e5701e0c-1df0-42ed-9068-48db861cee8c.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/focus-trap-zone", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-focus-zone-791e777d-b9d2-4725-8e1c-8aa43f44396c.json b/change/@fluentui-react-native-focus-zone-791e777d-b9d2-4725-8e1c-8aa43f44396c.json new file mode 100644 index 0000000000..beb01cc6dd --- /dev/null +++ b/change/@fluentui-react-native-focus-zone-791e777d-b9d2-4725-8e1c-8aa43f44396c.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/focus-zone", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-framework-afb2fbbf-e4d7-4a5f-8626-ff5ed3959c38.json b/change/@fluentui-react-native-framework-afb2fbbf-e4d7-4a5f-8626-ff5ed3959c38.json new file mode 100644 index 0000000000..d4027fdb61 --- /dev/null +++ b/change/@fluentui-react-native-framework-afb2fbbf-e4d7-4a5f-8626-ff5ed3959c38.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/framework", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-icon-bc67640e-b05b-476d-bf5c-14b5523f69ad.json b/change/@fluentui-react-native-icon-bc67640e-b05b-476d-bf5c-14b5523f69ad.json new file mode 100644 index 0000000000..9af50827bd --- /dev/null +++ b/change/@fluentui-react-native-icon-bc67640e-b05b-476d-bf5c-14b5523f69ad.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/icon", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-immutable-merge-ced1fad7-3161-49f9-8655-682cc3415d76.json b/change/@fluentui-react-native-immutable-merge-ced1fad7-3161-49f9-8655-682cc3415d76.json new file mode 100644 index 0000000000..e24a53f871 --- /dev/null +++ b/change/@fluentui-react-native-immutable-merge-ced1fad7-3161-49f9-8655-682cc3415d76.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/immutable-merge", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-interactive-hooks-485acb03-240b-4962-9c87-75095b69a017.json b/change/@fluentui-react-native-interactive-hooks-485acb03-240b-4962-9c87-75095b69a017.json new file mode 100644 index 0000000000..1891a1bf34 --- /dev/null +++ b/change/@fluentui-react-native-interactive-hooks-485acb03-240b-4962-9c87-75095b69a017.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/interactive-hooks", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-link-856b90c2-1505-4b5d-a9d8-cdba94641e11.json b/change/@fluentui-react-native-link-856b90c2-1505-4b5d-a9d8-cdba94641e11.json new file mode 100644 index 0000000000..492ac123b6 --- /dev/null +++ b/change/@fluentui-react-native-link-856b90c2-1505-4b5d-a9d8-cdba94641e11.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/link", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-memo-cache-9abd4cc2-08af-4f14-8ddf-a8ec00be0888.json b/change/@fluentui-react-native-memo-cache-9abd4cc2-08af-4f14-8ddf-a8ec00be0888.json new file mode 100644 index 0000000000..8fc3e6c2b8 --- /dev/null +++ b/change/@fluentui-react-native-memo-cache-9abd4cc2-08af-4f14-8ddf-a8ec00be0888.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/memo-cache", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-menu-button-194aebff-99ae-4265-9f58-06053c8cfa05.json b/change/@fluentui-react-native-menu-button-194aebff-99ae-4265-9f58-06053c8cfa05.json new file mode 100644 index 0000000000..5f4b6505cf --- /dev/null +++ b/change/@fluentui-react-native-menu-button-194aebff-99ae-4265-9f58-06053c8cfa05.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/menu-button", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-merge-props-c02f5227-1238-43d2-81bb-b6b7ff75cc1a.json b/change/@fluentui-react-native-merge-props-c02f5227-1238-43d2-81bb-b6b7ff75cc1a.json new file mode 100644 index 0000000000..6f3d75e5c9 --- /dev/null +++ b/change/@fluentui-react-native-merge-props-c02f5227-1238-43d2-81bb-b6b7ff75cc1a.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/merge-props", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-native-radio-button-be1bd5f8-09e2-45fd-a53d-ba03e4d6a683.json b/change/@fluentui-react-native-native-radio-button-be1bd5f8-09e2-45fd-a53d-ba03e4d6a683.json new file mode 100644 index 0000000000..4efcbbe2af --- /dev/null +++ b/change/@fluentui-react-native-native-radio-button-be1bd5f8-09e2-45fd-a53d-ba03e4d6a683.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/native-radio-button", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-persona-1bd46d85-ae28-4769-b4fc-77e682a11148.json b/change/@fluentui-react-native-persona-1bd46d85-ae28-4769-b4fc-77e682a11148.json new file mode 100644 index 0000000000..6523ceba76 --- /dev/null +++ b/change/@fluentui-react-native-persona-1bd46d85-ae28-4769-b4fc-77e682a11148.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/persona", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-persona-coin-0fb8998b-6e13-4eab-a5f5-d4b96a30c6f5.json b/change/@fluentui-react-native-persona-coin-0fb8998b-6e13-4eab-a5f5-d4b96a30c6f5.json new file mode 100644 index 0000000000..ee10424ef6 --- /dev/null +++ b/change/@fluentui-react-native-persona-coin-0fb8998b-6e13-4eab-a5f5-d4b96a30c6f5.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/persona-coin", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-pressable-b7b6e32d-5084-4536-9563-44147e3457cf.json b/change/@fluentui-react-native-pressable-b7b6e32d-5084-4536-9563-44147e3457cf.json new file mode 100644 index 0000000000..5d9ade5e05 --- /dev/null +++ b/change/@fluentui-react-native-pressable-b7b6e32d-5084-4536-9563-44147e3457cf.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/pressable", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-radio-group-34215e0c-d5fc-41fb-ab0c-4bc1d04f2cf1.json b/change/@fluentui-react-native-radio-group-34215e0c-d5fc-41fb-ab0c-4bc1d04f2cf1.json new file mode 100644 index 0000000000..afc89b9aaa --- /dev/null +++ b/change/@fluentui-react-native-radio-group-34215e0c-d5fc-41fb-ab0c-4bc1d04f2cf1.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/radio-group", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-separator-ec248cab-bd88-4f26-95f2-e99cd69f126b.json b/change/@fluentui-react-native-separator-ec248cab-bd88-4f26-95f2-e99cd69f126b.json new file mode 100644 index 0000000000..40bb9a6210 --- /dev/null +++ b/change/@fluentui-react-native-separator-ec248cab-bd88-4f26-95f2-e99cd69f126b.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/separator", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-stack-c9a3d82c-b085-41de-b59a-ed59cd890f6a.json b/change/@fluentui-react-native-stack-c9a3d82c-b085-41de-b59a-ed59cd890f6a.json new file mode 100644 index 0000000000..00718fa834 --- /dev/null +++ b/change/@fluentui-react-native-stack-c9a3d82c-b085-41de-b59a-ed59cd890f6a.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/stack", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-tester-1b1b4d96-2b82-4270-9e62-0ea9348f6e37.json b/change/@fluentui-react-native-tester-1b1b4d96-2b82-4270-9e62-0ea9348f6e37.json new file mode 100644 index 0000000000..2d60a97343 --- /dev/null +++ b/change/@fluentui-react-native-tester-1b1b4d96-2b82-4270-9e62-0ea9348f6e37.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/tester", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-tester-win32-66e279df-0e1a-41b0-8cf6-e3026b9c6cc4.json b/change/@fluentui-react-native-tester-win32-66e279df-0e1a-41b0-8cf6-e3026b9c6cc4.json new file mode 100644 index 0000000000..5618f28d21 --- /dev/null +++ b/change/@fluentui-react-native-tester-win32-66e279df-0e1a-41b0-8cf6-e3026b9c6cc4.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/tester-win32", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-text-15b0c91a-57a7-48ee-9e6f-1ff24cb42d93.json b/change/@fluentui-react-native-text-15b0c91a-57a7-48ee-9e6f-1ff24cb42d93.json new file mode 100644 index 0000000000..850457abe5 --- /dev/null +++ b/change/@fluentui-react-native-text-15b0c91a-57a7-48ee-9e6f-1ff24cb42d93.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/text", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-theme-844ff0d1-05fa-43fc-9c06-5135683fcba5.json b/change/@fluentui-react-native-theme-844ff0d1-05fa-43fc-9c06-5135683fcba5.json new file mode 100644 index 0000000000..35f60c794f --- /dev/null +++ b/change/@fluentui-react-native-theme-844ff0d1-05fa-43fc-9c06-5135683fcba5.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/theme", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-theme-tokens-52466e00-c918-42a9-b250-7db74a67e085.json b/change/@fluentui-react-native-theme-tokens-52466e00-c918-42a9-b250-7db74a67e085.json new file mode 100644 index 0000000000..c1d32fd472 --- /dev/null +++ b/change/@fluentui-react-native-theme-tokens-52466e00-c918-42a9-b250-7db74a67e085.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/theme-tokens", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-theme-types-6e96d5b1-49af-400d-a794-422a1d03cfbe.json b/change/@fluentui-react-native-theme-types-6e96d5b1-49af-400d-a794-422a1d03cfbe.json new file mode 100644 index 0000000000..f71d45b9bf --- /dev/null +++ b/change/@fluentui-react-native-theme-types-6e96d5b1-49af-400d-a794-422a1d03cfbe.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/theme-types", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-theming-utils-b1c21a7c-dbc0-470a-ab3d-f454fddd4786.json b/change/@fluentui-react-native-theming-utils-b1c21a7c-dbc0-470a-ab3d-f454fddd4786.json new file mode 100644 index 0000000000..17f08775fc --- /dev/null +++ b/change/@fluentui-react-native-theming-utils-b1c21a7c-dbc0-470a-ab3d-f454fddd4786.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/theming-utils", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-tokens-5154ad0a-3776-4f59-b6cb-7e32c1611720.json b/change/@fluentui-react-native-tokens-5154ad0a-3776-4f59-b6cb-7e32c1611720.json new file mode 100644 index 0000000000..ec6fd4ac74 --- /dev/null +++ b/change/@fluentui-react-native-tokens-5154ad0a-3776-4f59-b6cb-7e32c1611720.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/tokens", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-use-slot-35fb9856-7455-41db-8e94-df7ffadaaffd.json b/change/@fluentui-react-native-use-slot-35fb9856-7455-41db-8e94-df7ffadaaffd.json new file mode 100644 index 0000000000..06c500e7bb --- /dev/null +++ b/change/@fluentui-react-native-use-slot-35fb9856-7455-41db-8e94-df7ffadaaffd.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/use-slot", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-use-slots-bbdce544-8a15-4f5f-a7e6-014c5b6d6b02.json b/change/@fluentui-react-native-use-slots-bbdce544-8a15-4f5f-a7e6-014c5b6d6b02.json new file mode 100644 index 0000000000..b9097df7dc --- /dev/null +++ b/change/@fluentui-react-native-use-slots-bbdce544-8a15-4f5f-a7e6-014c5b6d6b02.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/use-slots", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-use-styling-ce6b1998-d638-4395-a347-f915ddc97230.json b/change/@fluentui-react-native-use-styling-ce6b1998-d638-4395-a347-f915ddc97230.json new file mode 100644 index 0000000000..cedae12601 --- /dev/null +++ b/change/@fluentui-react-native-use-styling-ce6b1998-d638-4395-a347-f915ddc97230.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/use-styling", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-use-tokens-c94f2b57-7fa8-4551-9ed6-9c899902061e.json b/change/@fluentui-react-native-use-tokens-c94f2b57-7fa8-4551-9ed6-9c899902061e.json new file mode 100644 index 0000000000..7b3de175d7 --- /dev/null +++ b/change/@fluentui-react-native-use-tokens-c94f2b57-7fa8-4551-9ed6-9c899902061e.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/use-tokens", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-win32-theme-059dc9ab-1a28-401c-a252-91f2a8130ce1.json b/change/@fluentui-react-native-win32-theme-059dc9ab-1a28-401c-a252-91f2a8130ce1.json new file mode 100644 index 0000000000..8098bb094d --- /dev/null +++ b/change/@fluentui-react-native-win32-theme-059dc9ab-1a28-401c-a252-91f2a8130ce1.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@fluentui-react-native/win32-theme", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@uifabricshared-foundation-composable-79de053c-262c-4c73-ad4c-9e616a5c822f.json b/change/@uifabricshared-foundation-composable-79de053c-262c-4c73-ad4c-9e616a5c822f.json new file mode 100644 index 0000000000..a2e9258e85 --- /dev/null +++ b/change/@uifabricshared-foundation-composable-79de053c-262c-4c73-ad4c-9e616a5c822f.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@uifabricshared/foundation-composable", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@uifabricshared-foundation-compose-a89cf93c-fc39-467c-9a2b-5805ebbf81ac.json b/change/@uifabricshared-foundation-compose-a89cf93c-fc39-467c-9a2b-5805ebbf81ac.json new file mode 100644 index 0000000000..751ed7241a --- /dev/null +++ b/change/@uifabricshared-foundation-compose-a89cf93c-fc39-467c-9a2b-5805ebbf81ac.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@uifabricshared/foundation-compose", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@uifabricshared-foundation-settings-1f0a4f6f-b386-4951-9b3c-0c80a7efda0e.json b/change/@uifabricshared-foundation-settings-1f0a4f6f-b386-4951-9b3c-0c80a7efda0e.json new file mode 100644 index 0000000000..059cf60929 --- /dev/null +++ b/change/@uifabricshared-foundation-settings-1f0a4f6f-b386-4951-9b3c-0c80a7efda0e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@uifabricshared/foundation-settings", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@uifabricshared-foundation-tokens-94ed7a50-1c42-406d-a30c-7c2fd365aa4c.json b/change/@uifabricshared-foundation-tokens-94ed7a50-1c42-406d-a30c-7c2fd365aa4c.json new file mode 100644 index 0000000000..d7b0ff6e6b --- /dev/null +++ b/change/@uifabricshared-foundation-tokens-94ed7a50-1c42-406d-a30c-7c2fd365aa4c.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@uifabricshared/foundation-tokens", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@uifabricshared-theme-registry-d1c539a8-6fba-48a7-9825-e806d86b0a85.json b/change/@uifabricshared-theme-registry-d1c539a8-6fba-48a7-9825-e806d86b0a85.json new file mode 100644 index 0000000000..d886ab3eeb --- /dev/null +++ b/change/@uifabricshared-theme-registry-d1c539a8-6fba-48a7-9825-e806d86b0a85.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@uifabricshared/theme-registry", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@uifabricshared-themed-settings-314ebd11-0795-47d5-b31c-de4baac1da81.json b/change/@uifabricshared-themed-settings-314ebd11-0795-47d5-b31c-de4baac1da81.json new file mode 100644 index 0000000000..15078cf10e --- /dev/null +++ b/change/@uifabricshared-themed-settings-314ebd11-0795-47d5-b31c-de4baac1da81.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@uifabricshared/themed-settings", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@uifabricshared-theming-ramp-5114ef9a-2ecf-4dbf-ac9d-b6227bc1141b.json b/change/@uifabricshared-theming-ramp-5114ef9a-2ecf-4dbf-ac9d-b6227bc1141b.json new file mode 100644 index 0000000000..9337587dce --- /dev/null +++ b/change/@uifabricshared-theming-ramp-5114ef9a-2ecf-4dbf-ac9d-b6227bc1141b.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@uifabricshared/theming-ramp", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@uifabricshared-theming-react-native-e9f1dcfe-6dc9-46de-ae28-275a1b6b5bf5.json b/change/@uifabricshared-theming-react-native-e9f1dcfe-6dc9-46de-ae28-275a1b6b5bf5.json new file mode 100644 index 0000000000..29ab998846 --- /dev/null +++ b/change/@uifabricshared-theming-react-native-e9f1dcfe-6dc9-46de-ae28-275a1b6b5bf5.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "enable usePressableState with stock Pressable component", + "packageName": "@uifabricshared/theming-react-native", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} From 42ea4d67f01b9d805277dc09411684c902f52c0c Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Fri, 6 Aug 2021 11:50:25 -0700 Subject: [PATCH 3/5] fix spacing issues in Pressability.ts --- .../src/Pressability/Pressability.ts | 1282 ++++++++--------- 1 file changed, 641 insertions(+), 641 deletions(-) diff --git a/packages/utils/interactive-hooks/src/Pressability/Pressability.ts b/packages/utils/interactive-hooks/src/Pressability/Pressability.ts index 66a4d3da69..d1568dff95 100644 --- a/packages/utils/interactive-hooks/src/Pressability/Pressability.ts +++ b/packages/utils/interactive-hooks/src/Pressability/Pressability.ts @@ -8,645 +8,645 @@ * @format */ - 'use strict'; - - import invariant from 'invariant'; - import * as React from 'react'; - import { Platform, UIManager } from 'react-native'; - import { BlurEvent, FocusEvent, MouseEvent, PressEvent } from './CoreEventTypes'; - import { isHoverEnabled } from './HoverState'; - import { HostComponent, normalizeRect, Rect } from './InternalTypes'; - import { PressabilityConfig, PressabilityEventHandlers } from './Pressability.types'; - - 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: { [K in TouchState]: { [T in TouchSignal]: TouchState } } = { - 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 = 370; // 500 - 130 - const DEFAULT_PRESS_DELAY_MS = 130; - const DEFAULT_PRESS_RECT_OFFSETS: Rect = { - bottom: 30, - left: 20, - right: 20, - top: 20, - }; - - function normalizeDelay(delay?: number, min: number = 0, fallback: number = 0): number { - return Math.max(min, delay ?? 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; - }; - - /** - * 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 `hitSlop`. - * │ │ │ 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 - * `pressRectOffset`, 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. +'use strict'; + +import invariant from 'invariant'; +import * as React from 'react'; +import { Platform, UIManager } from 'react-native'; +import { BlurEvent, FocusEvent, MouseEvent, PressEvent } from './CoreEventTypes'; +import { isHoverEnabled } from './HoverState'; +import { HostComponent, normalizeRect, Rect } from './InternalTypes'; +import { PressabilityConfig, PressabilityEventHandlers } from './Pressability.types'; + +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: { [K in TouchState]: { [T in TouchSignal]: TouchState } } = { + 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 = 370; // 500 - 130 +const DEFAULT_PRESS_DELAY_MS = 130; +const DEFAULT_PRESS_RECT_OFFSETS: Rect = { + bottom: 30, + left: 20, + right: 20, + top: 20, +}; + +function normalizeDelay(delay?: number, min: number = 0, fallback: number = 0): number { + return Math.max(min, delay ?? 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; +}; + +/** +* 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 `hitSlop`. +* │ │ │ 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 +* `pressRectOffset`, 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 class Pressability { + private _config: PressabilityConfig; + private _eventHandlers: PressabilityEventHandlers = null; + private _hoverInDelayTimeout: any /* TimeoutID */ = null; + private _hoverOutDelayTimeout: any /* TimeoutID */ = null; + private _isHovered: boolean = false; + private _longPressDelayTimeout: any /* TimeoutID */ = null; + private _pressDelayTimeout: any /* TimeoutID */ = null; + private _pressOutDelayTimeout: any /* TimeoutID */ = null; + private _responderID: number | React.ElementRef> = null; + private _responderRegion: Rect = null; + private _touchActivatePosition: Readonly<{ + pageX: number; + pageY: number; + }>; + private _touchState: TouchState = 'NOT_RESPONDER'; + + constructor(config: PressabilityConfig) { + this.configure(config); + } + + public configure(config: PressabilityConfig): void { + this._config = config; + } + + /** + * Resets any pending timers. This should be called on unmount. + */ + public reset(): void { + this._cancelHoverInDelayTimeout(); + this._cancelHoverOutDelayTimeout(); + this._cancelLongPressDelayTimeout(); + this._cancelPressDelayTimeout(); + this._cancelPressOutDelayTimeout(); + } + + /** + * Returns a set of props to spread into the interactive element. + */ + public getEventHandlers(): PressabilityEventHandlers { + if (this._eventHandlers == null) { + this._eventHandlers = this._createEventHandlers(); + } + return this._eventHandlers; + } + + private _createEventHandlers(): PressabilityEventHandlers { + 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: (): boolean => { + const { disabled } = this._config; + if (disabled == null) { + return true; + } + return !disabled; + }, + + onResponderGrant: (event: PressEvent): void => { + event.persist(); + + this._cancelPressOutDelayTimeout(); + + this._responderID = event.currentTarget; + this._touchState = 'NOT_RESPONDER'; + this._receiveSignal('RESPONDER_GRANT', event); + + const delayPressIn = normalizeDelay(this._config.delayPressIn, 0, DEFAULT_PRESS_DELAY_MS); + + if (delayPressIn > 0) { + this._pressDelayTimeout = setTimeout(() => { + this._receiveSignal('DELAY', event); + }, delayPressIn); + } else { + this._receiveSignal('DELAY', event); + } + + const delayLongPress = normalizeDelay(this._config.delayLongPress, 10, DEFAULT_LONG_PRESS_DELAY_MS); + this._longPressDelayTimeout = setTimeout(() => { + this._handleLongPress(event); + }, delayLongPress + delayPressIn); + }, + + 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 { cancelable } = this._config; + return cancelable || true; + }, + + onClick: (event): void => { + 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 } = this._config; + if (onHoverIn != null) { + const delayHoverIn = normalizeDelay(this._config.delayHoverIn); + if (delayHoverIn > 0) { + this._hoverInDelayTimeout = setTimeout(() => { + onHoverIn(event); + }, delayHoverIn); + } else { + onHoverIn(event); + } + } + } + }, + + onMouseLeave: (event: MouseEvent): void => { + if (this._isHovered) { + this._isHovered = false; + this._cancelHoverInDelayTimeout(); + const { onHoverOut } = this._config; + if (onHoverOut != null) { + const delayHoverOut = normalizeDelay(this._config.delayHoverOut); + if (delayHoverOut > 0) { + this._hoverInDelayTimeout = setTimeout(() => { + onHoverOut(event); + }, delayHoverOut); + } else { + onHoverOut(event); + } + } + } + }, + }; + + return { + ...focusEventHandlers, + ...responderEventHandlers, + ...mouseEventHandlers, + }; + } + + /** + * Receives a state machine signal, performs side effects of the transition + * and stores the new state. Validates the transition as well. + */ + private _receiveSignal(signal: TouchSignal, event: PressEvent): void { + const prevState = this._touchState; + const nextState: TouchState = 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). */ - export class Pressability { - private _config: PressabilityConfig; - private _eventHandlers: PressabilityEventHandlers = null; - private _hoverInDelayTimeout: any /* TimeoutID */ = null; - private _hoverOutDelayTimeout: any /* TimeoutID */ = null; - private _isHovered: boolean = false; - private _longPressDelayTimeout: any /* TimeoutID */ = null; - private _pressDelayTimeout: any /* TimeoutID */ = null; - private _pressOutDelayTimeout: any /* TimeoutID */ = null; - private _responderID: number | React.ElementRef> = null; - private _responderRegion: Rect = null; - private _touchActivatePosition: Readonly<{ - pageX: number; - pageY: number; - }>; - private _touchState: TouchState = 'NOT_RESPONDER'; - - constructor(config: PressabilityConfig) { - this.configure(config); - } - - public configure(config: PressabilityConfig): void { - this._config = config; - } - - /** - * Resets any pending timers. This should be called on unmount. - */ - public reset(): void { - this._cancelHoverInDelayTimeout(); - this._cancelHoverOutDelayTimeout(); - this._cancelLongPressDelayTimeout(); - this._cancelPressDelayTimeout(); - this._cancelPressOutDelayTimeout(); - } - - /** - * Returns a set of props to spread into the interactive element. - */ - public getEventHandlers(): PressabilityEventHandlers { - if (this._eventHandlers == null) { - this._eventHandlers = this._createEventHandlers(); - } - return this._eventHandlers; - } - - private _createEventHandlers(): PressabilityEventHandlers { - 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: (): boolean => { - const { disabled } = this._config; - if (disabled == null) { - return true; - } - return !disabled; - }, - - onResponderGrant: (event: PressEvent): void => { - event.persist(); - - this._cancelPressOutDelayTimeout(); - - this._responderID = event.currentTarget; - this._touchState = 'NOT_RESPONDER'; - this._receiveSignal('RESPONDER_GRANT', event); - - const delayPressIn = normalizeDelay(this._config.delayPressIn, 0, DEFAULT_PRESS_DELAY_MS); - - if (delayPressIn > 0) { - this._pressDelayTimeout = setTimeout(() => { - this._receiveSignal('DELAY', event); - }, delayPressIn); - } else { - this._receiveSignal('DELAY', event); - } - - const delayLongPress = normalizeDelay(this._config.delayLongPress, 10, DEFAULT_LONG_PRESS_DELAY_MS); - this._longPressDelayTimeout = setTimeout(() => { - this._handleLongPress(event); - }, delayLongPress + delayPressIn); - }, - - 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 { cancelable } = this._config; - return cancelable || true; - }, - - onClick: (event): void => { - 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 } = this._config; - if (onHoverIn != null) { - const delayHoverIn = normalizeDelay(this._config.delayHoverIn); - if (delayHoverIn > 0) { - this._hoverInDelayTimeout = setTimeout(() => { - onHoverIn(event); - }, delayHoverIn); - } else { - onHoverIn(event); - } - } - } - }, - - onMouseLeave: (event: MouseEvent): void => { - if (this._isHovered) { - this._isHovered = false; - this._cancelHoverInDelayTimeout(); - const { onHoverOut } = this._config; - if (onHoverOut != null) { - const delayHoverOut = normalizeDelay(this._config.delayHoverOut); - if (delayHoverOut > 0) { - this._hoverInDelayTimeout = setTimeout(() => { - onHoverOut(event); - }, delayHoverOut); - } else { - onHoverOut(event); - } - } - } - }, - }; - - return { - ...focusEventHandlers, - ...responderEventHandlers, - ...mouseEventHandlers, - }; - } - - /** - * Receives a state machine signal, performs side effects of the transition - * and stores the new state. Validates the transition as well. - */ - private _receiveSignal(signal: TouchSignal, event: PressEvent): void { - const prevState = this._touchState; - const nextState: TouchState = 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). - */ - private _performTransitionSideEffects(prevState: TouchState, nextState: TouchState, signal: TouchSignal, event): 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 /*, android_disableSound */ } = 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); - } - /* - if (Platform.OS === 'android' && android_disableSound !== true) { - SoundManager.playTouchSound(); - } - */ - onPress(event); - } - } - } - - this._cancelPressDelayTimeout(); - } - - private _activate(event): void { - const { onPressIn } = this._config; - const touch = getTouchFromPressEvent(event); - this._touchActivatePosition = { - pageX: touch.pageX, - pageY: touch.pageY, - }; - if (onPressIn != null) { - onPressIn(event); - } - } - - private _deactivate(event): void { - const { onPressOut } = this._config; - if (onPressOut != null) { - const delayPressOut = normalizeDelay(this._config.delayPressOut); - if (delayPressOut > 0) { - this._pressOutDelayTimeout = setTimeout(() => { - onPressOut(event); - }, delayPressOut); - } else { - onPressOut(event); - } - } - } - - private _measureResponderRegion(): void { - if (this._responderID == null) { - return; - } - - if (typeof this._responderID === 'number') { - UIManager.measure(this._responderID, this._measureCallback); - } else { - const measure = (this as any)?._responderID?.measure; - - if (typeof measure === 'function' && this._measureCallback) { - (this as any)?._responderID?.measure(this._measureCallback); - } - } - } - - private _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, - }; - }; - - private _isTouchWithinResponderRegion(touch: any /* PropertyType */, responderRegion: Rect): boolean { - const hitSlop = normalizeRect(this._config.hitSlop); - const pressRectOffset = normalizeRect(this._config.pressRectOffset); - - let regionBottom = responderRegion.bottom; - let regionLeft = responderRegion.left; - let regionRight = responderRegion.right; - let regionTop = responderRegion.top; - - 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; - } - } - - regionBottom += pressRectOffset?.bottom ?? DEFAULT_PRESS_RECT_OFFSETS.bottom; - regionLeft -= pressRectOffset?.left ?? DEFAULT_PRESS_RECT_OFFSETS.left; - regionRight += pressRectOffset?.right ?? DEFAULT_PRESS_RECT_OFFSETS.right; - regionTop -= pressRectOffset?.top ?? DEFAULT_PRESS_RECT_OFFSETS.top; - - return touch.pageX > regionLeft && touch.pageX < regionRight && touch.pageY > regionTop && touch.pageY < regionBottom; - } - - private _handleLongPress(event: PressEvent): void { - if (this._touchState === 'RESPONDER_ACTIVE_PRESS_IN' || this._touchState === 'RESPONDER_ACTIVE_LONG_PRESS_IN') { - this._receiveSignal('LONG_PRESS_DETECTED', event); - } - } - - private _shouldLongPressCancelPress(): boolean { - return true; - } - - private _cancelHoverInDelayTimeout(): void { - if (this._hoverInDelayTimeout != null) { - clearTimeout(this._hoverInDelayTimeout); - this._hoverInDelayTimeout = null; - } - } - - private _cancelHoverOutDelayTimeout(): void { - if (this._hoverOutDelayTimeout != null) { - clearTimeout(this._hoverOutDelayTimeout); - this._hoverOutDelayTimeout = null; - } - } - - private _cancelLongPressDelayTimeout(): void { - if (this._longPressDelayTimeout != null) { - clearTimeout(this._longPressDelayTimeout); - this._longPressDelayTimeout = null; - } - } - - private _cancelPressDelayTimeout(): void { - if (this._pressDelayTimeout != null) { - clearTimeout(this._pressDelayTimeout); - this._pressDelayTimeout = null; - } - } - - private _cancelPressOutDelayTimeout(): void { - if (this._pressOutDelayTimeout != null) { - clearTimeout(this._pressOutDelayTimeout); - this._pressOutDelayTimeout = null; - } - } - } + private _performTransitionSideEffects(prevState: TouchState, nextState: TouchState, signal: TouchSignal, event): 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 /*, android_disableSound */ } = 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); + } + /* + if (Platform.OS === 'android' && android_disableSound !== true) { + SoundManager.playTouchSound(); + } + */ + onPress(event); + } + } + } + + this._cancelPressDelayTimeout(); + } + + private _activate(event): void { + const { onPressIn } = this._config; + const touch = getTouchFromPressEvent(event); + this._touchActivatePosition = { + pageX: touch.pageX, + pageY: touch.pageY, + }; + if (onPressIn != null) { + onPressIn(event); + } + } + + private _deactivate(event): void { + const { onPressOut } = this._config; + if (onPressOut != null) { + const delayPressOut = normalizeDelay(this._config.delayPressOut); + if (delayPressOut > 0) { + this._pressOutDelayTimeout = setTimeout(() => { + onPressOut(event); + }, delayPressOut); + } else { + onPressOut(event); + } + } + } + + private _measureResponderRegion(): void { + if (this._responderID == null) { + return; + } + + if (typeof this._responderID === 'number') { + UIManager.measure(this._responderID, this._measureCallback); + } else { + const measure = (this as any)?._responderID?.measure; + + if (typeof measure === 'function' && this._measureCallback) { + (this as any)?._responderID?.measure(this._measureCallback); + } + } + } + + private _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, + }; + }; + + private _isTouchWithinResponderRegion(touch: any /* PropertyType */, responderRegion: Rect): boolean { + const hitSlop = normalizeRect(this._config.hitSlop); + const pressRectOffset = normalizeRect(this._config.pressRectOffset); + + let regionBottom = responderRegion.bottom; + let regionLeft = responderRegion.left; + let regionRight = responderRegion.right; + let regionTop = responderRegion.top; + + 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; + } + } + + regionBottom += pressRectOffset?.bottom ?? DEFAULT_PRESS_RECT_OFFSETS.bottom; + regionLeft -= pressRectOffset?.left ?? DEFAULT_PRESS_RECT_OFFSETS.left; + regionRight += pressRectOffset?.right ?? DEFAULT_PRESS_RECT_OFFSETS.right; + regionTop -= pressRectOffset?.top ?? DEFAULT_PRESS_RECT_OFFSETS.top; + + return touch.pageX > regionLeft && touch.pageX < regionRight && touch.pageY > regionTop && touch.pageY < regionBottom; + } + + private _handleLongPress(event: PressEvent): void { + if (this._touchState === 'RESPONDER_ACTIVE_PRESS_IN' || this._touchState === 'RESPONDER_ACTIVE_LONG_PRESS_IN') { + this._receiveSignal('LONG_PRESS_DETECTED', event); + } + } + + private _shouldLongPressCancelPress(): boolean { + return true; + } + + private _cancelHoverInDelayTimeout(): void { + if (this._hoverInDelayTimeout != null) { + clearTimeout(this._hoverInDelayTimeout); + this._hoverInDelayTimeout = null; + } + } + + private _cancelHoverOutDelayTimeout(): void { + if (this._hoverOutDelayTimeout != null) { + clearTimeout(this._hoverOutDelayTimeout); + this._hoverOutDelayTimeout = null; + } + } + + private _cancelLongPressDelayTimeout(): void { + if (this._longPressDelayTimeout != null) { + clearTimeout(this._longPressDelayTimeout); + this._longPressDelayTimeout = null; + } + } + + private _cancelPressDelayTimeout(): void { + if (this._pressDelayTimeout != null) { + clearTimeout(this._pressDelayTimeout); + this._pressDelayTimeout = null; + } + } + + private _cancelPressOutDelayTimeout(): void { + if (this._pressOutDelayTimeout != null) { + clearTimeout(this._pressOutDelayTimeout); + this._pressOutDelayTimeout = null; + } + } +} From d3a82c0497d566dbcfb8e5a904715e548295ad2d Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Fri, 6 Aug 2021 11:53:31 -0700 Subject: [PATCH 4/5] more spacing fixes --- .../src/Pressability/Pressability.ts | 204 +++++++++--------- 1 file changed, 102 insertions(+), 102 deletions(-) diff --git a/packages/utils/interactive-hooks/src/Pressability/Pressability.ts b/packages/utils/interactive-hooks/src/Pressability/Pressability.ts index d1568dff95..a6ee57d096 100644 --- a/packages/utils/interactive-hooks/src/Pressability/Pressability.ts +++ b/packages/utils/interactive-hooks/src/Pressability/Pressability.ts @@ -147,98 +147,98 @@ const getTouchFromPressEvent = (event: PressEvent) => { }; /** -* 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 `hitSlop`. -* │ │ │ 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 -* `pressRectOffset`, 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. -*/ + * 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 `hitSlop`. + * │ │ │ 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 + * `pressRectOffset`, 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 class Pressability { private _config: PressabilityConfig; private _eventHandlers: PressabilityEventHandlers = null; @@ -265,8 +265,8 @@ export class Pressability { } /** - * Resets any pending timers. This should be called on unmount. - */ + * Resets any pending timers. This should be called on unmount. + */ public reset(): void { this._cancelHoverInDelayTimeout(); this._cancelHoverOutDelayTimeout(); @@ -276,8 +276,8 @@ export class Pressability { } /** - * Returns a set of props to spread into the interactive element. - */ + * Returns a set of props to spread into the interactive element. + */ public getEventHandlers(): PressabilityEventHandlers { if (this._eventHandlers == null) { this._eventHandlers = this._createEventHandlers(); @@ -439,9 +439,9 @@ export class Pressability { } /** - * Receives a state machine signal, performs side effects of the transition - * and stores the new state. Validates the transition as well. - */ + * Receives a state machine signal, performs side effects of the transition + * and stores the new state. Validates the transition as well. + */ private _receiveSignal(signal: TouchSignal, event: PressEvent): void { const prevState = this._touchState; const nextState: TouchState = Transitions[prevState][signal]; @@ -462,9 +462,9 @@ export class Pressability { } /** - * Performs a transition between touchable states and identify any activations - * or deactivations (and callback invocations). - */ + * Performs a transition between touchable states and identify any activations + * or deactivations (and callback invocations). + */ private _performTransitionSideEffects(prevState: TouchState, nextState: TouchState, signal: TouchSignal, event): void { if (isTerminalSignal(signal)) { this._touchActivatePosition = null; From 339bc733a5a6d87620789d806531d51e2b3b9220 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Fri, 6 Aug 2021 15:42:56 -0700 Subject: [PATCH 5/5] address review feedback --- .../interactive-hooks/src/useAsPressable.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/utils/interactive-hooks/src/useAsPressable.ts b/packages/utils/interactive-hooks/src/useAsPressable.ts index 8be6d7ca27..a3faa84c34 100644 --- a/packages/utils/interactive-hooks/src/useAsPressable.ts +++ b/packages/utils/interactive-hooks/src/useAsPressable.ts @@ -16,11 +16,10 @@ import { usePressability } from './usePressability'; /** * hover specific state and callback helper */ -// eslint-disable-next-line @typescript-eslint/ban-types function useHoverHelper(props: PressableHoverProps): [PressableHoverProps, IHoverState] { const [hoverState, setHoverState] = React.useState({ hovered: false }); const onHoverIn = React.useCallback( - (e) => { + e => { setHoverState({ hovered: true }); if (props.onHoverIn) { props.onHoverIn(e); @@ -30,7 +29,7 @@ function useHoverHelper(props: PressableHoverProps): [PressableHoverProps, IHove ); const onHoverOut = React.useCallback( - (e) => { + e => { setHoverState({ hovered: false }); if (props.onHoverOut) { props.onHoverOut(e); @@ -44,11 +43,10 @@ function useHoverHelper(props: PressableHoverProps): [PressableHoverProps, IHove /** * focus specific state and callback helper */ -// eslint-disable-next-line @typescript-eslint/ban-types function useFocusHelper(props: PressableFocusProps): [PressableFocusProps, IFocusState] { const [focusState, setFocusState] = React.useState({ focused: false }); const onFocus = React.useCallback( - (e) => { + e => { setFocusState({ focused: true }); if (props.onFocus) { props.onFocus(e); @@ -58,7 +56,7 @@ function useFocusHelper(props: PressableFocusProps): [PressableFocusProps, IFocu ); const onBlur = React.useCallback( - (e) => { + e => { setFocusState({ focused: false }); if (props.onBlur) { props.onBlur(e); @@ -72,12 +70,11 @@ function useFocusHelper(props: PressableFocusProps): [PressableFocusProps, IFocu /** * press specific state and callback helper */ -// eslint-disable-next-line @typescript-eslint/ban-types function usePressHelper(props: PressablePressProps): [PressablePressProps, IPressState] { const [pressState, setPressState] = React.useState({ pressed: false }); const onPressIn = React.useCallback( - (e) => { + e => { setPressState({ pressed: true }); if (props.onPressIn) { props.onPressIn(e); @@ -87,7 +84,7 @@ function usePressHelper(props: PressablePressProps): [PressablePressProps, IPres ); const onPressOut = React.useCallback( - (e) => { + e => { setPressState({ pressed: false }); if (props.onPressOut) { props.onPressOut(e); @@ -163,8 +160,8 @@ export function usePressableState(props: PressablePropsExtended): { props: Press const { onPressIn, onPressOut, onHoverIn, onHoverOut, onFocus, onBlur, ...rest } = props; const [focusProps, focusState] = useFocusHelper({ onFocus, onBlur }); const [pressProps, pressState] = usePressHelper({ onPressIn, onPressOut }); - const [hoverProps, hoverState] = - Platform.OS !== 'android' && Platform.OS !== 'ios' ? useHoverHelper({ onHoverIn, onHoverOut }) : [{}, {}]; + const platformSupportsHover = Platform.OS !== 'android' && Platform.OS !== 'ios'; + const [hoverProps, hoverState] = platformSupportsHover ? useHoverHelper({ onHoverIn, onHoverOut }) : [{}, {}]; return { props: { ...rest, ...focusProps, ...pressProps, ...hoverProps }, state: { ...focusState, ...pressState, ...hoverState } }; }