Skip to content

Commit 73c50e7

Browse files
nhunzakersophiebits
authored andcommitted
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
1 parent df03318 commit 73c50e7

File tree

8 files changed

+91
-104
lines changed

8 files changed

+91
-104
lines changed

src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,25 @@ function getDictionaryKey(inst: ReactInstance): string {
146146
return '.' + inst._rootNodeID;
147147
}
148148

149+
function isInteractive(tag) {
150+
return (
151+
tag === 'button' || tag === 'input' ||
152+
tag === 'select' || tag === 'textarea'
153+
);
154+
}
155+
156+
function shouldPreventMouseEvent(inst) {
157+
if (inst) {
158+
var disabled = inst._currentElement && inst._currentElement.props.disabled;
159+
160+
if (disabled) {
161+
return isInteractive(inst._tag);
162+
}
163+
}
164+
165+
return false;
166+
}
167+
149168
var SimpleEventPlugin: PluginModule<MouseEvent> = {
150169

151170
eventTypes: eventTypes,
@@ -217,13 +236,18 @@ var SimpleEventPlugin: PluginModule<MouseEvent> = {
217236
return null;
218237
}
219238
/* falls through */
220-
case 'topContextMenu':
221239
case 'topDoubleClick':
222240
case 'topMouseDown':
223241
case 'topMouseMove':
242+
case 'topMouseUp':
243+
// Disabled elements should not respond to mouse events
244+
if (shouldPreventMouseEvent(targetInst)) {
245+
return null;
246+
}
247+
/* falls through */
224248
case 'topMouseOut':
225249
case 'topMouseOver':
226-
case 'topMouseUp':
250+
case 'topContextMenu':
227251
EventConstructor = SyntheticMouseEvent;
228252
break;
229253
case 'topDrag':
@@ -286,7 +310,8 @@ var SimpleEventPlugin: PluginModule<MouseEvent> = {
286310
// non-interactive elements, which means delegated click listeners do not
287311
// fire. The workaround for this bug involves attaching an empty click
288312
// listener on the target node.
289-
if (registrationName === 'onClick') {
313+
// http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
314+
if (registrationName === 'onClick' && !isInteractive(inst._tag)) {
290315
var key = getDictionaryKey(inst);
291316
var node = ReactDOMComponentTree.getNodeFromInstance(inst);
292317
if (!onClickListeners[key]) {
@@ -303,7 +328,7 @@ var SimpleEventPlugin: PluginModule<MouseEvent> = {
303328
inst: ReactInstance,
304329
registrationName: string,
305330
): void {
306-
if (registrationName === 'onClick') {
331+
if (registrationName === 'onClick' && !isInteractive(inst._tag)) {
307332
var key = getDictionaryKey(inst);
308333
onClickListeners[key].remove();
309334
delete onClickListeners[key];

src/renderers/dom/client/wrappers/__tests__/DisabledInputUtil-test.js renamed to src/renderers/dom/client/eventPlugins/__tests__/SimpleEventPlugin-test.js

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,22 @@
1212
'use strict';
1313

1414

15-
describe('DisabledInputUtils', () => {
15+
describe('SimpleEventPlugin', function() {
1616
var React;
1717
var ReactDOM;
1818
var ReactTestUtils;
1919

20-
var elements = ['button', 'input', 'select', 'textarea'];
20+
var onClick = jest.fn();
2121

2222
function expectClickThru(element) {
2323
onClick.mockClear();
24-
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(element));
24+
ReactTestUtils.SimulateNative.click(ReactDOM.findDOMNode(element));
2525
expect(onClick.mock.calls.length).toBe(1);
2626
}
2727

2828
function expectNoClickThru(element) {
2929
onClick.mockClear();
30-
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(element));
30+
ReactTestUtils.SimulateNative.click(ReactDOM.findDOMNode(element));
3131
expect(onClick.mock.calls.length).toBe(0);
3232
}
3333

@@ -36,17 +36,31 @@ describe('DisabledInputUtils', () => {
3636
return element;
3737
}
3838

39-
var onClick = jest.fn();
39+
beforeEach(function() {
40+
React = require('React');
41+
ReactDOM = require('ReactDOM');
42+
ReactTestUtils = require('ReactTestUtils');
43+
});
4044

41-
elements.forEach(function(tagName) {
45+
it('A non-interactive tags click when disabled', function() {
46+
var element = (<div onClick={ onClick } />);
47+
expectClickThru(mounted(element));
48+
});
4249

43-
describe(tagName, () => {
50+
it('A non-interactive tags clicks bubble when disabled', function() {
51+
var element = ReactTestUtils.renderIntoDocument(
52+
<div onClick={onClick}><div /></div>
53+
);
54+
var child = ReactDOM.findDOMNode(element).firstChild;
4455

45-
beforeEach(() => {
46-
React = require('React');
47-
ReactDOM = require('ReactDOM');
48-
ReactTestUtils = require('ReactTestUtils');
49-
});
56+
onClick.mockClear();
57+
ReactTestUtils.SimulateNative.click(child);
58+
expect(onClick.mock.calls.length).toBe(1);
59+
});
60+
61+
['button', 'input', 'select', 'textarea'].forEach(function(tagName) {
62+
63+
describe(tagName, function() {
5064

5165
it('should forward clicks when it starts out not disabled', () => {
5266
var element = React.createElement(tagName, {
@@ -105,4 +119,37 @@ describe('DisabledInputUtils', () => {
105119
});
106120
});
107121
});
122+
123+
124+
describe('iOS bubbling click fix', function() {
125+
// See http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
126+
127+
beforeEach(function() {
128+
onClick.mockClear();
129+
});
130+
131+
it ('does not add a local click to interactive elements', function() {
132+
var container = document.createElement('div');
133+
134+
ReactDOM.render(<button onClick={ onClick }></button>, container);
135+
136+
var node = container.firstChild;
137+
138+
node.dispatchEvent(new MouseEvent('click'));
139+
140+
expect(onClick.mock.calls.length).toBe(0);
141+
});
142+
143+
it ('adds a local click listener to non-interactive elements', function() {
144+
var container = document.createElement('div');
145+
146+
ReactDOM.render(<div onClick={ onClick }></div>, container);
147+
148+
var node = container.firstChild;
149+
150+
node.dispatchEvent(new MouseEvent('click'));
151+
152+
expect(onClick.mock.calls.length).toBe(0);
153+
});
154+
});
108155
});

src/renderers/dom/client/wrappers/DisabledInputUtils.js

Lines changed: 0 additions & 50 deletions
This file was deleted.

src/renderers/dom/client/wrappers/ReactDOMButton.js

Lines changed: 0 additions & 24 deletions
This file was deleted.

src/renderers/dom/client/wrappers/ReactDOMInput.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
'use strict';
1313

14-
var DisabledInputUtils = require('DisabledInputUtils');
1514
var DOMPropertyOperations = require('DOMPropertyOperations');
1615
var LinkedValueUtils = require('LinkedValueUtils');
1716
var ReactDOMComponentTree = require('ReactDOMComponentTree');
@@ -71,7 +70,7 @@ var ReactDOMInput = {
7170
// in corner cases such as min or max deriving from value, e.g. Issue #7170)
7271
min: undefined,
7372
max: undefined,
74-
}, DisabledInputUtils.getHostProps(inst, props), {
73+
}, props, {
7574
defaultChecked: undefined,
7675
defaultValue: undefined,
7776
value: value != null ? value : inst._wrapperState.initialValue,

src/renderers/dom/client/wrappers/ReactDOMSelect.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
'use strict';
1313

14-
var DisabledInputUtils = require('DisabledInputUtils');
1514
var LinkedValueUtils = require('LinkedValueUtils');
1615
var ReactDOMComponentTree = require('ReactDOMComponentTree');
1716
var ReactUpdates = require('ReactUpdates');
@@ -146,7 +145,7 @@ function updateOptions(inst, multiple, propValue) {
146145
*/
147146
var ReactDOMSelect = {
148147
getHostProps: function(inst, props) {
149-
return Object.assign({}, DisabledInputUtils.getHostProps(inst, props), {
148+
return Object.assign({}, props, {
150149
onChange: inst._wrapperState.onChange,
151150
value: undefined,
152151
});

src/renderers/dom/client/wrappers/ReactDOMTextarea.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
'use strict';
1313

14-
var DisabledInputUtils = require('DisabledInputUtils');
1514
var LinkedValueUtils = require('LinkedValueUtils');
1615
var ReactDOMComponentTree = require('ReactDOMComponentTree');
1716
var ReactUpdates = require('ReactUpdates');
@@ -56,7 +55,7 @@ var ReactDOMTextarea = {
5655
// to only set the value if/when the value differs from the node value (which would
5756
// completely solve this IE9 bug), but Sebastian+Ben seemed to like this solution.
5857
// The value can be a boolean or object so that's why it's forced to be a string.
59-
var hostProps = Object.assign({}, DisabledInputUtils.getHostProps(inst, props), {
58+
var hostProps = Object.assign({}, props, {
6059
value: undefined,
6160
defaultValue: undefined,
6261
children: '' + inst._wrapperState.initialValue,

src/renderers/dom/shared/ReactDOMComponent.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ var DOMPropertyOperations = require('DOMPropertyOperations');
2222
var EventPluginHub = require('EventPluginHub');
2323
var EventPluginRegistry = require('EventPluginRegistry');
2424
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
25-
var ReactDOMButton = require('ReactDOMButton');
2625
var ReactDOMComponentFlags = require('ReactDOMComponentFlags');
2726
var ReactDOMComponentTree = require('ReactDOMComponentTree');
2827
var ReactDOMInput = require('ReactDOMInput');
@@ -542,9 +541,6 @@ ReactDOMComponent.Mixin = {
542541
};
543542
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
544543
break;
545-
case 'button':
546-
props = ReactDOMButton.getHostProps(this, props, hostParent);
547-
break;
548544
case 'input':
549545
ReactDOMInput.mountWrapper(this, props, hostParent);
550546
props = ReactDOMInput.getHostProps(this, props);
@@ -887,10 +883,6 @@ ReactDOMComponent.Mixin = {
887883
var nextProps = this._currentElement.props;
888884

889885
switch (this._tag) {
890-
case 'button':
891-
lastProps = ReactDOMButton.getHostProps(this, lastProps);
892-
nextProps = ReactDOMButton.getHostProps(this, nextProps);
893-
break;
894886
case 'input':
895887
lastProps = ReactDOMInput.getHostProps(this, lastProps);
896888
nextProps = ReactDOMInput.getHostProps(this, nextProps);

0 commit comments

Comments
 (0)