From 2fbe0cd333ae8097c7cb1e4c4532844b0ce1df9f Mon Sep 17 00:00:00 2001 From: Nathan Hunzaker Date: Thu, 8 Sep 2016 14:47:52 -0400 Subject: [PATCH] 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 73c50e7d006856e37a22f3de390cbed859d9dce3) --- .../client/eventPlugins/SimpleEventPlugin.js | 33 +++++++-- .../__tests__/SimpleEventPlugin-test.js} | 71 +++++++++++++++---- .../dom/client/wrappers/DisabledInputUtils.js | 50 ------------- .../dom/client/wrappers/ReactDOMButton.js | 24 ------- .../dom/client/wrappers/ReactDOMInput.js | 3 +- .../dom/client/wrappers/ReactDOMSelect.js | 3 +- .../dom/client/wrappers/ReactDOMTextarea.js | 3 +- src/renderers/dom/shared/ReactDOMComponent.js | 8 --- 8 files changed, 91 insertions(+), 104 deletions(-) rename src/renderers/dom/client/{wrappers/__tests__/DisabledInputUtil-test.js => eventPlugins/__tests__/SimpleEventPlugin-test.js} (58%) delete mode 100644 src/renderers/dom/client/wrappers/DisabledInputUtils.js delete mode 100644 src/renderers/dom/client/wrappers/ReactDOMButton.js diff --git a/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js b/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js index e17d72889dd73..7f1e41b60d142 100644 --- a/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js @@ -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 = { eventTypes: eventTypes, @@ -217,13 +236,18 @@ var SimpleEventPlugin: PluginModule = { 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 = { // 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 = { inst: ReactInstance, registrationName: string, ): void { - if (registrationName === 'onClick') { + if (registrationName === 'onClick' && !isInteractive(inst._tag)) { var key = getDictionaryKey(inst); onClickListeners[key].remove(); delete onClickListeners[key]; diff --git a/src/renderers/dom/client/wrappers/__tests__/DisabledInputUtil-test.js b/src/renderers/dom/client/eventPlugins/__tests__/SimpleEventPlugin-test.js similarity index 58% rename from src/renderers/dom/client/wrappers/__tests__/DisabledInputUtil-test.js rename to src/renderers/dom/client/eventPlugins/__tests__/SimpleEventPlugin-test.js index ff8bcc87590bd..e063092b37d7b 100644 --- a/src/renderers/dom/client/wrappers/__tests__/DisabledInputUtil-test.js +++ b/src/renderers/dom/client/eventPlugins/__tests__/SimpleEventPlugin-test.js @@ -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 = (
); + expectClickThru(mounted(element)); + }); - describe(tagName, () => { + it('A non-interactive tags clicks bubble when disabled', function() { + var element = ReactTestUtils.renderIntoDocument( +
+ ); + 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(, 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(
, container); + + var node = container.firstChild; + + node.dispatchEvent(new MouseEvent('click')); + + expect(onClick.mock.calls.length).toBe(0); + }); + }); }); diff --git a/src/renderers/dom/client/wrappers/DisabledInputUtils.js b/src/renderers/dom/client/wrappers/DisabledInputUtils.js deleted file mode 100644 index 1393a14b4e80e..0000000000000 --- a/src/renderers/dom/client/wrappers/DisabledInputUtils.js +++ /dev/null @@ -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; diff --git a/src/renderers/dom/client/wrappers/ReactDOMButton.js b/src/renderers/dom/client/wrappers/ReactDOMButton.js deleted file mode 100644 index 09879c9397a66..0000000000000 --- a/src/renderers/dom/client/wrappers/ReactDOMButton.js +++ /dev/null @@ -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