Skip to content

Commit

Permalink
Locally attach wheel, scroll and touch events
Browse files Browse the repository at this point in the history
  • Loading branch information
nhunzaker committed Dec 9, 2017
1 parent d9869a4 commit 294c27e
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 32 deletions.
1 change: 1 addition & 0 deletions fixtures/dom/src/components/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class Header extends React.Component {
<option value="/error-handling">Error Handling</option>
<option value="/event-pooling">Event Pooling</option>
<option value="/custom-elements">Custom Elements</option>
<option value="/scroll">Scroll Events</option>
</select>
</label>
<label htmlFor="react_version">
Expand Down
3 changes: 3 additions & 0 deletions fixtures/dom/src/components/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import DateInputFixtures from './date-inputs';
import ErrorHandling from './error-handling';
import EventPooling from './event-pooling';
import CustomElementFixtures from './custom-elements';
import ScrollFixtures from './scroll';

const React = window.React;

Expand Down Expand Up @@ -43,6 +44,8 @@ function FixturesPage() {
return <EventPooling />;
case '/custom-elements':
return <CustomElementFixtures />;
case '/scroll':
return <ScrollFixtures />;
default:
return <p>Please select a test fixture.</p>;
}
Expand Down
72 changes: 72 additions & 0 deletions fixtures/dom/src/components/fixtures/scroll/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const React = global.React;

function wait(time) {
console.log('Blocking!');
var startTime = new Date().getTime();
var endTime = startTime + time;
while (new Date().getTime() < endTime) {
// wait for it...
}
console.log('Not blocking!');
}

var scrollable = {
width: 300,
height: 200,
overflowY: 'auto',
margin: '0 auto',
background: '#ededed',
};

class ScrollFixture extends React.Component {
componentDidMount() {
// jank up the main thread!
this.jank = setInterval(() => wait(3000), 4000);
}

componentWillUnmount() {
clearInterval(this.jank);
}

onWheel() {
console.log('wheel');
}

onTouchStart() {
console.log('touch start');
}

onTouchMove() {
console.log('touch move');
}

render() {
let listItems = [];

// This is to produce a long enough page to allow for scrolling
for (var i = 0; i < 50; i++) {
listItems.push(<li key={i}>List item #{i + 1}</li>);
}

return (
<section>
<h2>Scroll Testing</h2>
<p>
Mouse wheel, track pad scroll, and touch events should not be blocked
by JS execution in IE Edge, Safari, and Firefox.
</p>
<div
style={scrollable}
onTouchStart={this.onTouchStart}
onTouchMove={this.onTouchMove}
onWheel={this.onWheel}>
<h2>I am scrollable!</h2>
<ul>{listItems}</ul>
</div>
<ul>{listItems}</ul>
</section>
);
}
}

export default ScrollFixture;
Original file line number Diff line number Diff line change
Expand Up @@ -327,15 +327,15 @@ describe('ReactBrowserEventEmitter', () => {

it('should listen to events only once', () => {
spyOnDevAndProd(EventTarget.prototype, 'addEventListener');
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document);
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document);
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document, document);
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document, document);
expect(EventTarget.prototype.addEventListener.calls.count()).toBe(1);
});

it('should work with event plugins without dependencies', () => {
spyOnDevAndProd(EventTarget.prototype, 'addEventListener');

ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document);
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document, document);

expect(EventTarget.prototype.addEventListener.calls.argsFor(0)[0]).toBe(
'click',
Expand All @@ -345,7 +345,7 @@ describe('ReactBrowserEventEmitter', () => {
it('should work with event plugins with dependencies', () => {
spyOnDevAndProd(EventTarget.prototype, 'addEventListener');

ReactBrowserEventEmitter.listenTo(ON_CHANGE_KEY, document);
ReactBrowserEventEmitter.listenTo(ON_CHANGE_KEY, document, document);

const setEventListeners = [];
const listenCalls = EventTarget.prototype.addEventListener.calls.allArgs();
Expand All @@ -361,4 +361,38 @@ describe('ReactBrowserEventEmitter', () => {
expect(dependencies.indexOf(setEventListeners[i])).toBeTruthy();
}
});

describe('local listener attachment', function() {
it('does attach a new listener for the same event type', () => {
var container = document.createElement('div');
var spy = jest.fn();

ReactDOM.render(<div onTouchMove={() => spy()} />, container);
ReactDOM.render(<div onTouchMove={() => spy()} />, container);

var el = container.querySelector('div');

el.dispatchEvent(new Event('touchmove'));

expect(spy).toHaveBeenCalledTimes(1);
});

it('does not call old listeners on a second update with a new handler', () => {
var container = document.createElement('div');
var a = jest.fn();
var b = jest.fn();

ReactDOM.render(<div onTouchMove={a} />, container);
ReactDOM.render(<div onTouchMove={b} />, container);

var el = container.querySelector('div');

el.dispatchEvent(new Event('touchmove'));

// The first handler should have been torn down
expect(a).toHaveBeenCalledTimes(0);
// The second handler is now attached
expect(b).toHaveBeenCalledTimes(1);
});
});
});
15 changes: 15 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMComponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2581,4 +2581,19 @@ describe('ReactDOMComponent', () => {
expect(node.getAttribute('onx')).toBe('bar');
});
});

describe('Trapping local event listeners', () => {
it('triggers load if onload is not present on an image', () => {
return new Promise(function(resolve, reject) {
const el = ReactTestUtils.renderIntoDocument(
<div onLoad={resolve} onError={reject}>
<img />
</div>,
);

el.querySelector('img').src =
'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
});
});
});
});
22 changes: 11 additions & 11 deletions packages/react-dom/src/client/ReactDOMFiberComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,14 @@ if (__DEV__) {
};
}

function ensureListeningTo(rootContainerElement, registrationName) {
function ensureListeningTo(rootContainerElement, registrationName, domElement) {
const isDocumentOrFragment =
rootContainerElement.nodeType === DOCUMENT_NODE ||
rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
const doc = isDocumentOrFragment
? rootContainerElement
: rootContainerElement.ownerDocument;
listenTo(registrationName, doc);
listenTo(registrationName, doc, domElement);
}

function getOwnerDocumentFromRootContainer(
Expand Down Expand Up @@ -312,7 +312,7 @@ function setInitialDOMProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey);
ensureListeningTo(rootContainerElement, propKey, domElement);
}
} else if (isCustomComponentTag) {
DOMPropertyOperations.setValueForAttribute(domElement, propKey, nextProp);
Expand Down Expand Up @@ -509,7 +509,7 @@ export function setInitialProperties(
trapBubbledEvent('topInvalid', 'invalid', domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
case 'option':
ReactDOMFiberOption.validateProps(domElement, rawProps);
Expand All @@ -521,15 +521,15 @@ export function setInitialProperties(
trapBubbledEvent('topInvalid', 'invalid', domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
case 'textarea':
ReactDOMFiberTextarea.initWrapperState(domElement, rawProps);
props = ReactDOMFiberTextarea.getHostProps(domElement, rawProps);
trapBubbledEvent('topInvalid', 'invalid', domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
default:
props = rawProps;
Expand Down Expand Up @@ -750,7 +750,7 @@ export function diffProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey);
ensureListeningTo(rootContainerElement, propKey, domElement);
}
if (!updatePayload && lastProp !== nextProp) {
// This is a special case. If any listener updates we need to ensure
Expand Down Expand Up @@ -879,7 +879,7 @@ export function diffHydratedProperties(
trapBubbledEvent('topInvalid', 'invalid', domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
case 'option':
ReactDOMFiberOption.validateProps(domElement, rawProps);
Expand All @@ -889,14 +889,14 @@ export function diffHydratedProperties(
trapBubbledEvent('topInvalid', 'invalid', domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
case 'textarea':
ReactDOMFiberTextarea.initWrapperState(domElement, rawProps);
trapBubbledEvent('topInvalid', 'invalid', domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
}

Expand Down Expand Up @@ -963,7 +963,7 @@ export function diffHydratedProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey);
ensureListeningTo(rootContainerElement, propKey, domElement);
}
} else if (__DEV__) {
// Validate that the properties correspond to their expected values.
Expand Down
57 changes: 40 additions & 17 deletions packages/react-dom/src/events/ReactBrowserEventEmitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*/

import {registrationNameDependencies} from 'events/EventPluginRegistry';

import {
setEnabled,
isEnabled,
Expand All @@ -15,6 +14,7 @@ import {
} from './ReactDOMEventListener';
import isEventSupported from './isEventSupported';
import BrowserEventConstants from './BrowserEventConstants';
import invariant from 'fbjs/lib/invariant';

export * from 'events/ReactEventEmitterMixin';

Expand Down Expand Up @@ -84,16 +84,28 @@ let reactTopListenersCounter = 0;
*/
const topListenersIDKey = '_reactListenersID' + ('' + Math.random()).slice(2);

function getListeningForDocument(mountAt) {
// In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty`
function getListenerTrackingFor(node) {
// In IE8, `node` is a host object and doesn't have `hasOwnProperty`
// directly.
if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) {
mountAt[topListenersIDKey] = reactTopListenersCounter++;
alreadyListeningTo[mountAt[topListenersIDKey]] = {};
if (!Object.prototype.hasOwnProperty.call(node, topListenersIDKey)) {
node[topListenersIDKey] = reactTopListenersCounter++;
alreadyListeningTo[node[topListenersIDKey]] = {};
}
return alreadyListeningTo[mountAt[topListenersIDKey]];
return alreadyListeningTo[node[topListenersIDKey]];
}

const BUBBLE = 0;
const CAPTURE = 1;

const localEvents = {
topTouchMove: BUBBLE,
topTouchStart: BUBBLE,
topTouchCancel: BUBBLE,
topTouchEnd: BUBBLE,
topScroll: CAPTURE,
topWheel: CAPTURE,
};

/**
* We listen for bubbled touch events on the document object.
*
Expand All @@ -115,17 +127,27 @@ function getListeningForDocument(mountAt) {
* @param {string} registrationName Name of listener (e.g. `onClick`).
* @param {object} contentDocumentHandle Document which owns the container
*/
export function listenTo(registrationName, contentDocumentHandle) {
const mountAt = contentDocumentHandle;
const isListening = getListeningForDocument(mountAt);
export function listenTo(registrationName, mountAt, domElement) {
const isListening = getListenerTrackingFor(mountAt);
const dependencies = registrationNameDependencies[registrationName];

for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
if (dependency === 'topScroll') {
trapCapturedEvent('topScroll', 'scroll', mountAt);
} else if (dependency === 'topFocus' || dependency === 'topBlur') {

if (localEvents.hasOwnProperty(dependency)) {
const isLocallyListening = getListenerTrackingFor(domElement);

if (!isLocallyListening.hasOwnProperty(dependency)) {
if (localEvents[dependency] === CAPTURE) {
trapCapturedEvent(dependency, topLevelTypes[dependency], domElement);
} else {
trapBubbledEvent(dependency, topLevelTypes[dependency], domElement);
}

isLocallyListening[dependency] = true;
}
} else if (!isListening.hasOwnProperty(dependency)) {
if (dependency === 'topFocus' || dependency === 'topBlur') {
trapCapturedEvent('topFocus', 'focus', mountAt);
trapCapturedEvent('topBlur', 'blur', mountAt);

Expand All @@ -144,15 +166,16 @@ export function listenTo(registrationName, contentDocumentHandle) {
isListening.topClose = true;
} else if (topLevelTypes.hasOwnProperty(dependency)) {
trapBubbledEvent(dependency, topLevelTypes[dependency], mountAt);
isListening[dependency] = true;
} else {
invariant(false, 'Unexpected event dependency %s', dependency);
}

isListening[dependency] = true;
}
}
}

export function isListeningToAllDependencies(registrationName, mountAt) {
const isListening = getListeningForDocument(mountAt);
const isListening = getListenerTrackingFor(mountAt);
const dependencies = registrationNameDependencies[registrationName];
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
Expand Down

0 comments on commit 294c27e

Please sign in to comment.