React events: add delay props to Press module (#15340)
* Add delay props to Press event module * Minor naming changes to Hover events * Add examples to react-events README
Nicolas Gallagher committed
Apr 6, 2019 at 13:47 UTC
81a61b1d1ac4c3fa974a822382b2343a96f7cc31
5 files changed
+537
-132
packages/react-events/README.md
+117
-40
@@ -3,6 +3,10 @@
3
*This package is experimental. It is intended for use with the experimental React
4
events API that is not available in open source builds.*
5
6
+Event components do not render a host node. They listen to native browser events
7
+dispatched on the host node of their child and transform those events into
8
+high-level events for applications.
9
+
10
11
## Focus
12
@@ -10,7 +14,20 @@ The `Focus` module responds to focus and blur events on the element it wraps.
14
Focus events are dispatched for `mouse`, `pen`, `touch`, and `keyboard`
15
pointer types.
16
17
+```js
18
+// Example
19
+const TextField = (props) => (
20
+ <Focus
21
+ onBlur={props.onBlur}
22
+ onFocus={props.onFocus}
23
+ >
24
+ <textarea></textarea>
25
+ </Focus>
26
+);
27
```
28
+
29
+```js
30
+// Types
31
type FocusEvent = {}
32
```
33
@@ -38,69 +55,124 @@ The `Hover` module responds to hover events on the element it wraps. Hover
55
events are only dispatched for `mouse` pointer types. Hover begins when the
56
pointer enters the element's bounds and ends when the pointer leaves.
57
58
+```js
59
+// Example
60
+const Link = (props) => (
61
+ const [ hovered, setHovered ] = useState(false);
62
+ return (
63
+ <Hover onHoverChange={setHovered}>
64
+ <a
65
+ {...props}
66
+ href={props.href}
67
+ style={{
68
+ ...props.style,
69
+ textDecoration: hovered ? 'underline': 'none'
70
+ }}
71
+ />
72
+ </Hover>
73
+ );
74
+);
75
```
76
+
77
+```js
78
+// Types
79
type HoverEvent = {}
80
```
81
45
-### disabled: boolean
82
+### delayHoverEnd: number
83
47
-Disables all `Hover` events.
84
+The duration of the delay between when hover ends and when `onHoverEnd` is
85
+called.
86
49
-### onHoverStart: (e: HoverEvent) => void
87
+### delayHoverStart: number
88
51
-Called once the element is hovered. It will not be called if the pointer leaves
52
-the element before the `delayHoverStart` threshold is exceeded. And it will not
53
-be called more than once before `onHoverEnd` is called.
89
+The duration of the delay between when hover starts and when `onHoverStart` is
90
+called.
91
55
-### onHoverEnd: (e: HoverEvent) => void
92
+### disabled: boolean
93
57
-Called once the element is no longer hovered. It will be cancelled if the
58
-pointer leaves the element before the `delayHoverStart` threshold is exceeded.
94
+Disables all `Hover` events.
95
96
### onHoverChange: boolean => void
97
98
Called when the element changes hover state (i.e., after `onHoverStart` and
99
`onHoverEnd`).
100
65
-### delayHoverStart: number
101
+### onHoverEnd: (e: HoverEvent) => void
102
67
-The duration of the delay between when hover starts and when `onHoverStart` is
68
-called.
103
+Called once the element is no longer hovered. It will be cancelled if the
104
+pointer leaves the element before the `delayHoverStart` threshold is exceeded.
105
70
-### delayHoverEnd: number
106
+### onHoverStart: (e: HoverEvent) => void
107
72
-The duration of the delay between when hover ends and when `onHoverEnd` is
73
-called.
108
+Called once the element is hovered. It will not be called if the pointer leaves
109
+the element before the `delayHoverStart` threshold is exceeded. And it will not
110
+be called more than once before `onHoverEnd` is called.
111
112
113
## Press
114
115
The `Press` module responds to press events on the element it wraps. Press
116
events are dispatched for `mouse`, `pen`, `touch`, and `keyboard` pointer types.
80
-
117
+Press events are only dispatched for keyboards when pressing the Enter or
118
+Spacebar keys. If neither `onPress` nor `onLongPress` are called, this signifies
119
+that the press ended outside of the element hit bounds (i.e., the user aborted
120
+the press).
121
+
122
+```js
123
+// Example
124
+const Button = (props) => (
125
+ const [ pressed, setPressed ] = useState(false);
126
+ return (
127
+ <Press
128
+ onPress={props.onPress}
129
+ onPressChange={setPressed}
130
+ onLongPress={props.onLongPress}
131
+ >
132
+ <div
133
+ {...props}
134
+ role="button"
135
+ tabIndex={0}
136
+ style={
137
+ ...buttonStyles,
138
+ ...(pressed && pressedStyles)
139
+ }}
140
+ />
141
+ </Press>
142
+ );
143
+);
144
```
145
+
146
+```js
147
+// Types
148
type PressEvent = {}
149
+
150
+type PressOffset = {
151
+ top: number,
152
+ right: number,
153
+ bottom: number,
154
+ right: number
155
+};
156
```
157
85
-### disabled: boolean
158
+### delayLongPress: number = 500ms
159
87
-Disables all `Press` events.
160
+The duration of a press before `onLongPress` and `onLongPressChange` are called.
161
89
-### onPressStart: (e: PressEvent) => void
162
+### delayPressEnd: number
163
91
-Called once the element is pressed down. If the press is released before the
92
-`delayPressStart` threshold is exceeded then the delay is cut short and
93
-`onPressStart` is called immediately.
164
+The duration of the delay between when the press ends and when `onPressEnd` is
165
+called.
166
95
-### onPressEnd: (e: PressEvent) => void
167
+### delayPressStart: number
168
97
-Called once the element is no longer pressed. It will be cancelled if the press
98
-starts again before the `delayPressEnd` threshold is exceeded.
169
+The duration of a delay between when the press starts and when `onPressStart` is
170
+called. This delay is cut short (and `onPressStart` is called) if the press is
171
+released before the threshold is exceeded.
172
100
-### onPressChange: boolean => void
173
+### disabled: boolean
174
102
-Called when the element changes press state (i.e., after `onPressStart` and
103
-`onPressEnd`).
175
+Disables all `Press` events.
176
177
### onLongPress: (e: PressEvent) => void
178
@@ -117,25 +189,30 @@ Determines whether calling `onPress` should be cancelled if `onLongPress` or
189
190
### onPress: (e: PressEvent) => void
191
120
-Called after `onPressEnd` only if `onLongPressShouldCancelPress` returns
121
-`false`.
192
+Called immediately after a press is released, unless either 1) the press is
193
+released outside the hit bounds of the element (accounting for
194
+`pressRetentionOffset` and `TouchHitTarget`), or 2) the press was a long press,
195
+and `onLongPress` or `onLongPressChange` props are provided, and
196
+`onLongPressCancelsPress()` is `true`.
197
123
-### delayPressStart: number
198
+### onPressChange: boolean => void
199
125
-The duration of a delay between when the press starts and when `onPressStart` is
126
-called. This delay is cut short if the press ends released before the threshold
127
-is exceeded.
200
+Called when the element changes press state (i.e., after `onPressStart` and
201
+`onPressEnd`).
202
129
-### delayPressEnd: number
203
+### onPressEnd: (e: PressEvent) => void
204
131
-The duration of the delay between when the press ends and when `onPressEnd` is
132
-called.
205
+Called once the element is no longer pressed. If the press starts again before
206
+the `delayPressEnd` threshold is exceeded then the delay is reset to prevent
207
+`onPressEnd` being called during a press.
208
134
-### delayLongPress: number = 500ms
209
+### onPressStart: (e: PressEvent) => void
210
136
-The duration of a press before `onLongPress` and `onLongPressChange` are called.
211
+Called once the element is pressed down. If the press is released before the
212
+`delayPressStart` threshold is exceeded then the delay is cut short and
213
+`onPressStart` is called immediately.
214
138
-### pressRententionOffset: { top: number, right: number, bottom: number, right: number }
215
+### pressRententionOffset: PressOffset
216
217
Defines how far the pointer (while held down) may move outside the bounds of the
218
element before it is deactivated. Once deactivated, the pointer (still held
packages/react-events/src/Hover.js
+22
-14
@@ -36,8 +36,8 @@ type HoverEvent = {|
36
type: HoverEventType,
37
|};
38
39
-// const DEFAULT_HOVER_END_DELAY_MS = 0;
40
-// const DEFAULT_HOVER_START_DELAY_MS = 0;
39
+const DEFAULT_HOVER_END_DELAY_MS = 0;
40
+const DEFAULT_HOVER_START_DELAY_MS = 0;
41
42
const targetEventTypes = [
43
'pointerover',
@@ -98,7 +98,7 @@ function dispatchHoverStartEvents(
98
state.hoverEndTimeout = null;
99
}
100
101
- const dispatch = () => {
101
+ const activate = () => {
102
state.isActiveHovered = true;
103
104
if (props.onHoverStart) {
@@ -115,14 +115,18 @@ function dispatchHoverStartEvents(
115
};
116
117
if (!state.isActiveHovered) {
118
- const delay = calculateDelayMS(props.delayHoverStart, 0, 0);
119
- if (delay > 0) {
118
+ const delayHoverStart = calculateDelayMS(
119
+ props.delayHoverStart,
120
+ 0,
121
+ DEFAULT_HOVER_START_DELAY_MS,
122
+ );
123
+ if (delayHoverStart > 0) {
124
state.hoverStartTimeout = context.setTimeout(() => {
125
state.hoverStartTimeout = null;
122
- dispatch();
123
- }, delay);
126
+ activate();
127
+ }, delayHoverStart);
128
} else {
125
- dispatch();
129
+ activate();
130
}
131
}
132
}
@@ -145,7 +149,7 @@ function dispatchHoverEndEvents(
149
state.hoverStartTimeout = null;
150
}
151
148
- const dispatch = () => {
152
+ const deactivate = () => {
153
state.isActiveHovered = false;
154
155
if (props.onHoverEnd) {
@@ -162,13 +166,17 @@ function dispatchHoverEndEvents(
166
};
167
168
if (state.isActiveHovered) {
165
- const delay = calculateDelayMS(props.delayHoverEnd, 0, 0);
166
- if (delay > 0) {
169
+ const delayHoverEnd = calculateDelayMS(
170
+ props.delayHoverEnd,
171
+ 0,
172
+ DEFAULT_HOVER_END_DELAY_MS,
173
+ );
174
+ if (delayHoverEnd > 0) {
175
state.hoverEndTimeout = context.setTimeout(() => {
168
- dispatch();
169
- }, delay);
176
+ deactivate();
177
+ }, delayHoverEnd);
178
} else {
171
- dispatch();
179
+ deactivate();
180
}
181
}
182
}
packages/react-events/src/Press.js
+112
-51
@@ -27,11 +27,15 @@ type PressProps = {
27
28
type PressState = {
29
defaultPrevented: boolean,
30
+ isActivePressed: boolean,
31
+ isActivePressStart: boolean,
32
isAnchorTouched: boolean,
33
isLongPressed: boolean,
34
isPressed: boolean,
35
longPressTimeout: null | TimeoutID,
36
pressTarget: null | Element | Document,
37
+ pressEndTimeout: null | TimeoutID,
38
+ pressStartTimeout: null | TimeoutID,
39
shouldSkipMouseAfterTouch: boolean,
40
};
41
@@ -49,9 +53,8 @@ type PressEvent = {|
53
type: PressEventType,
54
|};
55
52
-// const DEFAULT_PRESS_DELAY_MS = 0;
53
-// const DEFAULT_PRESS_END_DELAY_MS = 0;
54
-// const DEFAULT_PRESS_START_DELAY_MS = 0;
56
+const DEFAULT_PRESS_END_DELAY_MS = 0;
57
+const DEFAULT_PRESS_START_DELAY_MS = 0;
58
const DEFAULT_LONG_PRESS_DELAY_MS = 500;
59
60
const targetEventTypes = [
@@ -102,7 +105,7 @@ function dispatchPressChangeEvent(
105
state: PressState,
106
): void {
107
const listener = () => {
105
- props.onPressChange(state.isPressed);
108
+ props.onPressChange(state.isActivePressed);
109
};
110
dispatchEvent(context, state, 'presschange', listener);
111
}
@@ -118,6 +121,34 @@ function dispatchLongPressChangeEvent(
121
dispatchEvent(context, state, 'longpresschange', listener);
122
}
123
124
+function activate(context, props, state) {
125
+ const wasActivePressed = state.isActivePressed;
126
+ state.isActivePressed = true;
127
+
128
+ if (props.onPressStart) {
129
+ dispatchEvent(context, state, 'pressstart', props.onPressStart);
130
+ }
131
+ if (!wasActivePressed && props.onPressChange) {
132
+ dispatchPressChangeEvent(context, props, state);
133
+ }
134
+}
135
+
136
+function deactivate(context, props, state) {
137
+ const wasLongPressed = state.isLongPressed;
138
+ state.isActivePressed = false;
139
+ state.isLongPressed = false;
140
+
141
+ if (props.onPressEnd) {
142
+ dispatchEvent(context, state, 'pressend', props.onPressEnd);
143
+ }
144
+ if (props.onPressChange) {
145
+ dispatchPressChangeEvent(context, props, state);
146
+ }
147
+ if (wasLongPressed && props.onLongPressChange) {
148
+ dispatchLongPressChangeEvent(context, props, state);
149
+ }
150
+}
151
+
152
function dispatchPressStartEvents(
153
context: ResponderContext,
154
props: PressProps,
@@ -125,38 +156,58 @@ function dispatchPressStartEvents(
156
): void {
157
state.isPressed = true;
158
128
- if (props.onPressStart) {
129
- dispatchEvent(context, state, 'pressstart', props.onPressStart);
159
+ if (state.pressEndTimeout !== null) {
160
+ clearTimeout(state.pressEndTimeout);
161
+ state.pressEndTimeout = null;
162
}
131
- if (props.onPressChange) {
132
- dispatchPressChangeEvent(context, props, state);
133
- }
134
- if ((props.onLongPress || props.onLongPressChange) && !state.isLongPressed) {
135
- const delayLongPress = calculateDelayMS(
136
- props.delayLongPress,
137
- 10,
138
- DEFAULT_LONG_PRESS_DELAY_MS,
139
- );
163
141
- state.longPressTimeout = context.setTimeout(() => {
142
- state.isLongPressed = true;
143
- state.longPressTimeout = null;
144
-
145
- if (props.onLongPress) {
146
- const listener = e => {
147
- props.onLongPress(e);
148
- // TODO address this again at some point
149
- // if (e.nativeEvent.defaultPrevented) {
150
- // state.defaultPrevented = true;
151
- // }
152
- };
153
- dispatchEvent(context, state, 'longpress', listener);
154
- }
164
+ const dispatch = () => {
165
+ state.isActivePressStart = true;
166
+ activate(context, props, state);
167
+
168
+ if (
169
+ (props.onLongPress || props.onLongPressChange) &&
170
+ !state.isLongPressed
171
+ ) {
172
+ const delayLongPress = calculateDelayMS(
173
+ props.delayLongPress,
174
+ 10,
175
+ DEFAULT_LONG_PRESS_DELAY_MS,
176
+ );
177
+ state.longPressTimeout = context.setTimeout(() => {
178
+ state.isLongPressed = true;
179
+ state.longPressTimeout = null;
180
+ if (props.onLongPress) {
181
+ const listener = e => {
182
+ props.onLongPress(e);
183
+ // TODO address this again at some point
184
+ // if (e.nativeEvent.defaultPrevented) {
185
+ // state.defaultPrevented = true;
186
+ // }
187
+ };
188
+ dispatchEvent(context, state, 'longpress', listener);
189
+ }
190
+ if (props.onLongPressChange) {
191
+ dispatchLongPressChangeEvent(context, props, state);
192
+ }
193
+ }, delayLongPress);
194
+ }
195
+ };
196
156
- if (props.onLongPressChange) {
157
- dispatchLongPressChangeEvent(context, props, state);
158
- }
159
- }, delayLongPress);
197
+ if (!state.isActivePressStart) {
198
+ const delayPressStart = calculateDelayMS(
199
+ props.delayPressStart,
200
+ 0,
201
+ DEFAULT_PRESS_START_DELAY_MS,
202
+ );
203
+ if (delayPressStart > 0) {
204
+ state.pressStartTimeout = context.setTimeout(() => {
205
+ state.pressStartTimeout = null;
206
+ dispatch();
207
+ }, delayPressStart);
208
+ } else {
209
+ dispatch();
210
+ }
211
}
212
}
213
@@ -165,25 +216,36 @@ function dispatchPressEndEvents(
216
props: PressProps,
217
state: PressState,
218
): void {
219
+ const wasActivePressStart = state.isActivePressStart;
220
+
221
+ state.isActivePressStart = false;
222
+ state.isPressed = false;
223
+
224
if (state.longPressTimeout !== null) {
225
clearTimeout(state.longPressTimeout);
226
state.longPressTimeout = null;
227
}
172
- if (props.onPressEnd) {
173
- dispatchEvent(context, state, 'pressend', props.onPressEnd);
174
- }
228
176
- if (state.isPressed) {
177
- state.isPressed = false;
178
- if (props.onPressChange) {
179
- dispatchPressChangeEvent(context, props, state);
180
- }
229
+ if (!wasActivePressStart && state.pressStartTimeout !== null) {
230
+ clearTimeout(state.pressStartTimeout);
231
+ state.pressStartTimeout = null;
232
+ // if we haven't yet activated (due to delays), activate now
233
+ activate(context, props, state);
234
}
235
183
- if (state.isLongPressed) {
184
- state.isLongPressed = false;
185
- if (props.onLongPressChange) {
186
- dispatchLongPressChangeEvent(context, props, state);
236
+ if (state.isActivePressed) {
237
+ const delayPressEnd = calculateDelayMS(
238
+ props.delayPressEnd,
239
+ 0,
240
+ DEFAULT_PRESS_END_DELAY_MS,
241
+ );
242
+ if (delayPressEnd > 0) {
243
+ state.pressEndTimeout = context.setTimeout(() => {
244
+ state.pressEndTimeout = null;
245
+ deactivate(context, props, state);
246
+ }, delayPressEnd);
247
+ } else {
248
+ deactivate(context, props, state);
249
}
250
}
251
}
@@ -208,13 +270,8 @@ function unmountResponder(
270
state: PressState,
271
): void {
272
if (state.isPressed) {
211
- state.isPressed = false;
212
- context.removeRootEventTypes(rootEventTypes);
273
dispatchPressEndEvents(context, props, state);
214
- if (state.longPressTimeout !== null) {
215
- clearTimeout(state.longPressTimeout);
216
- state.longPressTimeout = null;
217
- }
274
+ context.removeRootEventTypes(rootEventTypes);
275
}
276
}
277
@@ -223,10 +280,14 @@ const PressResponder = {
280
createInitialState(): PressState {
281
return {
282
defaultPrevented: false,
283
+ isActivePressed: false,
284
+ isActivePressStart: false,
285
isAnchorTouched: false,
286
isLongPressed: false,
287
isPressed: false,
288
longPressTimeout: null,
289
+ pressEndTimeout: null,
290
+ pressStartTimeout: null,
291
pressTarget: null,
292
shouldSkipMouseAfterTouch: false,
293
};
packages/react-events/src/__tests__/Hover-test.internal.js
+18
-15
@@ -100,6 +100,24 @@ describe('Hover event responder', () => {
100
expect(onHoverStart).toHaveBeenCalledTimes(1);
101
});
102
103
+ it('is reset if "pointerout" is dispatched during a delay', () => {
104
+ const element = (
105
+ <Hover delayHoverStart={500} onHoverStart={onHoverStart}>
106
+ <div ref={ref} />
107
+ </Hover>
108
+ );
109
+ ReactDOM.render(element, container);
110
+
111
+ ref.current.dispatchEvent(createPointerEvent('pointerover'));
112
+ jest.advanceTimersByTime(499);
113
+ ref.current.dispatchEvent(createPointerEvent('pointerout'));
114
+ jest.advanceTimersByTime(1);
115
+ expect(onHoverStart).not.toBeCalled();
116
+ ref.current.dispatchEvent(createPointerEvent('pointerover'));
117
+ jest.runAllTimers();
118
+ expect(onHoverStart).toHaveBeenCalledTimes(1);
119
+ });
120
+
121
it('onHoverStart is called synchronously if delay is 0ms', () => {
122
const element = (
123
<Hover delayHoverStart={0} onHoverStart={onHoverStart}>
@@ -132,21 +150,6 @@ describe('Hover event responder', () => {
150
jest.runAllTimers();
151
expect(onHoverStart).toHaveBeenCalledTimes(1);
152
});
135
-
136
- it('onHoverStart is not called if "pointerout" is dispatched during a delay', () => {
137
- const element = (
138
- <Hover delayHoverStart={500} onHoverStart={onHoverStart}>
139
- <div ref={ref} />
140
- </Hover>
141
- );
142
- ReactDOM.render(element, container);
143
-
144
- ref.current.dispatchEvent(createPointerEvent('pointerover'));
145
- jest.advanceTimersByTime(499);
146
- ref.current.dispatchEvent(createPointerEvent('pointerout'));
147
- jest.advanceTimersByTime(1);
148
- expect(onHoverStart).not.toBeCalled();
149
- });
153
});
154
});
155
packages/react-events/src/__tests__/Press-test.internal.js
+268
-12
@@ -104,8 +104,70 @@ describe('Event responder: Press', () => {
104
expect(onPressStart).toHaveBeenCalledTimes(1);
105
});
106
107
- // TODO: complete delayPressStart tests
108
- // describe('delayPressStart', () => {});
107
+ describe('delayPressStart', () => {
108
+ it('can be configured', () => {
109
+ const element = (
110
+ <Press delayPressStart={2000} onPressStart={onPressStart}>
111
+ <div ref={ref} />
112
+ </Press>
113
+ );
114
+ ReactDOM.render(element, container);
115
+
116
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
117
+ jest.advanceTimersByTime(1999);
118
+ expect(onPressStart).not.toBeCalled();
119
+ jest.advanceTimersByTime(1);
120
+ expect(onPressStart).toHaveBeenCalledTimes(1);
121
+ });
122
+
123
+ it('is cut short if the press is released during a delay', () => {
124
+ const element = (
125
+ <Press delayPressStart={2000} onPressStart={onPressStart}>
126
+ <div ref={ref} />
127
+ </Press>
128
+ );
129
+ ReactDOM.render(element, container);
130
+
131
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
132
+ jest.advanceTimersByTime(499);
133
+ expect(onPressStart).toHaveBeenCalledTimes(0);
134
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
135
+ expect(onPressStart).toHaveBeenCalledTimes(1);
136
+ jest.runAllTimers();
137
+ expect(onPressStart).toHaveBeenCalledTimes(1);
138
+ });
139
+
140
+ it('onPressStart is called synchronously if delay is 0ms', () => {
141
+ const element = (
142
+ <Press delayPressStart={0} onPressStart={onPressStart}>
143
+ <div ref={ref} />
144
+ </Press>
145
+ );
146
+ ReactDOM.render(element, container);
147
+
148
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
149
+ expect(onPressStart).toHaveBeenCalledTimes(1);
150
+ });
151
+ });
152
+
153
+ describe('delayPressEnd', () => {
154
+ it('onPressStart called each time a press is initiated', () => {
155
+ // This test makes sure that onPressStart is called each time a press
156
+ // starts, even if a delayPressEnd is delaying the deactivation of the
157
+ // previous press.
158
+ const element = (
159
+ <Press delayPressEnd={2000} onPressStart={onPressStart}>
160
+ <div ref={ref} />
161
+ </Press>
162
+ );
163
+ ReactDOM.render(element, container);
164
+
165
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
166
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
167
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
168
+ expect(onPressStart).toHaveBeenCalledTimes(2);
169
+ });
170
+ });
171
});
172
173
describe('onPressEnd', () => {
@@ -165,8 +227,55 @@ describe('Event responder: Press', () => {
227
expect(onPressEnd).toHaveBeenCalledTimes(1);
228
});
229
168
- // TODO: complete delayPressStart tests
169
- // describe('delayPressStart', () => {});
230
+ describe('delayPressEnd', () => {
231
+ it('can be configured', () => {
232
+ const element = (
233
+ <Press delayPressEnd={2000} onPressEnd={onPressEnd}>
234
+ <div ref={ref} />
235
+ </Press>
236
+ );
237
+ ReactDOM.render(element, container);
238
+
239
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
240
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
241
+ jest.advanceTimersByTime(1999);
242
+ expect(onPressEnd).not.toBeCalled();
243
+ jest.advanceTimersByTime(1);
244
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
245
+ });
246
+
247
+ it('is reset if "pointerdown" is dispatched during a delay', () => {
248
+ const element = (
249
+ <Press delayPressEnd={500} onPressEnd={onPressEnd}>
250
+ <div ref={ref} />
251
+ </Press>
252
+ );
253
+ ReactDOM.render(element, container);
254
+
255
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
256
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
257
+ jest.advanceTimersByTime(499);
258
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
259
+ jest.advanceTimersByTime(1);
260
+ expect(onPressEnd).not.toBeCalled();
261
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
262
+ jest.runAllTimers();
263
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
264
+ });
265
+ });
266
+
267
+ it('onPressEnd is called synchronously if delay is 0ms', () => {
268
+ const element = (
269
+ <Press delayPressEnd={0} onPressEnd={onPressEnd}>
270
+ <div ref={ref} />
271
+ </Press>
272
+ );
273
+ ReactDOM.render(element, container);
274
+
275
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
276
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
277
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
278
+ });
279
});
280
281
describe('onPressChange', () => {
@@ -201,6 +310,55 @@ describe('Event responder: Press', () => {
310
expect(onPressChange).toHaveBeenCalledWith(false);
311
});
312
313
+ it('is called after delayed onPressStart', () => {
314
+ const element = (
315
+ <Press delayPressStart={500} onPressChange={onPressChange}>
316
+ <div ref={ref} />
317
+ </Press>
318
+ );
319
+ ReactDOM.render(element, container);
320
+
321
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
322
+ jest.advanceTimersByTime(499);
323
+ expect(onPressChange).not.toBeCalled();
324
+ jest.advanceTimersByTime(1);
325
+ expect(onPressChange).toHaveBeenCalledTimes(1);
326
+ expect(onPressChange).toHaveBeenCalledWith(true);
327
+ });
328
+
329
+ it('is called after delayPressStart is cut short', () => {
330
+ const element = (
331
+ <Press delayPressStart={500} onPressChange={onPressChange}>
332
+ <div ref={ref} />
333
+ </Press>
334
+ );
335
+ ReactDOM.render(element, container);
336
+
337
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
338
+ jest.advanceTimersByTime(100);
339
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
340
+ expect(onPressChange).toHaveBeenCalledTimes(2);
341
+ });
342
+
343
+ it('is called after delayed onPressEnd', () => {
344
+ const element = (
345
+ <Press delayPressEnd={500} onPressChange={onPressChange}>
346
+ <div ref={ref} />
347
+ </Press>
348
+ );
349
+ ReactDOM.render(element, container);
350
+
351
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
352
+ expect(onPressChange).toHaveBeenCalledTimes(1);
353
+ expect(onPressChange).toHaveBeenCalledWith(true);
354
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
355
+ jest.advanceTimersByTime(499);
356
+ expect(onPressChange).toHaveBeenCalledTimes(1);
357
+ jest.advanceTimersByTime(1);
358
+ expect(onPressChange).toHaveBeenCalledTimes(2);
359
+ expect(onPressChange).toHaveBeenCalledWith(false);
360
+ });
361
+
362
// No PointerEvent fallbacks
363
it('is called after "mousedown" and "mouseup" events', () => {
364
ref.current.dispatchEvent(createPointerEvent('mousedown'));
@@ -246,13 +404,26 @@ describe('Event responder: Press', () => {
404
expect(onPress).toHaveBeenCalledTimes(1);
405
});
406
407
+ it('is always called immediately after press is released', () => {
408
+ const element = (
409
+ <Press delayPressEnd={500} onPress={onPress}>
410
+ <div ref={ref} />
411
+ </Press>
412
+ );
413
+ ReactDOM.render(element, container);
414
+
415
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
416
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
417
+ expect(onPress).toHaveBeenCalledTimes(1);
418
+ });
419
+
420
// No PointerEvent fallbacks
421
// TODO: jsdom missing APIs
251
- //it('is called after "touchend" event', () => {
252
- //ref.current.dispatchEvent(createPointerEvent('touchstart'));
253
- //ref.current.dispatchEvent(createPointerEvent('touchend'));
254
- //expect(onPress).toHaveBeenCalledTimes(1);
255
- //});
422
+ // it('is called after "touchend" event', () => {
423
+ // ref.current.dispatchEvent(createPointerEvent('touchstart'));
424
+ // ref.current.dispatchEvent(createPointerEvent('touchend'));
425
+ // expect(onPress).toHaveBeenCalledTimes(1);
426
+ // });
427
});
428
429
describe('onLongPress', () => {
@@ -332,7 +503,6 @@ describe('Event responder: Press', () => {
503
expect(onLongPress).toHaveBeenCalledTimes(1);
504
});
505
335
- /*
506
it('compounds with "delayPressStart"', () => {
507
const delayPressStart = 100;
508
const element = (
@@ -343,12 +513,13 @@ describe('Event responder: Press', () => {
513
ReactDOM.render(element, container);
514
515
ref.current.dispatchEvent(createPointerEvent('pointerdown'));
346
- jest.advanceTimersByTime(delayPressStart + DEFAULT_LONG_PRESS_DELAY - 1);
516
+ jest.advanceTimersByTime(
517
+ delayPressStart + DEFAULT_LONG_PRESS_DELAY - 1,
518
+ );
519
expect(onLongPress).not.toBeCalled();
520
jest.advanceTimersByTime(1);
521
expect(onLongPress).toHaveBeenCalledTimes(1);
522
});
351
- */
523
});
524
});
525
@@ -371,6 +542,28 @@ describe('Event responder: Press', () => {
542
expect(onLongPressChange).toHaveBeenCalledTimes(2);
543
expect(onLongPressChange).toHaveBeenCalledWith(false);
544
});
545
+
546
+ it('is called after delayed onPressEnd', () => {
547
+ const onLongPressChange = jest.fn();
548
+ const ref = React.createRef();
549
+ const element = (
550
+ <Press delayPressEnd={500} onLongPressChange={onLongPressChange}>
551
+ <div ref={ref} />
552
+ </Press>
553
+ );
554
+ ReactDOM.render(element, container);
555
+
556
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
557
+ jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY);
558
+ expect(onLongPressChange).toHaveBeenCalledTimes(1);
559
+ expect(onLongPressChange).toHaveBeenCalledWith(true);
560
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
561
+ jest.advanceTimersByTime(499);
562
+ expect(onLongPressChange).toHaveBeenCalledTimes(1);
563
+ jest.advanceTimersByTime(1);
564
+ expect(onLongPressChange).toHaveBeenCalledTimes(2);
565
+ expect(onLongPressChange).toHaveBeenCalledWith(false);
566
+ });
567
});
568
569
describe('onLongPressShouldCancelPress', () => {
@@ -430,6 +623,69 @@ describe('Event responder: Press', () => {
623
//});
624
//});
625
626
+ describe('delayed and multiple events', () => {
627
+ it('dispatches in the correct order', () => {
628
+ let events;
629
+ const ref = React.createRef();
630
+ const createEventHandler = msg => () => {
631
+ events.push(msg);
632
+ };
633
+
634
+ const element = (
635
+ <Press
636
+ delayPressStart={250}
637
+ delayPressEnd={250}
638
+ onLongPress={createEventHandler('onLongPress')}
639
+ onLongPressChange={createEventHandler('onLongPressChange')}
640
+ onPress={createEventHandler('onPress')}
641
+ onPressChange={createEventHandler('onPressChange')}
642
+ onPressStart={createEventHandler('onPressStart')}
643
+ onPressEnd={createEventHandler('onPressEnd')}>
644
+ <div ref={ref} />
645
+ </Press>
646
+ );
647
+
648
+ ReactDOM.render(element, container);
649
+
650
+ // 1
651
+ events = [];
652
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
653
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
654
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
655
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
656
+ jest.runAllTimers();
657
+
658
+ expect(events).toEqual([
659
+ 'onPressStart',
660
+ 'onPressChange',
661
+ 'onPress',
662
+ 'onPressStart',
663
+ 'onPress',
664
+ 'onPressEnd',
665
+ 'onPressChange',
666
+ ]);
667
+
668
+ // 2
669
+ events = [];
670
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
671
+ jest.advanceTimersByTime(250);
672
+ jest.advanceTimersByTime(500);
673
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
674
+ jest.runAllTimers();
675
+
676
+ expect(events).toEqual([
677
+ 'onPressStart',
678
+ 'onPressChange',
679
+ 'onLongPress',
680
+ 'onLongPressChange',
681
+ 'onPress',
682
+ 'onPressEnd',
683
+ 'onPressChange',
684
+ 'onLongPressChange',
685
+ ]);
686
+ });
687
+ });
688
+
689
describe('nested responders', () => {
690
it('dispatch events in the correct order', () => {
691
const events = [];