[change] better Touchable support for keyboards

Problem:

Although 'Touchable' supports basic keyboard usage, it doesn't support
delays or interaction via the Space key.

Solution:

Extend the 'Touchable' mixin to better support keyboard interactions.
All touchable callbacks and delays are now supported when interacted
with via a keyboard's Enter and Space keys (as would be expected of
native 'button' elements). However, events are not normalized to mimic
touch events.

Minor upstream changes to the Touchables in React Native are also
included.
This commit is contained in:
Nicolas Gallagher
2017-07-08 11:52:03 -07:00
parent 2607cb25ab
commit 6f3e29f630
13 changed files with 504 additions and 305 deletions
@@ -5,6 +5,8 @@
*/
import CustomStyleOverrides from './examples/CustomStyleOverrides';
import DelayEvents from './examples/DelayEvents';
import FeedbackEvents from './examples/FeedbackEvents';
import React from 'react';
import { storiesOf } from '@kadira/storybook';
import { TouchableHighlightDisabled } from './examples/PropDisabled';
@@ -45,13 +47,27 @@ const sections = [
title: 'More examples',
entries: [
<DocItem
description="Disabled TouchableHighlight"
description="Disabled"
example={{
code: '',
render: () => <TouchableHighlightDisabled />
}}
/>,
<DocItem
description="Feedback events"
example={{
render: () => <FeedbackEvents touchable="highlight" />
}}
/>,
<DocItem
description="Delay events"
example={{
render: () => <DelayEvents touchable="highlight" />
}}
/>,
<DocItem
description="Custom style overrides"
example={{
@@ -4,6 +4,8 @@
* @flow
*/
import DelayEvents from './examples/DelayEvents';
import FeedbackEvents from './examples/FeedbackEvents';
import React from 'react';
import { storiesOf } from '@kadira/storybook';
import { TouchableOpacityDisabled } from './examples/PropDisabled';
@@ -44,9 +46,22 @@ const sections = [
<DocItem
description="Disabled TouchableOpacity"
example={{
code: '',
render: () => <TouchableOpacityDisabled />
}}
/>,
<DocItem
description="Feedback events"
example={{
render: () => <FeedbackEvents touchable="opacity" />
}}
/>,
<DocItem
description="Delay events"
example={{
render: () => <DelayEvents touchable="opacity" />
}}
/>
]
}
@@ -4,9 +4,12 @@
* @flow
*/
import DelayEvents from './examples/DelayEvents';
import FeedbackEvents from './examples/FeedbackEvents';
import React from 'react';
import PropHitSlop from './examples/PropHitSlop';
import { storiesOf } from '@kadira/storybook';
import { TouchableWithoutFeedbackDisabled } from './examples/PropDisabled';
import UIExplorer, { AppText, Code, DocItem } from '../../ui-explorer';
const sections = [
@@ -53,6 +56,9 @@ const sections = [
If <Code>true</Code>, disable all interactions for this component.
</AppText>
}
example={{
render: () => <TouchableWithoutFeedbackDisabled />
}}
/>,
<DocItem name="onLongPress" typeInfo="?function" />,
@@ -83,7 +89,23 @@ constant to reduce memory allocations.`}
{
title: 'More examples',
entries: [<DocItem description="Hit slop" example={{ render: () => <PropHitSlop /> }} />]
entries: [
<DocItem
description="Feedback events"
example={{
render: () => <FeedbackEvents touchable="withoutFeedback" />
}}
/>,
<DocItem
description="Delay events"
example={{
render: () => <DelayEvents touchable="withoutFeedback" />
}}
/>,
<DocItem description="Hit slop" example={{ render: () => <PropHitSlop /> }} />
]
}
];
@@ -0,0 +1,101 @@
/**
* @flow
*/
import { oneOf } from 'prop-types';
import React, { PureComponent } from 'react';
import {
StyleSheet,
Text,
TouchableHighlight,
TouchableOpacity,
TouchableWithoutFeedback,
View
} from 'react-native';
const Touchables = {
highlight: TouchableHighlight,
opacity: TouchableOpacity,
withoutFeedback: TouchableWithoutFeedback
};
export default class TouchableDelayEvents extends PureComponent {
static propTypes = {
touchable: oneOf(['highlight', 'opacity', 'withoutFeedback'])
};
static defaultProps = {
touchable: 'highlight'
};
state = { eventLog: [] };
render() {
const Touchable = Touchables[this.props.touchable];
const { displayName } = Touchable;
return (
<View>
<View>
<Touchable
delayLongPress={800}
delayPressIn={400}
delayPressOut={1000}
onLongPress={this._createPressHandler('longPress: 800ms delay')}
onPress={this._createPressHandler('press')}
onPressIn={this._createPressHandler('pressIn: 400ms delay')}
onPressOut={this._createPressHandler('pressOut: 1000ms delay')}
>
<Text style={styles.touchableText}>
{displayName}
</Text>
</Touchable>
</View>
<View style={styles.eventLogBox}>
{this.state.eventLog.map((e, ii) =>
<Text key={ii}>
{e}
</Text>
)}
</View>
</View>
);
}
_createPressHandler = eventName => {
return () => {
const limit = 6;
this.setState(state => {
const eventLog = state.eventLog.slice(0, limit - 1);
eventLog.unshift(eventName);
return { eventLog };
});
};
};
}
const styles = StyleSheet.create({
touchableText: {
borderRadius: 8,
padding: 5,
borderWidth: 1,
borderColor: 'black',
color: '#007AFF',
borderStyle: 'solid',
textAlign: 'center'
},
logBox: {
padding: 20,
margin: 10,
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#f0f0f0',
backgroundColor: '#f9f9f9'
},
eventLogBox: {
padding: 10,
margin: 10,
height: 120,
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#f0f0f0',
backgroundColor: '#f9f9f9'
}
});
@@ -0,0 +1,95 @@
/**
* @flow
*/
import { oneOf } from 'prop-types';
import React, { PureComponent } from 'react';
import {
StyleSheet,
Text,
TouchableHighlight,
TouchableOpacity,
TouchableWithoutFeedback,
View
} from 'react-native';
const Touchables = {
highlight: TouchableHighlight,
opacity: TouchableOpacity,
withoutFeedback: TouchableWithoutFeedback
};
export default class TouchableFeedbackEvents extends PureComponent {
static propTypes = {
touchable: oneOf(['highlight', 'opacity', 'withoutFeedback'])
};
static defaultProps = {
touchable: 'highlight'
};
state = { eventLog: [] };
render() {
const Touchable = Touchables[this.props.touchable];
return (
<View>
<View>
<Touchable
onLongPress={this._createPressHandler('longPress')}
onPress={this._createPressHandler('press')}
onPressIn={this._createPressHandler('pressIn')}
onPressOut={this._createPressHandler('pressOut')}
>
<Text style={styles.touchableText}>Press Me</Text>
</Touchable>
</View>
<View style={styles.eventLogBox}>
{this.state.eventLog.map((e, ii) =>
<Text key={ii}>
{e}
</Text>
)}
</View>
</View>
);
}
_createPressHandler = eventName => {
return () => {
const limit = 6;
this.setState(state => {
const eventLog = state.eventLog.slice(0, limit - 1);
eventLog.unshift(eventName);
return { eventLog };
});
};
};
}
const styles = StyleSheet.create({
touchableText: {
borderRadius: 8,
padding: 5,
borderWidth: 1,
borderColor: 'black',
color: '#007AFF',
borderStyle: 'solid',
textAlign: 'center'
},
logBox: {
padding: 20,
margin: 10,
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#f0f0f0',
backgroundColor: '#f9f9f9'
},
eventLogBox: {
padding: 10,
margin: 10,
height: 120,
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#f0f0f0',
backgroundColor: '#f9f9f9'
}
});
@@ -4,7 +4,14 @@
import React from 'react';
import { action } from '@kadira/storybook';
import { StyleSheet, View, Text, TouchableHighlight, TouchableOpacity } from 'react-native';
import {
StyleSheet,
View,
Text,
TouchableHighlight,
TouchableOpacity,
TouchableWithoutFeedback
} from 'react-native';
class TouchableHighlightDisabled extends React.Component {
render() {
@@ -57,7 +64,27 @@ class TouchableOpacityDisabled extends React.Component {
}
}
export { TouchableHighlightDisabled, TouchableOpacityDisabled };
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: {
@@ -66,12 +93,5 @@ const styles = StyleSheet.create({
},
block: {
padding: 10
},
button: {
color: '#007AFF'
},
disabledButton: {
color: '#007AFF',
opacity: 0.5
}
});
@@ -17,92 +17,6 @@ import {
View
} from 'react-native';
class TouchableFeedbackEvents extends React.Component {
constructor(props) {
super(props);
this.state = { eventLog: [] };
}
render() {
return (
<View testID="touchable_feedback_events">
<View style={[styles.row, { justifyContent: 'center' }]}>
<TouchableOpacity
accessibilityComponentType="button"
accessibilityLabel="touchable feedback events"
accessibilityTraits="button"
onLongPress={this._createPressHandler('longPress')}
onPress={this._createPressHandler('press')}
onPressIn={this._createPressHandler('pressIn')}
onPressOut={this._createPressHandler('pressOut')}
style={styles.wrapper}
testID="touchable_feedback_events_button"
>
<Text style={styles.button}>
Press Me
</Text>
</TouchableOpacity>
</View>
<View style={styles.eventLogBox} testID="touchable_feedback_events_console">
{this.state.eventLog.map((e, ii) => <Text key={ii}>{e}</Text>)}
</View>
</View>
);
}
_createPressHandler = eventName => {
return () => {
const limit = 6;
const eventLog = this.state.eventLog.slice(0, limit - 1);
eventLog.unshift(eventName);
this.setState({ eventLog });
};
};
}
class TouchableDelayEvents extends React.Component {
constructor(props) {
super(props);
this.state = { eventLog: [] };
}
render() {
return (
<View testID="touchable_delay_events">
<View style={[styles.row, { justifyContent: 'center' }]}>
<TouchableOpacity
delayLongPress={800}
delayPressIn={400}
delayPressOut={1000}
onLongPress={this._createPressHandler('longPress - 800ms delay')}
onPress={this._createPressHandler('press')}
onPressIn={this._createPressHandler('pressIn - 400ms delay')}
onPressOut={this._createPressHandler('pressOut - 1000ms delay')}
style={styles.wrapper}
testID="touchable_delay_events_button"
>
<Text style={styles.button}>
Press Me
</Text>
</TouchableOpacity>
</View>
<View style={styles.eventLogBox} testID="touchable_delay_events_console">
{this.state.eventLog.map((e, ii) => <Text key={ii}>{e}</Text>)}
</View>
</View>
);
}
_createPressHandler = eventName => {
return () => {
const limit = 6;
const eventLog = this.state.eventLog.slice(0, limit - 1);
eventLog.unshift(eventName);
this.setState({ eventLog });
};
};
}
const heartImage = { uri: 'https://pbs.twimg.com/media/BlXBfT3CQAA6cVZ.png:small' };
const styles = StyleSheet.create({
@@ -214,33 +128,5 @@ const examples = [
);
}
},
{
title: 'Touchable feedback events',
description:
'<Touchable*> components accept onPress, onPressIn, ' +
'onPressOut, and onLongPress as props.',
render() {
return <TouchableFeedbackEvents />;
}
},
{
title: 'Touchable delay for events',
description:
'<Touchable*> components also accept delayPressIn, ' +
'delayPressOut, and delayLongPress as props. These props impact the ' +
'timing of feedback events.',
render() {
return <TouchableDelayEvents />;
}
},
{
title: 'Disabled Touchable*',
description:
'<Touchable*> components accept disabled prop which prevents ' +
'any interaction with component',
render() {
return <TouchableDisabled />;
}
}
];
*/