Skip to content

Commit 410f3ac

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 #1583 Fix #1564 Fix #1534 Fix #1419 Fix #1219 Fix #1166
1 parent 513a65a commit 410f3ac

File tree

25 files changed

+1506
-944
lines changed

25 files changed

+1506
-944
lines changed

.eslintrc

+4-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
"navigator": false,
3535
"window": false,
3636
// Flow global types,
37+
"$Diff": false,
38+
"$ElementType": false,
3739
"$Enum": false,
3840
"$PropertyType": false,
3941
"$ReadOnly": false,
@@ -48,7 +50,8 @@
4850
"ReactPropsCheckType": false,
4951
"ReactPropTypes": false,
5052
"ResizeObserver": false,
51-
"SyntheticEvent": false
53+
"SyntheticEvent": false,
54+
"TimeoutID": false,
5255
},
5356
"rules": {
5457
"camelcase": 0,

packages/babel-plugin-react-native-web/src/moduleMap.js

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ module.exports = {
3333
Picker: true,
3434
PixelRatio: true,
3535
Platform: true,
36+
Pressable: true,
3637
ProgressBar: true,
3738
RefreshControl: true,
3839
SafeAreaView: true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Meta, Props, Story, Preview } from '@storybook/addon-docs/blocks';
2+
import * as Stories from './examples';
3+
4+
<Meta title="Components|Pressable" />
5+
6+
# Pressable
7+
8+
...
9+
10+
## Props
11+
12+
| Name | Type | Default |
13+
| ------------------------- | --------- | ------- |
14+
| ...ViewProps | | |
15+
| delayLongPress | ?number | 500 |
16+
| delayPressIn | ?number | 0 |
17+
| delayPressOut | ?number | 0 |
18+
| disabled | ?boolean | false |
19+
| onLongPress | ?Function | |
20+
| onPress | ?Function | |
21+
| onPressIn | ?Function | |
22+
| onPressOut | ?Function | |
23+
24+
### delayLongPress
25+
26+
Delay in ms, from `onPressIn` to before `onLongPress` is called. The default is `500`.
27+
28+
### delayPressIn
29+
30+
Delay in ms, from pointer down to before `onPressIn` is called.
31+
32+
### delayPressOut
33+
34+
Delay in ms, from pointer up to before `onPressOut` is called.
35+
36+
### disabled
37+
38+
Disables all pointer interactions with the element.
39+
40+
<Preview withSource='none'>
41+
<Story name="disabled">
42+
<Stories.disabled />
43+
</Story>
44+
</Preview>
45+
46+
### onLongPress
47+
48+
Called when the pointer is held down for as long as the value of `delayLongPress`.
49+
50+
### onPress
51+
52+
Called when the pointer is released, but not if cancelled (e.g. by a scroll that steals the responder lock).
53+
54+
## Examples
55+
56+
<Preview withSource='none'>
57+
<Story name="delayEvents">
58+
<Stories.delayEvents />
59+
</Story>
60+
</Preview>
61+
62+
<Preview withSource='none'>
63+
<Story name="feedbackEvents">
64+
<Stories.feedbackEvents />
65+
</Story>
66+
</Preview>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from 'react';
2+
import { StyleSheet, Text, Pressable, View } from 'react-native';
3+
4+
export default function DelayEvents() {
5+
const [eventLog, updateEventLog] = React.useState([]);
6+
7+
const handlePress = eventName => {
8+
return () => {
9+
const limit = 6;
10+
updateEventLog(state => {
11+
const nextState = state.slice(0, limit - 1);
12+
nextState.unshift(eventName);
13+
return nextState;
14+
});
15+
};
16+
};
17+
18+
return (
19+
<View>
20+
<View>
21+
<Pressable
22+
delayLongPress={800}
23+
delayPressIn={400}
24+
delayPressOut={1000}
25+
onLongPress={handlePress('longPress: 800ms delay')}
26+
onPress={handlePress('press')}
27+
onPressIn={handlePress('pressIn: 400ms delay')}
28+
onPressOut={handlePress('pressOut: 1000ms delay')}
29+
>
30+
<View>
31+
<Text style={styles.touchableText}>Pressable</Text>
32+
</View>
33+
</Pressable>
34+
</View>
35+
<View style={styles.eventLogBox}>
36+
{eventLog.map((e, ii) => (
37+
<Text key={ii}>{e}</Text>
38+
))}
39+
</View>
40+
</View>
41+
);
42+
}
43+
44+
const styles = StyleSheet.create({
45+
touchableText: {
46+
borderRadius: 8,
47+
padding: 5,
48+
borderWidth: 1,
49+
borderColor: 'black',
50+
color: '#007AFF',
51+
borderStyle: 'solid',
52+
textAlign: 'center'
53+
},
54+
eventLogBox: {
55+
padding: 10,
56+
marginTop: 10,
57+
height: 120,
58+
borderWidth: StyleSheet.hairlineWidth,
59+
borderColor: '#f0f0f0',
60+
backgroundColor: '#f9f9f9'
61+
}
62+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
import { StyleSheet, View, Text, Pressable } from 'react-native';
3+
4+
const action = msg => () => {
5+
console.log(msg);
6+
};
7+
8+
export default function Disabled() {
9+
return (
10+
<View>
11+
<Pressable disabled={true} onPress={action('Pressable')}>
12+
<View style={[styles.row, styles.block]}>
13+
<Text style={styles.disabledButton}>Disabled Pressable</Text>
14+
</View>
15+
</Pressable>
16+
17+
<Pressable disabled={false} onPress={action('Pressable')}>
18+
<View style={[styles.row, styles.block]}>
19+
<Text style={styles.button}>Enabled Pressable</Text>
20+
</View>
21+
</Pressable>
22+
</View>
23+
);
24+
}
25+
26+
const styles = StyleSheet.create({
27+
row: {
28+
justifyContent: 'center',
29+
flexDirection: 'row'
30+
},
31+
block: {
32+
padding: 10
33+
}
34+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React from 'react';
2+
import { Pressable, StyleSheet, Text, View } from 'react-native';
3+
4+
export default function FeedbackEvents() {
5+
const [eventLog, updateEventLog] = React.useState([]);
6+
7+
const handlePress = eventName => {
8+
return () => {
9+
const limit = 6;
10+
updateEventLog(state => {
11+
const nextState = state.slice(0, limit - 1);
12+
nextState.unshift(eventName);
13+
return nextState;
14+
});
15+
};
16+
};
17+
18+
return (
19+
<View>
20+
<View>
21+
<Pressable
22+
onLongPress={handlePress('longPress')}
23+
onPress={handlePress('press')}
24+
onPressIn={handlePress('pressIn')}
25+
onPressOut={handlePress('pressOut')}
26+
>
27+
<View>
28+
<Text style={styles.touchableText}>Press Me</Text>
29+
</View>
30+
</Pressable>
31+
</View>
32+
33+
<View>
34+
<Pressable
35+
accessibilityRole="none"
36+
onLongPress={handlePress('longPress')}
37+
onPress={handlePress('press')}
38+
onPressIn={handlePress('pressIn')}
39+
onPressOut={handlePress('pressOut')}
40+
style={({ pressed, focused }) => ({
41+
padding: 10,
42+
margin: 10,
43+
borderWidth: 1,
44+
borderColor: focused ? 'blue' : null,
45+
backgroundColor: pressed ? 'lightblue' : 'white'
46+
})}
47+
>
48+
<Pressable
49+
accessibilityRole="none"
50+
onLongPress={handlePress('longPress - inner')}
51+
onPress={handlePress('press - inner')}
52+
onPressIn={handlePress('pressIn - inner')}
53+
onPressOut={handlePress('pressOut - inner')}
54+
style={({ pressed, focused }) => ({
55+
padding: 10,
56+
margin: 10,
57+
borderWidth: 1,
58+
borderColor: focused ? 'blue' : null,
59+
backgroundColor: pressed ? 'lightblue' : 'white'
60+
})}
61+
>
62+
<Text>Nested pressables</Text>
63+
</Pressable>
64+
</Pressable>
65+
</View>
66+
67+
<View style={styles.eventLogBox}>
68+
{eventLog.map((e, ii) => (
69+
<Text key={ii}>{e}</Text>
70+
))}
71+
</View>
72+
</View>
73+
);
74+
}
75+
76+
const styles = StyleSheet.create({
77+
touchableText: {
78+
borderRadius: 8,
79+
padding: 5,
80+
borderWidth: 1,
81+
borderColor: 'black',
82+
color: '#007AFF',
83+
borderStyle: 'solid',
84+
textAlign: 'center'
85+
},
86+
eventLogBox: {
87+
padding: 10,
88+
marginTop: 10,
89+
height: 120,
90+
borderWidth: StyleSheet.hairlineWidth,
91+
borderColor: '#f0f0f0',
92+
backgroundColor: '#f9f9f9'
93+
}
94+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default as delayEvents } from './DelayEvents';
2+
export { default as disabled } from './Disabled';
3+
export { default as feedbackEvents } from './FeedbackEvents';

packages/docs/src/components/ScrollView/helpers.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
2+
import { StyleSheet, Text, TouchableOpacity } from 'react-native';
33

44
export const Button = ({ label, onPress }) => (
55
<TouchableOpacity onPress={onPress} style={styles.button}>
@@ -9,9 +9,9 @@ export const Button = ({ label, onPress }) => (
99

1010
function Item(props) {
1111
return (
12-
<View style={[styles.item, props.style]}>
12+
<TouchableOpacity style={[styles.item, props.style]}>
1313
<Text>{props.msg}</Text>
14-
</View>
14+
</TouchableOpacity>
1515
);
1616
}
1717

packages/docs/src/components/TouchableHighlight/TouchableHighlight.stories.mdx

+27
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,30 @@ Called immediately after the underlay is shown
4242
### underlayColor
4343

4444
The color of the underlay that will show through when the touch is active.
45+
46+
## Examples
47+
48+
<Preview withSource='none'>
49+
<Story name="disabled">
50+
<Stories.disabled />
51+
</Story>
52+
</Preview>
53+
54+
<Preview withSource='none'>
55+
<Story name="delayEvents">
56+
<Stories.delayEvents />
57+
</Story>
58+
</Preview>
59+
60+
<Preview withSource='none'>
61+
<Story name="feedbackEvents">
62+
<Stories.feedbackEvents />
63+
</Story>
64+
</Preview>
65+
66+
67+
<Preview withSource='none'>
68+
<Story name="styleOverrides">
69+
<Stories.styleOverrides />
70+
</Story>
71+
</Preview>

packages/docs/src/components/TouchableHighlight/examples/DelayEvents.js

+1-19
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,10 @@
11
import React, { PureComponent } from 'react';
2-
import {
3-
StyleSheet,
4-
Text,
5-
TouchableHighlight,
6-
TouchableOpacity,
7-
TouchableWithoutFeedback,
8-
View
9-
} from 'react-native';
10-
11-
const Touchables = {
12-
highlight: TouchableHighlight,
13-
opacity: TouchableOpacity,
14-
withoutFeedback: TouchableWithoutFeedback
15-
};
2+
import { StyleSheet, Text, TouchableHighlight as Touchable, View } from 'react-native';
163

174
export default class TouchableDelayEvents extends PureComponent {
18-
static defaultProps = {
19-
touchable: 'highlight'
20-
};
21-
225
state = { eventLog: [] };
236

247
render() {
25-
const Touchable = Touchables[this.props.touchable];
268
const { displayName } = Touchable;
279
return (
2810
<View>

0 commit comments

Comments
 (0)