Skip to content

Commit 1442971

Browse files
authored
Bail-out of attaching non-delegated listeners (facebook#19466)
* Bail-out of attaching non-delegated listeners Revise comment * Fix tests/add tests * Add onInvalid test
1 parent 06d104e commit 1442971

File tree

2 files changed

+79
-29
lines changed

2 files changed

+79
-29
lines changed

packages/react-dom/src/__tests__/ReactDOMEventListener-test.js

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -348,12 +348,7 @@ describe('ReactDOMEventListener', () => {
348348
bubbles: false,
349349
}),
350350
);
351-
// As of the modern event system refactor, we now support
352-
// this on <img>. The reason for this, is because we allow
353-
// events to be attached to nodes regardless of if they
354-
// necessary support them. This is a strange test, as this
355-
// would never occur from normal browser behavior.
356-
expect(handleImgLoadStart).toHaveBeenCalledTimes(1);
351+
expect(handleImgLoadStart).toHaveBeenCalledTimes(0);
357352

358353
videoRef.current.dispatchEvent(
359354
new ProgressEvent('loadstart', {
@@ -535,36 +530,42 @@ describe('ReactDOMEventListener', () => {
535530
}
536531
});
537532

538-
it('should bubble non-native bubbling events', () => {
533+
it('should bubble non-native bubbling toggle events', () => {
539534
const container = document.createElement('div');
540535
const ref = React.createRef();
541-
const onPlay = jest.fn();
542-
const onCancel = jest.fn();
543-
const onClose = jest.fn();
544536
const onToggle = jest.fn();
545537
document.body.appendChild(container);
546538
try {
547539
ReactDOM.render(
548-
<div
549-
onPlay={onPlay}
550-
onCancel={onCancel}
551-
onClose={onClose}
552-
onToggle={onToggle}>
553-
<div
554-
ref={ref}
555-
onPlay={onPlay}
556-
onCancel={onCancel}
557-
onClose={onClose}
558-
onToggle={onToggle}
559-
/>
540+
<div onToggle={onToggle}>
541+
<details ref={ref} onToggle={onToggle} />
560542
</div>,
561543
container,
562544
);
563545
ref.current.dispatchEvent(
564-
new Event('play', {
546+
new Event('toggle', {
565547
bubbles: false,
566548
}),
567549
);
550+
expect(onToggle).toHaveBeenCalledTimes(2);
551+
} finally {
552+
document.body.removeChild(container);
553+
}
554+
});
555+
556+
it('should bubble non-native bubbling cancel/close events', () => {
557+
const container = document.createElement('div');
558+
const ref = React.createRef();
559+
const onCancel = jest.fn();
560+
const onClose = jest.fn();
561+
document.body.appendChild(container);
562+
try {
563+
ReactDOM.render(
564+
<div onCancel={onCancel} onClose={onClose}>
565+
<dialog ref={ref} onCancel={onCancel} onClose={onClose} />
566+
</div>,
567+
container,
568+
);
568569
ref.current.dispatchEvent(
569570
new Event('cancel', {
570571
bubbles: false,
@@ -575,17 +576,54 @@ describe('ReactDOMEventListener', () => {
575576
bubbles: false,
576577
}),
577578
);
579+
expect(onCancel).toHaveBeenCalledTimes(2);
580+
expect(onClose).toHaveBeenCalledTimes(2);
581+
} finally {
582+
document.body.removeChild(container);
583+
}
584+
});
585+
586+
it('should bubble non-native bubbling media events events', () => {
587+
const container = document.createElement('div');
588+
const ref = React.createRef();
589+
const onPlay = jest.fn();
590+
document.body.appendChild(container);
591+
try {
592+
ReactDOM.render(
593+
<div onPlay={onPlay}>
594+
<video ref={ref} onPlay={onPlay} />
595+
</div>,
596+
container,
597+
);
578598
ref.current.dispatchEvent(
579-
new Event('toggle', {
599+
new Event('play', {
580600
bubbles: false,
581601
}),
582602
);
583-
// Regression test: ensure we still emulate bubbling with non-bubbling
584-
// media
585603
expect(onPlay).toHaveBeenCalledTimes(2);
586-
expect(onCancel).toHaveBeenCalledTimes(2);
587-
expect(onClose).toHaveBeenCalledTimes(2);
588-
expect(onToggle).toHaveBeenCalledTimes(2);
604+
} finally {
605+
document.body.removeChild(container);
606+
}
607+
});
608+
609+
it('should bubble non-native bubbling invalid events', () => {
610+
const container = document.createElement('div');
611+
const ref = React.createRef();
612+
const onInvalid = jest.fn();
613+
document.body.appendChild(container);
614+
try {
615+
ReactDOM.render(
616+
<form onInvalid={onInvalid}>
617+
<input ref={ref} onInvalid={onInvalid} />
618+
</form>,
619+
container,
620+
);
621+
ref.current.dispatchEvent(
622+
new Event('invalid', {
623+
bubbles: false,
624+
}),
625+
);
626+
expect(onInvalid).toHaveBeenCalledTimes(2);
589627
} finally {
590628
document.body.removeChild(container);
591629
}

packages/react-dom/src/events/DOMPluginEventSystem.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,18 @@ export function listenToNativeEvent(
384384
!isCapturePhaseListener &&
385385
nonDelegatedEvents.has(topLevelType)
386386
) {
387+
// For all non-delegated events, apart from scroll, we attach
388+
// their event listeners to the respective elements that their
389+
// events fire on. That means we can skip this step, as event
390+
// listener has already been added previously. However, we
391+
// special case the scroll event because the reality is that any
392+
// element can scroll.
393+
// TODO: ideally, we'd eventually apply the same logic to all
394+
// events from the nonDelegatedEvents list. Then we can remove
395+
// this special case and use the same logic for all events.
396+
if (topLevelType !== TOP_SCROLL) {
397+
return;
398+
}
387399
eventSystemFlags |= IS_NON_DELEGATED;
388400
target = targetElement;
389401
}

0 commit comments

Comments
 (0)