Skip to content

Commit 06557c6

Browse files
author
Michael S. Kazmier
committed
Fix issue necolas#1011 - adds touchable event handlers to text
1 parent 000b92e commit 06557c6

File tree

5 files changed

+149
-16
lines changed

5 files changed

+149
-16
lines changed

packages/react-native-web/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"hyphenate-style-name": "^1.0.2",
2222
"inline-style-prefixer": "^5.0.3",
2323
"normalize-css-color": "^1.0.2",
24+
"nullthrows": "^1.1.0",
2425
"prop-types": "^15.6.0",
2526
"react-timer-mixin": "^0.13.4"
2627
},

packages/react-native-web/src/exports/Text/__tests__/__snapshots__/index-test.js.snap

+7-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ exports[`components/Text prop "onPress" 1`] = `
55
className="rn-borderTopWidth-13yce4e rn-borderRightWidth-fnigne rn-borderBottomWidth-ndvcnb rn-borderLeftWidth-gxnn5r rn-boxSizing-deolkf rn-color-homxoj rn-cursor-1loqt21 rn-display-1471scf rn-fontFamily-14xgk7a rn-fontSize-1b43r93 rn-fontStyle-o11vmf rn-fontVariant-ebii48 rn-fontWeight-gul640 rn-lineHeight-t9a87b rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw rn-paddingTop-wk8lta rn-paddingRight-9aemit rn-paddingBottom-1mdbw0j rn-paddingLeft-gy4na3 rn-textDecoration-bauka4 rn-whiteSpace-q42fyq rn-wordWrap-qvutc0"
66
data-focusable={true}
77
dir="auto"
8-
onClick={[Function]}
98
onKeyDown={[Function]}
9+
onKeyUp={[Function]}
10+
onResponderGrant={[Function]}
11+
onResponderMove={[Function]}
12+
onResponderRelease={[Function]}
13+
onResponderTerminate={[Function]}
14+
onResponderTerminationRequest={[Function]}
15+
onStartShouldSetResponder={[Function]}
1016
tabIndex="0"
1117
/>
1218
`;

packages/react-native-web/src/exports/Text/index.js

+133-15
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,27 @@ import applyLayout from '../../modules/applyLayout';
1212
import applyNativeMethods from '../../modules/applyNativeMethods';
1313
import { bool } from 'prop-types';
1414
import { Component } from 'react';
15+
import nullthrows from 'nullthrows';
1516
import createElement from '../createElement';
1617
import StyleSheet from '../StyleSheet';
1718
import TextPropTypes from './TextPropTypes';
19+
import Touchable from '../Touchable';
1820

19-
class Text extends Component<*> {
21+
type ResponseHandlers = {|
22+
onStartShouldSetResponder: () => boolean,
23+
onResponderGrant: (event: *, dispatchID: string) => void,
24+
onResponderMove: (event: *) => void,
25+
onResponderRelease: (event: *) => void,
26+
onResponderTerminate: (event: *) => void,
27+
onResponderTerminationRequest: () => boolean
28+
|};
29+
30+
const isTouchable = (props: *): boolean =>
31+
props.onPress != null || props.onLongPress != null || props.onStartShouldSetResponder != null;
32+
33+
const PRESS_RECT_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 };
34+
35+
class Text extends Component<*, *> {
2036
static displayName = 'Text';
2137

2238
static propTypes = TextPropTypes;
@@ -29,6 +45,40 @@ class Text extends Component<*> {
2945
isInAParentText: bool
3046
};
3147

48+
static getDerivedStateFromProps(nextProps: *, prevState: *): * | null {
49+
return prevState.responseHandlers == null && isTouchable(nextProps)
50+
? {
51+
responseHandlers: prevState.createResponderHandlers()
52+
}
53+
: null;
54+
}
55+
56+
constructor(props) {
57+
super(props);
58+
if (isTouchable(props)) this._attachTouchHandlers();
59+
}
60+
61+
state = {
62+
...Touchable.Mixin.touchableGetInitialState(),
63+
isHighlighted: false,
64+
createResponderHandlers: this._createResponseHandlers.bind(this),
65+
responseHandlers: null
66+
};
67+
68+
// Flow definitions for touchable events
69+
touchableGetPressRectOffset: ?() => *;
70+
touchableHandleActivePressIn: ?() => void;
71+
touchableHandleActivePressOut: ?() => void;
72+
touchableHandleKeyEvent: ?(event: *) => void;
73+
touchableHandleLongPress: ?(event: *) => void;
74+
touchableHandlePress: ?(event: *) => void;
75+
touchableHandleResponderGrant: ?(event: *, dispatchID: string) => void;
76+
touchableHandleResponderMove: ?(event: *) => void;
77+
touchableHandleResponderRelease: ?(event: *) => void;
78+
touchableHandleResponderTerminate: ?(event: *) => void;
79+
touchableHandleResponderTerminationRequest: ?() => boolean;
80+
touchableHandleStartShouldSetResponder: ?() => boolean;
81+
3282
getChildContext() {
3383
return { isInAParentText: true };
3484
}
@@ -37,7 +87,6 @@ class Text extends Component<*> {
3787
const {
3888
dir,
3989
numberOfLines,
40-
onPress,
4190
selectable,
4291
style,
4392
/* eslint-disable */
@@ -48,6 +97,7 @@ class Text extends Component<*> {
4897
minimumFontScale,
4998
onLayout,
5099
onLongPress,
100+
onPress,
51101
pressRetentionOffset,
52102
selectionColor,
53103
suppressHighlighting,
@@ -59,10 +109,16 @@ class Text extends Component<*> {
59109

60110
const { isInAParentText } = this.context;
61111

62-
if (onPress) {
112+
if (isTouchable(this.props)) {
63113
otherProps.accessible = true;
64-
otherProps.onClick = this._createPressHandler(onPress);
65-
otherProps.onKeyDown = this._createEnterHandler(onPress);
114+
otherProps.onKeyDown = this.touchableHandleKeyEvent;
115+
otherProps.onKeyUp = this.touchableHandleKeyEvent;
116+
otherProps.onResponderGrant = this.touchableHandleResponderGrant;
117+
otherProps.onResponderMove = this.touchableHandleResponderMove;
118+
otherProps.onResponderRelease = this.touchableHandleResponderRelease;
119+
otherProps.onResponderTerminate = this.touchableHandleResponderTerminate;
120+
otherProps.onResponderTerminationRequest = this.touchableHandleResponderTerminationRequest;
121+
otherProps.onStartShouldSetResponder = this.touchableHandleStartShouldSetResponder;
66122
}
67123

68124
// allow browsers to automatically infer the language writing direction
@@ -73,27 +129,89 @@ class Text extends Component<*> {
73129
style,
74130
selectable === false && styles.notSelectable,
75131
numberOfLines === 1 && styles.singleLineStyle,
76-
onPress && styles.pressable
132+
isTouchable(this.props) && styles.pressable
77133
];
78134

79135
const component = isInAParentText ? 'span' : 'div';
80-
81136
return createElement(component, otherProps);
82137
}
83138

84-
_createEnterHandler(fn) {
85-
return e => {
86-
if (e.keyCode === 13) {
87-
fn && fn(e);
139+
_createResponseHandlers(): ResponseHandlers {
140+
return {
141+
onStartShouldSetResponder: (): boolean => {
142+
return isTouchable(this.props);
143+
},
144+
onResponderGrant: (event: *, dispatchID: string): void => {
145+
nullthrows(this.touchableHandleResponderGrant)(event, dispatchID);
146+
if (this.props.onResponderGrant != null) {
147+
this.props.onResponderGrant.call(this, event, dispatchID);
148+
}
149+
},
150+
onResponderMove: (event: *): void => {
151+
nullthrows(this.touchableHandleResponderMove)(event);
152+
if (this.props.onResponderMove != null) {
153+
this.props.onResponderMove.call(this, event);
154+
}
155+
},
156+
onResponderRelease: (event: *): void => {
157+
nullthrows(this.touchableHandleResponderRelease)(event);
158+
if (this.props.onResponderRelease != null) {
159+
this.props.onResponderRelease.call(this, event);
160+
}
161+
},
162+
onResponderTerminate: (event: *): void => {
163+
nullthrows(this.touchableHandleResponderTerminate)(event);
164+
if (this.props.onResponderTerminate != null) {
165+
this.props.onResponderTerminate.call(this, event);
166+
}
167+
},
168+
onResponderTerminationRequest: (): boolean => {
169+
const { onResponderTerminationRequest } = this.props;
170+
if (!nullthrows(this.touchableHandleResponderTerminationRequest)()) {
171+
return false;
172+
}
173+
if (onResponderTerminationRequest == null) {
174+
return true;
175+
}
176+
return onResponderTerminationRequest();
88177
}
89178
};
90179
}
91180

92-
_createPressHandler(fn) {
93-
return e => {
94-
e.stopPropagation();
95-
fn && fn(e);
181+
/**
182+
* Lazily attaches Touchable.Mixin handlers.
183+
*/
184+
_attachTouchHandlers(): void {
185+
if (this.touchableGetPressRectOffset != null) {
186+
return;
187+
}
188+
for (const key in Touchable.Mixin) {
189+
if (typeof Touchable.Mixin[key] === 'function') {
190+
(this: any)[key] = Touchable.Mixin[key].bind(this);
191+
}
192+
}
193+
this.touchableHandleActivePressIn = (): void => {
194+
if (!this.props.suppressHighlighting && isTouchable(this.props)) {
195+
this.setState({ isHighlighted: true });
196+
}
197+
};
198+
this.touchableHandleActivePressOut = (): void => {
199+
if (!this.props.suppressHighlighting && isTouchable(this.props)) {
200+
this.setState({ isHighlighted: false });
201+
}
202+
};
203+
this.touchableHandlePress = (event: *): void => {
204+
if (this.props.onPress != null) {
205+
this.props.onPress(event);
206+
}
207+
};
208+
this.touchableHandleLongPress = (event: *): void => {
209+
if (this.props.onLongPress != null) {
210+
this.props.onLongPress(event);
211+
}
96212
};
213+
this.touchableGetPressRectOffset = (): * =>
214+
this.props.pressRetentionOffset == null ? PRESS_RECT_OFFSET : this.props.pressRetentionOffset;
97215
}
98216
}
99217

packages/react-native-web/src/exports/createElement/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const eventHandlerNames = {
3333
onClickCapture: true,
3434
onContextMenu: true,
3535
onFocus: true,
36+
onMouseDown: true,
37+
onMouseUp: true,
38+
onMouseOut: true,
3639
onResponderRelease: true,
3740
onTouchCancel: true,
3841
onTouchCancelCapture: true,

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -7615,6 +7615,11 @@ nth-check@~1.0.1:
76157615
dependencies:
76167616
boolbase "~1.0.0"
76177617

7618+
nullthrows@^1.1.0:
7619+
version "1.1.1"
7620+
resolved "https://nexus-gss.uscis.dhs.gov/nexus/repository/didit-npm-group/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
7621+
integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==
7622+
76187623
num2fraction@^1.2.2:
76197624
version "1.2.2"
76207625
resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"

0 commit comments

Comments
 (0)