mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Move mouse event disabling on interactive elements to SimpleEventPlugin. Related perf tweak to click handlers. (#7642)
* Cull disabled mouse events at plugin level. Remove component level filters
* DisabledInputUtils tests are now for SimpleEventPlugin
* Add click bubbling test
* Add isInteractive function. Use in iOS click exception rules
* Invert interactive check in local click listener. Add test coverage
* Reduce number of mouse events disabable. Formatting in isIteractive()
* Switch isInteractive tag order for alignment
* Update formatting of isInteractive method
(cherry picked from commit 73c50e7d00)
This commit is contained in:
committed by
Paul O’Shannessy
parent
747d65133a
commit
2fbe0cd333
@@ -146,6 +146,25 @@ function getDictionaryKey(inst: ReactInstance): string {
|
||||
return '.' + inst._rootNodeID;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -217,13 +236,18 @@ var SimpleEventPlugin: PluginModule<MouseEvent> = {
|
||||
return null;
|
||||
}
|
||||
/* falls through */
|
||||
case 'topContextMenu':
|
||||
case 'topDoubleClick':
|
||||
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 'topMouseUp':
|
||||
case 'topContextMenu':
|
||||
EventConstructor = SyntheticMouseEvent;
|
||||
break;
|
||||
case 'topDrag':
|
||||
@@ -286,7 +310,8 @@ var SimpleEventPlugin: PluginModule<MouseEvent> = {
|
||||
// non-interactive elements, which means delegated click listeners do not
|
||||
// fire. The workaround for this bug involves attaching an empty click
|
||||
// listener on the target node.
|
||||
if (registrationName === 'onClick') {
|
||||
// http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
|
||||
if (registrationName === 'onClick' && !isInteractive(inst._tag)) {
|
||||
var key = getDictionaryKey(inst);
|
||||
var node = ReactDOMComponentTree.getNodeFromInstance(inst);
|
||||
if (!onClickListeners[key]) {
|
||||
@@ -303,7 +328,7 @@ var SimpleEventPlugin: PluginModule<MouseEvent> = {
|
||||
inst: ReactInstance,
|
||||
registrationName: string,
|
||||
): void {
|
||||
if (registrationName === 'onClick') {
|
||||
if (registrationName === 'onClick' && !isInteractive(inst._tag)) {
|
||||
var key = getDictionaryKey(inst);
|
||||
onClickListeners[key].remove();
|
||||
delete onClickListeners[key];
|
||||
|
||||
+59
-12
@@ -12,22 +12,22 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
describe('DisabledInputUtils', () => {
|
||||
describe('SimpleEventPlugin', function() {
|
||||
var React;
|
||||
var ReactDOM;
|
||||
var ReactTestUtils;
|
||||
|
||||
var elements = ['button', 'input', 'select', 'textarea'];
|
||||
var onClick = jest.fn();
|
||||
|
||||
function expectClickThru(element) {
|
||||
onClick.mockClear();
|
||||
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(element));
|
||||
ReactTestUtils.SimulateNative.click(ReactDOM.findDOMNode(element));
|
||||
expect(onClick.mock.calls.length).toBe(1);
|
||||
}
|
||||
|
||||
function expectNoClickThru(element) {
|
||||
onClick.mockClear();
|
||||
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(element));
|
||||
ReactTestUtils.SimulateNative.click(ReactDOM.findDOMNode(element));
|
||||
expect(onClick.mock.calls.length).toBe(0);
|
||||
}
|
||||
|
||||
@@ -36,17 +36,31 @@ describe('DisabledInputUtils', () => {
|
||||
return element;
|
||||
}
|
||||
|
||||
var onClick = jest.fn();
|
||||
beforeEach(function() {
|
||||
React = require('React');
|
||||
ReactDOM = require('ReactDOM');
|
||||
ReactTestUtils = require('ReactTestUtils');
|
||||
});
|
||||
|
||||
elements.forEach(function(tagName) {
|
||||
it('A non-interactive tags click when disabled', function() {
|
||||
var element = (<div onClick={ onClick } />);
|
||||
expectClickThru(mounted(element));
|
||||
});
|
||||
|
||||
describe(tagName, () => {
|
||||
it('A non-interactive tags clicks bubble when disabled', function() {
|
||||
var element = ReactTestUtils.renderIntoDocument(
|
||||
<div onClick={onClick}><div /></div>
|
||||
);
|
||||
var child = ReactDOM.findDOMNode(element).firstChild;
|
||||
|
||||
beforeEach(() => {
|
||||
React = require('React');
|
||||
ReactDOM = require('ReactDOM');
|
||||
ReactTestUtils = require('ReactTestUtils');
|
||||
});
|
||||
onClick.mockClear();
|
||||
ReactTestUtils.SimulateNative.click(child);
|
||||
expect(onClick.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
['button', 'input', 'select', 'textarea'].forEach(function(tagName) {
|
||||
|
||||
describe(tagName, function() {
|
||||
|
||||
it('should forward clicks when it starts out not disabled', () => {
|
||||
var element = React.createElement(tagName, {
|
||||
@@ -105,4 +119,37 @@ describe('DisabledInputUtils', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('iOS bubbling click fix', function() {
|
||||
// See http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
|
||||
|
||||
beforeEach(function() {
|
||||
onClick.mockClear();
|
||||
});
|
||||
|
||||
it ('does not add a local click to interactive elements', function() {
|
||||
var container = document.createElement('div');
|
||||
|
||||
ReactDOM.render(<button onClick={ onClick }></button>, container);
|
||||
|
||||
var node = container.firstChild;
|
||||
|
||||
node.dispatchEvent(new MouseEvent('click'));
|
||||
|
||||
expect(onClick.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it ('adds a local click listener to non-interactive elements', function() {
|
||||
var container = document.createElement('div');
|
||||
|
||||
ReactDOM.render(<div onClick={ onClick }></div>, container);
|
||||
|
||||
var node = container.firstChild;
|
||||
|
||||
node.dispatchEvent(new MouseEvent('click'));
|
||||
|
||||
expect(onClick.mock.calls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* Copyright 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule DisabledInputUtils
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var disableableMouseListenerNames = {
|
||||
onClick: true,
|
||||
onDoubleClick: true,
|
||||
onMouseDown: true,
|
||||
onMouseMove: true,
|
||||
onMouseUp: true,
|
||||
|
||||
onClickCapture: true,
|
||||
onDoubleClickCapture: true,
|
||||
onMouseDownCapture: true,
|
||||
onMouseMoveCapture: true,
|
||||
onMouseUpCapture: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a host component that does not receive mouse events
|
||||
* when `disabled` is set.
|
||||
*/
|
||||
var DisabledInputUtils = {
|
||||
getHostProps: function(inst, props) {
|
||||
if (!props.disabled) {
|
||||
return props;
|
||||
}
|
||||
|
||||
// Copy the props, except the mouse listeners
|
||||
var hostProps = {};
|
||||
for (var key in props) {
|
||||
if (!disableableMouseListenerNames[key] && props.hasOwnProperty(key)) {
|
||||
hostProps[key] = props[key];
|
||||
}
|
||||
}
|
||||
|
||||
return hostProps;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = DisabledInputUtils;
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule ReactDOMButton
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var DisabledInputUtils = require('DisabledInputUtils');
|
||||
|
||||
/**
|
||||
* Implements a <button> host component that does not receive mouse events
|
||||
* when `disabled` is set.
|
||||
*/
|
||||
var ReactDOMButton = {
|
||||
getHostProps: DisabledInputUtils.getHostProps,
|
||||
};
|
||||
|
||||
module.exports = ReactDOMButton;
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var DisabledInputUtils = require('DisabledInputUtils');
|
||||
var DOMPropertyOperations = require('DOMPropertyOperations');
|
||||
var LinkedValueUtils = require('LinkedValueUtils');
|
||||
var ReactDOMComponentTree = require('ReactDOMComponentTree');
|
||||
@@ -71,7 +70,7 @@ var ReactDOMInput = {
|
||||
// in corner cases such as min or max deriving from value, e.g. Issue #7170)
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
}, DisabledInputUtils.getHostProps(inst, props), {
|
||||
}, props, {
|
||||
defaultChecked: undefined,
|
||||
defaultValue: undefined,
|
||||
value: value != null ? value : inst._wrapperState.initialValue,
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var DisabledInputUtils = require('DisabledInputUtils');
|
||||
var LinkedValueUtils = require('LinkedValueUtils');
|
||||
var ReactDOMComponentTree = require('ReactDOMComponentTree');
|
||||
var ReactUpdates = require('ReactUpdates');
|
||||
@@ -146,7 +145,7 @@ function updateOptions(inst, multiple, propValue) {
|
||||
*/
|
||||
var ReactDOMSelect = {
|
||||
getHostProps: function(inst, props) {
|
||||
return Object.assign({}, DisabledInputUtils.getHostProps(inst, props), {
|
||||
return Object.assign({}, props, {
|
||||
onChange: inst._wrapperState.onChange,
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var DisabledInputUtils = require('DisabledInputUtils');
|
||||
var LinkedValueUtils = require('LinkedValueUtils');
|
||||
var ReactDOMComponentTree = require('ReactDOMComponentTree');
|
||||
var ReactUpdates = require('ReactUpdates');
|
||||
@@ -56,7 +55,7 @@ var ReactDOMTextarea = {
|
||||
// to only set the value if/when the value differs from the node value (which would
|
||||
// completely solve this IE9 bug), but Sebastian+Ben seemed to like this solution.
|
||||
// The value can be a boolean or object so that's why it's forced to be a string.
|
||||
var hostProps = Object.assign({}, DisabledInputUtils.getHostProps(inst, props), {
|
||||
var hostProps = Object.assign({}, props, {
|
||||
value: undefined,
|
||||
defaultValue: undefined,
|
||||
children: '' + inst._wrapperState.initialValue,
|
||||
|
||||
@@ -22,7 +22,6 @@ var DOMPropertyOperations = require('DOMPropertyOperations');
|
||||
var EventPluginHub = require('EventPluginHub');
|
||||
var EventPluginRegistry = require('EventPluginRegistry');
|
||||
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
|
||||
var ReactDOMButton = require('ReactDOMButton');
|
||||
var ReactDOMComponentFlags = require('ReactDOMComponentFlags');
|
||||
var ReactDOMComponentTree = require('ReactDOMComponentTree');
|
||||
var ReactDOMInput = require('ReactDOMInput');
|
||||
@@ -537,9 +536,6 @@ ReactDOMComponent.Mixin = {
|
||||
};
|
||||
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
|
||||
break;
|
||||
case 'button':
|
||||
props = ReactDOMButton.getHostProps(this, props, hostParent);
|
||||
break;
|
||||
case 'input':
|
||||
ReactDOMInput.mountWrapper(this, props, hostParent);
|
||||
props = ReactDOMInput.getHostProps(this, props);
|
||||
@@ -880,10 +876,6 @@ ReactDOMComponent.Mixin = {
|
||||
var nextProps = this._currentElement.props;
|
||||
|
||||
switch (this._tag) {
|
||||
case 'button':
|
||||
lastProps = ReactDOMButton.getHostProps(this, lastProps);
|
||||
nextProps = ReactDOMButton.getHostProps(this, nextProps);
|
||||
break;
|
||||
case 'input':
|
||||
lastProps = ReactDOMInput.getHostProps(this, lastProps);
|
||||
nextProps = ReactDOMInput.getHostProps(this, nextProps);
|
||||
|
||||
Reference in New Issue
Block a user