@samitouri / QOS-React / commits / 81a61b1d1a

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 = [];