Skip to content

Commit

Permalink
replay capture phase for continuous events
Browse files Browse the repository at this point in the history
  • Loading branch information
salazarm committed Nov 2, 2021
1 parent a0d991f commit 04d2106
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -692,23 +692,50 @@ describe('ReactDOMServerSelectiveHydration', () => {
Scheduler.unstable_yieldValue(text);
return (
<span
id={text}
onClickCapture={e => {
e.preventDefault();
Scheduler.unstable_yieldValue('Capture Clicked' + e);
}}
onClick={e => {
e.preventDefault();
Scheduler.unstable_yieldValue('Clicked ' + text);
}}
onMouseEnter={e => {
e.preventDefault();
Scheduler.unstable_yieldValue('Hover ' + text);
Scheduler.unstable_yieldValue('Mouse Enter ' + text);
}}
onMouseOverCapture={e => {
e.preventDefault();
e.stopPropagation();
Scheduler.unstable_yieldValue('Mouse Over Capture ' + text);
}}
onMouseOver={e => {
e.preventDefault();
Scheduler.unstable_yieldValue('Mouse Over ' + text);
}}>
{text}
<div
onMouseOverCapture={e => {
e.preventDefault();
Scheduler.unstable_yieldValue('Mouse Over Capture Inner ' + text);
}}>
{text}
</div>
</span>
);
}

function App() {
Scheduler.unstable_yieldValue('App');
return (
<div>
<div
onClickCapture={e => {
e.preventDefault();
Scheduler.unstable_yieldValue('Capture Clicked Parent');
}}
onMouseOverCapture={e => {
Scheduler.unstable_yieldValue('Mouse Over Capture Parent');
}}>
<Suspense fallback="Loading...">
<Child text="A" />
</Suspense>
Expand All @@ -735,16 +762,15 @@ describe('ReactDOMServerSelectiveHydration', () => {

container.innerHTML = finalHTML;

const spanB = container.getElementsByTagName('span')[1];
const spanC = container.getElementsByTagName('span')[2];
const spanD = container.getElementsByTagName('span')[3];
const spanB = document.getElementById('B').firstChild;
const spanC = document.getElementById('C').firstChild;
const spanD = document.getElementById('D').firstChild;

suspend = true;

// A and D will be suspended. We'll click on D which should take
// priority, after we unsuspend.
const root = ReactDOM.createRoot(container, {hydrate: true});
root.render(<App />);
ReactDOM.hydrateRoot(container, <App />);

// Nothing has been hydrated so far.
expect(Scheduler).toHaveYielded([]);
Expand Down Expand Up @@ -776,9 +802,22 @@ describe('ReactDOMServerSelectiveHydration', () => {
'D',
'B', // Ideally this should be later.
'C',
'Hover C',
'Mouse Enter C',
'Mouse Over Capture Parent',
'Mouse Over Capture C',
// Stop propagation stops these
// 'Mouse Over Capture Inner C',
// 'Mouse Over C',
'A',
]);

dispatchMouseHoverEvent(spanC, spanB);

expect(Scheduler).toHaveYielded([
'Mouse Enter C',
'Mouse Over Capture Parent',
'Mouse Over Capture C',
]);
} else {
// We should prioritize hydrating D first because we clicked it.
// Next we should hydrate C since that's the current hover target.
Expand Down
5 changes: 2 additions & 3 deletions packages/react-dom/src/events/EventSystemFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ export const IS_EVENT_HANDLE_NON_MANAGED_NODE = 1;
export const IS_NON_DELEGATED = 1 << 1;
export const IS_CAPTURE_PHASE = 1 << 2;
export const IS_PASSIVE = 1 << 3;
export const IS_REPLAYED = 1 << 4;
export const IS_LEGACY_FB_SUPPORT_MODE = 1 << 5;
export const IS_LEGACY_FB_SUPPORT_MODE = 1 << 4;

export const SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE =
IS_LEGACY_FB_SUPPORT_MODE | IS_REPLAYED | IS_CAPTURE_PHASE;
IS_LEGACY_FB_SUPPORT_MODE | IS_CAPTURE_PHASE;

// We do not want to defer if the event system has already been
// set to LEGACY_FB_SUPPORT. LEGACY_FB_SUPPORT only gets set when
Expand Down
58 changes: 25 additions & 33 deletions packages/react-dom/src/events/ReactDOMEventListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,9 @@ export function dispatchEvent(
return;
}

// TODO: replaying capture phase events is currently broken
// because we used to do it during top-level native bubble handlers
// but now we use different bubble and capture handlers.
// In eager mode, we attach capture listeners early, so we need
// to filter them out until we fix the logic to handle them correctly.
const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;
const allowReplay =
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay ||
(eventSystemFlags & IS_CAPTURE_PHASE) === 0;

if (
allowReplay &&
Expand All @@ -180,18 +177,25 @@ export function dispatchEvent(
return;
}

let blockedOn = attemptToDispatchEvent(
let blockedOn = findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);

// We can dispatch the event now
if (blockedOn === null) {
// We successfully dispatched this event.
if (allowReplay) {
clearIfContinuousEvent(domEventName, nativeEvent);
}
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
getClosestInstanceFromNode(getEventTarget(nativeEvent)),
targetContainer,
);
return;
}

Expand Down Expand Up @@ -219,6 +223,7 @@ export function dispatchEvent(
nativeEvent,
)
) {
nativeEvent.stopPropagation();
return;
}
// We need to clear only if we didn't queue because
Expand All @@ -236,7 +241,7 @@ export function dispatchEvent(
if (fiber !== null) {
attemptSynchronousHydration(fiber);
}
const nextBlockedOn = attemptToDispatchEvent(
const nextBlockedOn = findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
Expand All @@ -251,6 +256,14 @@ export function dispatchEvent(
nativeEvent.stopPropagation();
return;
}
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
getClosestInstanceFromNode(getEventTarget(nativeEvent)),
targetContainer,
);
return;
}

// This is not replayable so we'll invoke it but without a target,
Expand All @@ -265,7 +278,7 @@ export function dispatchEvent(
}

// Attempt dispatching an event. Returns a SuspenseInstance or Container if it's blocked.
export function attemptToDispatchEvent(
export function findInstanceBlockingEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
Expand All @@ -274,14 +287,11 @@ export function attemptToDispatchEvent(
// TODO: Warn if _enabled is false.

const nativeEventTarget = getEventTarget(nativeEvent);
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
const targetInst = getClosestInstanceFromNode(nativeEventTarget);

if (targetInst !== null) {
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// This tree has been unmounted already. Dispatch without a target.
targetInst = null;
} else {
if (nearestMounted !== null) {
const tag = nearestMounted.tag;
if (tag === SuspenseComponent) {
const instance = getSuspenseInstanceFromFiber(nearestMounted);
Expand All @@ -292,34 +302,16 @@ export function attemptToDispatchEvent(
// priority for this boundary.
return instance;
}
// This shouldn't happen, something went wrong but to avoid blocking
// the whole system, dispatch the event without a target.
// TODO: Warn.
targetInst = null;
} else if (tag === HostRoot) {
const root: FiberRoot = nearestMounted.stateNode;
if (root.isDehydrated) {
// If this happens during a replay something went wrong and it might block
// the whole system.
return getContainerFromFiber(nearestMounted);
}
targetInst = null;
} else if (nearestMounted !== targetInst) {
// If we get an event (ex: img onload) before committing that
// component's mount, ignore it for now (that is, treat it as if it was an
// event on a non-React tree). We might also consider queueing events and
// dispatching them after the mount.
targetInst = null;
}
}
}
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
// We're not blocked on anything.
return null;
}
Expand Down
Loading

0 comments on commit 04d2106

Please sign in to comment.