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
},