Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replay capture phase for continuous events #22680

Closed
Original file line number Diff line number Diff line change
Expand Up @@ -692,23 +692,59 @@ describe('ReactDOMServerSelectiveHydration', () => {
Scheduler.unstable_yieldValue(text);
return (
<span
id={text}
onClickCapture={e => {
e.preventDefault();
Scheduler.unstable_yieldValue('Capture Clicked ' + text);
}}
onClick={e => {
e.preventDefault();
Scheduler.unstable_yieldValue('Clicked ' + text);
}}
onMouseEnter={e => {
e.preventDefault();
Scheduler.unstable_yieldValue('Hover ' + text);
Scheduler.unstable_yieldValue('Mouse Enter ' + text);
}}
onMouseOut={e => {
e.preventDefault();
Scheduler.unstable_yieldValue('Mouse Out ' + text);
}}
onMouseOutCapture={e => {
e.preventDefault();
e.stopPropagation();
salazarm marked this conversation as resolved.
Show resolved Hide resolved
Scheduler.unstable_yieldValue('Mouse Out Capture ' + 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 +771,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 +811,31 @@ describe('ReactDOMServerSelectiveHydration', () => {
'D',
'B', // Ideally this should be later.
'C',
'Hover C',
// Mouse out events aren't replayed
// 'Mouse Out Capture B',
// stopPropagation stops these
// 'Mouse Out B',
'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 Out Capture B',
// stopPropagation stops these
// 'Mouse Out B',
// 'Mouse Enter C',
'Mouse Over Capture Parent',
'Mouse Over Capture C',
// Stop propagation stops these
// 'Mouse Over Capture Inner C',
// 'Mouse Over 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 All @@ -791,7 +848,8 @@ describe('ReactDOMServerSelectiveHydration', () => {
'Clicked D',
'B', // Ideally this should be later.
'C',
'Hover C',
'Mouse Over C',
'Mouse Enter C',
'A',
]);
}
Expand Down
4 changes: 3 additions & 1 deletion packages/react-dom/src/events/DOMPluginEventSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import * as ChangeEventPlugin from './plugins/ChangeEventPlugin';
import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin';
import * as SelectEventPlugin from './plugins/SelectEventPlugin';
import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';
import {isReplayingEvent} from './replayedEvent';

type DispatchListener = {|
instance: null | Fiber,
Expand Down Expand Up @@ -557,7 +558,8 @@ export function dispatchEventForPluginEventSystem(
// for legacy FB support, where the expected behavior was to
// match React < 16 behavior of delegated clicks to the doc.
domEventName === 'click' &&
(eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0
(eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0 &&
!isReplayingEvent(nativeEvent)
) {
deferClickToDocumentForLegacyFBSupport(domEventName, targetContainer);
return;
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
90 changes: 44 additions & 46 deletions packages/react-dom/src/events/ReactDOMEventListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,37 @@ 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;

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

// We can dispatch the event now
salazarm marked this conversation as resolved.
Show resolved Hide resolved
if (blockedOn === null) {
if (allowReplay) {
clearIfContinuousEvent(domEventName, nativeEvent);
}
if (
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay ||
!hasQueuedDiscreteEvents() ||
!isDiscreteEventThatRequiresHydration(domEventName)
) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
getClosestInstanceFromNode(getEventTarget(nativeEvent)),
targetContainer,
);
return;
}
}

if (
allowReplay &&
Expand All @@ -180,21 +205,6 @@ export function dispatchEvent(
return;
}

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

if (blockedOn === null) {
// We successfully dispatched this event.
if (allowReplay) {
clearIfContinuousEvent(domEventName, nativeEvent);
}
return;
}

if (allowReplay) {
if (
!enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
Expand All @@ -219,6 +229,7 @@ export function dispatchEvent(
nativeEvent,
)
) {
nativeEvent.stopPropagation();
salazarm marked this conversation as resolved.
Show resolved Hide resolved
return;
}
// We need to clear only if we didn't queue because
Expand All @@ -236,7 +247,7 @@ export function dispatchEvent(
if (fiber !== null) {
attemptSynchronousHydration(fiber);
}
const nextBlockedOn = attemptToDispatchEvent(
const nextBlockedOn = findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
Expand All @@ -251,6 +262,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 +284,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 +293,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.
salazarm marked this conversation as resolved.
Show resolved Hide resolved
targetInst = null;
} else {
if (nearestMounted !== null) {
const tag = nearestMounted.tag;
if (tag === SuspenseComponent) {
const instance = getSuspenseInstanceFromFiber(nearestMounted);
Expand All @@ -292,34 +308,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.
salazarm marked this conversation as resolved.
Show resolved Hide resolved
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);
salazarm marked this conversation as resolved.
Show resolved Hide resolved
}
targetInst = null;
salazarm marked this conversation as resolved.
Show resolved Hide resolved
} 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