Skip to content

Commit 1ae409d

Browse files
authored
React events: fix nested Hover components error (#15428)
* Add failing test for nested Hover * Fix error caused by nested Hover event components
1 parent c73ab39 commit 1ae409d

File tree

4 files changed

+91
-20
lines changed

4 files changed

+91
-20
lines changed

packages/react-events/src/Focus.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type FocusProps = {
2020
onBlur: (e: FocusEvent) => void,
2121
onFocus: (e: FocusEvent) => void,
2222
onFocusChange: boolean => void,
23+
stopPropagation: boolean,
2324
};
2425

2526
type FocusState = {

packages/react-events/src/Hover.js

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ type HoverProps = {
2323
onHoverEnd: (e: HoverEvent) => void,
2424
onHoverMove: (e: HoverEvent) => void,
2525
onHoverStart: (e: HoverEvent) => void,
26+
preventDefault: boolean,
27+
stopPropagation: boolean,
2628
};
2729

2830
type HoverState = {
@@ -178,6 +180,11 @@ function dispatchHoverEndEvents(
178180
if (props.onHoverChange) {
179181
dispatchHoverChangeEvent(context, props, state);
180182
}
183+
184+
state.isInHitSlop = false;
185+
state.hoverTarget = null;
186+
state.skipMouseAfterPointer = false;
187+
state.isTouched = false;
181188
};
182189

183190
if (state.isActiveHovered) {
@@ -231,7 +238,8 @@ const HoverResponder = {
231238
props: HoverProps,
232239
state: HoverState,
233240
): boolean {
234-
const {type, phase, target, nativeEvent} = event;
241+
const {type, phase, target} = event;
242+
const nativeEvent: any = event.nativeEvent;
235243

236244
// Hover doesn't handle capture target events at this point
237245
if (phase === CAPTURE_PHASE) {
@@ -247,11 +255,18 @@ const HoverResponder = {
247255
}
248256
break;
249257
}
258+
case 'touchcancel':
259+
case 'touchend': {
260+
if (state.isTouched) {
261+
state.isTouched = false;
262+
}
263+
break;
264+
}
250265

251266
case 'pointerover':
252267
case 'mouseover': {
253268
if (!state.isHovered && !state.isTouched) {
254-
if ((nativeEvent: any).pointerType === 'touch') {
269+
if (nativeEvent.pointerType === 'touch') {
255270
state.isTouched = true;
256271
return false;
257272
}
@@ -261,8 +276,8 @@ const HoverResponder = {
261276
if (
262277
context.isPositionWithinTouchHitTarget(
263278
target.ownerDocument,
264-
(nativeEvent: any).x,
265-
(nativeEvent: any).y,
279+
nativeEvent.x,
280+
nativeEvent.y,
266281
)
267282
) {
268283
state.isInHitSlop = true;
@@ -278,10 +293,6 @@ const HoverResponder = {
278293
if (state.isHovered && !state.isTouched) {
279294
dispatchHoverEndEvents(event, context, props, state);
280295
}
281-
state.isInHitSlop = false;
282-
state.hoverTarget = null;
283-
state.isTouched = false;
284-
state.skipMouseAfterPointer = false;
285296
break;
286297
}
287298

@@ -296,8 +307,8 @@ const HoverResponder = {
296307
if (
297308
!context.isPositionWithinTouchHitTarget(
298309
target.ownerDocument,
299-
(nativeEvent: any).x,
300-
(nativeEvent: any).y,
310+
nativeEvent.x,
311+
nativeEvent.y,
301312
)
302313
) {
303314
dispatchHoverStartEvents(event, context, props, state);
@@ -307,18 +318,15 @@ const HoverResponder = {
307318
if (
308319
context.isPositionWithinTouchHitTarget(
309320
target.ownerDocument,
310-
(nativeEvent: any).x,
311-
(nativeEvent: any).y,
321+
nativeEvent.x,
322+
nativeEvent.y,
312323
)
313324
) {
314325
dispatchHoverEndEvents(event, context, props, state);
315326
state.isInHitSlop = true;
316327
} else {
317328
if (props.onHoverMove) {
318-
const syntheticEvent = createHoverEvent(
319-
'hovermove',
320-
event.target,
321-
);
329+
const syntheticEvent = createHoverEvent('hovermove', target);
322330
context.dispatchEvent(syntheticEvent, props.onHoverMove, {
323331
discrete: false,
324332
});

packages/react-events/src/__tests__/Focus-test.internal.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ describe('Focus event responder', () => {
106106
});
107107

108108
describe('nested Focus components', () => {
109-
it('does not propagate events by default', () => {
109+
it('do not propagate events by default', () => {
110110
const events = [];
111111
const innerRef = React.createRef();
112112
const outerRef = React.createRef();

packages/react-events/src/__tests__/Hover-test.internal.js

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@ let ReactFeatureFlags;
1414
let ReactDOM;
1515
let Hover;
1616

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+
}
2025
return event;
2126
};
2227

@@ -361,6 +366,63 @@ describe('Hover event responder', () => {
361366
});
362367
});
363368

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+
364426
it('expect displayName to show up for event component', () => {
365427
expect(Hover.displayName).toBe('Hover');
366428
});

0 commit comments

Comments
 (0)