From 37ff6b48885bb38079db7e8b4739078dc360be19 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Mon, 20 Apr 2020 11:40:05 -0700 Subject: [PATCH] Reorganize usePressEvents and PressResponder --- .../src/exports/Pressable/index.js | 4 +- .../src/exports/TouchableHighlight/index.js | 2 +- .../src/exports/TouchableOpacity/index.js | 2 +- .../exports/TouchableWithoutFeedback/index.js | 4 +- .../usePressEvents/PressResponder.js} | 38 +++++++++++-------- .../usePressEvents/index.js} | 8 +++- .../src/modules/isLink/index.js | 15 ++++++++ 7 files changed, 50 insertions(+), 23 deletions(-) rename packages/react-native-web/src/{modules/PressResponder/index.js => hooks/usePressEvents/PressResponder.js} (94%) rename packages/react-native-web/src/{modules/PressResponder/usePressEvents.js => hooks/usePressEvents/index.js} (82%) create mode 100644 packages/react-native-web/src/modules/isLink/index.js diff --git a/packages/react-native-web/src/exports/Pressable/index.js b/packages/react-native-web/src/exports/Pressable/index.js index 19d5ab62..fc001788 100644 --- a/packages/react-native-web/src/exports/Pressable/index.js +++ b/packages/react-native-web/src/exports/Pressable/index.js @@ -10,13 +10,13 @@ 'use strict'; -import type { PressResponderConfig } from '../../modules/PressResponder'; +import type { PressResponderConfig } from '../../hooks/usePressEvents/PressResponder'; import type { ViewProps } from '../View'; import * as React from 'react'; import { forwardRef, memo, useMemo, useState, useRef } from 'react'; import setAndForwardRef from '../../modules/setAndForwardRef'; -import usePressEvents from '../../modules/PressResponder/usePressEvents'; +import usePressEvents from '../../hooks/usePressEvents'; import View from '../View'; export type StateCallbackType = $ReadOnly<{| diff --git a/packages/react-native-web/src/exports/TouchableHighlight/index.js b/packages/react-native-web/src/exports/TouchableHighlight/index.js index eae948a8..09d0d742 100644 --- a/packages/react-native-web/src/exports/TouchableHighlight/index.js +++ b/packages/react-native-web/src/exports/TouchableHighlight/index.js @@ -16,7 +16,7 @@ import type { ViewProps } from '../View'; import * as React from 'react'; import { useCallback, useMemo, useState, useRef } from 'react'; -import usePressEvents from '../../modules/PressResponder/usePressEvents'; +import usePressEvents from '../../hooks/usePressEvents'; import setAndForwardRef from '../../modules/setAndForwardRef'; import StyleSheet from '../StyleSheet'; import View from '../View'; diff --git a/packages/react-native-web/src/exports/TouchableOpacity/index.js b/packages/react-native-web/src/exports/TouchableOpacity/index.js index 355545b2..a151b9ea 100644 --- a/packages/react-native-web/src/exports/TouchableOpacity/index.js +++ b/packages/react-native-web/src/exports/TouchableOpacity/index.js @@ -15,7 +15,7 @@ import type { ViewProps } from '../View'; import * as React from 'react'; import { useCallback, useMemo, useState, useRef } from 'react'; -import usePressEvents from '../../modules/PressResponder/usePressEvents'; +import usePressEvents from '../../hooks/usePressEvents'; import setAndForwardRef from '../../modules/setAndForwardRef'; import StyleSheet from '../StyleSheet'; import View from '../View'; diff --git a/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js b/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js index 320c5cdd..d06002fd 100644 --- a/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js +++ b/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js @@ -10,14 +10,14 @@ 'use strict'; -import type { PressResponderConfig } from '../../modules/PressResponder'; +import type { PressResponderConfig } from '../../hooks/usePressEvents/PressResponder'; import type { ViewProps } from '../View'; import * as React from 'react'; import { useMemo, useRef } from 'react'; import pick from '../../modules/pick'; import setAndForwardRef from '../../modules/setAndForwardRef'; -import usePressEvents from '../../modules/PressResponder/usePressEvents'; +import usePressEvents from '../../hooks/usePressEvents'; export type Props = $ReadOnly<{| accessibilityLabel?: $PropertyType, diff --git a/packages/react-native-web/src/modules/PressResponder/index.js b/packages/react-native-web/src/hooks/usePressEvents/PressResponder.js similarity index 94% rename from packages/react-native-web/src/modules/PressResponder/index.js rename to packages/react-native-web/src/hooks/usePressEvents/PressResponder.js index bbc6d8b1..fbe812ec 100644 --- a/packages/react-native-web/src/modules/PressResponder/index.js +++ b/packages/react-native-web/src/hooks/usePressEvents/PressResponder.js @@ -11,6 +11,8 @@ 'use strict'; import invariant from 'fbjs/lib/invariant'; +import isLink from '../../modules/isLink'; +import isSelectionValid from '../../modules/isSelectionValid'; type ClickEvent = any; type KeyboardEvent = any; @@ -129,6 +131,11 @@ const isPressStartSignal = signal => const isTerminalSignal = signal => signal === RESPONDER_TERMINATED || signal === RESPONDER_RELEASE; +const isKeyPress = event => { + const target = event.currentTarget; + return (!isLink(target) && event.key === ' ') || event.key === 'Enter'; +}; + const DEFAULT_LONG_PRESS_DELAY_MS = 450; // 500 - 50 const DEFAULT_PRESS_DELAY_MS = 50; @@ -299,21 +306,19 @@ export default class PressResponder { }, onKeyDown: event => { - if (this._touchState === NOT_RESPONDER) { - if (event.key === ' ' || event.key === 'Enter') { + if (isKeyPress(event)) { + if (this._touchState === NOT_RESPONDER) { start(event, false); } - } - if (this._responderID) { event.stopPropagation(); } }, onKeyUp: event => { - if (event.key === ' ' || event.key === 'Enter') { + if (isKeyPress(event)) { end(event); + event.stopPropagation(); } - event.stopPropagation(); }, onResponderGrant: event => start(event), @@ -356,7 +361,7 @@ export default class PressResponder { return cancelable; }, - // NOTE: this diverges from react-native@0.62 in 2 significant ways + // NOTE: this diverges from react-native in 3 significant ways: // * The `onPress` callback is not connected to the responder system (the native // `click` event must be used but is dispatched in many scenarios where no pointers // are on the screen.) Therefore, it's possible for `onPress` to be called without @@ -367,14 +372,15 @@ export default class PressResponder { onClick: (event: any): void => { const { disabled, onPress } = this._config; if (!disabled) { - if (event.nativeEvent.__responderStoppedPropagation !== true) { - if (this._longPressDispatched) { - event.preventDefault(); - } else if (event.ctrlKey === false && event.altKey === false && onPress != null) { - onPress(event); - } - event.nativeEvent.__responderStoppedPropagation = true; + // If long press dispatched, cancel default click behavior. + // If text is selected it means the user selected text during the gesture, + // cancel default click behavior. + if (this._longPressDispatched || isSelectionValid()) { + event.preventDefault(); + } else if (onPress != null && event.ctrlKey === false && event.altKey === false) { + onPress(event); } + event.stopPropagation(); } }, @@ -433,7 +439,9 @@ export default class PressResponder { if (isPressStartSignal(prevState) && signal === LONG_PRESS_DETECTED) { const { onLongPress } = this._config; - if (onLongPress != null) { + // Long press is not supported for keyboards because 'click' can be dispatched + // immediately (and multiple times) after 'keydown'. + if (onLongPress != null && event.nativeEvent.key == null) { onLongPress(event); this._longPressDispatched = true; } diff --git a/packages/react-native-web/src/modules/PressResponder/usePressEvents.js b/packages/react-native-web/src/hooks/usePressEvents/index.js similarity index 82% rename from packages/react-native-web/src/modules/PressResponder/usePressEvents.js rename to packages/react-native-web/src/hooks/usePressEvents/index.js index 029422b2..6555be62 100644 --- a/packages/react-native-web/src/modules/PressResponder/usePressEvents.js +++ b/packages/react-native-web/src/hooks/usePressEvents/index.js @@ -10,8 +10,10 @@ 'use strict'; -import PressResponder, { type EventHandlers, type PressResponderConfig } from './index'; -import { useEffect, useRef } from 'react'; +import type { EventHandlers, PressResponderConfig } from './PressResponder'; + +import PressResponder from './PressResponder'; +import { useDebugValue, useEffect, useRef } from 'react'; export default function usePressEvents(hostRef: any, config: PressResponderConfig): EventHandlers { const pressResponderRef = useRef(null); @@ -33,5 +35,7 @@ export default function usePressEvents(hostRef: any, config: PressResponderConfi }; }, [pressResponder]); + useDebugValue(config); + return pressResponder.getEventHandlers(); } diff --git a/packages/react-native-web/src/modules/isLink/index.js b/packages/react-native-web/src/modules/isLink/index.js new file mode 100644 index 00000000..3a8d7bb9 --- /dev/null +++ b/packages/react-native-web/src/modules/isLink/index.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Nicolas Gallagher + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export default function isLink(node: HTMLElement) { + return ( + (node.nodeName === 'A' && node.getAttribute('href') != null) || + node.getAttribute('role') === 'link' + ); +}