diff --git a/fixtures/dom/.gitignore b/fixtures/dom/.gitignore index bcc245d36a251..936509155acbf 100644 --- a/fixtures/dom/.gitignore +++ b/fixtures/dom/.gitignore @@ -8,8 +8,8 @@ coverage # production build -public/react.development.js -public/react-dom.development.js +public/react.*.js +public/react-dom.*.js # misc .DS_Store diff --git a/fixtures/dom/package.json b/fixtures/dom/package.json index e461702d6ae3f..4afb2be454016 100644 --- a/fixtures/dom/package.json +++ b/fixtures/dom/package.json @@ -16,7 +16,8 @@ }, "scripts": { "start": "react-scripts start", - "prestart": "cp ../../build/dist/{react,react-dom}.development.js public/", + "prestart": "cp ../../build/dist/{react,react-dom}.{development,production.min}.js public/", + "prebuild": "cp ../../build/dist/{react,react-dom}.{development,production.min}.js public/", "build": "react-scripts build && cp build/index.html build/200.html", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" diff --git a/packages/react-dom/src/client/ReactDOMFiberComponent.js b/packages/react-dom/src/client/ReactDOMFiberComponent.js index 0eabc364af4f8..4bda1da6bec14 100644 --- a/packages/react-dom/src/client/ReactDOMFiberComponent.js +++ b/packages/react-dom/src/client/ReactDOMFiberComponent.js @@ -21,12 +21,12 @@ import * as ReactDOMFiberTextarea from './ReactDOMFiberTextarea'; import * as inputValueTracking from './inputValueTracking'; import setInnerHTML from './setInnerHTML'; import setTextContent from './setTextContent'; -import {listenTo, trapBubbledEvent} from '../events/ReactBrowserEventEmitter'; +import {listenTo} from '../events/ReactBrowserEventEmitter'; import * as CSSPropertyOperations from '../shared/CSSPropertyOperations'; import {Namespaces, getIntrinsicNamespace} from '../shared/DOMNamespaces'; import {getPropertyInfo, shouldSetAttribute} from '../shared/DOMProperty'; import assertValidProps from '../shared/assertValidProps'; -import {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE} from '../shared/HTMLNodeType'; +import {DOCUMENT_NODE} from '../shared/HTMLNodeType'; import isCustomComponent from '../shared/isCustomComponent'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook'; @@ -188,16 +188,6 @@ if (__DEV__) { }; } -function ensureListeningTo(rootContainerElement, registrationName) { - var isDocumentOrFragment = - rootContainerElement.nodeType === DOCUMENT_NODE || - rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE; - var doc = isDocumentOrFragment - ? rootContainerElement - : rootContainerElement.ownerDocument; - listenTo(registrationName, doc); -} - function getOwnerDocumentFromRootContainer( rootContainerElement: Element | Document, ): Document { @@ -206,47 +196,6 @@ function getOwnerDocumentFromRootContainer( : rootContainerElement.ownerDocument; } -// There are so many media events, it makes sense to just -// maintain a list rather than create a `trapBubbledEvent` for each -var mediaEvents = { - topAbort: 'abort', - topCanPlay: 'canplay', - topCanPlayThrough: 'canplaythrough', - topDurationChange: 'durationchange', - topEmptied: 'emptied', - topEncrypted: 'encrypted', - topEnded: 'ended', - topError: 'error', - topLoadedData: 'loadeddata', - topLoadedMetadata: 'loadedmetadata', - topLoadStart: 'loadstart', - topPause: 'pause', - topPlay: 'play', - topPlaying: 'playing', - topProgress: 'progress', - topRateChange: 'ratechange', - topSeeked: 'seeked', - topSeeking: 'seeking', - topStalled: 'stalled', - topSuspend: 'suspend', - topTimeUpdate: 'timeupdate', - topVolumeChange: 'volumechange', - topWaiting: 'waiting', -}; - -function trapClickOnNonInteractiveElement(node: HTMLElement) { - // Mobile Safari does not fire properly bubble click events on - // 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. - // http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html - // Just set it using the onclick property so that we don't have to manage any - // bookkeeping for it. Not sure if we need to clear it when the listener is - // removed. - // TODO: Only do this for the relevant Safaris maybe? - node.onclick = emptyFunction; -} - function setInitialDOMProperties( tag: string, domElement: Element, @@ -300,7 +249,7 @@ function setInitialDOMProperties( if (__DEV__ && typeof nextProp !== 'function') { warnForInvalidEventListener(propKey, nextProp); } - ensureListeningTo(rootContainerElement, propKey); + listenTo(propKey, domElement, rootContainerElement); } } else if (isCustomComponentTag) { DOMPropertyOperations.setValueForAttribute(domElement, propKey, nextProp); @@ -455,47 +404,12 @@ export function setInitialProperties( // TODO: Make sure that we check isMounted before firing any of these events. var props: Object; switch (tag) { - case 'iframe': - case 'object': - trapBubbledEvent('topLoad', 'load', domElement); - props = rawProps; - break; - case 'video': - case 'audio': - // Create listener for each media event - for (var event in mediaEvents) { - if (mediaEvents.hasOwnProperty(event)) { - trapBubbledEvent(event, mediaEvents[event], domElement); - } - } - props = rawProps; - break; - case 'source': - trapBubbledEvent('topError', 'error', domElement); - props = rawProps; - break; - case 'img': - case 'image': - trapBubbledEvent('topError', 'error', domElement); - trapBubbledEvent('topLoad', 'load', domElement); - props = rawProps; - break; - case 'form': - trapBubbledEvent('topReset', 'reset', domElement); - trapBubbledEvent('topSubmit', 'submit', domElement); - props = rawProps; - break; - case 'details': - trapBubbledEvent('topToggle', 'toggle', domElement); - props = rawProps; - break; case 'input': ReactDOMFiberInput.initWrapperState(domElement, rawProps); props = ReactDOMFiberInput.getHostProps(domElement, rawProps); - trapBubbledEvent('topInvalid', 'invalid', domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. - ensureListeningTo(rootContainerElement, 'onChange'); + listenTo('onChange', domElement, rootContainerElement); break; case 'option': ReactDOMFiberOption.validateProps(domElement, rawProps); @@ -504,18 +418,16 @@ export function setInitialProperties( case 'select': ReactDOMFiberSelect.initWrapperState(domElement, rawProps); props = ReactDOMFiberSelect.getHostProps(domElement, rawProps); - trapBubbledEvent('topInvalid', 'invalid', domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. - ensureListeningTo(rootContainerElement, 'onChange'); + listenTo('onChange', domElement); break; case 'textarea': ReactDOMFiberTextarea.initWrapperState(domElement, rawProps); props = ReactDOMFiberTextarea.getHostProps(domElement, rawProps); - trapBubbledEvent('topInvalid', 'invalid', domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. - ensureListeningTo(rootContainerElement, 'onChange'); + listenTo('onChange', domElement); break; default: props = rawProps; @@ -551,10 +463,6 @@ export function setInitialProperties( ReactDOMFiberSelect.postMountWrapper(domElement, rawProps); break; default: - if (typeof props.onClick === 'function') { - // TODO: This cast may not be sound for SVG, MathML or custom elements. - trapClickOnNonInteractiveElement(((domElement: any): HTMLElement)); - } break; } } @@ -599,13 +507,6 @@ export function diffProperties( default: lastProps = lastRawProps; nextProps = nextRawProps; - if ( - typeof lastProps.onClick !== 'function' && - typeof nextProps.onClick === 'function' - ) { - // TODO: This cast may not be sound for SVG, MathML or custom elements. - trapClickOnNonInteractiveElement(((domElement: any): HTMLElement)); - } break; } @@ -736,7 +637,7 @@ export function diffProperties( if (__DEV__ && typeof nextProp !== 'function') { warnForInvalidEventListener(propKey, nextProp); } - ensureListeningTo(rootContainerElement, propKey); + listenTo(propKey, domElement, rootContainerElement); } if (!updatePayload && lastProp !== nextProp) { // This is a special case. If any listener updates we need to ensure @@ -823,57 +724,26 @@ export function diffHydratedProperties( // TODO: Make sure that we check isMounted before firing any of these events. switch (tag) { - case 'iframe': - case 'object': - trapBubbledEvent('topLoad', 'load', domElement); - break; - case 'video': - case 'audio': - // Create listener for each media event - for (var event in mediaEvents) { - if (mediaEvents.hasOwnProperty(event)) { - trapBubbledEvent(event, mediaEvents[event], domElement); - } - } - break; - case 'source': - trapBubbledEvent('topError', 'error', domElement); - break; - case 'img': - case 'image': - trapBubbledEvent('topError', 'error', domElement); - trapBubbledEvent('topLoad', 'load', domElement); - break; - case 'form': - trapBubbledEvent('topReset', 'reset', domElement); - trapBubbledEvent('topSubmit', 'submit', domElement); - break; - case 'details': - trapBubbledEvent('topToggle', 'toggle', domElement); - break; case 'input': ReactDOMFiberInput.initWrapperState(domElement, rawProps); - trapBubbledEvent('topInvalid', 'invalid', domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. - ensureListeningTo(rootContainerElement, 'onChange'); + listenTo('onChange', rootContainerElement); break; case 'option': ReactDOMFiberOption.validateProps(domElement, rawProps); break; case 'select': ReactDOMFiberSelect.initWrapperState(domElement, rawProps); - trapBubbledEvent('topInvalid', 'invalid', domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. - ensureListeningTo(rootContainerElement, 'onChange'); + listenTo('onChange', rootContainerElement); break; case 'textarea': ReactDOMFiberTextarea.initWrapperState(domElement, rawProps); - trapBubbledEvent('topInvalid', 'invalid', domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. - ensureListeningTo(rootContainerElement, 'onChange'); + listenTo('onChange', rootContainerElement); break; } @@ -940,7 +810,7 @@ export function diffHydratedProperties( if (__DEV__ && typeof nextProp !== 'function') { warnForInvalidEventListener(propKey, nextProp); } - ensureListeningTo(rootContainerElement, propKey); + listenTo(propKey, domElement); } } else if (__DEV__) { // Validate that the properties correspond to their expected values. @@ -1052,10 +922,6 @@ export function diffHydratedProperties( // TODO: Consider not doing this for input and textarea. break; default: - if (typeof rawProps.onClick === 'function') { - // TODO: This cast may not be sound for SVG, MathML or custom elements. - trapClickOnNonInteractiveElement(((domElement: any): HTMLElement)); - } break; } diff --git a/packages/react-dom/src/events/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js index f9a5ee52f41ff..c05eff2c95316 100644 --- a/packages/react-dom/src/events/ReactBrowserEventEmitter.js +++ b/packages/react-dom/src/events/ReactBrowserEventEmitter.js @@ -13,6 +13,7 @@ import { trapBubbledEvent, trapCapturedEvent, } from './ReactDOMEventListener'; +import {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE} from '../shared/HTMLNodeType'; import isEventSupported from './isEventSupported'; import BrowserEventConstants from './BrowserEventConstants'; @@ -20,6 +21,23 @@ export * from 'events/ReactEventEmitterMixin'; var {topLevelTypes} = BrowserEventConstants; +const forceDelegation = { + topMouseOver: 1, + topMouseOut: 2, + topMouseEnter: 3, + topMouseLeave: 4, +}; + +function documentForRoot(rootContainerElement, registrationName) { + var isDocumentOrFragment = + rootContainerElement.nodeType === DOCUMENT_NODE || + rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE; + + return isDocumentOrFragment + ? rootContainerElement + : rootContainerElement.ownerDocument; +} + /** * Summary of `ReactBrowserEventEmitter` event handling: * @@ -115,15 +133,22 @@ function getListeningForDocument(mountAt) { * @param {string} registrationName Name of listener (e.g. `onClick`). * @param {object} contentDocumentHandle Document which owns the container */ -export function listenTo(registrationName, contentDocumentHandle) { +export function listenTo(registrationName, contentDocumentHandle, root) { var mountAt = contentDocumentHandle; var isListening = getListeningForDocument(mountAt); var dependencies = registrationNameDependencies[registrationName]; for (var i = 0; i < dependencies.length; i++) { var dependency = dependencies[i]; + if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) { - if (dependency === 'topWheel') { + if (forceDelegation.hasOwnProperty(dependency)) { + trapBubbledEvent( + dependency, + topLevelTypes[dependency], + documentForRoot(root), + ); + } else if (dependency === 'topWheel') { if (isEventSupported('wheel')) { trapBubbledEvent('topWheel', 'wheel', mountAt); } else if (isEventSupported('mousewheel')) { diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index df136f67ec3e6..7e16c7f2b7d20 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -110,6 +110,18 @@ export function isEnabled() { return _enabled; } +let typeCache = {}; + +function getDispatcher(topLevelType) { + if (topLevelType in typeCache) { + return typeCache[topLevelType]; + } + + typeCache[topLevelType] = dispatchEvent.bind(null, topLevelType); + + return typeCache[topLevelType]; +} + /** * Traps top-level events by using event bubbling. * @@ -124,11 +136,8 @@ export function trapBubbledEvent(topLevelType, handlerBaseName, element) { if (!element) { return null; } - return EventListener.listen( - element, - handlerBaseName, - dispatchEvent.bind(null, topLevelType), - ); + + EventListener.listen(element, handlerBaseName, getDispatcher(topLevelType)); } /** @@ -145,11 +154,8 @@ export function trapCapturedEvent(topLevelType, handlerBaseName, element) { if (!element) { return null; } - return EventListener.capture( - element, - handlerBaseName, - dispatchEvent.bind(null, topLevelType), - ); + + EventListener.capture(element, handlerBaseName, getDispatcher(topLevelType)); } export function dispatchEvent(topLevelType, nativeEvent) { diff --git a/packages/react-dom/src/events/SelectEventPlugin.js b/packages/react-dom/src/events/SelectEventPlugin.js index cae7a53d31ec4..4d32136f251b8 100644 --- a/packages/react-dom/src/events/SelectEventPlugin.js +++ b/packages/react-dom/src/events/SelectEventPlugin.js @@ -15,7 +15,6 @@ import shallowEqual from 'fbjs/lib/shallowEqual'; import {isListeningToAllDependencies} from './ReactBrowserEventEmitter'; import {getNodeFromInstance} from '../client/ReactDOMComponentTree'; import * as ReactInputSelection from '../client/ReactInputSelection'; -import {DOCUMENT_NODE} from '../shared/HTMLNodeType'; var skipSelectionChangeEvent = ExecutionEnvironment.canUseDOM && @@ -140,15 +139,9 @@ var SelectEventPlugin = { nativeEvent, nativeEventTarget, ) { - var doc = - nativeEventTarget.window === nativeEventTarget - ? nativeEventTarget.document - : nativeEventTarget.nodeType === DOCUMENT_NODE - ? nativeEventTarget - : nativeEventTarget.ownerDocument; // Track whether all listeners exists for this plugin. If none exist, we do // not extract events. See #3639. - if (!doc || !isListeningToAllDependencies('onSelect', doc)) { + if (!isListeningToAllDependencies('onSelect', nativeEventTarget)) { return null; } diff --git a/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.js b/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.js index 9f08f605c9c8d..2582510bc12c6 100644 --- a/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.js +++ b/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.js @@ -165,32 +165,4 @@ describe('SimpleEventPlugin', function() { }); }); }); - - describe('iOS bubbling click fix', function() { - // See http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html - - it('does not add a local click to interactive elements', function() { - var container = document.createElement('div'); - - ReactDOM.render(