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

Do not extract mouse events for children of disabled parents #8329

Closed
wants to merge 6 commits 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
12 changes: 0 additions & 12 deletions scripts/fiber/tests-failing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,10 @@ src/renderers/dom/shared/__tests__/ReactEventIndependence-test.js

src/renderers/dom/shared/__tests__/ReactEventListener-test.js
* should batch between handlers from different roots
* should not fire duplicate events for a React DOM tree

src/renderers/dom/shared/eventPlugins/__tests__/ChangeEventPlugin-test.js
* should not fire change when setting checked programmatically

src/renderers/dom/shared/eventPlugins/__tests__/SimpleEventPlugin-test.js
* A non-interactive tags clicks bubble when disabled
* should not forward clicks when it starts out disabled
* should not forward clicks when it becomes disabled
* should not forward clicks when it starts out disabled
* should not forward clicks when it becomes disabled
* should not forward clicks when it starts out disabled
* should not forward clicks when it becomes disabled
* should not forward clicks when it starts out disabled
* should not forward clicks when it becomes disabled

src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
* should control a value in reentrant events

Expand Down
16 changes: 16 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,7 @@ src/renderers/dom/shared/__tests__/ReactEventListener-test.js
* should propagate events one level down
* should propagate events two levels down
* should not get confused by disappearing elements
* should not fire duplicate events for a React DOM tree

src/renderers/dom/shared/__tests__/escapeTextContentForBrowser-test.js
* should escape boolean to string
Expand Down Expand Up @@ -689,17 +690,32 @@ src/renderers/dom/shared/eventPlugins/__tests__/SelectEventPlugin-test.js

src/renderers/dom/shared/eventPlugins/__tests__/SimpleEventPlugin-test.js
* A non-interactive tags click when disabled
* A non-interactive tags clicks bubble when disabled
* clicking a child of a disabled element does not register a click
* triggers click events for children of disabled elements
* triggers parent captured click events when target is a child of a disabled elements
* does not trigger captured click events when target is a disabled element
* triggers captured click events for children of disabled elements
* buttons inside of disabled fieldsets do not click
* should forward clicks when it starts out not disabled
* should not forward clicks when it starts out disabled
* should forward clicks when it becomes not disabled
* should not forward clicks when it becomes disabled
* should work correctly if the listener is changed
* should forward clicks when it starts out not disabled
* should not forward clicks when it starts out disabled
* should forward clicks when it becomes not disabled
* should not forward clicks when it becomes disabled
* should work correctly if the listener is changed
* should forward clicks when it starts out not disabled
* should not forward clicks when it starts out disabled
* should forward clicks when it becomes not disabled
* should not forward clicks when it becomes disabled
* should work correctly if the listener is changed
* should forward clicks when it starts out not disabled
* should not forward clicks when it starts out disabled
* should forward clicks when it becomes not disabled
* should not forward clicks when it becomes disabled
* should work correctly if the listener is changed
* does not add a local click to interactive elements
* adds a local click listener to non-interactive elements
Expand Down
14 changes: 9 additions & 5 deletions src/renderers/dom/shared/ReactEventListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var ExecutionEnvironment = require('ExecutionEnvironment');
var PooledClass = require('PooledClass');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
var ReactGenericBatching = require('ReactGenericBatching');

var ReactTreeTraversal = require('ReactTreeTraversal');
var getEventTarget = require('getEventTarget');
var getUnboundedScrollPosition = require('getUnboundedScrollPosition');

Expand All @@ -26,13 +26,17 @@ var getUnboundedScrollPosition = require('getUnboundedScrollPosition');
* other). If React trees are not nested, returns null.
*/
function findParent(inst) {
var root;

// TODO: It may be a good idea to cache this to prevent unnecessary DOM
// traversal, but caching is difficult to do correctly without using a
// mutation observer to listen for all DOM changes.
while (inst._hostParent) {
inst = inst._hostParent;
}
var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst);
do {
root = inst;
inst = ReactTreeTraversal.getParentInstance(inst);
} while (inst);

var rootNode = ReactDOMComponentTree.getNodeFromInstance(root);
var container = rootNode.parentNode;
return ReactDOMComponentTree.getClosestInstanceFromNode(container);
}
Expand Down
24 changes: 0 additions & 24 deletions src/renderers/dom/shared/eventPlugins/SimpleEventPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,25 +138,6 @@ var topLevelEventsToDispatchConfig: {[key: TopLevelTypes]: DispatchConfig} = {};
topLevelEventsToDispatchConfig[topEvent] = type;
});

function isInteractive(tag) {
return (
tag === 'button' || tag === 'input' ||
tag === 'select' || tag === 'textarea'
);
}

function shouldPreventMouseEvent(inst) {
if (inst) {
var disabled = inst._currentElement && inst._currentElement.props.disabled;

if (disabled) {
return isInteractive(inst._tag);
}
}

return false;
}

var SimpleEventPlugin: PluginModule<MouseEvent> = {

eventTypes: eventTypes,
Expand Down Expand Up @@ -232,11 +213,6 @@ var SimpleEventPlugin: PluginModule<MouseEvent> = {
case 'topMouseDown':
case 'topMouseMove':
case 'topMouseUp':
// Disabled elements should not respond to mouse events
if (shouldPreventMouseEvent(targetInst)) {
return null;
}
/* falls through */
case 'topMouseOut':
case 'topMouseOver':
case 'topContextMenu':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,78 @@ describe('SimpleEventPlugin', function() {
expect(onClick.mock.calls.length).toBe(1);
});

it('clicking a child of a disabled element does not register a click', function() {
var element = ReactTestUtils.renderIntoDocument(
<button onClick={onClick} disabled={true}><span /></button>
);
var child = ReactDOM.findDOMNode(element).querySelector('span');

onClick.mockClear();
ReactTestUtils.SimulateNative.click(child);
expect(onClick.mock.calls.length).toBe(0);
});

it('triggers click events for children of disabled elements', function() {
var element = ReactTestUtils.renderIntoDocument(
<button disabled={true}><span onClick={onClick} /></button>
);
var child = ReactDOM.findDOMNode(element).querySelector('span');

onClick.mockClear();
ReactTestUtils.SimulateNative.click(child);
expect(onClick.mock.calls.length).toBe(1);
});

it('triggers parent captured click events when target is a child of a disabled elements', function() {
var element = ReactTestUtils.renderIntoDocument(
<div onClickCapture={onClick}>
<button disabled={true}><span /></button>
</div>
);
var child = ReactDOM.findDOMNode(element).querySelector('span');

onClick.mockClear();
ReactTestUtils.SimulateNative.click(child);
expect(onClick.mock.calls.length).toBe(1);
});

it('does not trigger captured click events when target is a disabled element', function() {
var element = ReactTestUtils.renderIntoDocument(
<div onClickCapture={onClick}>
<button disabled={true} />
</div>
);
var button = ReactDOM.findDOMNode(element).querySelector('button');

onClick.mockClear();
ReactTestUtils.SimulateNative.click(button);
expect(onClick.mock.calls.length).toBe(0);
});

it('triggers captured click events for children of disabled elements', function() {
var element = ReactTestUtils.renderIntoDocument(
<button disabled={true}><span onClickCapture={onClick} /></button>
);
var child = ReactDOM.findDOMNode(element).querySelector('span');

onClick.mockClear();
ReactTestUtils.SimulateNative.click(child);
expect(onClick.mock.calls.length).toBe(1);
});

it('buttons inside of disabled fieldsets do not click', function() {
var element = ReactTestUtils.renderIntoDocument(
<fieldset disabled={true}>
<button onClick={onClick} />
</fieldset>
);
var child = ReactDOM.findDOMNode(element).querySelector('button');

onClick.mockClear();
ReactTestUtils.SimulateNative.click(child);
expect(onClick.mock.calls.length).toBe(0);
});

['button', 'input', 'select', 'textarea'].forEach(function(tagName) {

describe(tagName, function() {
Expand Down
42 changes: 42 additions & 0 deletions src/renderers/shared/shared/ReactTreeTraversal.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

'use strict';

var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
var { HostComponent } = require('ReactTypeOfWork');

function getParent(inst) {
Expand Down Expand Up @@ -90,13 +91,54 @@ function getParentInstance(inst) {
/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*/

function isInteractive(inst) {
var tag = ReactDOMFeatureFlags.useFiber ? inst.type : inst._tag;

return (
tag === 'button' || tag === 'input' ||
tag === 'select' || tag === 'textarea' ||
tag === 'fieldset'
);
}

function shouldIgnoreElement(inst) {
if (inst && isInteractive(inst)) {
if (ReactDOMFeatureFlags.useFiber) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not sufficient because we can use ReactDOM and ReactDOMFiber at the same time when the flag is not on.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm. ReactTreeTraversal uses this check:

https://github.com/facebook/react/blob/master/src/renderers/shared/shared/ReactTreeTraversal.js#L20

  if (typeof inst.tag === 'number') {

Is that the preferred route? Would it be worth making that a more established convention?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry... tunnel vision. There's also the condition above it:

https://github.com/facebook/react/blob/master/src/renderers/shared/shared/ReactTreeTraversal.js#L17

  if (inst._hostParent !== undefined) {
    return inst._hostParent;
  }

Which could be subbed out for inst._currentElement in this case.

return inst.stateNode.disabled;
} else if (inst._currentElement) {
return inst._currentElement.props.disabled;
}
}

return false;
}

function traverseTwoPhase(inst, fn, arg) {
// Do not traverse an tree that originates with a disabled element
if (shouldIgnoreElement(inst)) {
return false;
}

var path = [];
while (inst) {
path.push(inst);
inst = getParent(inst);
}

var i;
var disabled = false;
// walking from parent to child
for (i = path.length; i-- > 0;) {
// Are we currently, our about to be, within a disabled tree
disabled = disabled || shouldIgnoreElement(path[i]);
// If so, remove the current element from traversion if it
// is interactive
if (disabled && isInteractive(path[i])) {
path.splice(i, 1);
}
}

for (i = path.length; i-- > 0;) {
fn(path[i], 'captured', arg);
}
Expand Down