diff --git a/.eslintrc b/.eslintrc
index 919c9ea5..874dbebd 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -34,6 +34,8 @@
"navigator": false,
"window": false,
// Flow global types,
+ "$Diff": false,
+ "$ElementType": false,
"$Enum": false,
"$PropertyType": false,
"$ReadOnly": false,
@@ -48,7 +50,8 @@
"ReactPropsCheckType": false,
"ReactPropTypes": false,
"ResizeObserver": false,
- "SyntheticEvent": false
+ "SyntheticEvent": false,
+ "TimeoutID": false,
},
"rules": {
"camelcase": 0,
diff --git a/packages/babel-plugin-react-native-web/src/moduleMap.js b/packages/babel-plugin-react-native-web/src/moduleMap.js
index 5bc0ee6c..c3156222 100644
--- a/packages/babel-plugin-react-native-web/src/moduleMap.js
+++ b/packages/babel-plugin-react-native-web/src/moduleMap.js
@@ -33,6 +33,7 @@ module.exports = {
Picker: true,
PixelRatio: true,
Platform: true,
+ Pressable: true,
ProgressBar: true,
RefreshControl: true,
SafeAreaView: true,
diff --git a/packages/docs/src/components/Pressable/Pressable.stories.mdx b/packages/docs/src/components/Pressable/Pressable.stories.mdx
new file mode 100644
index 00000000..5ac540e8
--- /dev/null
+++ b/packages/docs/src/components/Pressable/Pressable.stories.mdx
@@ -0,0 +1,66 @@
+import { Meta, Props, Story, Preview } from '@storybook/addon-docs/blocks';
+import * as Stories from './examples';
+
+
+
+# Pressable
+
+...
+
+## Props
+
+| Name | Type | Default |
+| ------------------------- | --------- | ------- |
+| ...ViewProps | | |
+| delayLongPress | ?number | 500 |
+| delayPressIn | ?number | 0 |
+| delayPressOut | ?number | 0 |
+| disabled | ?boolean | false |
+| onLongPress | ?Function | |
+| onPress | ?Function | |
+| onPressIn | ?Function | |
+| onPressOut | ?Function | |
+
+### delayLongPress
+
+Delay in ms, from `onPressIn` to before `onLongPress` is called. The default is `500`.
+
+### delayPressIn
+
+Delay in ms, from pointer down to before `onPressIn` is called.
+
+### delayPressOut
+
+Delay in ms, from pointer up to before `onPressOut` is called.
+
+### disabled
+
+Disables all pointer interactions with the element.
+
+
+
+
+
+
+
+### onLongPress
+
+Called when the pointer is held down for as long as the value of `delayLongPress`.
+
+### onPress
+
+Called when the pointer is released, but not if cancelled (e.g. by a scroll that steals the responder lock).
+
+## Examples
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/components/Pressable/examples/DelayEvents.js b/packages/docs/src/components/Pressable/examples/DelayEvents.js
new file mode 100644
index 00000000..c7075e73
--- /dev/null
+++ b/packages/docs/src/components/Pressable/examples/DelayEvents.js
@@ -0,0 +1,62 @@
+import React from 'react';
+import { StyleSheet, Text, Pressable, View } from 'react-native';
+
+export default function DelayEvents() {
+ const [eventLog, updateEventLog] = React.useState([]);
+
+ const handlePress = eventName => {
+ return () => {
+ const limit = 6;
+ updateEventLog(state => {
+ const nextState = state.slice(0, limit - 1);
+ nextState.unshift(eventName);
+ return nextState;
+ });
+ };
+ };
+
+ return (
+
+
+
+
+ Pressable
+
+
+
+
+ {eventLog.map((e, ii) => (
+ {e}
+ ))}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ touchableText: {
+ borderRadius: 8,
+ padding: 5,
+ borderWidth: 1,
+ borderColor: 'black',
+ color: '#007AFF',
+ borderStyle: 'solid',
+ textAlign: 'center'
+ },
+ eventLogBox: {
+ padding: 10,
+ marginTop: 10,
+ height: 120,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: '#f0f0f0',
+ backgroundColor: '#f9f9f9'
+ }
+});
diff --git a/packages/docs/src/components/Pressable/examples/Disabled.js b/packages/docs/src/components/Pressable/examples/Disabled.js
new file mode 100644
index 00000000..b85593e4
--- /dev/null
+++ b/packages/docs/src/components/Pressable/examples/Disabled.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import { StyleSheet, View, Text, Pressable } from 'react-native';
+
+const action = msg => () => {
+ console.log(msg);
+};
+
+export default function Disabled() {
+ return (
+
+
+
+ Disabled Pressable
+
+
+
+
+
+ Enabled Pressable
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ row: {
+ justifyContent: 'center',
+ flexDirection: 'row'
+ },
+ block: {
+ padding: 10
+ }
+});
diff --git a/packages/docs/src/components/Pressable/examples/FeedbackEvents.js b/packages/docs/src/components/Pressable/examples/FeedbackEvents.js
new file mode 100644
index 00000000..173bf55e
--- /dev/null
+++ b/packages/docs/src/components/Pressable/examples/FeedbackEvents.js
@@ -0,0 +1,94 @@
+import React from 'react';
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+export default function FeedbackEvents() {
+ const [eventLog, updateEventLog] = React.useState([]);
+
+ const handlePress = eventName => {
+ return () => {
+ const limit = 6;
+ updateEventLog(state => {
+ const nextState = state.slice(0, limit - 1);
+ nextState.unshift(eventName);
+ return nextState;
+ });
+ };
+ };
+
+ return (
+
+
+
+
+ Press Me
+
+
+
+
+
+ ({
+ padding: 10,
+ margin: 10,
+ borderWidth: 1,
+ borderColor: focused ? 'blue' : null,
+ backgroundColor: pressed ? 'lightblue' : 'white'
+ })}
+ >
+ ({
+ padding: 10,
+ margin: 10,
+ borderWidth: 1,
+ borderColor: focused ? 'blue' : null,
+ backgroundColor: pressed ? 'lightblue' : 'white'
+ })}
+ >
+ Nested pressables
+
+
+
+
+
+ {eventLog.map((e, ii) => (
+ {e}
+ ))}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ touchableText: {
+ borderRadius: 8,
+ padding: 5,
+ borderWidth: 1,
+ borderColor: 'black',
+ color: '#007AFF',
+ borderStyle: 'solid',
+ textAlign: 'center'
+ },
+ eventLogBox: {
+ padding: 10,
+ marginTop: 10,
+ height: 120,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: '#f0f0f0',
+ backgroundColor: '#f9f9f9'
+ }
+});
diff --git a/packages/docs/src/components/Pressable/examples/index.js b/packages/docs/src/components/Pressable/examples/index.js
new file mode 100644
index 00000000..5e41fe04
--- /dev/null
+++ b/packages/docs/src/components/Pressable/examples/index.js
@@ -0,0 +1,3 @@
+export { default as delayEvents } from './DelayEvents';
+export { default as disabled } from './Disabled';
+export { default as feedbackEvents } from './FeedbackEvents';
diff --git a/packages/docs/src/components/ScrollView/helpers.js b/packages/docs/src/components/ScrollView/helpers.js
index d10c688a..777041aa 100644
--- a/packages/docs/src/components/ScrollView/helpers.js
+++ b/packages/docs/src/components/ScrollView/helpers.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+import { StyleSheet, Text, TouchableOpacity } from 'react-native';
export const Button = ({ label, onPress }) => (
@@ -9,9 +9,9 @@ export const Button = ({ label, onPress }) => (
function Item(props) {
return (
-
+
{props.msg}
-
+
);
}
diff --git a/packages/docs/src/components/TouchableHighlight/TouchableHighlight.stories.mdx b/packages/docs/src/components/TouchableHighlight/TouchableHighlight.stories.mdx
index 51a3cdbc..9112a4a2 100644
--- a/packages/docs/src/components/TouchableHighlight/TouchableHighlight.stories.mdx
+++ b/packages/docs/src/components/TouchableHighlight/TouchableHighlight.stories.mdx
@@ -42,3 +42,30 @@ Called immediately after the underlay is shown
### underlayColor
The color of the underlay that will show through when the touch is active.
+
+## Examples
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/components/TouchableHighlight/examples/DelayEvents.js b/packages/docs/src/components/TouchableHighlight/examples/DelayEvents.js
index d9c3d9c6..964493f3 100644
--- a/packages/docs/src/components/TouchableHighlight/examples/DelayEvents.js
+++ b/packages/docs/src/components/TouchableHighlight/examples/DelayEvents.js
@@ -1,28 +1,10 @@
import React, { PureComponent } from 'react';
-import {
- StyleSheet,
- Text,
- TouchableHighlight,
- TouchableOpacity,
- TouchableWithoutFeedback,
- View
-} from 'react-native';
-
-const Touchables = {
- highlight: TouchableHighlight,
- opacity: TouchableOpacity,
- withoutFeedback: TouchableWithoutFeedback
-};
+import { StyleSheet, Text, TouchableHighlight as Touchable, View } from 'react-native';
export default class TouchableDelayEvents extends PureComponent {
- static defaultProps = {
- touchable: 'highlight'
- };
-
state = { eventLog: [] };
render() {
- const Touchable = Touchables[this.props.touchable];
const { displayName } = Touchable;
return (
diff --git a/packages/docs/src/components/TouchableHighlight/examples/Disabled.js b/packages/docs/src/components/TouchableHighlight/examples/Disabled.js
index 81e953e9..6a46e8eb 100644
--- a/packages/docs/src/components/TouchableHighlight/examples/Disabled.js
+++ b/packages/docs/src/components/TouchableHighlight/examples/Disabled.js
@@ -1,18 +1,11 @@
import React from 'react';
-import {
- StyleSheet,
- View,
- Text,
- TouchableHighlight,
- TouchableOpacity,
- TouchableWithoutFeedback
-} from 'react-native';
+import { StyleSheet, View, Text, TouchableHighlight } from 'react-native';
const action = msg => () => {
console.log(msg);
};
-class TouchableHighlightDisabled extends React.Component {
+export default class TouchableHighlightDisabled extends React.Component {
render() {
return (
@@ -39,52 +32,6 @@ class TouchableHighlightDisabled extends React.Component {
}
}
-class TouchableOpacityDisabled extends React.Component {
- render() {
- return (
-
-
- Disabled TouchableOpacity
-
-
-
- Enabled TouchableOpacity
-
-
- );
- }
-}
-
-class TouchableWithoutFeedbackDisabled extends React.Component {
- render() {
- return (
-
-
-
- Disabled TouchableWithoutFeedback
-
-
-
-
-
- Enabled TouchableWithoutFeedback
-
-
-
- );
- }
-}
-
-export { TouchableHighlightDisabled, TouchableOpacityDisabled, TouchableWithoutFeedbackDisabled };
-
const styles = StyleSheet.create({
row: {
justifyContent: 'center',
diff --git a/packages/docs/src/components/TouchableHighlight/examples/FeedbackEvents.js b/packages/docs/src/components/TouchableHighlight/examples/FeedbackEvents.js
index 283240e1..dc4081b3 100644
--- a/packages/docs/src/components/TouchableHighlight/examples/FeedbackEvents.js
+++ b/packages/docs/src/components/TouchableHighlight/examples/FeedbackEvents.js
@@ -1,28 +1,10 @@
import React, { PureComponent } from 'react';
-import {
- StyleSheet,
- Text,
- TouchableHighlight,
- TouchableOpacity,
- TouchableWithoutFeedback,
- View
-} from 'react-native';
-
-const Touchables = {
- highlight: TouchableHighlight,
- opacity: TouchableOpacity,
- withoutFeedback: TouchableWithoutFeedback
-};
+import { StyleSheet, Text, TouchableHighlight as Touchable, View } from 'react-native';
export default class TouchableFeedbackEvents extends PureComponent {
- static defaultProps = {
- touchable: 'highlight'
- };
-
state = { eventLog: [] };
render() {
- const Touchable = Touchables[this.props.touchable];
return (
diff --git a/packages/docs/src/components/TouchableHighlight/examples/StyleOverrides.js b/packages/docs/src/components/TouchableHighlight/examples/StyleOverrides.js
index 064e4675..9ecc7330 100644
--- a/packages/docs/src/components/TouchableHighlight/examples/StyleOverrides.js
+++ b/packages/docs/src/components/TouchableHighlight/examples/StyleOverrides.js
@@ -1,36 +1,33 @@
import React from 'react';
import { StyleSheet, View, Text, TouchableHighlight } from 'react-native';
-export default class TouchableCustomStyleOverridesExample extends React.Component {
- buttons = ['One', 'Two', 'Three'];
- state = {};
+const buttons = ['One', 'Two', 'Three'];
- select = selectedButton => event => {
- const newState = {};
- this.buttons.forEach(button => {
- newState[button] = selectedButton === button;
- });
- this.setState(newState);
- };
+export default function TouchableCustomStyleOverridesExample() {
+ const [state, setState] = React.useState({});
- render() {
- return (
-
- {this.buttons.map(button => {
- return (
-
- {button}
-
- );
- })}
-
- );
+ function select(item) {
+ return function handler(e) {
+ setState({ [item]: true });
+ };
}
+
+ return (
+
+ {buttons.map(button => {
+ return (
+
+ {button}
+
+ );
+ })}
+
+ );
}
const styles = StyleSheet.create({
diff --git a/packages/docs/src/components/TouchableOpacity/TouchableOpacity.stories.mdx b/packages/docs/src/components/TouchableOpacity/TouchableOpacity.stories.mdx
index 3d858d68..ac65d03a 100644
--- a/packages/docs/src/components/TouchableOpacity/TouchableOpacity.stories.mdx
+++ b/packages/docs/src/components/TouchableOpacity/TouchableOpacity.stories.mdx
@@ -22,8 +22,22 @@ added to the view hierarchy. Be aware that this can affect layout.
Determines what the opacity of the wrapped view should be when active.
-## Instance methods
+## Examples
-### setOpacityTo(number)
+
+
+
+
+
-Sets the opacity.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/components/TouchableOpacity/examples/DelayEvents.js b/packages/docs/src/components/TouchableOpacity/examples/DelayEvents.js
index d9c3d9c6..83659b32 100644
--- a/packages/docs/src/components/TouchableOpacity/examples/DelayEvents.js
+++ b/packages/docs/src/components/TouchableOpacity/examples/DelayEvents.js
@@ -1,28 +1,10 @@
import React, { PureComponent } from 'react';
-import {
- StyleSheet,
- Text,
- TouchableHighlight,
- TouchableOpacity,
- TouchableWithoutFeedback,
- View
-} from 'react-native';
-
-const Touchables = {
- highlight: TouchableHighlight,
- opacity: TouchableOpacity,
- withoutFeedback: TouchableWithoutFeedback
-};
+import { StyleSheet, Text, TouchableOpacity as Touchable, View } from 'react-native';
export default class TouchableDelayEvents extends PureComponent {
- static defaultProps = {
- touchable: 'highlight'
- };
-
state = { eventLog: [] };
render() {
- const Touchable = Touchables[this.props.touchable];
const { displayName } = Touchable;
return (
diff --git a/packages/docs/src/components/TouchableOpacity/examples/Disabled.js b/packages/docs/src/components/TouchableOpacity/examples/Disabled.js
index 81e953e9..4adfcd26 100644
--- a/packages/docs/src/components/TouchableOpacity/examples/Disabled.js
+++ b/packages/docs/src/components/TouchableOpacity/examples/Disabled.js
@@ -1,49 +1,16 @@
import React from 'react';
-import {
- StyleSheet,
- View,
- Text,
- TouchableHighlight,
- TouchableOpacity,
- TouchableWithoutFeedback
-} from 'react-native';
+import { StyleSheet, View, Text, TouchableOpacity } from 'react-native';
const action = msg => () => {
console.log(msg);
};
-class TouchableHighlightDisabled extends React.Component {
- render() {
- return (
-
-
- Disabled TouchableHighlight
-
-
-
- Enabled TouchableHighlight
-
-
- );
- }
-}
-
-class TouchableOpacityDisabled extends React.Component {
+export default class TouchableOpacityDisabled extends React.Component {
render() {
return (
@@ -63,28 +30,6 @@ class TouchableOpacityDisabled extends React.Component {
}
}
-class TouchableWithoutFeedbackDisabled extends React.Component {
- render() {
- return (
-
-
-
- Disabled TouchableWithoutFeedback
-
-
-
-
-
- Enabled TouchableWithoutFeedback
-
-
-
- );
- }
-}
-
-export { TouchableHighlightDisabled, TouchableOpacityDisabled, TouchableWithoutFeedbackDisabled };
-
const styles = StyleSheet.create({
row: {
justifyContent: 'center',
diff --git a/packages/docs/src/components/TouchableOpacity/examples/FeedbackEvents.js b/packages/docs/src/components/TouchableOpacity/examples/FeedbackEvents.js
index 283240e1..11755d71 100644
--- a/packages/docs/src/components/TouchableOpacity/examples/FeedbackEvents.js
+++ b/packages/docs/src/components/TouchableOpacity/examples/FeedbackEvents.js
@@ -1,32 +1,14 @@
import React, { PureComponent } from 'react';
-import {
- StyleSheet,
- Text,
- TouchableHighlight,
- TouchableOpacity,
- TouchableWithoutFeedback,
- View
-} from 'react-native';
-
-const Touchables = {
- highlight: TouchableHighlight,
- opacity: TouchableOpacity,
- withoutFeedback: TouchableWithoutFeedback
-};
+import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
export default class TouchableFeedbackEvents extends PureComponent {
- static defaultProps = {
- touchable: 'highlight'
- };
-
state = { eventLog: [] };
render() {
- const Touchable = Touchables[this.props.touchable];
return (
-
Press Me
-
+
{this.state.eventLog.map((e, ii) => (
diff --git a/packages/docs/src/components/TouchableOpacity/examples/index.js b/packages/docs/src/components/TouchableOpacity/examples/index.js
index e41b4b00..5e41fe04 100644
--- a/packages/docs/src/components/TouchableOpacity/examples/index.js
+++ b/packages/docs/src/components/TouchableOpacity/examples/index.js
@@ -1,3 +1,3 @@
-//export { default as color } from './Color';
+export { default as delayEvents } from './DelayEvents';
export { default as disabled } from './Disabled';
-//export { default as onPress } from './OnPress';
+export { default as feedbackEvents } from './FeedbackEvents';
diff --git a/packages/react-native-web/src/exports/Pressable/index.js b/packages/react-native-web/src/exports/Pressable/index.js
new file mode 100644
index 00000000..02008353
--- /dev/null
+++ b/packages/react-native-web/src/exports/Pressable/index.js
@@ -0,0 +1,174 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict-local
+ * @format
+ */
+
+'use strict';
+
+import type { PressResponderConfig } from '../../modules/PressResponder';
+import type { ViewProps } from '../View';
+
+import * as React from 'react';
+import { useMemo, useState, useRef, useImperativeHandle } from 'react';
+import usePressEvents from '../../modules/PressResponder/usePressEvents';
+import View from '../View';
+
+export type StateCallbackType = $ReadOnly<{|
+ focused: boolean,
+ pressed: boolean
+|}>;
+
+type ViewStyleProp = $PropertyType;
+
+type Props = $ReadOnly<{|
+ accessibilityLabel?: $PropertyType,
+ accessibilityLiveRegion?: $PropertyType,
+ accessibilityRole?: $PropertyType,
+ accessibilityState?: $PropertyType,
+ accessibilityValue?: $PropertyType,
+ accessible?: $PropertyType,
+ focusable?: ?boolean,
+ importantForAccessibility?: $PropertyType,
+ children: React.Node | ((state: StateCallbackType) => React.Node),
+ // Duration (in milliseconds) from `onPressIn` before `onLongPress` is called.
+ delayLongPress?: ?number,
+ // Duration (in milliseconds) from `onPressStart` is called after pointerdown
+ delayPressIn?: ?number,
+ // Duration (in milliseconds) from `onPressEnd` is called after pointerup.
+ delayPressOut?: ?number,
+ // Whether the press behavior is disabled.
+ disabled?: ?boolean,
+ // Additional distance outside of this view in which a press is detected.
+ hitSlop?: $PropertyType,
+ // Called when the view blurs
+ onBlur?: $PropertyType,
+ // Called when the view is focused
+ onFocus?: $PropertyType,
+ // Called when this view's layout changes
+ onLayout?: $PropertyType,
+ // Called when a long-tap gesture is detected.
+ onLongPress?: $PropertyType,
+ // Called when a single tap gesture is detected.
+ onPress?: $PropertyType,
+ // Called when a touch is engaged, before `onPress`.
+ onPressIn?: $PropertyType,
+ // Called when a touch is moving, after `onPressIn`.
+ onPressMove?: $PropertyType,
+ // Called when a touch is released, before `onPress`.
+ onPressOut?: $PropertyType,
+ style?: ViewStyleProp | ((state: StateCallbackType) => ViewStyleProp),
+ testID?: $PropertyType,
+ /**
+ * Used only for documentation or testing (e.g. snapshot testing).
+ */
+ testOnly_pressed?: ?boolean
+|}>;
+
+/**
+ * Component used to build display components that should respond to whether the
+ * component is currently pressed or not.
+ */
+function Pressable(props: Props, forwardedRef): React.Node {
+ const {
+ accessible,
+ children,
+ delayLongPress,
+ delayPressIn,
+ delayPressOut,
+ disabled,
+ focusable,
+ onBlur,
+ onFocus,
+ onLongPress,
+ onPress,
+ onPressMove,
+ onPressIn,
+ onPressOut,
+ style,
+ testOnly_pressed,
+ ...rest
+ } = props;
+
+ const hostRef = useRef(null);
+ const viewRef = useRef | null>(null);
+ const [focused, setFocused] = useForceableState(false);
+ const [pressed, setPressed] = useForceableState(testOnly_pressed === true);
+ useImperativeHandle(forwardedRef, () => viewRef.current);
+
+ const pressEventHandlers = usePressEvents(
+ hostRef,
+ useMemo(
+ () => ({
+ delayLongPress,
+ delayPressStart: delayPressIn,
+ delayPressEnd: delayPressOut,
+ disabled,
+ onLongPress,
+ onPress,
+ onPressChange: setPressed,
+ onPressStart: onPressIn,
+ onPressMove,
+ onPressEnd: onPressOut
+ }),
+ [
+ delayLongPress,
+ delayPressIn,
+ delayPressOut,
+ disabled,
+ onLongPress,
+ onPress,
+ onPressIn,
+ onPressMove,
+ onPressOut,
+ setPressed
+ ]
+ )
+ );
+
+ const accessibilityState = { disabled, ...props.accessibilityState };
+ const interactionState = { focused, pressed };
+
+ function createFocusHandler(callback, value) {
+ return function(event) {
+ if (event.nativeEvent.target === hostRef.current) {
+ setFocused(value);
+ if (callback != null) {
+ callback(event);
+ }
+ }
+ };
+ }
+
+ return (
+
+ {typeof children === 'function' ? children(interactionState) : children}
+
+ );
+}
+
+function useForceableState(forced: boolean): [boolean, (boolean) => void] {
+ const [pressed, setPressed] = useState(false);
+ return [pressed || forced, setPressed];
+}
+
+const MemoedPressable = React.memo(React.forwardRef(Pressable));
+MemoedPressable.displayName = 'Pressable';
+
+export default (MemoedPressable: React.AbstractComponent>);
diff --git a/packages/react-native-web/src/exports/TouchableHighlight/index.js b/packages/react-native-web/src/exports/TouchableHighlight/index.js
index db3db8aa..aa7c4a9e 100644
--- a/packages/react-native-web/src/exports/TouchableHighlight/index.js
+++ b/packages/react-native-web/src/exports/TouchableHighlight/index.js
@@ -4,42 +4,58 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
- * @flow
+ * @flow strict-local
* @format
*/
+
'use strict';
+import type { ColorValue } from '../../types';
import type { Props as TouchableWithoutFeedbackProps } from '../TouchableWithoutFeedback';
+import type { ViewProps } from '../View';
-import applyNativeMethods from '../../modules/applyNativeMethods';
-import createReactClass from 'create-react-class';
-import ensurePositiveDelayProps from '../Touchable/ensurePositiveDelayProps';
import * as React from 'react';
+import { useCallback, useMemo, useState, useRef, useImperativeHandle } from 'react';
+import usePressEvents from '../../modules/PressResponder/usePressEvents';
import StyleSheet from '../StyleSheet';
-import Touchable from '../Touchable';
import View from '../View';
-type Event = Object;
-type PressEvent = Object;
-
-const DEFAULT_PROPS = {
- activeOpacity: 0.85,
- delayPressOut: 100,
- underlayColor: 'black'
-};
-
-const PRESS_RETENTION_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 };
+type ViewStyle = $PropertyType;
type Props = $ReadOnly<{|
...TouchableWithoutFeedbackProps,
activeOpacity?: ?number,
- underlayColor?: ?any,
- style?: ?any,
- onShowUnderlay?: ?() => void,
+ hostRef: React.Ref,
onHideUnderlay?: ?() => void,
- testOnly_pressed?: ?boolean
+ onShowUnderlay?: ?() => void,
+ style?: ViewStyle,
+ testOnly_pressed?: ?boolean,
+ underlayColor?: ?ColorValue
|}>;
+type ExtraStyles = $ReadOnly<{|
+ child: ViewStyle,
+ underlay: ViewStyle
+|}>;
+
+function createExtraStyles(activeOpacity, underlayColor): ExtraStyles {
+ return {
+ child: { opacity: activeOpacity ?? 0.85 },
+ underlay: {
+ backgroundColor: underlayColor === undefined ? 'black' : underlayColor
+ }
+ };
+}
+
+function hasPressHandler(props): boolean {
+ return (
+ props.onPress != null ||
+ props.onPressIn != null ||
+ props.onPressOut != null ||
+ props.onLongPress != null
+ );
+}
+
/**
* A wrapper for making views respond properly to touches.
* On press down, the opacity of the wrapped view is decreased, which allows
@@ -52,295 +68,130 @@ type Props = $ReadOnly<{|
*
* TouchableHighlight must have one child (not zero or more than one).
* If you wish to have several child components, wrap them in a View.
- *
- * Example:
- *
- * ```
- * renderButton: function() {
- * return (
- *
- *
- *
- * );
- * },
- * ```
- *
- *
- * ### Example
- *
- * ```ReactNativeWebPlayer
- * import React, { Component } from 'react'
- * import {
- * AppRegistry,
- * StyleSheet,
- * TouchableHighlight,
- * Text,
- * View,
- * } from 'react-native'
- *
- * class App extends Component {
- * constructor(props) {
- * super(props)
- * this.state = { count: 0 }
- * }
- *
- * onPress = () => {
- * this.setState({
- * count: this.state.count+1
- * })
- * }
- *
- * render() {
- * return (
- *
- *
- * Touch Here
- *
- *
- *
- * { this.state.count !== 0 ? this.state.count: null}
- *
- *
- *
- * )
- * }
- * }
- *
- * const styles = StyleSheet.create({
- * container: {
- * flex: 1,
- * justifyContent: 'center',
- * paddingHorizontal: 10
- * },
- * button: {
- * alignItems: 'center',
- * backgroundColor: '#DDDDDD',
- * padding: 10
- * },
- * countContainer: {
- * alignItems: 'center',
- * padding: 10
- * },
- * countText: {
- * color: '#FF00FF'
- * }
- * })
- *
- * AppRegistry.registerComponent('App', () => App)
- * ```
- *
*/
+function TouchableHighlight(props: Props, forwardedRef): React.Node {
+ const {
+ accessible,
+ activeOpacity,
+ children,
+ delayPressIn,
+ delayPressOut,
+ delayLongPress,
+ disabled,
+ focusable,
+ onHideUnderlay,
+ onLongPress,
+ onPress,
+ onPressIn,
+ onPressOut,
+ onShowUnderlay,
+ rejectResponderTermination,
+ style,
+ testOnly_pressed,
+ underlayColor,
+ ...rest
+ } = props;
-// eslint-disable-next-line react/prefer-es6-class
-const TouchableHighlight = ((createReactClass({
- displayName: 'TouchableHighlight',
+ const hostRef = useRef(null);
+ const viewRef = useRef | null>(null);
+ useImperativeHandle(forwardedRef, () => viewRef.current);
- mixins: [Touchable.Mixin.withoutDefaultFocusAndBlur],
+ const [extraStyles, setExtraStyles] = useState(
+ testOnly_pressed === true ? createExtraStyles(activeOpacity, underlayColor) : null
+ );
- getDefaultProps: () => DEFAULT_PROPS,
-
- getInitialState: function() {
- this._isMounted = false;
- if (this.props.testOnly_pressed) {
- return {
- ...this.touchableGetInitialState(),
- extraChildStyle: {
- opacity: this.props.activeOpacity
- },
- extraUnderlayStyle: {
- backgroundColor: this.props.underlayColor
- }
- };
- } else {
- return {
- ...this.touchableGetInitialState(),
- extraChildStyle: null,
- extraUnderlayStyle: null
- };
- }
- },
-
- componentDidMount: function() {
- this._isMounted = true;
- ensurePositiveDelayProps(this.props);
- },
-
- componentWillUnmount: function() {
- this._isMounted = false;
- clearTimeout(this._hideTimeout);
- },
-
- UNSAFE_componentWillReceiveProps: function(nextProps) {
- ensurePositiveDelayProps(nextProps);
- },
-
- /*
- viewConfig: {
- uiViewClassName: 'RCTView',
- validAttributes: ReactNativeViewAttributes.RCTView,
- },
- */
-
- /**
- * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are
- * defined on your component.
- */
- touchableHandleActivePressIn: function(e: PressEvent) {
- clearTimeout(this._hideTimeout);
- this._hideTimeout = null;
- this._showUnderlay();
- this.props.onPressIn && this.props.onPressIn(e);
- },
-
- touchableHandleActivePressOut: function(e: PressEvent) {
- if (!this._hideTimeout) {
- this._hideUnderlay();
- }
- this.props.onPressOut && this.props.onPressOut(e);
- },
-
- touchableHandleFocus: function(e: Event) {
- this.props.onFocus && this.props.onFocus(e);
- },
-
- touchableHandleBlur: function(e: Event) {
- this.props.onBlur && this.props.onBlur(e);
- },
-
- touchableHandlePress: function(e: PressEvent) {
- clearTimeout(this._hideTimeout);
- this._showUnderlay();
- this._hideTimeout = setTimeout(this._hideUnderlay, this.props.delayPressOut);
- this.props.onPress && this.props.onPress(e);
- },
-
- touchableHandleLongPress: function(e: PressEvent) {
- this.props.onLongPress && this.props.onLongPress(e);
- },
-
- touchableGetPressRectOffset: function() {
- return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET;
- },
-
- touchableGetHitSlop: function() {
- return this.props.hitSlop;
- },
-
- touchableGetHighlightDelayMS: function() {
- return this.props.delayPressIn;
- },
-
- touchableGetLongPressDelayMS: function() {
- return this.props.delayLongPress;
- },
-
- touchableGetPressOutDelayMS: function() {
- return this.props.delayPressOut;
- },
-
- _showUnderlay: function() {
- if (!this._isMounted || !this._hasPressHandler()) {
+ const showUnderlay = useCallback(() => {
+ if (!hasPressHandler(props)) {
return;
}
- this.setState({
- extraChildStyle: {
- opacity: this.props.activeOpacity
- },
- extraUnderlayStyle: {
- backgroundColor: this.props.underlayColor
+ setExtraStyles(createExtraStyles(activeOpacity, underlayColor));
+ if (onShowUnderlay != null) {
+ onShowUnderlay();
+ }
+ }, [activeOpacity, onShowUnderlay, props, underlayColor]);
+
+ const hideUnderlay = useCallback(() => {
+ if (testOnly_pressed === true) {
+ return;
+ }
+ if (hasPressHandler(props)) {
+ setExtraStyles(null);
+ if (onHideUnderlay != null) {
+ onHideUnderlay();
}
- });
- this.props.onShowUnderlay && this.props.onShowUnderlay();
- },
-
- _hideUnderlay: function() {
- clearTimeout(this._hideTimeout);
- this._hideTimeout = null;
- if (this.props.testOnly_pressed) {
- return;
}
- if (this._hasPressHandler()) {
- this.setState({
- extraChildStyle: null,
- extraUnderlayStyle: null
- });
- this.props.onHideUnderlay && this.props.onHideUnderlay();
- }
- },
+ }, [onHideUnderlay, props, testOnly_pressed]);
- _hasPressHandler: function() {
- return !!(
- this.props.onPress ||
- this.props.onPressIn ||
- this.props.onPressOut ||
- this.props.onLongPress
- );
- },
+ const pressEventHandlers = usePressEvents(
+ hostRef,
+ useMemo(
+ () => ({
+ cancelable: !rejectResponderTermination,
+ disabled,
+ delayLongPress,
+ delayPressStart: delayPressIn,
+ delayPressEnd: delayPressOut,
+ onLongPress,
+ onPress,
+ onPressStart(event) {
+ showUnderlay();
+ if (onPressIn != null) {
+ onPressIn(event);
+ }
+ },
+ onPressEnd(event) {
+ hideUnderlay();
+ if (onPressOut != null) {
+ onPressOut(event);
+ }
+ }
+ }),
+ [
+ delayLongPress,
+ delayPressIn,
+ delayPressOut,
+ disabled,
+ onLongPress,
+ onPress,
+ onPressIn,
+ onPressOut,
+ rejectResponderTermination,
+ showUnderlay,
+ hideUnderlay
+ ]
+ )
+ );
- render: function() {
- const child = React.Children.only(this.props.children);
- return (
-
- {React.cloneElement(child, {
- style: StyleSheet.compose(
- child.props.style,
- this.state.extraChildStyle
- )
- })}
- {Touchable.renderDebugView({
- color: 'green',
- hitSlop: this.props.hitSlop
- })}
-
- );
- }
-}): any): React.ComponentType);
+ const child = React.Children.only(children);
+
+ return (
+
+ {React.cloneElement(child, {
+ style: StyleSheet.compose(
+ child.props.style,
+ extraStyles && extraStyles.child
+ )
+ })}
+
+ );
+}
const styles = StyleSheet.create({
root: {
@@ -352,4 +203,10 @@ const styles = StyleSheet.create({
}
});
-export default applyNativeMethods(TouchableHighlight);
+const MemoedTouchableHighlight = React.memo(React.forwardRef(TouchableHighlight));
+MemoedTouchableHighlight.displayName = 'TouchableHighlight';
+
+export default (MemoedTouchableHighlight: React.AbstractComponent<
+ Props,
+ React.ElementRef
+>);
diff --git a/packages/react-native-web/src/exports/TouchableOpacity/index.js b/packages/react-native-web/src/exports/TouchableOpacity/index.js
index e1833db9..60e4dc82 100644
--- a/packages/react-native-web/src/exports/TouchableOpacity/index.js
+++ b/packages/react-native-web/src/exports/TouchableOpacity/index.js
@@ -4,293 +4,151 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
+ * @flow strict-local
* @format
- * @flow
*/
'use strict';
import type { Props as TouchableWithoutFeedbackProps } from '../TouchableWithoutFeedback';
+import type { ViewProps } from '../View';
-import applyNativeMethods from '../../modules/applyNativeMethods';
-import createReactClass from 'create-react-class';
-import ensurePositiveDelayProps from '../Touchable/ensurePositiveDelayProps';
import * as React from 'react';
+import { useCallback, useMemo, useState, useRef, useImperativeHandle } from 'react';
+import usePressEvents from '../../modules/PressResponder/usePressEvents';
import StyleSheet from '../StyleSheet';
-import Touchable from '../Touchable';
import View from '../View';
-const flattenStyle = StyleSheet.flatten;
-
-type Event = Object;
-type PressEvent = Object;
-
-const PRESS_RETENTION_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 };
+type ViewStyle = $PropertyType;
type Props = $ReadOnly<{|
...TouchableWithoutFeedbackProps,
activeOpacity?: ?number,
- style?: ?any
+ style?: ?ViewStyle
|}>;
+function getStyleOpacityWithDefault(style): number {
+ const flatStyle = StyleSheet.flatten(style);
+ const opacityValue = flatStyle != null ? flatStyle.opacity : null;
+ return typeof opacityValue === 'number' ? opacityValue : 1;
+}
+
/**
* A wrapper for making views respond properly to touches.
* On press down, the opacity of the wrapped view is decreased, dimming it.
- *
- * Opacity is controlled by wrapping the children in an Animated.View, which is
- * added to the view hiearchy. Be aware that this can affect layout.
- *
- * Example:
- *
- * ```
- * renderButton: function() {
- * return (
- *
- *
- *
- * );
- * },
- * ```
- * ### Example
- *
- * ```ReactNativeWebPlayer
- * import React, { Component } from 'react'
- * import {
- * AppRegistry,
- * StyleSheet,
- * TouchableOpacity,
- * Text,
- * View,
- * } from 'react-native'
- *
- * class App extends Component {
- * constructor(props) {
- * super(props)
- * this.state = { count: 0 }
- * }
- *
- * onPress = () => {
- * this.setState({
- * count: this.state.count+1
- * })
- * }
- *
- * render() {
- * return (
- *
- *
- * Touch Here
- *
- *
- *
- * { this.state.count !== 0 ? this.state.count: null}
- *
- *
- *
- * )
- * }
- * }
- *
- * const styles = StyleSheet.create({
- * container: {
- * flex: 1,
- * justifyContent: 'center',
- * paddingHorizontal: 10
- * },
- * button: {
- * alignItems: 'center',
- * backgroundColor: '#DDDDDD',
- * padding: 10
- * },
- * countContainer: {
- * alignItems: 'center',
- * padding: 10
- * },
- * countText: {
- * color: '#FF00FF'
- * }
- * })
- *
- * AppRegistry.registerComponent('App', () => App)
- * ```
- *
*/
+function TouchableOpacity(props: Props, forwardedRef): React.Node {
+ const {
+ accessible,
+ activeOpacity,
+ delayPressIn,
+ delayPressOut,
+ delayLongPress,
+ disabled,
+ focusable,
+ onLongPress,
+ onPress,
+ onPressIn,
+ onPressOut,
+ rejectResponderTermination,
+ style,
+ ...rest
+ } = props;
-// eslint-disable-next-line react/prefer-es6-class
-const TouchableOpacity = ((createReactClass({
- displayName: 'TouchableOpacity',
- mixins: [Touchable.Mixin.withoutDefaultFocusAndBlur],
+ const hostRef = useRef(null);
+ const viewRef = useRef | null>(null);
+ useImperativeHandle(forwardedRef, () => viewRef.current);
- getDefaultProps: function() {
- return {
- activeOpacity: 0.2
- };
- },
+ const styleOpacity = getStyleOpacityWithDefault(style);
+ const [duration, setDuration] = useState('0s');
+ const [opacity, setOpacity] = useState(styleOpacity);
- getInitialState: function() {
- return {
- ...this.touchableGetInitialState(),
- anim: this._getChildStyleOpacityWithDefault()
- };
- },
+ const setOpacityTo = useCallback(
+ (value: number, duration: number) => {
+ setOpacity(value);
+ setDuration(duration ? `${duration / 1000}s` : '0s');
+ },
+ [setOpacity, setDuration]
+ );
- componentDidMount: function() {
- ensurePositiveDelayProps(this.props);
- },
+ const opacityActive = useCallback(
+ (duration: number) => {
+ setOpacityTo(activeOpacity ?? 0.2, duration);
+ },
+ [activeOpacity, setOpacityTo]
+ );
- UNSAFE_componentWillReceiveProps: function(nextProps) {
- ensurePositiveDelayProps(nextProps);
- },
+ const opacityInactive = useCallback(
+ (duration: number) => {
+ setOpacityTo(styleOpacity, duration);
+ },
+ [setOpacityTo, styleOpacity]
+ );
- componentDidUpdate: function(prevProps, prevState) {
- if (this.props.disabled !== prevProps.disabled) {
- this._opacityInactive(250);
- }
- },
+ const pressEventHandlers = usePressEvents(
+ hostRef,
+ useMemo(
+ () => ({
+ cancelable: !rejectResponderTermination,
+ disabled,
+ delayLongPress,
+ delayPressStart: delayPressIn,
+ delayPressEnd: delayPressOut,
+ onLongPress,
+ onPress,
+ onPressStart(event) {
+ opacityActive(event.dispatchConfig.registrationName === 'onResponderGrant' ? 0 : 150);
+ if (onPressIn != null) {
+ onPressIn(event);
+ }
+ },
+ onPressEnd(event) {
+ opacityInactive(250);
+ if (onPressOut != null) {
+ onPressOut(event);
+ }
+ }
+ }),
+ [
+ delayLongPress,
+ delayPressIn,
+ delayPressOut,
+ disabled,
+ onLongPress,
+ onPress,
+ onPressIn,
+ onPressOut,
+ opacityActive,
+ opacityInactive,
+ rejectResponderTermination
+ ]
+ )
+ );
- /**
- * Animate the touchable to a new opacity.
- */
- setOpacityTo: function(value: number, duration: number) {
- this.setNativeProps({
- style: {
- opacity: value,
- transitionDuration: duration ? `${duration / 1000}s` : '0s'
- }
- });
- },
-
- /**
- * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are
- * defined on your component.
- */
- touchableHandleActivePressIn: function(e: PressEvent) {
- if (e.dispatchConfig.registrationName === 'onResponderGrant') {
- this._opacityActive(0);
- } else {
- this._opacityActive(150);
- }
- this.props.onPressIn && this.props.onPressIn(e);
- },
-
- touchableHandleActivePressOut: function(e: PressEvent) {
- this._opacityInactive(250);
- this.props.onPressOut && this.props.onPressOut(e);
- },
-
- touchableHandleFocus: function(e: Event) {
- //if (Platform.isTV) {
- // this._opacityActive(150);
- //}
- this.props.onFocus && this.props.onFocus(e);
- },
-
- touchableHandleBlur: function(e: Event) {
- //if (Platform.isTV) {
- // this._opacityInactive(250);
- //}
- this.props.onBlur && this.props.onBlur(e);
- },
-
- touchableHandlePress: function(e: PressEvent) {
- this.props.onPress && this.props.onPress(e);
- },
-
- touchableHandleLongPress: function(e: PressEvent) {
- this.props.onLongPress && this.props.onLongPress(e);
- },
-
- touchableGetPressRectOffset: function() {
- return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET;
- },
-
- touchableGetHitSlop: function() {
- return this.props.hitSlop;
- },
-
- touchableGetHighlightDelayMS: function() {
- return this.props.delayPressIn || 0;
- },
-
- touchableGetLongPressDelayMS: function() {
- return this.props.delayLongPress === 0 ? 0 : this.props.delayLongPress || 500;
- },
-
- touchableGetPressOutDelayMS: function() {
- return this.props.delayPressOut;
- },
-
- _opacityActive: function(duration: number) {
- this.setOpacityTo(this.props.activeOpacity, duration);
- },
-
- _opacityInactive: function(duration: number) {
- this.setOpacityTo(this._getChildStyleOpacityWithDefault(), duration);
- },
-
- _getChildStyleOpacityWithDefault: function() {
- const childStyle = flattenStyle(this.props.style) || {};
- return childStyle.opacity == null ? 1 : childStyle.opacity;
- },
-
- render: function() {
- return (
-
- {this.props.children}
- {Touchable.renderDebugView({
- color: 'cyan',
- hitSlop: this.props.hitSlop
- })}
-
- );
- }
-}): any): React.ComponentType);
+ return (
+
+ );
+}
const styles = StyleSheet.create({
root: {
@@ -304,4 +162,10 @@ const styles = StyleSheet.create({
}
});
-export default applyNativeMethods(TouchableOpacity);
+const MemoedTouchableOpacity = React.memo(React.forwardRef(TouchableOpacity));
+MemoedTouchableOpacity.displayName = 'TouchableOpacity';
+
+export default (MemoedTouchableOpacity: React.AbstractComponent<
+ Props,
+ React.ElementRef
+>);
diff --git a/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js b/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js
index e8c915cb..3d06c3a5 100644
--- a/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js
+++ b/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js
@@ -4,33 +4,54 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
+ * @flow strict-local
* @format
- * @flow
*/
'use strict';
-import createReactClass from 'create-react-class';
-import ensurePositiveDelayProps from '../Touchable/ensurePositiveDelayProps';
+import type { PressResponderConfig } from '../../modules/PressResponder';
+import type { ViewProps } from '../View';
+
import * as React from 'react';
-import Touchable from '../Touchable';
-import View from '../View';
+import { useMemo, useRef, useImperativeHandle } from 'react';
+import usePressEvents from '../../modules/PressResponder/usePressEvents';
-type BlurEvent = Object;
-type FocusEvent = Object;
-type PressEvent = Object;
-type LayoutEvent = Object;
-type EdgeInsetsProp = Object;
+export type Props = $ReadOnly<{|
+ accessibilityLabel?: $PropertyType,
+ accessibilityLiveRegion?: $PropertyType,
+ accessibilityRole?: $PropertyType,
+ accessibilityState?: $PropertyType,
+ accessibilityValue?: $PropertyType,
+ accessible?: $PropertyType,
+ children?: ?React.Node,
+ delayLongPress?: ?number,
+ delayPressIn?: ?number,
+ delayPressOut?: ?number,
+ disabled?: ?boolean,
+ focusable?: ?boolean,
+ hitSlop?: $PropertyType,
+ importantForAccessibility?: $PropertyType,
+ nativeID?: $PropertyType,
+ onBlur?: $PropertyType,
+ onFocus?: $PropertyType,
+ onLayout?: $PropertyType,
+ onLongPress?: $PropertyType,
+ onPress?: $PropertyType,
+ onPressIn?: $PropertyType,
+ onPressOut?: $PropertyType,
+ rejectResponderTermination?: ?boolean,
+ testID?: $PropertyType
+|}>;
-const PRESS_RETENTION_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 };
-
-const OVERRIDE_PROPS = [
+const PASSTHROUGH_PROPS = [
'accessibilityLabel',
- 'accessibilityHint',
- 'accessibilityIgnoresInvertColors',
+ 'accessibilityLiveRegion',
'accessibilityRole',
'accessibilityState',
+ 'accessibilityValue',
'hitSlop',
+ 'importantForAccessibility',
'nativeID',
'onBlur',
'onFocus',
@@ -38,138 +59,80 @@ const OVERRIDE_PROPS = [
'testID'
];
-export type Props = $ReadOnly<{|
- accessible?: ?boolean,
- accessibilityLabel?: ?string,
- accessibilityHint?: ?string,
- accessibilityIgnoresInvertColors?: ?boolean,
- accessibilityRole?: ?string,
- accessibilityState?: ?Object,
- children?: ?React.Node,
- delayLongPress?: ?number,
- delayPressIn?: ?number,
- delayPressOut?: ?number,
- disabled?: ?boolean,
- hitSlop?: ?EdgeInsetsProp,
- nativeID?: ?string,
- touchSoundDisabled?: ?boolean,
- onBlur?: ?(e: BlurEvent) => void,
- onFocus?: ?(e: FocusEvent) => void,
- onLayout?: ?(event: LayoutEvent) => mixed,
- onLongPress?: ?(event: PressEvent) => mixed,
- onPress?: ?(event: PressEvent) => mixed,
- onPressIn?: ?(event: PressEvent) => mixed,
- onPressOut?: ?(event: PressEvent) => mixed,
- pressRetentionOffset?: ?EdgeInsetsProp,
- rejectResponderTermination?: ?boolean,
- testID?: ?string
-|}>;
+function TouchableWithoutFeedback(props: Props, forwardedRef): React.Node {
+ const {
+ accessible,
+ delayPressIn,
+ delayPressOut,
+ delayLongPress,
+ disabled,
+ focusable,
+ onLongPress,
+ onPress,
+ onPressIn,
+ onPressOut,
+ rejectResponderTermination
+ } = props;
-/**
- * Do not use unless you have a very good reason. All elements that
- * respond to press should have a visual feedback when touched.
- *
- * TouchableWithoutFeedback supports only one child.
- * If you wish to have several child components, wrap them in a View.
- */
-// eslint-disable-next-line react/prefer-es6-class
-const TouchableWithoutFeedback = ((createReactClass({
- displayName: 'TouchableWithoutFeedback',
- mixins: [Touchable.Mixin],
+ const hostRef = useRef(null);
+ const viewRef = useRef(null);
+ useImperativeHandle(forwardedRef, () => viewRef.current);
- getInitialState: function() {
- return this.touchableGetInitialState();
- },
+ const pressEventHandlers = usePressEvents(
+ hostRef,
+ useMemo(
+ () => ({
+ cancelable: !rejectResponderTermination,
+ disabled,
+ delayLongPress,
+ delayPressStart: delayPressIn,
+ delayPressEnd: delayPressOut,
+ onLongPress,
+ onPress,
+ onPressStart: onPressIn,
+ onPressEnd: onPressOut
+ }),
+ [
+ disabled,
+ delayPressIn,
+ delayPressOut,
+ delayLongPress,
+ onLongPress,
+ onPress,
+ onPressIn,
+ onPressOut,
+ rejectResponderTermination
+ ]
+ )
+ );
- componentDidMount: function() {
- ensurePositiveDelayProps(this.props);
- },
+ const element = React.Children.only(props.children);
+ const children = [element.props.children];
+ const elementProps: { [string]: mixed, ... } = {
+ ...pressEventHandlers,
+ accessible: accessible !== false,
+ accessibilityState: {
+ disabled,
+ ...props.accessibilityState
+ },
+ focusable: focusable !== false && onPress !== undefined,
+ forwardedRef: hostRef,
+ ref: viewRef
+ };
- UNSAFE_componentWillReceiveProps: function(nextProps: Object) {
- ensurePositiveDelayProps(nextProps);
- },
-
- /**
- * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are
- * defined on your component.
- */
- touchableHandlePress: function(e: PressEvent) {
- this.props.onPress && this.props.onPress(e);
- },
-
- touchableHandleActivePressIn: function(e: PressEvent) {
- this.props.onPressIn && this.props.onPressIn(e);
- },
-
- touchableHandleActivePressOut: function(e: PressEvent) {
- this.props.onPressOut && this.props.onPressOut(e);
- },
-
- touchableHandleLongPress: function(e: PressEvent) {
- this.props.onLongPress && this.props.onLongPress(e);
- },
-
- touchableGetPressRectOffset: function(): typeof PRESS_RETENTION_OFFSET {
- // $FlowFixMe Invalid prop usage
- return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET;
- },
-
- touchableGetHitSlop: function(): ?Object {
- return this.props.hitSlop;
- },
-
- touchableGetHighlightDelayMS: function(): number {
- return this.props.delayPressIn || 0;
- },
-
- touchableGetLongPressDelayMS: function(): number {
- return this.props.delayLongPress === 0 ? 0 : this.props.delayLongPress || 500;
- },
-
- touchableGetPressOutDelayMS: function(): number {
- return this.props.delayPressOut || 0;
- },
-
- render: function(): React.Element {
- // Note(avik): remove dynamic typecast once Flow has been upgraded
- // $FlowFixMe(>=0.41.0)
- // eslint-disable-next-line
- const child = React.Children.only(this.props.children);
- let children = child.props.children;
- if (Touchable.TOUCH_TARGET_DEBUG && child.type === View) {
- children = React.Children.toArray(children);
- children.push(Touchable.renderDebugView({ color: 'red', hitSlop: this.props.hitSlop }));
+ for (const prop of PASSTHROUGH_PROPS) {
+ if (props[prop] !== undefined) {
+ elementProps[prop] = props[prop];
}
-
- const overrides = {};
- for (const prop of OVERRIDE_PROPS) {
- if (this.props[prop] !== undefined) {
- overrides[prop] = this.props[prop];
- }
- }
-
- overrides.accessibilityState = {
- disabled: this.props.disabled,
- ...this.props.accessibilityState
- };
-
- return (React: any).cloneElement(child, {
- ...overrides,
- accessible: this.props.accessible !== false,
- //clickable:
- // this.props.clickable !== false && this.props.onPress !== undefined,
- //onClick: this.touchableHandlePress,
- onKeyDown: this.touchableHandleKeyEvent,
- onKeyUp: this.touchableHandleKeyEvent,
- onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder,
- onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest,
- onResponderGrant: this.touchableHandleResponderGrant,
- onResponderMove: this.touchableHandleResponderMove,
- onResponderRelease: this.touchableHandleResponderRelease,
- onResponderTerminate: this.touchableHandleResponderTerminate,
- children
- });
}
-}): any): React.ComponentType);
-export default TouchableWithoutFeedback;
+ return React.cloneElement(element, elementProps, ...children);
+}
+
+const MemoedTouchableWithoutFeedback = React.memo(React.forwardRef(TouchableWithoutFeedback));
+MemoedTouchableWithoutFeedback.displayName = 'TouchableWithoutFeedback';
+
+export default (MemoedTouchableWithoutFeedback: React.AbstractComponent<
+ Props,
+ React.ElementRef
+>);
diff --git a/packages/react-native-web/src/index.js b/packages/react-native-web/src/index.js
index 9e584ae6..9248a3e0 100644
--- a/packages/react-native-web/src/index.js
+++ b/packages/react-native-web/src/index.js
@@ -40,6 +40,7 @@ export { default as ImageBackground } from './exports/ImageBackground';
export { default as KeyboardAvoidingView } from './exports/KeyboardAvoidingView';
export { default as Modal } from './exports/Modal';
export { default as Picker } from './exports/Picker';
+export { default as Pressable } from './exports/Pressable';
export { default as ProgressBar } from './exports/ProgressBar';
export { default as RefreshControl } from './exports/RefreshControl';
export { default as SafeAreaView } from './exports/SafeAreaView';
diff --git a/packages/react-native-web/src/modules/PressResponder/index.js b/packages/react-native-web/src/modules/PressResponder/index.js
new file mode 100644
index 00000000..bbc6d8b1
--- /dev/null
+++ b/packages/react-native-web/src/modules/PressResponder/index.js
@@ -0,0 +1,548 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict-local
+ * @format
+ */
+
+'use strict';
+
+import invariant from 'fbjs/lib/invariant';
+
+type ClickEvent = any;
+type KeyboardEvent = any;
+type ResponderEvent = any;
+
+export type PressResponderConfig = $ReadOnly<{|
+ // The gesture can be interrupted by a parent gesture, e.g., scroll.
+ // Defaults to true.
+ cancelable?: ?boolean,
+ // Whether to disable initialization of the press gesture.
+ disabled?: ?boolean,
+ // Duration (in addition to `delayPressStart`) after which a press gesture is
+ // considered a long press gesture. Defaults to 500 (milliseconds).
+ delayLongPress?: ?number,
+ // Duration to wait after press down before calling `onPressStart`.
+ delayPressStart?: ?number,
+ // Duration to wait after letting up before calling `onPressEnd`.
+ delayPressEnd?: ?number,
+ // Called when a long press gesture has been triggered.
+ onLongPress?: ?(event: ResponderEvent) => void,
+ // Called when a press gestute has been triggered.
+ onPress?: ?(event: ClickEvent) => void,
+ // Called when the press is activated to provide visual feedback.
+ onPressChange?: ?(event: ResponderEvent) => void,
+ // Called when the press is activated to provide visual feedback.
+ onPressStart?: ?(event: ResponderEvent) => void,
+ // Called when the press location moves. (This should rarely be used.)
+ onPressMove?: ?(event: ResponderEvent) => void,
+ // Called when the press is deactivated to undo visual feedback.
+ onPressEnd?: ?(event: ResponderEvent) => void
+|}>;
+
+export type EventHandlers = $ReadOnly<{|
+ onClick: (event: ClickEvent) => void,
+ onContextMenu: (event: ClickEvent) => void,
+ onKeyDown: (event: KeyboardEvent) => void,
+ onKeyUp: (event: KeyboardEvent) => void,
+ onResponderGrant: (event: ResponderEvent) => void,
+ onResponderMove: (event: ResponderEvent) => void,
+ onResponderRelease: (event: ResponderEvent) => void,
+ onResponderTerminate: (event: ResponderEvent) => void,
+ onResponderTerminationRequest: (event: ResponderEvent) => boolean,
+ onStartShouldSetResponder: (event: ResponderEvent) => boolean
+|}>;
+
+type TouchState =
+ | 'NOT_RESPONDER'
+ | 'RESPONDER_INACTIVE_PRESS_START'
+ | 'RESPONDER_ACTIVE_PRESS_START'
+ | 'RESPONDER_ACTIVE_LONG_PRESS_START'
+ | 'ERROR';
+
+type TouchSignal =
+ | 'DELAY'
+ | 'RESPONDER_GRANT'
+ | 'RESPONDER_RELEASE'
+ | 'RESPONDER_TERMINATED'
+ | 'LONG_PRESS_DETECTED';
+
+const DELAY = 'DELAY';
+const ERROR = 'ERROR';
+const LONG_PRESS_DETECTED = 'LONG_PRESS_DETECTED';
+const NOT_RESPONDER = 'NOT_RESPONDER';
+const RESPONDER_ACTIVE_LONG_PRESS_START = 'RESPONDER_ACTIVE_LONG_PRESS_START';
+const RESPONDER_ACTIVE_PRESS_START = 'RESPONDER_ACTIVE_PRESS_START';
+const RESPONDER_INACTIVE_PRESS_START = 'RESPONDER_INACTIVE_PRESS_START';
+const RESPONDER_GRANT = 'RESPONDER_GRANT';
+const RESPONDER_RELEASE = 'RESPONDER_RELEASE';
+const RESPONDER_TERMINATED = 'RESPONDER_TERMINATED';
+
+const Transitions = Object.freeze({
+ NOT_RESPONDER: {
+ DELAY: ERROR,
+ RESPONDER_GRANT: RESPONDER_INACTIVE_PRESS_START,
+ RESPONDER_RELEASE: ERROR,
+ RESPONDER_TERMINATED: ERROR,
+ LONG_PRESS_DETECTED: ERROR
+ },
+ RESPONDER_INACTIVE_PRESS_START: {
+ DELAY: RESPONDER_ACTIVE_PRESS_START,
+ RESPONDER_GRANT: ERROR,
+ RESPONDER_RELEASE: NOT_RESPONDER,
+ RESPONDER_TERMINATED: NOT_RESPONDER,
+ LONG_PRESS_DETECTED: ERROR
+ },
+ RESPONDER_ACTIVE_PRESS_START: {
+ DELAY: ERROR,
+ RESPONDER_GRANT: ERROR,
+ RESPONDER_RELEASE: NOT_RESPONDER,
+ RESPONDER_TERMINATED: NOT_RESPONDER,
+ LONG_PRESS_DETECTED: RESPONDER_ACTIVE_LONG_PRESS_START
+ },
+ RESPONDER_ACTIVE_LONG_PRESS_START: {
+ DELAY: ERROR,
+ RESPONDER_GRANT: ERROR,
+ RESPONDER_RELEASE: NOT_RESPONDER,
+ RESPONDER_TERMINATED: NOT_RESPONDER,
+ LONG_PRESS_DETECTED: RESPONDER_ACTIVE_LONG_PRESS_START
+ },
+ ERROR: {
+ DELAY: NOT_RESPONDER,
+ RESPONDER_GRANT: RESPONDER_INACTIVE_PRESS_START,
+ RESPONDER_RELEASE: NOT_RESPONDER,
+ RESPONDER_TERMINATED: NOT_RESPONDER,
+ LONG_PRESS_DETECTED: NOT_RESPONDER
+ }
+});
+
+const isActiveSignal = signal =>
+ signal === RESPONDER_ACTIVE_PRESS_START || signal === RESPONDER_ACTIVE_LONG_PRESS_START;
+
+const isPressStartSignal = signal =>
+ signal === RESPONDER_INACTIVE_PRESS_START ||
+ signal === RESPONDER_ACTIVE_PRESS_START ||
+ signal === RESPONDER_ACTIVE_LONG_PRESS_START;
+
+const isTerminalSignal = signal => signal === RESPONDER_TERMINATED || signal === RESPONDER_RELEASE;
+
+const DEFAULT_LONG_PRESS_DELAY_MS = 450; // 500 - 50
+const DEFAULT_PRESS_DELAY_MS = 50;
+
+/**
+ * =========================== PressResponder Tutorial ===========================
+ *
+ * The `PressResponder` class helps you create press interactions by analyzing the
+ * geometry of elements and observing when another responder (e.g. ScrollView)
+ * has stolen the touch lock. It offers hooks for your component to provide
+ * interaction feedback to the user:
+ *
+ * - When a press has activated (e.g. highlight an element)
+ * - When a press has deactivated (e.g. un-highlight an element)
+ * - When a press sould trigger an action, meaning it activated and deactivated
+ * while within the geometry of the element without the lock being stolen.
+ *
+ * A high quality interaction isn't as simple as you might think. There should
+ * be a slight delay before activation. Moving your finger beyond an element's
+ * bounds should trigger deactivation, but moving the same finger back within an
+ * element's bounds should trigger reactivation.
+ *
+ * In order to use `PressResponder`, do the following:
+ *
+ * const pressResponder = new PressResponder(config);
+ *
+ * 2. Choose the rendered component who should collect the press events. On that
+ * element, spread `pressability.getEventHandlers()` into its props.
+ *
+ * return (
+ *
+ * );
+ *
+ * 3. Reset `PressResponder` when your component unmounts.
+ *
+ * componentWillUnmount() {
+ * this.state.pressResponder.reset();
+ * }
+ *
+ * ==================== Implementation Details ====================
+ *
+ * `PressResponder` only assumes that there exists a `HitRect` node. The `PressRect`
+ * is an abstract box that is extended beyond the `HitRect`.
+ *
+ * # Geometry
+ *
+ * ┌────────────────────────┐
+ * │ ┌──────────────────┐ │ - Presses start anywhere within `HitRect`.
+ * │ │ ┌────────────┐ │ │
+ * │ │ │ VisualRect │ │ │
+ * │ │ └────────────┘ │ │ - When pressed down for sufficient amount of time
+ * │ │ HitRect │ │ before letting up, `VisualRect` activates.
+ * │ └──────────────────┘ │
+ * │ Out Region o │
+ * └────────────────────│───┘
+ * └────── When the press is released outside the `HitRect`,
+ * the responder is NOT eligible for a "press".
+ *
+ * # State Machine
+ *
+ * ┌───────────────┐ ◀──── RESPONDER_RELEASE
+ * │ NOT_RESPONDER │
+ * └───┬───────────┘ ◀──── RESPONDER_TERMINATED
+ * │
+ * │ RESPONDER_GRANT (HitRect)
+ * │
+ * ▼
+ * ┌─────────────────────┐ ┌───────────────────┐ ┌───────────────────┐
+ * │ RESPONDER_INACTIVE_ │ DELAY │ RESPONDER_ACTIVE_ │ T + DELAY │ RESPONDER_ACTIVE_ │
+ * │ PRESS_START ├────────▶ │ PRESS_START ├────────────▶ │ LONG_PRESS_START │
+ * └─────────────────────┘ └───────────────────┘ └───────────────────┘
+ *
+ * T + DELAY => LONG_PRESS_DELAY + DELAY
+ *
+ * Not drawn are the side effects of each transition. The most important side
+ * effect is the invocation of `onLongPress`. Only when the browser produces a
+ * `click` event is `onPress` invoked.
+ */
+export default class PressResponder {
+ _config: PressResponderConfig;
+ _eventHandlers: ?EventHandlers = null;
+ _isPointerTouch: ?boolean = false;
+ _longPressDelayTimeout: ?TimeoutID = null;
+ _longPressDispatched: ?boolean = false;
+ _pressDelayTimeout: ?TimeoutID = null;
+ _pressOutDelayTimeout: ?TimeoutID = null;
+ _responderID: ?any;
+ _touchActivatePosition: ?$ReadOnly<{|
+ pageX: number,
+ pageY: number
+ |}>;
+ _touchState: TouchState = NOT_RESPONDER;
+
+ constructor(config: PressResponderConfig) {
+ this.configure(config);
+ }
+
+ configure(config: PressResponderConfig): void {
+ this._config = config;
+ }
+
+ /**
+ * Resets any pending timers. This should be called on unmount.
+ */
+ reset(): void {
+ this._cancelLongPressDelayTimeout();
+ this._cancelPressDelayTimeout();
+ this._cancelPressOutDelayTimeout();
+ }
+
+ /**
+ * Returns a set of props to spread into the interactive element.
+ */
+ getEventHandlers(): EventHandlers {
+ if (this._eventHandlers == null) {
+ this._eventHandlers = this._createEventHandlers();
+ }
+ return this._eventHandlers;
+ }
+
+ _createEventHandlers(): EventHandlers {
+ const start = (event: ResponderEvent, shouldDelay?: boolean): void => {
+ event.persist();
+
+ this._cancelPressOutDelayTimeout();
+
+ this._longPressDispatched = false;
+ this._responderID = event.currentTarget;
+ this._touchState = NOT_RESPONDER;
+ this._isPointerTouch = event.nativeEvent.type === 'touchstart';
+
+ this._receiveSignal(RESPONDER_GRANT, event);
+
+ const delayPressStart = normalizeDelay(
+ this._config.delayPressStart,
+ 0,
+ DEFAULT_PRESS_DELAY_MS
+ );
+
+ if (shouldDelay !== false && delayPressStart > 0) {
+ this._pressDelayTimeout = setTimeout(() => {
+ this._receiveSignal(DELAY, event);
+ }, delayPressStart);
+ } else {
+ this._receiveSignal(DELAY, event);
+ }
+
+ const delayLongPress = normalizeDelay(
+ this._config.delayLongPress,
+ 10,
+ DEFAULT_LONG_PRESS_DELAY_MS
+ );
+ this._longPressDelayTimeout = setTimeout(() => {
+ this._handleLongPress(event);
+ }, delayLongPress + delayPressStart);
+ };
+
+ const end = (event: ResponderEvent): void => {
+ this._receiveSignal(RESPONDER_RELEASE, event);
+ };
+
+ return {
+ onStartShouldSetResponder: (): boolean => {
+ const { disabled } = this._config;
+ if (disabled == null) {
+ return true;
+ }
+ return !disabled;
+ },
+
+ onKeyDown: event => {
+ if (this._touchState === NOT_RESPONDER) {
+ if (event.key === ' ' || event.key === 'Enter') {
+ start(event, false);
+ }
+ }
+ if (this._responderID) {
+ event.stopPropagation();
+ }
+ },
+
+ onKeyUp: event => {
+ if (event.key === ' ' || event.key === 'Enter') {
+ end(event);
+ }
+ event.stopPropagation();
+ },
+
+ onResponderGrant: event => start(event),
+
+ onResponderMove: event => {
+ if (this._config.onPressMove != null) {
+ this._config.onPressMove(event);
+ }
+ const touch = getTouchFromResponderEvent(event);
+ 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();
+ }
+ }
+ },
+
+ onResponderRelease: event => end(event),
+
+ onResponderTerminate: event => {
+ this._receiveSignal(RESPONDER_TERMINATED, event);
+ },
+
+ onResponderTerminationRequest: (event): boolean => {
+ const { cancelable, disabled, onLongPress } = this._config;
+ // If `onLongPress` is provided, don't terminate on `contextmenu` as default
+ // behavior will be prevented for non-mouse pointers.
+ if (
+ !disabled &&
+ onLongPress != null &&
+ this._isPointerTouch &&
+ event.nativeEvent.type === 'contextmenu'
+ ) {
+ return false;
+ }
+ if (cancelable == null) {
+ return true;
+ }
+ return cancelable;
+ },
+
+ // NOTE: this diverges from react-native@0.62 in 2 significant ways
+ // * The `onPress` callback is not connected to the responder system (the native
+ // `click` event must be used but is dispatched in many scenarios where no pointers
+ // are on the screen.) Therefore, it's possible for `onPress` to be called without
+ // `onPress{Start,End}` being called first.
+ // * The `onPress` callback is only be called on the first ancestor of the native
+ // `click` target that is using the PressResponder.
+ // * The event's `nativeEvent` is a `MouseEvent` not a `TouchEvent`.
+ onClick: (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 `onLongPress` is provided and a touch pointer is being used, prevent the
+ // default context menu from opening.
+ onContextMenu: (event: any): void => {
+ const { disabled, onLongPress } = this._config;
+ if (!disabled && onLongPress != null && this._isPointerTouch && !event.defaultPrevented) {
+ event.preventDefault();
+ }
+ }
+ };
+ }
+
+ /**
+ * Receives a state machine signal, performs side effects of the transition
+ * and stores the new state. Validates the transition as well.
+ */
+ _receiveSignal(signal: TouchSignal, event: ResponderEvent): void {
+ const prevState = this._touchState;
+ let nextState = null;
+ if (Transitions[prevState] != null) {
+ nextState = Transitions[prevState][signal];
+ }
+ if (this._responderID == null && signal === RESPONDER_RELEASE) {
+ return;
+ }
+ invariant(
+ nextState != null && nextState !== ERROR,
+ 'PressResponder: Invalid signal `%s` for state `%s` on responder: %s',
+ signal,
+ prevState,
+ this._responderID
+ );
+ if (prevState !== nextState) {
+ this._performTransitionSideEffects(prevState, nextState, signal, event);
+ this._touchState = nextState;
+ }
+ }
+
+ /**
+ * Performs a transition between touchable states and identify any activations
+ * or deactivations (and callback invocations).
+ */
+ _performTransitionSideEffects(
+ prevState: TouchState,
+ nextState: TouchState,
+ signal: TouchSignal,
+ event: ResponderEvent
+ ): void {
+ if (isTerminalSignal(signal)) {
+ this._isPointerTouch = false;
+ this._touchActivatePosition = null;
+ this._cancelLongPressDelayTimeout();
+ }
+
+ if (isPressStartSignal(prevState) && signal === LONG_PRESS_DETECTED) {
+ const { onLongPress } = this._config;
+ if (onLongPress != null) {
+ onLongPress(event);
+ this._longPressDispatched = true;
+ }
+ }
+
+ const isPrevActive = isActiveSignal(prevState);
+ const isNextActive = isActiveSignal(nextState);
+
+ if (!isPrevActive && isNextActive) {
+ this._activate(event);
+ } else if (isPrevActive && !isNextActive) {
+ this._deactivate(event);
+ }
+
+ if (isPressStartSignal(prevState) && signal === RESPONDER_RELEASE) {
+ const { onLongPress, onPress } = this._config;
+ if (onPress != null) {
+ const isPressCanceledByLongPress =
+ onLongPress != null && prevState === RESPONDER_ACTIVE_LONG_PRESS_START;
+ if (!isPressCanceledByLongPress) {
+ // If we never activated (due to delays), activate and deactivate now.
+ if (!isNextActive && !isPrevActive) {
+ this._activate(event);
+ this._deactivate(event);
+ }
+ }
+ }
+ }
+
+ this._cancelPressDelayTimeout();
+ }
+
+ _activate(event: ResponderEvent): void {
+ const { onPressChange, onPressStart } = this._config;
+ const touch = getTouchFromResponderEvent(event);
+ this._touchActivatePosition = {
+ pageX: touch.pageX,
+ pageY: touch.pageY
+ };
+ if (onPressStart != null) {
+ onPressStart(event);
+ }
+ if (onPressChange != null) {
+ onPressChange(true);
+ }
+ }
+
+ _deactivate(event: ResponderEvent): void {
+ const { onPressChange, onPressEnd } = this._config;
+ function end() {
+ if (onPressEnd != null) {
+ onPressEnd(event);
+ }
+ if (onPressChange != null) {
+ onPressChange(false);
+ }
+ }
+ const delayPressEnd = normalizeDelay(this._config.delayPressEnd);
+ if (delayPressEnd > 0) {
+ this._pressOutDelayTimeout = setTimeout(() => {
+ end();
+ }, delayPressEnd);
+ } else {
+ end();
+ }
+ }
+
+ _handleLongPress(event: ResponderEvent): void {
+ if (
+ this._touchState === RESPONDER_ACTIVE_PRESS_START ||
+ this._touchState === RESPONDER_ACTIVE_LONG_PRESS_START
+ ) {
+ this._receiveSignal(LONG_PRESS_DETECTED, event);
+ }
+ }
+
+ _cancelLongPressDelayTimeout(): void {
+ if (this._longPressDelayTimeout != null) {
+ clearTimeout(this._longPressDelayTimeout);
+ this._longPressDelayTimeout = null;
+ }
+ }
+
+ _cancelPressDelayTimeout(): void {
+ if (this._pressDelayTimeout != null) {
+ clearTimeout(this._pressDelayTimeout);
+ this._pressDelayTimeout = null;
+ }
+ }
+
+ _cancelPressOutDelayTimeout(): void {
+ if (this._pressOutDelayTimeout != null) {
+ clearTimeout(this._pressOutDelayTimeout);
+ this._pressOutDelayTimeout = null;
+ }
+ }
+}
+
+function normalizeDelay(delay: ?number, min = 0, fallback = 0): number {
+ return Math.max(min, delay ?? fallback);
+}
+
+function getTouchFromResponderEvent(event: ResponderEvent) {
+ const { changedTouches, touches } = event.nativeEvent;
+ if (touches != null && touches.length > 0) {
+ return touches[0];
+ }
+ if (changedTouches != null && changedTouches.length > 0) {
+ return changedTouches[0];
+ }
+ return event.nativeEvent;
+}
diff --git a/packages/react-native-web/src/modules/PressResponder/usePressEvents.js b/packages/react-native-web/src/modules/PressResponder/usePressEvents.js
new file mode 100644
index 00000000..029422b2
--- /dev/null
+++ b/packages/react-native-web/src/modules/PressResponder/usePressEvents.js
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict-local
+ * @format
+ */
+
+'use strict';
+
+import PressResponder, { type EventHandlers, type PressResponderConfig } from './index';
+import { useEffect, useRef } from 'react';
+
+export default function usePressEvents(hostRef: any, config: PressResponderConfig): EventHandlers {
+ const pressResponderRef = useRef(null);
+ if (pressResponderRef.current == null) {
+ pressResponderRef.current = new PressResponder(config);
+ }
+ const pressResponder = pressResponderRef.current;
+
+ // Re-configure to use the current node and configuration.
+ useEffect(() => {
+ pressResponder.configure(config);
+ }, [config, pressResponder]);
+
+ // Reset the `pressResponder` when cleanup needs to occur. This is
+ // a separate effect because we do not want to rest the responder when `config` changes.
+ useEffect(() => {
+ return () => {
+ pressResponder.reset();
+ };
+ }, [pressResponder]);
+
+ return pressResponder.getEventHandlers();
+}