Skip to content

Commit 3212f7d

Browse files
elicwhitefacebook-github-bot
authored andcommitted
Release Pressable!
Summary: *Pressable* is a component which is intended to replace the Touchable* components such as *TouchableWithoutFeedback* and *TouchableOpacity*. The motivation is to make it easier to create custom visual touch feedback so that React Native apps are not easily identified by the “signature opacity fade” touch feedback. We see this component as eventually deprecating all of the existing Touchable components. Changelog: [Added][General] New <Pressable> Component to make it easier to create touchable elements Reviewed By: yungsters Differential Revision: D19674480 fbshipit-source-id: 765d657f023caea459f02da25376e4d5a2efff8b
1 parent 6239ace commit 3212f7d

File tree

8 files changed

+852
-0
lines changed

8 files changed

+852
-0
lines changed
+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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 * as React from 'react';
14+
import {useMemo, useState, useRef, useImperativeHandle} from 'react';
15+
import useAndroidRippleForView from './useAndroidRippleForView.js';
16+
import type {
17+
AccessibilityActionEvent,
18+
AccessibilityActionInfo,
19+
AccessibilityRole,
20+
AccessibilityState,
21+
AccessibilityValue,
22+
} from '../View/ViewAccessibility.js';
23+
import usePressability from '../../Pressability/usePressability.js';
24+
import {normalizeRect, type RectOrSize} from '../../StyleSheet/Rect.js';
25+
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes.js';
26+
import type {LayoutEvent, PressEvent} from '../../Types/CoreEventTypes.js';
27+
import View from '../View/View';
28+
29+
type ViewStyleProp = $ElementType<React.ElementConfig<typeof View>, 'style'>;
30+
31+
export type StateCallbackType = $ReadOnly<{|
32+
pressed: boolean,
33+
|}>;
34+
35+
type Props = $ReadOnly<{|
36+
/**
37+
* Accessibility.
38+
*/
39+
accessibilityActions?: ?$ReadOnlyArray<AccessibilityActionInfo>,
40+
accessibilityElementsHidden?: ?boolean,
41+
accessibilityHint?: ?Stringish,
42+
accessibilityIgnoresInvertColors?: ?boolean,
43+
accessibilityLabel?: ?Stringish,
44+
accessibilityLiveRegion?: ?('none' | 'polite' | 'assertive'),
45+
accessibilityRole?: ?AccessibilityRole,
46+
accessibilityState?: ?AccessibilityState,
47+
accessibilityValue?: ?AccessibilityValue,
48+
accessibilityViewIsModal?: ?boolean,
49+
accessible?: ?boolean,
50+
focusable?: ?boolean,
51+
importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'),
52+
onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed,
53+
54+
/**
55+
* Either children or a render prop that receives a boolean reflecting whether
56+
* the component is currently pressed.
57+
*/
58+
children: React.Node | ((state: StateCallbackType) => React.Node),
59+
60+
/**
61+
* Duration (in milliseconds) from `onPressIn` before `onLongPress` is called.
62+
*/
63+
delayLongPress?: ?number,
64+
65+
/**
66+
* Whether the press behavior is disabled.
67+
*/
68+
disabled?: ?boolean,
69+
70+
/**
71+
* Additional distance outside of this view in which a press is detected.
72+
*/
73+
hitSlop?: ?RectOrSize,
74+
75+
/**
76+
* Additional distance outside of this view in which a touch is considered a
77+
* press before `onPressOut` is triggered.
78+
*/
79+
pressRectOffset?: ?RectOrSize,
80+
81+
/**
82+
* Called when this view's layout changes.
83+
*/
84+
onLayout?: ?(event: LayoutEvent) => void,
85+
86+
/**
87+
* Called when a long-tap gesture is detected.
88+
*/
89+
onLongPress?: ?(event: PressEvent) => void,
90+
91+
/**
92+
* Called when a single tap gesture is detected.
93+
*/
94+
onPress?: ?(event: PressEvent) => void,
95+
96+
/**
97+
* Called when a touch is engaged before `onPress`.
98+
*/
99+
onPressIn?: ?(event: PressEvent) => void,
100+
101+
/**
102+
* Called when a touch is released before `onPress`.
103+
*/
104+
onPressOut?: ?(event: PressEvent) => void,
105+
106+
/**
107+
* Either view styles or a function that receives a boolean reflecting whether
108+
* the component is currently pressed and returns view styles.
109+
*/
110+
style?: ViewStyleProp | ((state: StateCallbackType) => ViewStyleProp),
111+
112+
/**
113+
* Identifier used to find this view in tests.
114+
*/
115+
testID?: ?string,
116+
117+
/**
118+
* If true, doesn't play system sound on touch.
119+
*/
120+
android_disableSound?: ?boolean,
121+
122+
/**
123+
* Enables the Android ripple effect and configures its color.
124+
*/
125+
android_rippleColor?: ?ColorValue,
126+
127+
/**
128+
* Used only for documentation or testing (e.g. snapshot testing).
129+
*/
130+
testOnly_pressed?: ?boolean,
131+
|}>;
132+
133+
/**
134+
* Component used to build display components that should respond to whether the
135+
* component is currently pressed or not.
136+
*/
137+
function Pressable(props: Props, forwardedRef): React.Node {
138+
const {
139+
accessible,
140+
android_disableSound,
141+
android_rippleColor,
142+
children,
143+
delayLongPress,
144+
disabled,
145+
focusable,
146+
onLongPress,
147+
onPress,
148+
onPressIn,
149+
onPressOut,
150+
pressRectOffset,
151+
style,
152+
testOnly_pressed,
153+
...restProps
154+
} = props;
155+
156+
const viewRef = useRef<React.ElementRef<typeof View> | null>(null);
157+
useImperativeHandle(forwardedRef, () => viewRef.current);
158+
159+
const android_ripple = useAndroidRippleForView(android_rippleColor, viewRef);
160+
161+
const [pressed, setPressed] = usePressState(testOnly_pressed === true);
162+
163+
const hitSlop = normalizeRect(props.hitSlop);
164+
165+
const config = useMemo(
166+
() => ({
167+
disabled,
168+
hitSlop,
169+
pressRectOffset,
170+
android_disableSound,
171+
delayLongPress,
172+
onLongPress,
173+
onPress,
174+
onPressIn(event: PressEvent): void {
175+
if (android_ripple != null) {
176+
android_ripple.onPressIn(event);
177+
}
178+
setPressed(true);
179+
if (onPressIn != null) {
180+
onPressIn(event);
181+
}
182+
},
183+
onPressMove: android_ripple?.onPressMove,
184+
onPressOut(event: PressEvent): void {
185+
if (android_ripple != null) {
186+
android_ripple.onPressOut(event);
187+
}
188+
setPressed(false);
189+
if (onPressOut != null) {
190+
onPressOut(event);
191+
}
192+
},
193+
}),
194+
[
195+
android_disableSound,
196+
android_ripple,
197+
delayLongPress,
198+
disabled,
199+
hitSlop,
200+
onLongPress,
201+
onPress,
202+
onPressIn,
203+
onPressOut,
204+
pressRectOffset,
205+
setPressed,
206+
],
207+
);
208+
const eventHandlers = usePressability(config);
209+
210+
return (
211+
<View
212+
{...restProps}
213+
{...eventHandlers}
214+
{...android_ripple?.viewProps}
215+
accessible={accessible !== false}
216+
focusable={focusable !== false}
217+
hitSlop={hitSlop}
218+
ref={viewRef}
219+
style={typeof style === 'function' ? style({pressed}) : style}>
220+
{typeof children === 'function' ? children({pressed}) : children}
221+
</View>
222+
);
223+
}
224+
225+
function usePressState(forcePressed: boolean): [boolean, (boolean) => void] {
226+
const [pressed, setPressed] = useState(false);
227+
return [pressed || forcePressed, setPressed];
228+
}
229+
230+
const MemodPressable = React.memo(React.forwardRef(Pressable));
231+
MemodPressable.displayName = 'Pressable';
232+
233+
export default (MemodPressable: React.AbstractComponent<
234+
Props,
235+
React.ElementRef<typeof View>,
236+
>);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
* @format
8+
* @emails oncall+react_native
9+
* @flow strict-local
10+
*/
11+
12+
'use strict';
13+
14+
import * as React from 'react';
15+
16+
import Pressable from '../Pressable';
17+
import View from '../../View/View';
18+
import {expectRendersMatchingSnapshot} from '../../../Utilities/ReactNativeTestTools';
19+
20+
describe('<Pressable />', () => {
21+
it('should render as expected', () => {
22+
expectRendersMatchingSnapshot(
23+
'Pressable',
24+
() => (
25+
<Pressable>
26+
<View />
27+
</Pressable>
28+
),
29+
() => {
30+
jest.dontMock('../Pressable');
31+
},
32+
);
33+
});
34+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`<Pressable /> should render as expected: should deep render when mocked (please verify output manually) 1`] = `
4+
<View
5+
accessible={true}
6+
focusable={true}
7+
onBlur={[Function]}
8+
onClick={[Function]}
9+
onFocus={[Function]}
10+
onResponderGrant={[Function]}
11+
onResponderMove={[Function]}
12+
onResponderRelease={[Function]}
13+
onResponderTerminate={[Function]}
14+
onResponderTerminationRequest={[Function]}
15+
onStartShouldSetResponder={[Function]}
16+
>
17+
<View />
18+
</View>
19+
`;
20+
21+
exports[`<Pressable /> should render as expected: should deep render when not mocked (please verify output manually) 1`] = `
22+
<View
23+
accessible={true}
24+
focusable={true}
25+
onBlur={[Function]}
26+
onClick={[Function]}
27+
onFocus={[Function]}
28+
onResponderGrant={[Function]}
29+
onResponderMove={[Function]}
30+
onResponderRelease={[Function]}
31+
onResponderTerminate={[Function]}
32+
onResponderTerminationRequest={[Function]}
33+
onStartShouldSetResponder={[Function]}
34+
>
35+
<View />
36+
</View>
37+
`;
38+
39+
exports[`<Pressable /> should render as expected: should shallow render as <Pressable /> when mocked 1`] = `
40+
<Memo(Pressable)>
41+
<View />
42+
</Memo(Pressable)>
43+
`;
44+
45+
exports[`<Pressable /> should render as expected: should shallow render as <Pressable /> when not mocked 1`] = `
46+
<Memo(Pressable)>
47+
<View />
48+
</Memo(Pressable)>
49+
`;

0 commit comments

Comments
 (0)