Skip to content

Commit

Permalink
Attach touch events locally, add tests to ensure bubble order
Browse files Browse the repository at this point in the history
  • Loading branch information
nhunzaker committed Sep 5, 2018
1 parent ec4c2e2 commit e7884fc
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 60 deletions.
124 changes: 124 additions & 0 deletions packages/react-dom/src/__tests__/EventDispatchOrder-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* 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';

const React = require('react');
const ReactDOM = require('react-dom');

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

// The order we receive here is not ideal since it is expected that the
// capture listener fire before all bubble listeners. Other React apps
// might depend on this.
//
// @see https://github.com/facebook/react/pull/12919#issuecomment-395224674
const expectedOrder = [
'document capture',
'inner capture',
'inner bubble',
'outer capture',
'outer bubble',
'document bubble',
];

beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});

afterEach(() => {
container.remove();
});

function produceDispatchOrder(event, reactEvent) {
const eventOrder = [];
const track = tag => () => eventOrder.push(tag);
const outerRef = React.createRef();
const innerRef = React.createRef();

function OuterReactApp() {
return React.createElement('div', {
ref: outerRef,
[reactEvent]: track('outer bubble'),
[reactEvent + 'Capture']: track('outer capture'),
});
}

function InnerReactApp() {
return React.createElement('div', {
ref: innerRef,
[reactEvent]: track('inner bubble'),
[reactEvent + 'Capture']: track('inner capture'),
});
}

ReactDOM.unmountComponentAtNode(container);
ReactDOM.render(<OuterReactApp />, container);

ReactDOM.unmountComponentAtNode(outerRef.current);
ReactDOM.render(<InnerReactApp />, outerRef.current);

const trackDocBubble = track('document bubble');
const trackDocCapture = track('document capture');

document.addEventListener(event, trackDocBubble);
document.addEventListener(event, trackDocCapture, true);

innerRef.current.dispatchEvent(new Event(event, {bubbles: true}));

document.removeEventListener(event, trackDocBubble);
document.removeEventListener(event, trackDocCapture, true);

return eventOrder;
}

it('dispatches standard events in the correct order', () => {
expect(produceDispatchOrder('click', 'onClick')).toEqual(expectedOrder);
});

// Scroll and Wheel are attached as captured events directly to the element
describe('scrolling events', () => {
it('dispatches scroll events in the correct order', () => {
expect(produceDispatchOrder('scroll', 'onScroll')).toEqual(expectedOrder);
});

it('dispatches scroll events in the correct order', () => {
expect(produceDispatchOrder('wheel', 'onWheel')).toEqual(expectedOrder);
});
});

// Touch events are attached as bubbled events directly to the element
describe('touch events', () => {
it('dispatches touchstart in the correct order', () => {
expect(produceDispatchOrder('touchstart', 'onTouchStart')).toEqual(
expectedOrder,
);
});

it('dispatches touchend in the correct order', () => {
expect(produceDispatchOrder('touchend', 'onTouchEnd')).toEqual(
expectedOrder,
);
});

it('dispatches touchmove in the correct order', () => {
expect(produceDispatchOrder('touchmove', 'onTouchMove')).toEqual(
expectedOrder,
);
});

it('dispatches touchmove in the correct order', () => {
expect(produceDispatchOrder('touchcancel', 'onTouchCancel')).toEqual(
expectedOrder,
);
});
});
});
56 changes: 0 additions & 56 deletions packages/react-dom/src/__tests__/ReactDOMComponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2603,60 +2603,4 @@ describe('ReactDOMComponent', () => {
expect(node.getAttribute('onx')).toBe('bar');
});
});

it('receives events in specific order', () => {
let eventOrder = [];
let track = tag => () => eventOrder.push(tag);
let outerRef = React.createRef();
let innerRef = React.createRef();

function OuterReactApp() {
return (
<div
ref={outerRef}
onClick={track('outer bubble')}
onClickCapture={track('outer capture')}
/>
);
}

function InnerReactApp() {
return (
<div
ref={innerRef}
onClick={track('inner bubble')}
onClickCapture={track('inner capture')}
/>
);
}

const container = document.createElement('div');
document.body.appendChild(container);

try {
ReactDOM.render(<OuterReactApp />, container);
ReactDOM.render(<InnerReactApp />, outerRef.current);

document.addEventListener('click', track('document bubble'));
document.addEventListener('click', track('document capture'), true);

innerRef.current.click();

// The order we receive here is not ideal since it is expected that the
// capture listener fire before all bubble listeners. Other React apps
// might depend on this.
//
// @see https://github.com/facebook/react/pull/12919#issuecomment-395224674
expect(eventOrder).toEqual([
'document capture',
'inner capture',
'inner bubble',
'outer capture',
'outer bubble',
'document bubble',
]);
} finally {
document.body.removeChild(container);
}
});
});
7 changes: 3 additions & 4 deletions packages/react-dom/src/events/ReactBrowserEventEmitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,15 @@ export function listenTo(
) {
const mountAtListeners = getListenerTrackingFor(mountAt);
const dependencies = registrationNameDependencies[registrationName];
let elementListeners;

for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];

switch (dependency) {
case TOP_SCROLL:
case TOP_WHEEL:
const elementListeners = getListenerTrackingFor(element);

elementListeners = getListenerTrackingFor(element);
if (!elementListeners.hasOwnProperty(dependency)) {
trapCapturedEvent(dependency, element);
elementListeners[dependency] = true;
Expand All @@ -153,8 +153,7 @@ export function listenTo(
case TOP_TOUCH_END:
case TOP_TOUCH_MOVE:
case TOP_TOUCH_CANCEL:
const elementListeners = getListenerTrackingFor(element);

elementListeners = getListenerTrackingFor(element);

This comment has been minimized.

Copy link
@nhunzaker

nhunzaker Sep 5, 2018

Author Contributor

I suppose this could be hoisted up to like 138, but I want to be sensitive to the fact that this runs for every event attachment. Not really sure what's better. I'll give it some thought.

if (!elementListeners.hasOwnProperty(dependency)) {
trapBubbledEvent(dependency, element);
elementListeners[dependency] = true;
Expand Down

0 comments on commit e7884fc

Please sign in to comment.