mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Modern Event System: Support nested portal/root boundaries (#18201)
This commit is contained in:
+65
-1
@@ -19,6 +19,8 @@ import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching';
|
||||
import {executeDispatchesInOrder} from 'legacy-events/EventPluginUtils';
|
||||
import {plugins} from 'legacy-events/EventPluginRegistry';
|
||||
|
||||
import {HostRoot, HostPortal} from 'shared/ReactWorkTags';
|
||||
|
||||
import {trapEventForPluginEventSystem} from './ReactDOMEventListener';
|
||||
import getEventTarget from './getEventTarget';
|
||||
import {getListenerMapForElement} from './DOMEventListenerMap';
|
||||
@@ -56,7 +58,8 @@ import {
|
||||
TOP_PROGRESS,
|
||||
TOP_PLAYING,
|
||||
} from './DOMTopLevelEventTypes';
|
||||
import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
|
||||
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
|
||||
import {DOCUMENT_NODE, COMMENT_NODE} from '../shared/HTMLNodeType';
|
||||
|
||||
import {enableLegacyFBPrimerSupport} from 'shared/ReactFeatureFlags';
|
||||
|
||||
@@ -206,6 +209,17 @@ function willDeferLaterForFBLegacyPrimer(nativeEvent: any): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isMatchingRootContainer(
|
||||
grandContainer: Element,
|
||||
rootContainer: Document | Element,
|
||||
): boolean {
|
||||
return (
|
||||
grandContainer === rootContainer ||
|
||||
(grandContainer.nodeType === COMMENT_NODE &&
|
||||
grandContainer.parentNode === rootContainer)
|
||||
);
|
||||
}
|
||||
|
||||
export function dispatchEventForPluginEventSystem(
|
||||
topLevelType: DOMTopLevelEventType,
|
||||
eventSystemFlags: EventSystemFlags,
|
||||
@@ -224,6 +238,56 @@ export function dispatchEventForPluginEventSystem(
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// The below logic attempts to work out if we need to change
|
||||
// the target fiber to a different ancestor. We had similar logic
|
||||
// in the legacy event system, except the big difference between
|
||||
// systems is that the modern event system now has an event listener
|
||||
// attached to each React Root and React Portal Root. Together,
|
||||
// the DOM nodes representing these roots are the "rootContainer".
|
||||
// To figure out which ancestor instance we should use, we traverse
|
||||
// up the fiber tree from the target instance and attempt to find
|
||||
// root boundaries that match that of our current "rootContainer".
|
||||
// If we find that "rootContainer", we find the parent fiber
|
||||
// sub-tree for that root and make that our ancestor instance.
|
||||
let node = targetInst;
|
||||
|
||||
while (true) {
|
||||
if (node === null) {
|
||||
return;
|
||||
}
|
||||
if (node.tag === HostRoot || node.tag === HostPortal) {
|
||||
const container = node.stateNode.containerInfo;
|
||||
if (isMatchingRootContainer(container, rootContainer)) {
|
||||
break;
|
||||
}
|
||||
if (node.tag === HostPortal) {
|
||||
// The target is a portal, but it's not the rootContainer we're looking for.
|
||||
// Normally portals handle their own events all the way down to the root.
|
||||
// So we should be able to stop now. However, we don't know if this portal
|
||||
// was part of *our* root.
|
||||
let grandNode = node.return;
|
||||
while (grandNode !== null) {
|
||||
if (grandNode.tag === HostRoot || grandNode.tag === HostPortal) {
|
||||
const grandContainer = grandNode.stateNode.containerInfo;
|
||||
if (isMatchingRootContainer(grandContainer, rootContainer)) {
|
||||
// This is the rootContainer we're looking for and we found it as
|
||||
// a parent of the Portal. That means we can ignore it because the
|
||||
// Portal will bubble through to us.
|
||||
return;
|
||||
}
|
||||
}
|
||||
grandNode = grandNode.return;
|
||||
}
|
||||
}
|
||||
const parentSubtreeInst = getClosestInstanceFromNode(container);
|
||||
if (parentSubtreeInst === null) {
|
||||
return;
|
||||
}
|
||||
node = ancestorInst = parentSubtreeInst;
|
||||
continue;
|
||||
}
|
||||
node = node.return;
|
||||
}
|
||||
}
|
||||
|
||||
batchedEventUpdates(() =>
|
||||
|
||||
+871
-1
@@ -12,6 +12,8 @@
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
let Scheduler;
|
||||
|
||||
function dispatchClickEvent(element) {
|
||||
const event = document.createEvent('Event');
|
||||
@@ -29,6 +31,8 @@ describe('DOMModernPluginEventSystem', () => {
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
Scheduler = require('scheduler');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
@@ -77,6 +81,500 @@ describe('DOMModernPluginEventSystem', () => {
|
||||
expect(log[5]).toEqual(['bubble', buttonElement]);
|
||||
});
|
||||
|
||||
it('handle propagation of click events between roots', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const childRef = React.createRef();
|
||||
const log = [];
|
||||
const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
|
||||
const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
|
||||
Click me!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Parent() {
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={onClick}
|
||||
onClickCapture={onClickCapture}>
|
||||
<div ref={childRef} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Parent />, container);
|
||||
ReactDOM.render(<Child />, childRef.current);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(1);
|
||||
expect(log[0]).toEqual(['capture', buttonElement]);
|
||||
expect(log[1]).toEqual(['bubble', buttonElement]);
|
||||
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(3);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(3);
|
||||
expect(log[2]).toEqual(['capture', divElement]);
|
||||
expect(log[3]).toEqual(['bubble', divElement]);
|
||||
expect(log[4]).toEqual(['capture', buttonElement]);
|
||||
expect(log[5]).toEqual(['bubble', buttonElement]);
|
||||
});
|
||||
|
||||
it('handle propagation of click events between disjointed roots', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const log = [];
|
||||
const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
|
||||
const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
|
||||
Click me!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Parent() {
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={onClick}
|
||||
onClickCapture={onClickCapture}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const disjointedNode = document.createElement('div');
|
||||
ReactDOM.render(<Parent />, container);
|
||||
buttonRef.current.appendChild(disjointedNode);
|
||||
ReactDOM.render(<Child />, disjointedNode);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(1);
|
||||
expect(log[0]).toEqual(['capture', buttonElement]);
|
||||
expect(log[1]).toEqual(['bubble', buttonElement]);
|
||||
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(3);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(3);
|
||||
expect(log[2]).toEqual(['capture', divElement]);
|
||||
expect(log[3]).toEqual(['bubble', divElement]);
|
||||
expect(log[4]).toEqual(['capture', buttonElement]);
|
||||
expect(log[5]).toEqual(['bubble', buttonElement]);
|
||||
});
|
||||
|
||||
it('handle propagation of click events between disjointed comment roots', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const log = [];
|
||||
const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
|
||||
const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
|
||||
Click me!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Parent() {
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={onClick}
|
||||
onClickCapture={onClickCapture}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// We use a comment node here, then mount to it
|
||||
const disjointedNode = document.createComment(
|
||||
' react-mount-point-unstable ',
|
||||
);
|
||||
ReactDOM.render(<Parent />, container);
|
||||
buttonRef.current.appendChild(disjointedNode);
|
||||
ReactDOM.render(<Child />, disjointedNode);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(1);
|
||||
expect(log[0]).toEqual(['capture', buttonElement]);
|
||||
expect(log[1]).toEqual(['bubble', buttonElement]);
|
||||
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(3);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(3);
|
||||
expect(log[2]).toEqual(['capture', divElement]);
|
||||
expect(log[3]).toEqual(['bubble', divElement]);
|
||||
expect(log[4]).toEqual(['capture', buttonElement]);
|
||||
expect(log[5]).toEqual(['bubble', buttonElement]);
|
||||
});
|
||||
|
||||
it('handle propagation of click events between disjointed comment roots #2', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const spanRef = React.createRef();
|
||||
const log = [];
|
||||
const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
|
||||
const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
|
||||
Click me!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Parent() {
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={onClick}
|
||||
onClickCapture={onClickCapture}>
|
||||
<span ref={spanRef} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// We use a comment node here, then mount to it
|
||||
const disjointedNode = document.createComment(
|
||||
' react-mount-point-unstable ',
|
||||
);
|
||||
ReactDOM.render(<Parent />, container);
|
||||
spanRef.current.appendChild(disjointedNode);
|
||||
ReactDOM.render(<Child />, disjointedNode);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(1);
|
||||
expect(log[0]).toEqual(['capture', buttonElement]);
|
||||
expect(log[1]).toEqual(['bubble', buttonElement]);
|
||||
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(3);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(3);
|
||||
expect(log[2]).toEqual(['capture', divElement]);
|
||||
expect(log[3]).toEqual(['bubble', divElement]);
|
||||
expect(log[4]).toEqual(['capture', buttonElement]);
|
||||
expect(log[5]).toEqual(['bubble', buttonElement]);
|
||||
});
|
||||
|
||||
it('handle propagation of click events between portals', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const log = [];
|
||||
const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
|
||||
const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
|
||||
|
||||
const portalElement = document.createElement('div');
|
||||
document.body.appendChild(portalElement);
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
|
||||
Click me!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Parent() {
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={onClick}
|
||||
onClickCapture={onClickCapture}>
|
||||
{ReactDOM.createPortal(<Child />, portalElement)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Parent />, container);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(1);
|
||||
expect(log[0]).toEqual(['capture', buttonElement]);
|
||||
expect(log[1]).toEqual(['bubble', buttonElement]);
|
||||
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(3);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(3);
|
||||
expect(log[2]).toEqual(['capture', buttonElement]);
|
||||
expect(log[3]).toEqual(['capture', divElement]);
|
||||
expect(log[4]).toEqual(['bubble', divElement]);
|
||||
expect(log[5]).toEqual(['bubble', buttonElement]);
|
||||
|
||||
document.body.removeChild(portalElement);
|
||||
});
|
||||
|
||||
it('handle click events on document.body portals', () => {
|
||||
const log = [];
|
||||
|
||||
function Child({label}) {
|
||||
return <div onClick={() => log.push(label)}>{label}</div>;
|
||||
}
|
||||
|
||||
function Parent() {
|
||||
return (
|
||||
<>
|
||||
{ReactDOM.createPortal(<Child label={'first'} />, document.body)}
|
||||
{ReactDOM.createPortal(<Child label={'second'} />, document.body)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Parent />, container);
|
||||
|
||||
const second = document.body.lastChild;
|
||||
expect(second.textContent).toEqual('second');
|
||||
dispatchClickEvent(second);
|
||||
|
||||
expect(log).toEqual(['second']);
|
||||
|
||||
const first = second.previousSibling;
|
||||
expect(first.textContent).toEqual('first');
|
||||
dispatchClickEvent(first);
|
||||
|
||||
expect(log).toEqual(['second', 'first']);
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'does not invoke an event on a parent tree when a subtree is dehydrated',
|
||||
async () => {
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
|
||||
|
||||
let clicks = 0;
|
||||
let childSlotRef = React.createRef();
|
||||
|
||||
function Parent() {
|
||||
return <div onClick={() => clicks++} ref={childSlotRef} />;
|
||||
}
|
||||
|
||||
function Child({text}) {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return <a>Click me</a>;
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
// The root is a Suspense boundary.
|
||||
return (
|
||||
<React.Suspense fallback="Loading...">
|
||||
<Child />
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
suspend = false;
|
||||
let finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
|
||||
let parentContainer = document.createElement('div');
|
||||
let childContainer = document.createElement('div');
|
||||
|
||||
// We need this to be in the document since we'll dispatch events on it.
|
||||
document.body.appendChild(parentContainer);
|
||||
|
||||
// We're going to use a different root as a parent.
|
||||
// This lets us detect whether an event goes through React's event system.
|
||||
let parentRoot = ReactDOM.createRoot(parentContainer);
|
||||
parentRoot.render(<Parent />);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
childSlotRef.current.appendChild(childContainer);
|
||||
|
||||
childContainer.innerHTML = finalHTML;
|
||||
|
||||
let a = childContainer.getElementsByTagName('a')[0];
|
||||
|
||||
suspend = true;
|
||||
|
||||
// Hydrate asynchronously.
|
||||
let root = ReactDOM.createRoot(childContainer, {hydrate: true});
|
||||
root.render(<App />);
|
||||
jest.runAllTimers();
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
// The Suspense boundary is not yet hydrated.
|
||||
a.click();
|
||||
expect(clicks).toBe(0);
|
||||
|
||||
// Resolving the promise so that rendering can complete.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
||||
// We're now full hydrated.
|
||||
|
||||
expect(clicks).toBe(1);
|
||||
|
||||
document.body.removeChild(parentContainer);
|
||||
},
|
||||
);
|
||||
|
||||
it('handle click events on dynamic portals', () => {
|
||||
const log = [];
|
||||
|
||||
function Parent() {
|
||||
const ref = React.useRef(null);
|
||||
const [portal, setPortal] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
setPortal(
|
||||
ReactDOM.createPortal(
|
||||
<span onClick={() => log.push('child')} id="child" />,
|
||||
ref.current,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref} onClick={() => log.push('parent')} id="parent">
|
||||
{portal}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Parent />, container);
|
||||
|
||||
const parent = container.lastChild;
|
||||
expect(parent.id).toEqual('parent');
|
||||
dispatchClickEvent(parent);
|
||||
|
||||
expect(log).toEqual(['parent']);
|
||||
|
||||
const child = parent.lastChild;
|
||||
expect(child.id).toEqual('child');
|
||||
dispatchClickEvent(child);
|
||||
|
||||
// we add both 'child' and 'parent' due to bubbling
|
||||
expect(log).toEqual(['parent', 'child', 'parent']);
|
||||
});
|
||||
|
||||
// Slight alteration to the last test, to catch
|
||||
// a subtle difference in traversal.
|
||||
it('handle click events on dynamic portals #2', () => {
|
||||
const log = [];
|
||||
|
||||
function Parent() {
|
||||
const ref = React.useRef(null);
|
||||
const [portal, setPortal] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
setPortal(
|
||||
ReactDOM.createPortal(
|
||||
<span onClick={() => log.push('child')} id="child" />,
|
||||
ref.current,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref} onClick={() => log.push('parent')} id="parent">
|
||||
<div>{portal}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Parent />, container);
|
||||
|
||||
const parent = container.lastChild;
|
||||
expect(parent.id).toEqual('parent');
|
||||
dispatchClickEvent(parent);
|
||||
|
||||
expect(log).toEqual(['parent']);
|
||||
|
||||
const child = parent.lastChild;
|
||||
expect(child.id).toEqual('child');
|
||||
dispatchClickEvent(child);
|
||||
|
||||
// we add both 'child' and 'parent' due to bubbling
|
||||
expect(log).toEqual(['parent', 'child', 'parent']);
|
||||
});
|
||||
|
||||
it('native stopPropagation on click events between portals', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const middelDivRef = React.createRef();
|
||||
const log = [];
|
||||
const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
|
||||
const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
|
||||
|
||||
const portalElement = document.createElement('div');
|
||||
document.body.appendChild(portalElement);
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<div ref={middelDivRef}>
|
||||
<div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
|
||||
Click me!
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Parent() {
|
||||
React.useLayoutEffect(() => {
|
||||
// This should prevent the portalElement listeners from
|
||||
// capturing the events in the bubble phase.
|
||||
middelDivRef.current.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={onClick}
|
||||
onClickCapture={onClickCapture}>
|
||||
{ReactDOM.createPortal(<Child />, portalElement)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Parent />, container);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(1);
|
||||
expect(log[0]).toEqual(['capture', buttonElement]);
|
||||
expect(log[1]).toEqual(['bubble', buttonElement]);
|
||||
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(1);
|
||||
|
||||
document.body.removeChild(portalElement);
|
||||
});
|
||||
|
||||
it('handle propagation of focus events', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
@@ -120,6 +618,377 @@ describe('DOMModernPluginEventSystem', () => {
|
||||
expect(log[5]).toEqual(['bubble', buttonElement]);
|
||||
});
|
||||
|
||||
it('handle propagation of focus events between roots', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const childRef = React.createRef();
|
||||
const log = [];
|
||||
const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
|
||||
const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
onFocus={onFocus}
|
||||
onFocusCapture={onFocusCapture}
|
||||
tabIndex={0}>
|
||||
Click me!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Parent() {
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onFocus={onFocus}
|
||||
onFocusCapture={onFocusCapture}>
|
||||
<div ref={childRef} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Parent />, container);
|
||||
ReactDOM.render(<Child />, childRef.current);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
buttonElement.focus();
|
||||
expect(onFocus).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusCapture).toHaveBeenCalledTimes(1);
|
||||
expect(log[0]).toEqual(['capture', buttonElement]);
|
||||
expect(log[1]).toEqual(['bubble', buttonElement]);
|
||||
|
||||
let divElement = divRef.current;
|
||||
divElement.focus();
|
||||
expect(onFocus).toHaveBeenCalledTimes(3);
|
||||
expect(onFocusCapture).toHaveBeenCalledTimes(3);
|
||||
expect(log[2]).toEqual(['capture', buttonElement]);
|
||||
expect(log[3]).toEqual(['bubble', buttonElement]);
|
||||
expect(log[4]).toEqual(['capture', divElement]);
|
||||
expect(log[5]).toEqual(['bubble', divElement]);
|
||||
});
|
||||
|
||||
it('handle propagation of focus events between portals', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const log = [];
|
||||
const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
|
||||
const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
|
||||
|
||||
const portalElement = document.createElement('div');
|
||||
document.body.appendChild(portalElement);
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
onFocus={onFocus}
|
||||
onFocusCapture={onFocusCapture}
|
||||
tabIndex={0}>
|
||||
Click me!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Parent() {
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onFocus={onFocus}
|
||||
onFocusCapture={onFocusCapture}>
|
||||
{ReactDOM.createPortal(<Child />, portalElement)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Parent />, container);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
buttonElement.focus();
|
||||
expect(onFocus).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusCapture).toHaveBeenCalledTimes(1);
|
||||
expect(log[0]).toEqual(['capture', buttonElement]);
|
||||
expect(log[1]).toEqual(['bubble', buttonElement]);
|
||||
|
||||
let divElement = divRef.current;
|
||||
divElement.focus();
|
||||
expect(onFocus).toHaveBeenCalledTimes(3);
|
||||
expect(onFocusCapture).toHaveBeenCalledTimes(3);
|
||||
expect(log[2]).toEqual(['capture', buttonElement]);
|
||||
expect(log[3]).toEqual(['capture', divElement]);
|
||||
expect(log[4]).toEqual(['bubble', divElement]);
|
||||
expect(log[5]).toEqual(['bubble', buttonElement]);
|
||||
|
||||
document.body.removeChild(portalElement);
|
||||
});
|
||||
|
||||
it('native stopPropagation on focus events between portals', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const middelDivRef = React.createRef();
|
||||
const log = [];
|
||||
const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
|
||||
const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
|
||||
|
||||
const portalElement = document.createElement('div');
|
||||
document.body.appendChild(portalElement);
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<div ref={middelDivRef}>
|
||||
<div
|
||||
ref={divRef}
|
||||
onClick={onFocus}
|
||||
onClickCapture={onFocusCapture}
|
||||
tabIndex={0}>
|
||||
Click me!
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Parent() {
|
||||
React.useLayoutEffect(() => {
|
||||
// This should prevent the portalElement listeners from
|
||||
// capturing the events in the bubble phase.
|
||||
middelDivRef.current.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onFocus={onFocus}
|
||||
onFocusCapture={onFocusCapture}>
|
||||
{ReactDOM.createPortal(<Child />, portalElement)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Parent />, container);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
buttonElement.focus();
|
||||
expect(onFocus).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusCapture).toHaveBeenCalledTimes(1);
|
||||
expect(log[0]).toEqual(['capture', buttonElement]);
|
||||
expect(log[1]).toEqual(['bubble', buttonElement]);
|
||||
|
||||
let divElement = divRef.current;
|
||||
divElement.focus();
|
||||
expect(onFocus).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusCapture).toHaveBeenCalledTimes(1);
|
||||
|
||||
document.body.removeChild(portalElement);
|
||||
});
|
||||
|
||||
it('handle propagation of enter and leave events between portals', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const log = [];
|
||||
const onMouseEnter = jest.fn(e => log.push(e.currentTarget));
|
||||
const onMouseLeave = jest.fn(e => log.push(e.currentTarget));
|
||||
|
||||
const portalElement = document.createElement('div');
|
||||
document.body.appendChild(portalElement);
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Parent() {
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}>
|
||||
{ReactDOM.createPortal(<Child />, portalElement)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Parent />, container);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
buttonElement.dispatchEvent(
|
||||
new MouseEvent('mouseover', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: null,
|
||||
}),
|
||||
);
|
||||
expect(onMouseEnter).toHaveBeenCalledTimes(1);
|
||||
expect(onMouseLeave).toHaveBeenCalledTimes(0);
|
||||
expect(log[0]).toEqual(buttonElement);
|
||||
|
||||
let divElement = divRef.current;
|
||||
buttonElement.dispatchEvent(
|
||||
new MouseEvent('mouseout', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: divElement,
|
||||
}),
|
||||
);
|
||||
divElement.dispatchEvent(
|
||||
new MouseEvent('mouseover', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: buttonElement,
|
||||
}),
|
||||
);
|
||||
expect(onMouseEnter).toHaveBeenCalledTimes(2);
|
||||
expect(onMouseLeave).toHaveBeenCalledTimes(0);
|
||||
expect(log[1]).toEqual(divElement);
|
||||
|
||||
document.body.removeChild(portalElement);
|
||||
});
|
||||
|
||||
it('handle propagation of enter and leave events between portals #2', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const portalRef = React.createRef();
|
||||
const log = [];
|
||||
const onMouseEnter = jest.fn(e => log.push(e.currentTarget));
|
||||
const onMouseLeave = jest.fn(e => log.push(e.currentTarget));
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Parent() {
|
||||
const [portal, setPortal] = React.useState(null);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
setPortal(ReactDOM.createPortal(<Child />, portalRef.current));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}>
|
||||
<div ref={portalRef}>{portal}</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Parent />, container);
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
buttonElement.dispatchEvent(
|
||||
new MouseEvent('mouseover', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: null,
|
||||
}),
|
||||
);
|
||||
expect(onMouseEnter).toHaveBeenCalledTimes(1);
|
||||
expect(onMouseLeave).toHaveBeenCalledTimes(0);
|
||||
expect(log[0]).toEqual(buttonElement);
|
||||
|
||||
let divElement = divRef.current;
|
||||
buttonElement.dispatchEvent(
|
||||
new MouseEvent('mouseout', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: divElement,
|
||||
}),
|
||||
);
|
||||
divElement.dispatchEvent(
|
||||
new MouseEvent('mouseover', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: buttonElement,
|
||||
}),
|
||||
);
|
||||
expect(onMouseEnter).toHaveBeenCalledTimes(2);
|
||||
expect(onMouseLeave).toHaveBeenCalledTimes(0);
|
||||
expect(log[1]).toEqual(divElement);
|
||||
});
|
||||
|
||||
it('should preserve bubble/capture order between roots and nested portals', () => {
|
||||
const targetRef = React.createRef();
|
||||
let log = [];
|
||||
const onClickRoot = jest.fn(e => log.push('bubble root'));
|
||||
const onClickCaptureRoot = jest.fn(e => log.push('capture root'));
|
||||
const onClickPortal = jest.fn(e => log.push('bubble portal'));
|
||||
const onClickCapturePortal = jest.fn(e => log.push('capture portal'));
|
||||
|
||||
function Portal() {
|
||||
return (
|
||||
<div
|
||||
onClick={onClickPortal}
|
||||
onClickCapture={onClickCapturePortal}
|
||||
ref={targetRef}>
|
||||
Click me!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
let shouldStopPropagation = false;
|
||||
portalContainer.addEventListener(
|
||||
'click',
|
||||
e => {
|
||||
if (shouldStopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
function Root() {
|
||||
let portalTargetRef = React.useRef(null);
|
||||
React.useLayoutEffect(() => {
|
||||
portalTargetRef.current.appendChild(portalContainer);
|
||||
});
|
||||
return (
|
||||
<div onClick={onClickRoot} onClickCapture={onClickCaptureRoot}>
|
||||
<div ref={portalTargetRef} />
|
||||
{ReactDOM.createPortal(<Portal />, portalContainer)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Root />, container);
|
||||
|
||||
let divElement = targetRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(log).toEqual([
|
||||
'capture root',
|
||||
'capture portal',
|
||||
'bubble portal',
|
||||
'bubble root',
|
||||
]);
|
||||
|
||||
log = [];
|
||||
|
||||
shouldStopPropagation = true;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(log).toEqual([
|
||||
// The events on root probably shouldn't fire if a non-React intermediated. but current behavior is that they do.
|
||||
'capture root',
|
||||
'capture portal',
|
||||
'bubble portal',
|
||||
'bubble root',
|
||||
]);
|
||||
});
|
||||
|
||||
it('handle propagation of click events correctly with FB primer', () => {
|
||||
ReactFeatureFlags.enableLegacyFBPrimerSupport = true;
|
||||
const aRef = React.createRef();
|
||||
@@ -164,8 +1033,9 @@ describe('DOMModernPluginEventSystem', () => {
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<Test2 />, container);
|
||||
aElement = aRef.current;
|
||||
dispatchClickEvent(aElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
expect(onClick).toHaveBeenCalledTimes(2);
|
||||
expect(log).toEqual([]);
|
||||
expect(onDivClick).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user