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

[WIP] Replay load/error events that happen earlier than commit #13862

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMEarlyEvent-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/

'use strict';

let React;
let ReactDOM;
let ConcurrentMode;

beforeEach(() => {
React = require('react');
ConcurrentMode = React.unstable_ConcurrentMode;
ReactDOM = require('react-dom');
});

describe('ReactDOMImg', () => {
let container;

beforeEach(() => {
// TODO pull this into helper method, reduce repetition.
// mock the browser APIs which are used in schedule:
// - requestAnimationFrame should pass the DOMHighResTimeStamp argument
// - calling 'window.postMessage' should actually fire postmessage handlers
global.requestAnimationFrame = function(cb) {
return setTimeout(() => {
cb(Date.now());
});
};
const originalAddEventListener = global.addEventListener;
let postMessageCallback;
global.addEventListener = function(eventName, callback, useCapture) {
if (eventName === 'message') {
postMessageCallback = callback;
} else {
originalAddEventListener(eventName, callback, useCapture);
}
};
global.postMessage = function(messageKey, targetOrigin) {
const postMessageEvent = {source: window, data: messageKey};
if (postMessageCallback) {
postMessageCallback(postMessageEvent);
}
};
jest.resetModules();
container = document.createElement('div');
ReactDOM = require('react-dom');

document.body.appendChild(container);
});

afterEach(() => {
document.body.removeChild(container);
});

it('should trigger load events even if they fire early', () => {
const onLoadSpy = jest.fn();

const loadEvent = document.createEvent('Event');
loadEvent.initEvent('load', false, false);

// TODO: Write test

ReactDOM.render(
<ConcurrentMode>
<img onLoad={onLoadSpy} />
</ConcurrentMode>,
);

// someHowGetAnImage.dispatchEvent(loadEvent);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trueadm I think we’ll need to spy on document.createElement by wrapping it so we can detect which img instance React creates.

We can’t use a ref to simulate this because it doesn’t resolve in time.


// expect(onLoadSpy).toHaveBeenCalled();
});
});
4 changes: 2 additions & 2 deletions packages/react-dom/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -491,8 +491,8 @@ export function setInitialProperties(
case 'img':
case 'image':
case 'link':
trapBubbledEvent(TOP_ERROR, domElement);
trapBubbledEvent(TOP_LOAD, domElement);
trapBubbledEvent(TOP_ERROR, domElement, true);
trapBubbledEvent(TOP_LOAD, domElement, true);
props = rawProps;
break;
case 'form':
Expand Down
78 changes: 56 additions & 22 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import * as ReactInputSelection from './ReactInputSelection';
import setTextContent from './setTextContent';
import {validateDOMNesting, updatedAncestorInfo} from './validateDOMNesting';
import * as ReactBrowserEventEmitter from '../events/ReactBrowserEventEmitter';
import {replayEarlyEvent} from '../events/ReactBrowserEventEmitter';
import {getChildNamespace} from '../shared/DOMNamespaces';
import {
ELEMENT_NODE,
Expand All @@ -41,6 +42,11 @@ import type {DOMContainer} from './ReactDOM';
export type Type = string;
export type Props = {
autoFocus?: boolean,
'xlink:href'?: string,
src?: string,
href?: string,
onLoad?: Function,
onError?: Function,
children?: mixed,
hidden?: boolean,
suppressHydrationWarning?: boolean,
Expand Down Expand Up @@ -76,17 +82,6 @@ if (__DEV__) {
let eventsEnabled: ?boolean = null;
let selectionInformation: ?mixed = null;

function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
switch (type) {
case 'button':
case 'input':
case 'select':
case 'textarea':
return !!props.autoFocus;
}
return false;
}

export * from 'shared/HostConfigWithNoPersistence';

export function getRootHostContext(
Expand Down Expand Up @@ -210,7 +205,35 @@ export function finalizeInitialChildren(
hostContext: HostContext,
): boolean {
setInitialProperties(domElement, type, props, rootContainerInstance);
return shouldAutoFocusHostComponent(type, props);
switch (type) {
case 'button':
case 'input':
case 'select':
case 'textarea':
// should auto focus
return !!props.autoFocus;
case 'img':
// if src is set, we might get a load/error event before commit
// we need to schedule an effect to replay the event.
return (
!!props.src &&
(typeof props.onLoad === 'function' ||
typeof props.onError === 'function')
);
case 'image':
return (
!!props['xlink:href'] &&
(typeof props.onLoad === 'function' ||
typeof props.onError === 'function')
);
case 'link':
return (
!!props.href &&
(typeof props.onLoad === 'function' ||
typeof props.onError === 'function')
);
}
return false;
}

export function prepareUpdate(
Expand Down Expand Up @@ -296,16 +319,27 @@ export function commitMount(
): void {
// Despite the naming that might imply otherwise, this method only
// fires if there is an `Update` effect scheduled during mounting.
// This happens if `finalizeInitialChildren` returns `true` (which it
// does to implement the `autoFocus` attribute on the client). But
// there are also other cases when this might happen (such as patching
// up text content during hydration mismatch). So we'll check this again.
if (shouldAutoFocusHostComponent(type, newProps)) {
((domElement: any):
| HTMLButtonElement
| HTMLInputElement
| HTMLSelectElement
| HTMLTextAreaElement).focus();
// This happens if `finalizeInitialChildren` returns `true`.
switch (type) {
case 'button':
case 'input':
case 'select':
case 'textarea': {
if (newProps.autoFocus) {
((domElement: any):
| HTMLButtonElement
| HTMLInputElement
| HTMLSelectElement
| HTMLTextAreaElement).focus();
}
break;
}
case 'img':
case 'image':
case 'link': {
replayEarlyEvent(domElement);
break;
}
}
}

Expand Down
9 changes: 8 additions & 1 deletion packages/react-dom/src/events/ReactBrowserEventEmitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
isEnabled,
trapBubbledEvent,
trapCapturedEvent,
replayEarlyEvent,
} from './ReactDOMEventListener';
import isEventSupported from './isEventSupported';

Expand Down Expand Up @@ -187,4 +188,10 @@ export function isListeningToAllDependencies(
return true;
}

export {setEnabled, isEnabled, trapBubbledEvent, trapCapturedEvent};
export {
setEnabled,
isEnabled,
trapBubbledEvent,
replayEarlyEvent,
trapCapturedEvent,
};
20 changes: 19 additions & 1 deletion packages/react-dom/src/events/ReactDOMEventListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,16 @@ export function isEnabled() {
export function trapBubbledEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element,
expectEarlyEvents: boolean = false,
) {
if (!element) {
return null;
}
const dispatch = isInteractiveTopLevelEventType(topLevelType)
? dispatchInteractiveEvent
: dispatchEvent;
: expectEarlyEvents
? dispatchAndStoreEvent
: dispatchEvent;

addEventBubbleListener(
element,
Expand Down Expand Up @@ -185,6 +188,21 @@ function dispatchInteractiveEvent(topLevelType, nativeEvent) {
interactiveUpdates(dispatchEvent, topLevelType, nativeEvent);
}

function dispatchAndStoreEvent(
topLevelType: DOMTopLevelEventType,
nativeEvent: AnyNativeEvent,
) {
(nativeEvent.target: any)._pendingEvent = nativeEvent;
dispatchEvent(topLevelType, nativeEvent);
}

export function replayEarlyEvent(element: Document | Element) {
let pendingEvent: Event = (element: any)._pendingEvent;
if (pendingEvent) {
element.dispatchEvent(pendingEvent);
}
}

export function dispatchEvent(
topLevelType: DOMTopLevelEventType,
nativeEvent: AnyNativeEvent,
Expand Down