React events: fix nested Hover components error (#15428)
* Add failing test for nested Hover * Fix error caused by nested Hover event components
Nicolas Gallagher committed
Apr 17, 2019 at 11:33 UTC
1ae409d2c75741cceb1ce0bf12115cfffefb9c91
4 files changed
+91
-20
packages/react-events/src/Focus.js
+1
@@ -20,6 +20,7 @@ type FocusProps = {
20
onBlur: (e: FocusEvent) => void,
21
onFocus: (e: FocusEvent) => void,
22
onFocusChange: boolean => void,
23
+ stopPropagation: boolean,
24
};
25
26
type FocusState = {
packages/react-events/src/Hover.js
+24
-16
@@ -23,6 +23,8 @@ type HoverProps = {
23
onHoverEnd: (e: HoverEvent) => void,
24
onHoverMove: (e: HoverEvent) => void,
25
onHoverStart: (e: HoverEvent) => void,
26
+ preventDefault: boolean,
27
+ stopPropagation: boolean,
28
};
29
30
type HoverState = {
@@ -178,6 +180,11 @@ function dispatchHoverEndEvents(
180
if (props.onHoverChange) {
181
dispatchHoverChangeEvent(context, props, state);
182
}
183
+
184
+ state.isInHitSlop = false;
185
+ state.hoverTarget = null;
186
+ state.skipMouseAfterPointer = false;
187
+ state.isTouched = false;
188
};
189
190
if (state.isActiveHovered) {
@@ -231,7 +238,8 @@ const HoverResponder = {
238
props: HoverProps,
239
state: HoverState,
240
): boolean {
234
- const {type, phase, target, nativeEvent} = event;
241
+ const {type, phase, target} = event;
242
+ const nativeEvent: any = event.nativeEvent;
243
244
// Hover doesn't handle capture target events at this point
245
if (phase === CAPTURE_PHASE) {
@@ -247,11 +255,18 @@ const HoverResponder = {
255
}
256
break;
257
}
258
+ case 'touchcancel':
259
+ case 'touchend': {
260
+ if (state.isTouched) {
261
+ state.isTouched = false;
262
+ }
263
+ break;
264
+ }
265
266
case 'pointerover':
267
case 'mouseover': {
268
if (!state.isHovered && !state.isTouched) {
254
- if ((nativeEvent: any).pointerType === 'touch') {
269
+ if (nativeEvent.pointerType === 'touch') {
270
state.isTouched = true;
271
return false;
272
}
@@ -261,8 +276,8 @@ const HoverResponder = {
276
if (
277
context.isPositionWithinTouchHitTarget(
278
target.ownerDocument,
264
- (nativeEvent: any).x,
265
- (nativeEvent: any).y,
279
+ nativeEvent.x,
280
+ nativeEvent.y,
281
)
282
) {
283
state.isInHitSlop = true;
@@ -278,10 +293,6 @@ const HoverResponder = {
293
if (state.isHovered && !state.isTouched) {
294
dispatchHoverEndEvents(event, context, props, state);
295
}
281
- state.isInHitSlop = false;
282
- state.hoverTarget = null;
283
- state.isTouched = false;
284
- state.skipMouseAfterPointer = false;
296
break;
297
}
298
@@ -296,8 +307,8 @@ const HoverResponder = {
307
if (
308
!context.isPositionWithinTouchHitTarget(
309
target.ownerDocument,
299
- (nativeEvent: any).x,
300
- (nativeEvent: any).y,
310
+ nativeEvent.x,
311
+ nativeEvent.y,
312
)
313
) {
314
dispatchHoverStartEvents(event, context, props, state);
@@ -307,18 +318,15 @@ const HoverResponder = {
318
if (
319
context.isPositionWithinTouchHitTarget(
320
target.ownerDocument,
310
- (nativeEvent: any).x,
311
- (nativeEvent: any).y,
321
+ nativeEvent.x,
322
+ nativeEvent.y,
323
)
324
) {
325
dispatchHoverEndEvents(event, context, props, state);
326
state.isInHitSlop = true;
327
} else {
328
if (props.onHoverMove) {
318
- const syntheticEvent = createHoverEvent(
319
- 'hovermove',
320
- event.target,
321
- );
329
+ const syntheticEvent = createHoverEvent('hovermove', target);
330
context.dispatchEvent(syntheticEvent, props.onHoverMove, {
331
discrete: false,
332
});
packages/react-events/src/__tests__/Focus-test.internal.js
+1
-1
@@ -106,7 +106,7 @@ describe('Focus event responder', () => {
106
});
107
108
describe('nested Focus components', () => {
109
- it('does not propagate events by default', () => {
109
+ it('do not propagate events by default', () => {
110
const events = [];
111
const innerRef = React.createRef();
112
const outerRef = React.createRef();
packages/react-events/src/__tests__/Hover-test.internal.js
+65
-3
@@ -14,9 +14,14 @@ let ReactFeatureFlags;
14
let ReactDOM;
15
let Hover;
16
17
-const createPointerEvent = type => {
18
- const event = document.createEvent('Event');
19
- event.initEvent(type, true, true);
17
+const createPointerEvent = (type, data) => {
18
+ const event = document.createEvent('CustomEvent');
19
+ event.initCustomEvent(type, true, true);
20
+ if (data != null) {
21
+ Object.entries(data).forEach(([key, value]) => {
22
+ event[key] = value;
23
+ });
24
+ }
25
return event;
26
};
27
@@ -361,6 +366,63 @@ describe('Hover event responder', () => {
366
});
367
});
368
369
+ describe('nested Hover components', () => {
370
+ it('do not propagate events by default', () => {
371
+ const events = [];
372
+ const innerRef = React.createRef();
373
+ const outerRef = React.createRef();
374
+ const createEventHandler = msg => () => {
375
+ events.push(msg);
376
+ };
377
+
378
+ const element = (
379
+ <Hover
380
+ onHoverStart={createEventHandler('outer: onHoverStart')}
381
+ onHoverEnd={createEventHandler('outer: onHoverEnd')}
382
+ onHoverChange={createEventHandler('outer: onHoverChange')}>
383
+ <div ref={outerRef}>
384
+ <Hover
385
+ onHoverStart={createEventHandler('inner: onHoverStart')}
386
+ onHoverEnd={createEventHandler('inner: onHoverEnd')}
387
+ onHoverChange={createEventHandler('inner: onHoverChange')}>
388
+ <div ref={innerRef} />
389
+ </Hover>
390
+ </div>
391
+ </Hover>
392
+ );
393
+
394
+ ReactDOM.render(element, container);
395
+
396
+ outerRef.current.dispatchEvent(createPointerEvent('pointerover'));
397
+ outerRef.current.dispatchEvent(
398
+ createPointerEvent('pointerout', {relatedTarget: innerRef.current}),
399
+ );
400
+ innerRef.current.dispatchEvent(createPointerEvent('pointerover'));
401
+ innerRef.current.dispatchEvent(
402
+ createPointerEvent('pointerout', {relatedTarget: outerRef.current}),
403
+ );
404
+ outerRef.current.dispatchEvent(
405
+ createPointerEvent('pointerover', {relatedTarget: innerRef.current}),
406
+ );
407
+ outerRef.current.dispatchEvent(createPointerEvent('pointerout'));
408
+ // TODO: correct result should include commented events
409
+ expect(events).toEqual([
410
+ 'outer: onHoverStart',
411
+ 'outer: onHoverChange',
412
+ // 'outer: onHoverEnd',
413
+ // 'outer: onHoverChange',
414
+ 'inner: onHoverStart',
415
+ 'inner: onHoverChange',
416
+ 'inner: onHoverEnd',
417
+ 'inner: onHoverChange',
418
+ // 'outer: onHoverStart',
419
+ // 'outer: onHoverChange',
420
+ 'outer: onHoverEnd',
421
+ 'outer: onHoverChange',
422
+ ]);
423
+ });
424
+ });
425
+
426
it('expect displayName to show up for event component', () => {
427
expect(Hover.displayName).toBe('Hover');
428
});