[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
This commit is contained in:
Nicolas Gallagher
2020-06-26 14:51:43 -07:00
parent 7e724b279d
commit 88f5dedffd
25 changed files with 1495 additions and 930 deletions
@@ -0,0 +1,66 @@
import { Meta, Props, Story, Preview } from '@storybook/addon-docs/blocks';
import * as Stories from './examples';
<Meta title="Components|Pressable" />
# 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.
<Preview withSource='none'>
<Story name="disabled">
<Stories.disabled />
</Story>
</Preview>
### 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
<Preview withSource='none'>
<Story name="delayEvents">
<Stories.delayEvents />
</Story>
</Preview>
<Preview withSource='none'>
<Story name="feedbackEvents">
<Stories.feedbackEvents />
</Story>
</Preview>
@@ -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 (
<View>
<View>
<Pressable
delayLongPress={800}
delayPressIn={400}
delayPressOut={1000}
onLongPress={handlePress('longPress: 800ms delay')}
onPress={handlePress('press')}
onPressIn={handlePress('pressIn: 400ms delay')}
onPressOut={handlePress('pressOut: 1000ms delay')}
>
<View>
<Text style={styles.touchableText}>Pressable</Text>
</View>
</Pressable>
</View>
<View style={styles.eventLogBox}>
{eventLog.map((e, ii) => (
<Text key={ii}>{e}</Text>
))}
</View>
</View>
);
}
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'
}
});
@@ -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 (
<View>
<Pressable disabled={true} onPress={action('Pressable')}>
<View style={[styles.row, styles.block]}>
<Text style={styles.disabledButton}>Disabled Pressable</Text>
</View>
</Pressable>
<Pressable disabled={false} onPress={action('Pressable')}>
<View style={[styles.row, styles.block]}>
<Text style={styles.button}>Enabled Pressable</Text>
</View>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
row: {
justifyContent: 'center',
flexDirection: 'row'
},
block: {
padding: 10
}
});
@@ -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 (
<View>
<View>
<Pressable
onLongPress={handlePress('longPress')}
onPress={handlePress('press')}
onPressIn={handlePress('pressIn')}
onPressOut={handlePress('pressOut')}
>
<View>
<Text style={styles.touchableText}>Press Me</Text>
</View>
</Pressable>
</View>
<View>
<Pressable
accessibilityRole="none"
onLongPress={handlePress('longPress')}
onPress={handlePress('press')}
onPressIn={handlePress('pressIn')}
onPressOut={handlePress('pressOut')}
style={({ pressed, focused }) => ({
padding: 10,
margin: 10,
borderWidth: 1,
borderColor: focused ? 'blue' : null,
backgroundColor: pressed ? 'lightblue' : 'white'
})}
>
<Pressable
accessibilityRole="none"
onLongPress={handlePress('longPress - inner')}
onPress={handlePress('press - inner')}
onPressIn={handlePress('pressIn - inner')}
onPressOut={handlePress('pressOut - inner')}
style={({ pressed, focused }) => ({
padding: 10,
margin: 10,
borderWidth: 1,
borderColor: focused ? 'blue' : null,
backgroundColor: pressed ? 'lightblue' : 'white'
})}
>
<Text>Nested pressables</Text>
</Pressable>
</Pressable>
</View>
<View style={styles.eventLogBox}>
{eventLog.map((e, ii) => (
<Text key={ii}>{e}</Text>
))}
</View>
</View>
);
}
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'
}
});
@@ -0,0 +1,3 @@
export { default as delayEvents } from './DelayEvents';
export { default as disabled } from './Disabled';
export { default as feedbackEvents } from './FeedbackEvents';
@@ -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 }) => (
<TouchableOpacity onPress={onPress} style={styles.button}>
@@ -9,9 +9,9 @@ export const Button = ({ label, onPress }) => (
function Item(props) {
return (
<View style={[styles.item, props.style]}>
<TouchableOpacity style={[styles.item, props.style]}>
<Text>{props.msg}</Text>
</View>
</TouchableOpacity>
);
}
@@ -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
<Preview withSource='none'>
<Story name="disabled">
<Stories.disabled />
</Story>
</Preview>
<Preview withSource='none'>
<Story name="delayEvents">
<Stories.delayEvents />
</Story>
</Preview>
<Preview withSource='none'>
<Story name="feedbackEvents">
<Stories.feedbackEvents />
</Story>
</Preview>
<Preview withSource='none'>
<Story name="styleOverrides">
<Stories.styleOverrides />
</Story>
</Preview>
@@ -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 (
<View>
@@ -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 (
<View>
@@ -39,52 +32,6 @@ class TouchableHighlightDisabled extends React.Component {
}
}
class TouchableOpacityDisabled extends React.Component {
render() {
return (
<View>
<TouchableOpacity
disabled={true}
onPress={action('TouchableOpacity')}
style={[styles.row, styles.block]}
>
<Text style={styles.disabledButton}>Disabled TouchableOpacity</Text>
</TouchableOpacity>
<TouchableOpacity
disabled={false}
onPress={action('TouchableOpacity')}
style={[styles.row, styles.block]}
>
<Text style={styles.button}>Enabled TouchableOpacity</Text>
</TouchableOpacity>
</View>
);
}
}
class TouchableWithoutFeedbackDisabled extends React.Component {
render() {
return (
<View>
<TouchableWithoutFeedback disabled={true} onPress={action('TouchableWithoutFeedback')}>
<View style={[styles.row, styles.block]}>
<Text style={styles.disabledButton}>Disabled TouchableWithoutFeedback</Text>
</View>
</TouchableWithoutFeedback>
<TouchableWithoutFeedback disabled={false} onPress={action('TouchableWithoutFeedback')}>
<View style={[styles.row, styles.block]}>
<Text style={styles.button}>Enabled TouchableWithoutFeedback</Text>
</View>
</TouchableWithoutFeedback>
</View>
);
}
}
export { TouchableHighlightDisabled, TouchableOpacityDisabled, TouchableWithoutFeedbackDisabled };
const styles = StyleSheet.create({
row: {
justifyContent: 'center',
@@ -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 (
<View>
<View>
@@ -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 (
<View style={styles.container}>
{this.buttons.map(button => {
return (
<TouchableHighlight
key={button}
onPress={this.select(button)}
style={[styles.touchable, this.state[button] && styles.blue]}
underlayColor="#1b95e020"
>
<Text style={[!this.state[button] && styles.text]}>{button}</Text>
</TouchableHighlight>
);
})}
</View>
);
function select(item) {
return function handler(e) {
setState({ [item]: true });
};
}
return (
<View style={styles.container}>
{buttons.map(button => {
return (
<TouchableHighlight
key={button}
onPress={select(button)}
style={[styles.touchable, state[button] && styles.blue]}
underlayColor="#1b95e020"
>
<Text style={[!state[button] && styles.text]}>{button}</Text>
</TouchableHighlight>
);
})}
</View>
);
}
const styles = StyleSheet.create({
@@ -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)
<Preview withSource='none'>
<Story name="disabled">
<Stories.disabled />
</Story>
</Preview>
Sets the opacity.
<Preview withSource='none'>
<Story name="delayEvents">
<Stories.delayEvents />
</Story>
</Preview>
<Preview withSource='none'>
<Story name="feedbackEvents">
<Stories.feedbackEvents />
</Story>
</Preview>
@@ -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 (
<View>
@@ -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 (
<View>
<TouchableHighlight
activeOpacity={1}
disabled={true}
onPress={action('TouchableHighlight')}
style={[styles.row, styles.block]}
underlayColor="rgb(210, 230, 255)"
>
<Text style={styles.disabledButton}>Disabled TouchableHighlight</Text>
</TouchableHighlight>
<TouchableHighlight
activeOpacity={1}
onPress={action('TouchableHighlight')}
style={[styles.row, styles.block]}
underlayColor="rgb(210, 230, 255)"
>
<Text style={styles.button}>Enabled TouchableHighlight</Text>
</TouchableHighlight>
</View>
);
}
}
class TouchableOpacityDisabled extends React.Component {
export default class TouchableOpacityDisabled extends React.Component {
render() {
return (
<View>
<TouchableOpacity
activeOpacity={0.5}
disabled={true}
onPress={action('TouchableOpacity')}
style={[styles.row, styles.block]}
@@ -52,7 +19,7 @@ class TouchableOpacityDisabled extends React.Component {
</TouchableOpacity>
<TouchableOpacity
disabled={false}
activeOpacity={0.5}
onPress={action('TouchableOpacity')}
style={[styles.row, styles.block]}
>
@@ -63,28 +30,6 @@ class TouchableOpacityDisabled extends React.Component {
}
}
class TouchableWithoutFeedbackDisabled extends React.Component {
render() {
return (
<View>
<TouchableWithoutFeedback disabled={true} onPress={action('TouchableWithoutFeedback')}>
<View style={[styles.row, styles.block]}>
<Text style={styles.disabledButton}>Disabled TouchableWithoutFeedback</Text>
</View>
</TouchableWithoutFeedback>
<TouchableWithoutFeedback disabled={false} onPress={action('TouchableWithoutFeedback')}>
<View style={[styles.row, styles.block]}>
<Text style={styles.button}>Enabled TouchableWithoutFeedback</Text>
</View>
</TouchableWithoutFeedback>
</View>
);
}
}
export { TouchableHighlightDisabled, TouchableOpacityDisabled, TouchableWithoutFeedbackDisabled };
const styles = StyleSheet.create({
row: {
justifyContent: 'center',
@@ -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 (
<View>
<View>
<Touchable
<TouchableOpacity
onLongPress={this._createPressHandler('longPress')}
onPress={this._createPressHandler('press')}
onPressIn={this._createPressHandler('pressIn')}
@@ -35,7 +17,7 @@ export default class TouchableFeedbackEvents extends PureComponent {
<View>
<Text style={styles.touchableText}>Press Me</Text>
</View>
</Touchable>
</TouchableOpacity>
</View>
<View style={styles.eventLogBox}>
{this.state.eventLog.map((e, ii) => (
@@ -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';