Skip to content

Commit 8e421c2

Browse files
committed
[change] Add Pressable and replace Touchables
Port and rewrite "Pressability" from React Native as "PressResponder". This integrates a press target with the responder system on web. It avoids performing layout measurement during gestures by eschewing React Native's iOS-like UX in favor of expected Web UX: a press target will look pressed until the pointer is released, even if the pointer has moved outside the bounding rect of the target. The PressResponder is used to reimplement the existing Touchables. It's expected that they will eventually be removed in favor of Pressable. Fix necolas#1583 Fix necolas#1564 Fix necolas#1534 Fix necolas#1419 Fix necolas#1219 Fix necolas#1166
1 parent beb6328 commit 8e421c2

File tree

7 files changed

+1158
-714
lines changed

7 files changed

+1158
-714
lines changed

src/exports/Pressable/index.js

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
import type { PressResponderConfig } from '../../modules/PressResponder';
14+
import type { ViewProps } from '../View';
15+
16+
import * as React from 'react';
17+
import { useMemo, useState, useRef, useImperativeHandle } from 'react';
18+
import usePressEvents from '../../modules/PressResponder/usePressEvents';
19+
import View from '../View';
20+
21+
export type StateCallbackType = $ReadOnly<{|
22+
focused: boolean,
23+
pressed: boolean
24+
|}>;
25+
26+
type ViewStyleProp = $PropertyType<ViewProps, 'style'>;
27+
28+
type Props = $ReadOnly<{|
29+
accessibilityLabel?: $PropertyType<ViewProps, 'accessibilityLabel'>,
30+
accessibilityLiveRegion?: $PropertyType<ViewProps, 'accessibilityLiveRegion'>,
31+
accessibilityRole?: $PropertyType<ViewProps, 'accessibilityRole'>,
32+
accessibilityState?: $PropertyType<ViewProps, 'accessibilityState'>,
33+
accessibilityValue?: $PropertyType<ViewProps, 'accessibilityValue'>,
34+
accessible?: $PropertyType<ViewProps, 'accessible'>,
35+
focusable?: ?boolean,
36+
importantForAccessibility?: $PropertyType<ViewProps, 'importantForAccessibility'>,
37+
children: React.Node | ((state: StateCallbackType) => React.Node),
38+
// Duration (in milliseconds) from `onPressIn` before `onLongPress` is called.
39+
delayLongPress?: ?number,
40+
// Duration (in milliseconds) from `onPressStart` is called after pointerdown
41+
delayPressIn?: ?number,
42+
// Duration (in milliseconds) from `onPressEnd` is called after pointerup.
43+
delayPressOut?: ?number,
44+
// Whether the press behavior is disabled.
45+
disabled?: ?boolean,
46+
// Additional distance outside of this view in which a press is detected.
47+
hitSlop?: $PropertyType<ViewProps, 'hitSlop'>,
48+
// Called when the view blurs
49+
onBlur?: $PropertyType<ViewProps, 'onBlur'>,
50+
// Called when the view is focused
51+
onFocus?: $PropertyType<ViewProps, 'onFocus'>,
52+
// Called when this view's layout changes
53+
onLayout?: $PropertyType<ViewProps, 'onLayout'>,
54+
// Called when a long-tap gesture is detected.
55+
onLongPress?: $PropertyType<PressResponderConfig, 'onLongPress'>,
56+
// Called when a single tap gesture is detected.
57+
onPress?: $PropertyType<PressResponderConfig, 'onPress'>,
58+
// Called when a touch is engaged, before `onPress`.
59+
onPressIn?: $PropertyType<PressResponderConfig, 'onPressStart'>,
60+
// Called when a touch is moving, after `onPressIn`.
61+
onPressMove?: $PropertyType<PressResponderConfig, 'onPressMove'>,
62+
// Called when a touch is released, before `onPress`.
63+
onPressOut?: $PropertyType<PressResponderConfig, 'onPressEnd'>,
64+
style?: ViewStyleProp | ((state: StateCallbackType) => ViewStyleProp),
65+
testID?: $PropertyType<ViewProps, 'testID'>,
66+
/**
67+
* Used only for documentation or testing (e.g. snapshot testing).
68+
*/
69+
testOnly_pressed?: ?boolean
70+
|}>;
71+
72+
/**
73+
* Component used to build display components that should respond to whether the
74+
* component is currently pressed or not.
75+
*/
76+
function Pressable(props: Props, forwardedRef): React.Node {
77+
const {
78+
accessible,
79+
children,
80+
delayLongPress,
81+
delayPressIn,
82+
delayPressOut,
83+
disabled,
84+
focusable,
85+
onBlur,
86+
onFocus,
87+
onLongPress,
88+
onPress,
89+
onPressMove,
90+
onPressIn,
91+
onPressOut,
92+
style,
93+
testOnly_pressed,
94+
...rest
95+
} = props;
96+
97+
const hostRef = useRef(null);
98+
const viewRef = useRef<React.ElementRef<typeof View> | null>(null);
99+
const [focused, setFocused] = useForceableState(false);
100+
const [pressed, setPressed] = useForceableState(testOnly_pressed === true);
101+
useImperativeHandle(forwardedRef, () => viewRef.current);
102+
103+
const pressEventHandlers = usePressEvents(
104+
hostRef,
105+
useMemo(
106+
() => ({
107+
delayLongPress,
108+
delayPressStart: delayPressIn,
109+
delayPressEnd: delayPressOut,
110+
disabled,
111+
onLongPress,
112+
onPress,
113+
onPressChange: setPressed,
114+
onPressStart: onPressIn,
115+
onPressMove,
116+
onPressEnd: onPressOut
117+
}),
118+
[
119+
delayLongPress,
120+
delayPressIn,
121+
delayPressOut,
122+
disabled,
123+
onLongPress,
124+
onPress,
125+
onPressIn,
126+
onPressMove,
127+
onPressOut,
128+
setPressed
129+
]
130+
)
131+
);
132+
133+
const accessibilityState = { disabled, ...props.accessibilityState };
134+
const interactionState = { focused, pressed };
135+
136+
function createFocusHandler(callback, value) {
137+
return function(event) {
138+
if (event.nativeEvent.target === hostRef.current) {
139+
setFocused(value);
140+
if (callback != null) {
141+
callback(event);
142+
}
143+
}
144+
};
145+
}
146+
147+
return (
148+
<View
149+
{...rest}
150+
{...pressEventHandlers}
151+
accessibilityRole={props.accessibilityRole ?? 'button'}
152+
accessibilityState={accessibilityState}
153+
accessible={accessible !== false}
154+
focusable={focusable !== false}
155+
forwardedRef={hostRef}
156+
onBlur={createFocusHandler(onBlur, false)}
157+
onFocus={createFocusHandler(onFocus, true)}
158+
ref={viewRef}
159+
style={typeof style === 'function' ? style(interactionState) : style}
160+
>
161+
{typeof children === 'function' ? children(interactionState) : children}
162+
</View>
163+
);
164+
}
165+
166+
function useForceableState(forced: boolean): [boolean, (boolean) => void] {
167+
const [pressed, setPressed] = useState(false);
168+
return [pressed || forced, setPressed];
169+
}
170+
171+
const MemoedPressable = React.memo(React.forwardRef(Pressable));
172+
MemoedPressable.displayName = 'Pressable';
173+
174+
export default (MemoedPressable: React.AbstractComponent<Props, React.ElementRef<typeof View>>);

0 commit comments

Comments
 (0)