@samitouri / QOS-React-2 / commits / e096000bb5

Attach empty onclick listener to each node

Fixes #1169. This is a more robust way of fixing what MobileSafariClickPlugin previously tried to. Now even if you don't want anything to do with touch events, click events still work properly. Test Plan: Added a click handler to an `<img />` element and triggered it in the iOS simulator -- it didn't execute before.

Ben Alpert committed May 15, 2014 at 11:12 UTC e096000bb5449dc3a2749ea8f7f10b2fc0d28080
6 files changed +67 -67
src/browser/eventPlugins/DefaultEventPluginOrder.js
+1 -2
@@ -38,8 +38,7 @@ var DefaultEventPluginOrder = [
38 keyOf({SelectEventPlugin: null}),
39 keyOf({CompositionEventPlugin: null}),
40 keyOf({BeforeInputEventPlugin: null}),
41 - keyOf({AnalyticsEventPlugin: null}),
42 - keyOf({MobileSafariClickEventPlugin: null})
41 + keyOf({AnalyticsEventPlugin: null})
42 ];
43
44 module.exports = DefaultEventPluginOrder;
src/browser/eventPlugins/MobileSafariClickEventPlugin.js deleted
-63
@@ -1,63 +0,0 @@
1 -/**
2 - * Copyright 2013-2014 Facebook, Inc.
3 - *
4 - * Licensed under the Apache License, Version 2.0 (the "License");
5 - * you may not use this file except in compliance with the License.
6 - * You may obtain a copy of the License at
7 - *
8 - * http://www.apache.org/licenses/LICENSE-2.0
9 - *
10 - * Unless required by applicable law or agreed to in writing, software
11 - * distributed under the License is distributed on an "AS IS" BASIS,
12 - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 - * See the License for the specific language governing permissions and
14 - * limitations under the License.
15 - *
16 - * @providesModule MobileSafariClickEventPlugin
17 - * @typechecks static-only
18 - */
19 -
20 -"use strict";
21 -
22 -var EventConstants = require('EventConstants');
23 -
24 -var emptyFunction = require('emptyFunction');
25 -
26 -var topLevelTypes = EventConstants.topLevelTypes;
27 -
28 -/**
29 - * Mobile Safari does not fire properly bubble click events on non-interactive
30 - * elements, which means delegated click listeners do not fire. The workaround
31 - * for this bug involves attaching an empty click listener on the target node.
32 - *
33 - * This particular plugin works around the bug by attaching an empty click
34 - * listener on `touchstart` (which does fire on every element).
35 - */
36 -var MobileSafariClickEventPlugin = {
37 -
38 - eventTypes: null,
39 -
40 - /**
41 - * @param {string} topLevelType Record from `EventConstants`.
42 - * @param {DOMEventTarget} topLevelTarget The listening component root node.
43 - * @param {string} topLevelTargetID ID of `topLevelTarget`.
44 - * @param {object} nativeEvent Native browser event.
45 - * @return {*} An accumulation of synthetic events.
46 - * @see {EventPluginHub.extractEvents}
47 - */
48 - extractEvents: function(
49 - topLevelType,
50 - topLevelTarget,
51 - topLevelTargetID,
52 - nativeEvent) {
53 - if (topLevelType === topLevelTypes.topTouchStart) {
54 - var target = nativeEvent.target;
55 - if (target && !target.onclick) {
56 - target.onclick = emptyFunction;
57 - }
58 - }
59 - }
60 -
61 -};
62 -
63 -module.exports = MobileSafariClickEventPlugin;
src/browser/eventPlugins/SimpleEventPlugin.js
+23
@@ -19,8 +19,10 @@
19 "use strict";
20
21 var EventConstants = require('EventConstants');
22 +var EventListener = require('EventListener');
23 var EventPluginUtils = require('EventPluginUtils');
24 var EventPropagators = require('EventPropagators');
25 +var ReactMount = require('ReactMount');
26 var SyntheticClipboardEvent = require('SyntheticClipboardEvent');
27 var SyntheticEvent = require('SyntheticEvent');
28 var SyntheticFocusEvent = require('SyntheticFocusEvent');
@@ -31,6 +33,7 @@ var SyntheticTouchEvent = require('SyntheticTouchEvent');
33 var SyntheticUIEvent = require('SyntheticUIEvent');
34 var SyntheticWheelEvent = require('SyntheticWheelEvent');
35
36 +var emptyFunction = require('emptyFunction');
37 var invariant = require('invariant');
38 var keyOf = require('keyOf');
39
@@ -293,6 +296,9 @@ for (var topLevelType in topLevelEventsToDispatchConfig) {
296 topLevelEventsToDispatchConfig[topLevelType].dependencies = [topLevelType];
297 }
298
299 +var ON_CLICK_KEY = keyOf({onClick: null});
300 +var onClickListeners = {};
301 +
302 var SimpleEventPlugin = {
303
304 eventTypes: eventTypes,
@@ -412,6 +418,23 @@ var SimpleEventPlugin = {
418 );
419 EventPropagators.accumulateTwoPhaseDispatches(event);
420 return event;
421 + },
422 +
423 + didPutListener: function(id, registrationName, listener) {
424 + // Mobile Safari does not fire properly bubble click events on
425 + // non-interactive elements, which means delegated click listeners do not
426 + // fire. The workaround for this bug involves attaching an empty click
427 + // listener on the target node.
428 + if (registrationName === ON_CLICK_KEY) {
429 + var node = ReactMount.getNode(id);
430 + onClickListeners[id] = EventListener.listen(node, 'click', emptyFunction);
431 + }
432 + },
433 +
434 + willDeleteListener: function(id, registrationName) {
435 + if (registrationName === ON_CLICK_KEY) {
436 + onClickListeners[id].remove();
437 + }
438 }
439
440 };
src/browser/ui/ReactDefaultInjection.js
-2
@@ -26,7 +26,6 @@ var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
26 var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin');
27 var ExecutionEnvironment = require('ExecutionEnvironment');
28 var HTMLDOMPropertyConfig = require('HTMLDOMPropertyConfig');
29 -var MobileSafariClickEventPlugin = require('MobileSafariClickEventPlugin');
29 var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
30 var ReactComponentBrowserEnvironment =
31 require('ReactComponentBrowserEnvironment');
@@ -71,7 +70,6 @@ function inject() {
70 EnterLeaveEventPlugin: EnterLeaveEventPlugin,
71 ChangeEventPlugin: ChangeEventPlugin,
72 CompositionEventPlugin: CompositionEventPlugin,
74 - MobileSafariClickEventPlugin: MobileSafariClickEventPlugin,
73 SelectEventPlugin: SelectEventPlugin,
74 BeforeInputEventPlugin: BeforeInputEventPlugin
75 });
src/browser/ui/__tests__/ReactDOMComponent-test.js
+20
@@ -343,6 +343,26 @@ describe('ReactDOMComponent', function() {
343 'properties to values, not a string.'
344 );
345 });
346 +
347 + it("should execute custom event plugin listening behavior", function() {
348 + var React = require('React');
349 + var SimpleEventPlugin = require('SimpleEventPlugin');
350 +
351 + SimpleEventPlugin.didPutListener = mocks.getMockFunction();
352 + SimpleEventPlugin.willDeleteListener = mocks.getMockFunction();
353 +
354 + var container = document.createElement('div');
355 + React.renderComponent(
356 + <div onClick={() => true} />,
357 + container
358 + );
359 +
360 + expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(1);
361 +
362 + React.unmountComponentAtNode(container);
363 +
364 + expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
365 + });
366 });
367
368 describe('updateComponent', function() {
src/event/EventPluginHub.js
+23
@@ -171,6 +171,12 @@ var EventPluginHub = {
171 var bankForRegistrationName =
172 listenerBank[registrationName] || (listenerBank[registrationName] = {});
173 bankForRegistrationName[id] = listener;
174 +
175 + var PluginModule =
176 + EventPluginRegistry.registrationNameModules[registrationName];
177 + if (PluginModule && PluginModule.didPutListener) {
178 + PluginModule.didPutListener(id, registrationName, listener);
179 + }
180 },
181
182 /**
@@ -190,7 +196,14 @@ var EventPluginHub = {
196 * @param {string} registrationName Name of listener (e.g. `onClick`).
197 */
198 deleteListener: function(id, registrationName) {
199 + var PluginModule =
200 + EventPluginRegistry.registrationNameModules[registrationName];
201 + if (PluginModule && PluginModule.willDeleteListener) {
202 + PluginModule.willDeleteListener(id, registrationName);
203 + }
204 +
205 var bankForRegistrationName = listenerBank[registrationName];
206 + // TODO: This should never be null -- when is it?
207 if (bankForRegistrationName) {
208 delete bankForRegistrationName[id];
209 }
@@ -203,6 +216,16 @@ var EventPluginHub = {
216 */
217 deleteAllListeners: function(id) {
218 for (var registrationName in listenerBank) {
219 + if (!listenerBank[registrationName][id]) {
220 + continue;
221 + }
222 +
223 + var PluginModule =
224 + EventPluginRegistry.registrationNameModules[registrationName];
225 + if (PluginModule && PluginModule.willDeleteListener) {
226 + PluginModule.willDeleteListener(id, registrationName);
227 + }
228 +
229 delete listenerBank[registrationName][id];
230 }
231 },