@samitouri / QOS-React-2 / commits / 5e21157d86

[react-interactions] Fix Hover issues with portals (#17765)

Dominic Gannaway committed Jan 7, 2020 at 19:45 UTC 5e21157d86a4f589891e90f4103b3a411af889a3
2 files changed +111 -45
packages/react-interactions/events/src/dom/Hover.js
+85 -45
@@ -112,14 +112,14 @@ function dispatchHoverStartEvents(
112 context: ReactDOMResponderContext,
113 props: HoverProps,
114 state: HoverState,
115 -): void {
115 +): boolean {
116 const target = state.hoverTarget;
117 if (event !== null) {
118 const {nativeEvent} = event;
119 if (
120 context.isTargetWithinResponderScope((nativeEvent: any).relatedTarget)
121 ) {
122 - return;
122 + return false;
123 }
124 }
125
@@ -139,6 +139,7 @@ function dispatchHoverStartEvents(
139 }
140 dispatchHoverChangeEvent(event, context, props, state);
141 }
142 + return true;
143 }
144
145 function dispatchHoverMoveEvent(event, context, props, state) {
@@ -160,14 +161,14 @@ function dispatchHoverEndEvents(
161 context: ReactDOMResponderContext,
162 props: HoverProps,
163 state: HoverState,
163 -) {
164 +): boolean {
165 const target = state.hoverTarget;
166 if (event !== null) {
167 const {nativeEvent} = event;
168 if (
169 context.isTargetWithinResponderScope((nativeEvent: any).relatedTarget)
170 ) {
170 - return;
171 + return false;
172 }
173 }
174
@@ -189,6 +190,7 @@ function dispatchHoverEndEvents(
190 state.hoverTarget = null;
191 state.isTouched = false;
192 }
193 + return true;
194 }
195
196 function unmountResponder(
@@ -201,13 +203,10 @@ function unmountResponder(
203 }
204 }
205
206 +const rootPointerEventTypes = ['pointerout', 'pointermove', 'pointercancel'];
207 +
208 const hoverResponderImpl = {
205 - targetEventTypes: [
206 - 'pointerover',
207 - 'pointermove',
208 - 'pointerout',
209 - 'pointercancel',
210 - ],
209 + targetEventTypes: ['pointerover'],
210 getInitialState() {
211 return {
212 isActiveHovered: false,
@@ -216,6 +215,38 @@ const hoverResponderImpl = {
215 },
216 allowMultipleHostChildren: false,
217 allowEventHooks: true,
218 + onRootEvent(
219 + event: ReactDOMResponderEvent,
220 + context: ReactDOMResponderContext,
221 + props: HoverProps,
222 + state: HoverState,
223 + ): void {
224 + const {type} = event;
225 +
226 + switch (type) {
227 + // MOVE
228 + case 'pointermove': {
229 + if (state.isHovered && state.hoverTarget !== null) {
230 + dispatchHoverMoveEvent(event, context, props, state);
231 + }
232 + break;
233 + }
234 +
235 + // END
236 + case 'pointercancel':
237 + case 'pointerout': {
238 + if (state.isHovered) {
239 + if (
240 + dispatchHoverEndEvents(event, context, props, state) ||
241 + type === 'pointercancel'
242 + ) {
243 + context.removeRootEventTypes(rootPointerEventTypes);
244 + }
245 + }
246 + break;
247 + }
248 + }
249 + },
250 onEvent(
251 event: ReactDOMResponderEvent,
252 context: ReactDOMResponderContext,
@@ -226,6 +257,7 @@ const hoverResponderImpl = {
257
258 if (props.disabled) {
259 if (state.isHovered) {
260 + context.removeRootEventTypes(rootPointerEventTypes);
261 dispatchHoverEndEvents(event, context, props, state);
262 }
263 return;
@@ -236,24 +268,9 @@ const hoverResponderImpl = {
268 case 'pointerover': {
269 if (!state.isHovered && pointerType !== 'touch') {
270 state.hoverTarget = context.getResponderNode();
239 - dispatchHoverStartEvents(event, context, props, state);
240 - }
241 - break;
242 - }
243 -
244 - // MOVE
245 - case 'pointermove': {
246 - if (state.isHovered && state.hoverTarget !== null) {
247 - dispatchHoverMoveEvent(event, context, props, state);
248 - }
249 - break;
250 - }
251 -
252 - // END
253 - case 'pointerout':
254 - case 'pointercancel': {
255 - if (state.isHovered) {
256 - dispatchHoverEndEvents(event, context, props, state);
271 + if (dispatchHoverStartEvents(event, context, props, state)) {
272 + context.addRootEventTypes(rootPointerEventTypes);
273 + }
274 }
275 break;
276 }
@@ -262,8 +279,10 @@ const hoverResponderImpl = {
279 onUnmount: unmountResponder,
280 };
281
282 +const rootMouseEventTypes = ['mousemove', 'mouseout'];
283 +
284 const hoverResponderFallbackImpl = {
266 - targetEventTypes: ['mouseover', 'mousemove', 'mouseout', 'touchstart'],
285 + targetEventTypes: ['mouseover', 'mousemove', 'touchstart'],
286 getInitialState() {
287 return {
288 isActiveHovered: false,
@@ -274,6 +293,38 @@ const hoverResponderFallbackImpl = {
293 },
294 allowMultipleHostChildren: false,
295 allowEventHooks: true,
296 + onRootEvent(
297 + event: ReactDOMResponderEvent,
298 + context: ReactDOMResponderContext,
299 + props: HoverProps,
300 + state: HoverState,
301 + ): void {
302 + const {type} = event;
303 +
304 + switch (type) {
305 + // MOVE
306 + case 'mousemove': {
307 + if (
308 + state.isHovered &&
309 + state.hoverTarget !== null &&
310 + !state.ignoreEmulatedMouseEvents
311 + ) {
312 + dispatchHoverMoveEvent(event, context, props, state);
313 + }
314 + break;
315 + }
316 +
317 + // END
318 + case 'mouseout': {
319 + if (state.isHovered) {
320 + if (dispatchHoverEndEvents(event, context, props, state)) {
321 + context.removeRootEventTypes(rootMouseEventTypes);
322 + }
323 + }
324 + break;
325 + }
326 + }
327 + },
328 onEvent(
329 event: ReactDOMResponderEvent,
330 context: ReactDOMResponderContext,
@@ -284,6 +335,7 @@ const hoverResponderFallbackImpl = {
335
336 if (props.disabled) {
337 if (state.isHovered) {
338 + context.removeRootEventTypes(rootMouseEventTypes);
339 dispatchHoverEndEvents(event, context, props, state);
340 state.ignoreEmulatedMouseEvents = false;
341 }
@@ -296,34 +348,22 @@ const hoverResponderFallbackImpl = {
348 case 'mouseover': {
349 if (!state.isHovered && !state.ignoreEmulatedMouseEvents) {
350 state.hoverTarget = context.getResponderNode();
299 - dispatchHoverStartEvents(event, context, props, state);
351 + if (dispatchHoverStartEvents(event, context, props, state)) {
352 + context.addRootEventTypes(rootMouseEventTypes);
353 + }
354 }
355 break;
356 }
357
358 // MOVE
359 case 'mousemove': {
306 - if (
307 - state.isHovered &&
308 - state.hoverTarget !== null &&
309 - !state.ignoreEmulatedMouseEvents
310 - ) {
311 - dispatchHoverMoveEvent(event, context, props, state);
312 - } else if (!state.isHovered && type === 'mousemove') {
360 + if (!state.isHovered && type === 'mousemove') {
361 state.ignoreEmulatedMouseEvents = false;
362 state.isTouched = false;
363 }
364 break;
365 }
366
319 - // END
320 - case 'mouseout': {
321 - if (state.isHovered) {
322 - dispatchHoverEndEvents(event, context, props, state);
323 - }
324 - break;
325 - }
326 -
367 case 'touchstart': {
368 if (!state.isHovered) {
369 state.isTouched = true;
packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js
+26
@@ -193,6 +193,32 @@ describe.each(table)('Hover responder', hasPointerEvents => {
193 target.pointerup({pointerType: 'touch'});
194 expect(onHoverEnd).not.toBeCalled();
195 });
196 +
197 + it('should correctly work with React Portals', () => {
198 + const portalNode = document.createElement('div');
199 + const divRef = React.createRef();
200 + const spanRef = React.createRef();
201 +
202 + function Test() {
203 + const listener = useHover({
204 + onHoverEnd,
205 + });
206 + return (
207 + <div ref={divRef} DEPRECATED_flareListeners={listener}>
208 + {ReactDOM.createPortal(<span ref={spanRef} />, portalNode)}
209 + </div>
210 + );
211 + }
212 + ReactDOM.render(<Test />, container);
213 + const div = createEventTarget(divRef.current);
214 + div.pointerenter();
215 + const span = createEventTarget(spanRef.current);
216 + span.pointerexit();
217 + expect(onHoverEnd).not.toBeCalled();
218 + const body = createEventTarget(document.body);
219 + body.pointerexit();
220 + expect(onHoverEnd).toBeCalled();
221 + });
222 });
223
224 describe('onHoverMove', () => {