@samitouri / QOS-React / commits / 958b6173fd

Add delay props to Hover event module (#15325)

Nicolas Gallagher committed Apr 5, 2019 at 04:23 UTC 958b6173fdcba503a4333d3a2e42c10781201f17
3 files changed +249 -42
packages/react-events/src/Hover.js
+103 -37
@@ -20,9 +20,12 @@ type HoverProps = {
20 };
21
22 type HoverState = {
23 + isActiveHovered: boolean,
24 isHovered: boolean,
25 isInHitSlop: boolean,
26 isTouched: boolean,
27 + hoverStartTimeout: null | TimeoutID,
28 + hoverEndTimeout: null | TimeoutID,
29 };
30
31 type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange';
@@ -60,29 +63,67 @@ function createHoverEvent(
63 };
64 }
65
66 +function dispatchHoverChangeEvent(
67 + event: ResponderEvent,
68 + context: ResponderContext,
69 + props: HoverProps,
70 + state: HoverState,
71 +): void {
72 + const listener = () => {
73 + props.onHoverChange(state.isActiveHovered);
74 + };
75 + const syntheticEvent = createHoverEvent(
76 + 'hoverchange',
77 + event.target,
78 + listener,
79 + );
80 + context.dispatchEvent(syntheticEvent, {discrete: true});
81 +}
82 +
83 function dispatchHoverStartEvents(
84 event: ResponderEvent,
85 context: ResponderContext,
86 props: HoverProps,
87 + state: HoverState,
88 ): void {
89 const {nativeEvent, target} = event;
90 if (context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)) {
91 return;
92 }
72 - if (props.onHoverStart) {
73 - const syntheticEvent = createHoverEvent(
74 - 'hoverstart',
75 - target,
76 - props.onHoverStart,
77 - );
78 - context.dispatchEvent(syntheticEvent, {discrete: true});
93 +
94 + state.isHovered = true;
95 +
96 + if (state.hoverEndTimeout !== null) {
97 + clearTimeout(state.hoverEndTimeout);
98 + state.hoverEndTimeout = null;
99 }
80 - if (props.onHoverChange) {
81 - const listener = () => {
82 - props.onHoverChange(true);
83 - };
84 - const syntheticEvent = createHoverEvent('hoverchange', target, listener);
85 - context.dispatchEvent(syntheticEvent, {discrete: true});
100 +
101 + const dispatch = () => {
102 + state.isActiveHovered = true;
103 +
104 + if (props.onHoverStart) {
105 + const syntheticEvent = createHoverEvent(
106 + 'hoverstart',
107 + target,
108 + props.onHoverStart,
109 + );
110 + context.dispatchEvent(syntheticEvent, {discrete: true});
111 + }
112 + if (props.onHoverChange) {
113 + dispatchHoverChangeEvent(event, context, props, state);
114 + }
115 + };
116 +
117 + if (!state.isActiveHovered) {
118 + const delay = calculateDelayMS(props.delayHoverStart, 0, 0);
119 + if (delay > 0) {
120 + state.hoverStartTimeout = context.setTimeout(() => {
121 + state.hoverStartTimeout = null;
122 + dispatch();
123 + }, delay);
124 + } else {
125 + dispatch();
126 + }
127 }
128 }
129
@@ -90,35 +131,63 @@ function dispatchHoverEndEvents(
131 event: ResponderEvent,
132 context: ResponderContext,
133 props: HoverProps,
134 + state: HoverState,
135 ) {
136 const {nativeEvent, target} = event;
137 if (context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)) {
138 return;
139 }
98 - if (props.onHoverEnd) {
99 - const syntheticEvent = createHoverEvent(
100 - 'hoverend',
101 - target,
102 - props.onHoverEnd,
103 - );
104 - context.dispatchEvent(syntheticEvent, {discrete: true});
140 +
141 + state.isHovered = false;
142 +
143 + if (state.hoverStartTimeout !== null) {
144 + clearTimeout(state.hoverStartTimeout);
145 + state.hoverStartTimeout = null;
146 }
106 - if (props.onHoverChange) {
107 - const listener = () => {
108 - props.onHoverChange(false);
109 - };
110 - const syntheticEvent = createHoverEvent('hoverchange', target, listener);
111 - context.dispatchEvent(syntheticEvent, {discrete: true});
147 +
148 + const dispatch = () => {
149 + state.isActiveHovered = false;
150 +
151 + if (props.onHoverEnd) {
152 + const syntheticEvent = createHoverEvent(
153 + 'hoverend',
154 + target,
155 + props.onHoverEnd,
156 + );
157 + context.dispatchEvent(syntheticEvent, {discrete: true});
158 + }
159 + if (props.onHoverChange) {
160 + dispatchHoverChangeEvent(event, context, props, state);
161 + }
162 + };
163 +
164 + if (state.isActiveHovered) {
165 + const delay = calculateDelayMS(props.delayHoverEnd, 0, 0);
166 + if (delay > 0) {
167 + state.hoverEndTimeout = context.setTimeout(() => {
168 + dispatch();
169 + }, delay);
170 + } else {
171 + dispatch();
172 + }
173 }
174 }
175
176 +function calculateDelayMS(delay: ?number, min = 0, fallback = 0) {
177 + const maybeNumber = delay == null ? null : delay;
178 + return Math.max(min, maybeNumber != null ? maybeNumber : fallback);
179 +}
180 +
181 const HoverResponder = {
182 targetEventTypes,
183 createInitialState() {
184 return {
185 + isActiveHovered: false,
186 isHovered: false,
187 isInHitSlop: false,
188 isTouched: false,
189 + hoverStartTimeout: null,
190 + hoverEndTimeout: null,
191 };
192 },
193 onEvent(
@@ -156,23 +225,22 @@ const HoverResponder = {
225 state.isInHitSlop = true;
226 return;
227 }
159 - dispatchHoverStartEvents(event, context, props);
160 - state.isHovered = true;
228 + dispatchHoverStartEvents(event, context, props, state);
229 }
230 break;
231 }
232 case 'pointerout':
233 case 'mouseout': {
234 if (state.isHovered && !state.isTouched) {
167 - dispatchHoverEndEvents(event, context, props);
168 - state.isHovered = false;
235 + dispatchHoverEndEvents(event, context, props, state);
236 }
237 state.isInHitSlop = false;
238 state.isTouched = false;
239 break;
240 }
241 +
242 case 'pointermove': {
175 - if (!state.isTouched) {
243 + if (state.isHovered && !state.isTouched) {
244 if (state.isInHitSlop) {
245 if (
246 !context.isPositionWithinTouchHitTarget(
@@ -180,8 +248,7 @@ const HoverResponder = {
248 (nativeEvent: any).y,
249 )
250 ) {
183 - dispatchHoverStartEvents(event, context, props);
184 - state.isHovered = true;
251 + dispatchHoverStartEvents(event, context, props, state);
252 state.isInHitSlop = false;
253 }
254 } else if (
@@ -191,17 +258,16 @@ const HoverResponder = {
258 (nativeEvent: any).y,
259 )
260 ) {
194 - dispatchHoverEndEvents(event, context, props);
195 - state.isHovered = false;
261 + dispatchHoverEndEvents(event, context, props, state);
262 state.isInHitSlop = true;
263 }
264 }
265 break;
266 }
267 +
268 case 'pointercancel': {
269 if (state.isHovered && !state.isTouched) {
203 - dispatchHoverEndEvents(event, context, props);
204 - state.isHovered = false;
270 + dispatchHoverEndEvents(event, context, props, state);
271 state.isTouched = false;
272 }
273 break;
packages/react-events/src/Swipe.js
+1 -1
@@ -104,7 +104,7 @@ const SwipeResponder = {
104 case 'mousedown':
105 case 'pointerdown': {
106 if (!state.isSwiping && !context.hasOwnership()) {
107 - let obj = event;
107 + let obj = nativeEvent;
108 if (type === 'touchstart') {
109 obj = (nativeEvent: any).targetTouches[0];
110 state.touchId = obj.identifier;
packages/react-events/src/__tests__/Hover-test.internal.js
+145 -4
@@ -84,8 +84,70 @@ describe('Hover event responder', () => {
84 expect(onHoverStart).not.toBeCalled();
85 });
86
87 - // TODO: complete delayHoverStart tests
88 - // describe('delayHoverStart', () => {});
87 + describe('delayHoverStart', () => {
88 + it('can be configured', () => {
89 + const element = (
90 + <Hover delayHoverStart={2000} onHoverStart={onHoverStart}>
91 + <div ref={ref} />
92 + </Hover>
93 + );
94 + ReactDOM.render(element, container);
95 +
96 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
97 + jest.advanceTimersByTime(1999);
98 + expect(onHoverStart).not.toBeCalled();
99 + jest.advanceTimersByTime(1);
100 + expect(onHoverStart).toHaveBeenCalledTimes(1);
101 + });
102 +
103 + it('onHoverStart is called synchronously if delay is 0ms', () => {
104 + const element = (
105 + <Hover delayHoverStart={0} onHoverStart={onHoverStart}>
106 + <div ref={ref} />
107 + </Hover>
108 + );
109 + ReactDOM.render(element, container);
110 +
111 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
112 + expect(onHoverStart).toHaveBeenCalledTimes(1);
113 + });
114 +
115 + it('onHoverStart is only called once per active hover', () => {
116 + const element = (
117 + <Hover
118 + delayHoverStart={500}
119 + delayHoverEnd={100}
120 + onHoverStart={onHoverStart}>
121 + <div ref={ref} />
122 + </Hover>
123 + );
124 + ReactDOM.render(element, container);
125 +
126 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
127 + jest.advanceTimersByTime(500);
128 + expect(onHoverStart).toHaveBeenCalledTimes(1);
129 + ref.current.dispatchEvent(createPointerEvent('pointerout'));
130 + jest.advanceTimersByTime(10);
131 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
132 + jest.runAllTimers();
133 + expect(onHoverStart).toHaveBeenCalledTimes(1);
134 + });
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 + });
150 + });
151 });
152
153 describe('onHoverChange', () => {
@@ -183,8 +245,87 @@ describe('Hover event responder', () => {
245 expect(onHoverEnd).not.toBeCalled();
246 });
247
186 - // TODO: complete delayHoverStart tests
187 - // describe('delayHoverEnd', () => {});
248 + describe('delayHoverEnd', () => {
249 + it('can be configured', () => {
250 + const element = (
251 + <Hover delayHoverEnd={2000} onHoverEnd={onHoverEnd}>
252 + <div ref={ref} />
253 + </Hover>
254 + );
255 + ReactDOM.render(element, container);
256 +
257 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
258 + ref.current.dispatchEvent(createPointerEvent('pointerout'));
259 + jest.advanceTimersByTime(1999);
260 + expect(onHoverEnd).not.toBeCalled();
261 + jest.advanceTimersByTime(1);
262 + expect(onHoverEnd).toHaveBeenCalledTimes(1);
263 + });
264 +
265 + it('delayHoverEnd is called synchronously if delay is 0ms', () => {
266 + const element = (
267 + <Hover delayHoverEnd={0} onHoverEnd={onHoverEnd}>
268 + <div ref={ref} />
269 + </Hover>
270 + );
271 + ReactDOM.render(element, container);
272 +
273 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
274 + ref.current.dispatchEvent(createPointerEvent('pointerout'));
275 + expect(onHoverEnd).toHaveBeenCalledTimes(1);
276 + });
277 +
278 + it('onHoverEnd is only called once per active hover', () => {
279 + const element = (
280 + <Hover delayHoverEnd={500} onHoverEnd={onHoverEnd}>
281 + <div ref={ref} />
282 + </Hover>
283 + );
284 + ReactDOM.render(element, container);
285 +
286 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
287 + ref.current.dispatchEvent(createPointerEvent('pointerout'));
288 + jest.advanceTimersByTime(499);
289 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
290 + jest.advanceTimersByTime(100);
291 + ref.current.dispatchEvent(createPointerEvent('pointerout'));
292 + jest.runAllTimers();
293 + expect(onHoverEnd).toHaveBeenCalledTimes(1);
294 + });
295 +
296 + it('onHoverEnd is not called if "pointerover" is dispatched during a delay', () => {
297 + const element = (
298 + <Hover delayHoverEnd={500} onHoverEnd={onHoverEnd}>
299 + <div ref={ref} />
300 + </Hover>
301 + );
302 + ReactDOM.render(element, container);
303 +
304 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
305 + ref.current.dispatchEvent(createPointerEvent('pointerout'));
306 + jest.advanceTimersByTime(499);
307 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
308 + jest.advanceTimersByTime(1);
309 + expect(onHoverEnd).not.toBeCalled();
310 + });
311 +
312 + it('onHoverEnd is not called if there was no active hover', () => {
313 + const element = (
314 + <Hover
315 + delayHoverStart={500}
316 + delayHoverEnd={100}
317 + onHoverEnd={onHoverEnd}>
318 + <div ref={ref} />
319 + </Hover>
320 + );
321 + ReactDOM.render(element, container);
322 +
323 + ref.current.dispatchEvent(createPointerEvent('pointerover'));
324 + ref.current.dispatchEvent(createPointerEvent('pointerout'));
325 + jest.runAllTimers();
326 + expect(onHoverEnd).not.toBeCalled();
327 + });
328 + });
329 });
330
331 it('expect displayName to show up for event component', () => {