[react-interactions] Add Portal propagation configuration (#16889)
Dominic Gannaway committed
Sep 25, 2019 at 16:56 UTC
d6d83d7060388c6b1b3bc0a28708995de6240ce3
6 files changed
+62
-3
packages/react-dom/src/events/DOMEventResponderSystem.js
+7
-3
@@ -12,7 +12,7 @@ import {
12
PASSIVE_NOT_SUPPORTED,
13
} from 'legacy-events/EventSystemFlags';
14
import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
15
-import {HostComponent, ScopeComponent} from 'shared/ReactWorkTags';
15
+import {HostComponent, ScopeComponent, HostPortal} from 'shared/ReactWorkTags';
16
import type {EventPriority} from 'shared/ReactTypes';
17
import type {
18
ReactDOMEventResponder,
@@ -451,9 +451,12 @@ function traverseAndHandleEventResponderInstances(
451
isPassiveSupported,
452
);
453
let node = targetFiber;
454
+ let insidePortal = false;
455
while (node !== null) {
456
const {dependencies, tag} = node;
456
- if (
457
+ if (tag === HostPortal) {
458
+ insidePortal = true;
459
+ } else if (
460
(tag === HostComponent || tag === ScopeComponent) &&
461
dependencies !== null
462
) {
@@ -465,7 +468,8 @@ function traverseAndHandleEventResponderInstances(
468
const {props, responder, state} = responderInstance;
469
if (
470
!visitedResponders.has(responder) &&
468
- validateResponderTargetEventTypes(eventType, responder)
471
+ validateResponderTargetEventTypes(eventType, responder) &&
472
+ (!insidePortal || responder.targetPortalPropagation)
473
) {
474
visitedResponders.add(responder);
475
const onEvent = responder.onEvent;
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
+49
@@ -28,6 +28,7 @@ function createEventResponder({
28
onMount,
29
onUnmount,
30
getInitialState,
31
+ targetPortalPropagation,
32
}) {
33
return React.unstable_createResponder('TestEventResponder', {
34
targetEventTypes,
@@ -37,6 +38,7 @@ function createEventResponder({
38
onMount,
39
onUnmount,
40
getInitialState,
41
+ targetPortalPropagation,
42
});
43
}
44
@@ -1034,4 +1036,51 @@ describe('DOMEventResponderSystem', () => {
1036
},
1037
]);
1038
});
1039
+
1040
+ it('should not propagate target events through portals by default', () => {
1041
+ const buttonRef = React.createRef();
1042
+ const onEvent = jest.fn();
1043
+ const TestResponder = createEventResponder({
1044
+ targetEventTypes: ['click'],
1045
+ onEvent,
1046
+ });
1047
+ const domNode = document.createElement('div');
1048
+ document.body.appendChild(domNode);
1049
+ const Component = () => {
1050
+ const listener = React.unstable_useResponder(TestResponder, {});
1051
+ return (
1052
+ <div listeners={listener}>
1053
+ {ReactDOM.createPortal(<button ref={buttonRef} />, domNode)}
1054
+ </div>
1055
+ );
1056
+ };
1057
+ ReactDOM.render(<Component />, container);
1058
+ dispatchClickEvent(buttonRef.current);
1059
+ document.body.removeChild(domNode);
1060
+ expect(onEvent).not.toBeCalled();
1061
+ });
1062
+
1063
+ it('should propagate target events through portals when enabled', () => {
1064
+ const buttonRef = React.createRef();
1065
+ const onEvent = jest.fn();
1066
+ const TestResponder = createEventResponder({
1067
+ targetPortalPropagation: true,
1068
+ targetEventTypes: ['click'],
1069
+ onEvent,
1070
+ });
1071
+ const domNode = document.createElement('div');
1072
+ document.body.appendChild(domNode);
1073
+ const Component = () => {
1074
+ const listener = React.unstable_useResponder(TestResponder, {});
1075
+ return (
1076
+ <div listeners={listener}>
1077
+ {ReactDOM.createPortal(<button ref={buttonRef} />, domNode)}
1078
+ </div>
1079
+ );
1080
+ };
1081
+ ReactDOM.render(<Component />, container);
1082
+ dispatchClickEvent(buttonRef.current);
1083
+ document.body.removeChild(domNode);
1084
+ expect(onEvent).toBeCalled();
1085
+ });
1086
});
packages/react-interactions/events/src/dom/Focus.js
+2
@@ -293,6 +293,7 @@ function unmountFocusResponder(
293
294
const focusResponderImpl = {
295
targetEventTypes,
296
+ targetPortalPropagation: true,
297
rootEventTypes,
298
getInitialState(): FocusState {
299
return {
@@ -430,6 +431,7 @@ function unmountFocusWithinResponder(
431
432
const focusWithinResponderImpl = {
433
targetEventTypes,
434
+ targetPortalPropagation: true,
435
rootEventTypes,
436
getInitialState(): FocusState {
437
return {
packages/react-interactions/events/src/dom/Keyboard.js
+1
@@ -180,6 +180,7 @@ function dispatchKeyboardEvent(
180
181
const keyboardResponderImpl = {
182
targetEventTypes,
183
+ targetPortalPropagation: true,
184
getInitialState(): KeyboardState {
185
return {
186
isActive: false,
packages/shared/ReactTypes.js
+1
@@ -96,6 +96,7 @@ export type ReactEventResponder<E, C> = {
96
$$typeof: Symbol | number,
97
displayName: string,
98
targetEventTypes: null | Array<string>,
99
+ targetPortalPropagation: boolean,
100
rootEventTypes: null | Array<string>,
101
getInitialState: null | ((props: Object) => Object),
102
onEvent:
packages/shared/createEventResponder.js
+2
@@ -22,6 +22,7 @@ export default function createEventResponder<E, C>(
22
onRootEvent,
23
rootEventTypes,
24
targetEventTypes,
25
+ targetPortalPropagation,
26
} = responderConfig;
27
const eventResponder = {
28
$$typeof: REACT_RESPONDER_TYPE,
@@ -33,6 +34,7 @@ export default function createEventResponder<E, C>(
34
onUnmount: onUnmount || null,
35
rootEventTypes: rootEventTypes || null,
36
targetEventTypes: targetEventTypes || null,
37
+ targetPortalPropagation: targetPortalPropagation || false,
38
};
39
// We use responder as a Map key later on. When we have a bad
40
// polyfill, then we can't use it as a key as the polyfill tries