From 260353e977848c202e267ba2b9a328075650f52b Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Mon, 8 Feb 2016 21:31:45 -0800 Subject: [PATCH] Added support for synthetic animation/transition events. --- docs/docs/ref-05-events.md | 37 +++++++ .../transitions/ReactTransitionEvents.js | 50 ++------- .../dom/client/ReactBrowserEventEmitter.js | 5 + .../client/eventPlugins/SimpleEventPlugin.js | 38 +++++++ .../SyntheticAnimationEvent.js | 47 ++++++++ .../SyntheticTransitionEvent.js | 47 ++++++++ .../utils/getVendorPrefixedEventName.js | 102 ++++++++++++++++++ src/renderers/shared/event/EventConstants.js | 4 + 8 files changed, 287 insertions(+), 43 deletions(-) create mode 100644 src/renderers/dom/client/syntheticEvents/SyntheticAnimationEvent.js create mode 100644 src/renderers/dom/client/syntheticEvents/SyntheticTransitionEvent.js create mode 100644 src/renderers/dom/client/utils/getVendorPrefixedEventName.js diff --git a/docs/docs/ref-05-events.md b/docs/docs/ref-05-events.md index a751405afebb6..93ad7fe009a40 100644 --- a/docs/docs/ref-05-events.md +++ b/docs/docs/ref-05-events.md @@ -139,6 +139,7 @@ DOMEventTarget relatedTarget These focus events work on all elements in the React DOM, not just form elements. + ### Form Events Event names: @@ -246,6 +247,7 @@ number deltaY number deltaZ ``` + ### Media Events Event names: @@ -254,6 +256,7 @@ Event names: onAbort onCanPlay onCanPlayThrough onDurationChange onEmptied onEncrypted onEnded onError onLoadedData onLoadedMetadata onLoadStart onPause onPlay onPlaying onProgress onRateChange onSeeked onSeeking onStalled onSuspend onTimeUpdate onVolumeChange onWaiting ``` + ### Image Events Event names: @@ -261,3 +264,37 @@ Event names: ``` onLoad onError ``` + + +### Animation Events + +Event names: + +``` +onAnimationStart onAnimationEnd onAnimationIteration +``` + +Properties: + +```javascript +string animationName +string pseudoElement +float elapsedTime +``` + + +### Transition Events + +Event names: + +``` +onTransitionEnd +``` + +Properties: + +```javascript +string propertyName +string pseudoElement +float elapsedTime +``` diff --git a/src/addons/transitions/ReactTransitionEvents.js b/src/addons/transitions/ReactTransitionEvents.js index 49804670c4575..64e8e440625ef 100644 --- a/src/addons/transitions/ReactTransitionEvents.js +++ b/src/addons/transitions/ReactTransitionEvents.js @@ -13,56 +13,20 @@ var ExecutionEnvironment = require('ExecutionEnvironment'); -/** - * EVENT_NAME_MAP is used to determine which event fired when a - * transition/animation ends, based on the style property used to - * define that event. - */ -var EVENT_NAME_MAP = { - transitionend: { - 'transition': 'transitionend', - 'WebkitTransition': 'webkitTransitionEnd', - 'MozTransition': 'mozTransitionEnd', - 'OTransition': 'oTransitionEnd', - 'msTransition': 'MSTransitionEnd', - }, - - animationend: { - 'animation': 'animationend', - 'WebkitAnimation': 'webkitAnimationEnd', - 'MozAnimation': 'mozAnimationEnd', - 'OAnimation': 'oAnimationEnd', - 'msAnimation': 'MSAnimationEnd', - }, -}; +var getVendorPrefixedEventName = require('getVendorPrefixedEventName'); var endEvents = []; function detectEvents() { - var testEl = document.createElement('div'); - var style = testEl.style; - - // On some platforms, in particular some releases of Android 4.x, - // the un-prefixed "animation" and "transition" properties are defined on the - // style object but the events that fire will still be prefixed, so we need - // to check if the un-prefixed events are useable, and if not remove them - // from the map - if (!('AnimationEvent' in window)) { - delete EVENT_NAME_MAP.animationend.animation; - } + var animEnd = getVendorPrefixedEventName('animationend'); + var transEnd = getVendorPrefixedEventName('transitionend'); - if (!('TransitionEvent' in window)) { - delete EVENT_NAME_MAP.transitionend.transition; + if (animEnd) { + endEvents.push(animEnd); } - for (var baseEventName in EVENT_NAME_MAP) { - var baseEvents = EVENT_NAME_MAP[baseEventName]; - for (var styleName in baseEvents) { - if (styleName in style) { - endEvents.push(baseEvents[styleName]); - break; - } - } + if (transEnd) { + endEvents.push(transEnd); } } diff --git a/src/renderers/dom/client/ReactBrowserEventEmitter.js b/src/renderers/dom/client/ReactBrowserEventEmitter.js index 7f203aace9c6c..5f540dd6d68e8 100644 --- a/src/renderers/dom/client/ReactBrowserEventEmitter.js +++ b/src/renderers/dom/client/ReactBrowserEventEmitter.js @@ -17,6 +17,7 @@ var ReactEventEmitterMixin = require('ReactEventEmitterMixin'); var ViewportMetrics = require('ViewportMetrics'); var assign = require('Object.assign'); +var getVendorPrefixedEventName = require('getVendorPrefixedEventName'); var isEventSupported = require('isEventSupported'); /** @@ -83,6 +84,9 @@ var reactTopListenersCounter = 0; // events so we don't include them here var topEventMapping = { topAbort: 'abort', + topAnimationEnd: getVendorPrefixedEventName('animationend') || 'animationend', + topAnimationIteration: getVendorPrefixedEventName('animationiteration') || 'animationiteration', + topAnimationStart: getVendorPrefixedEventName('animationstart') || 'animationstart', topBlur: 'blur', topCanPlay: 'canplay', topCanPlayThrough: 'canplaythrough', @@ -139,6 +143,7 @@ var topEventMapping = { topTouchEnd: 'touchend', topTouchMove: 'touchmove', topTouchStart: 'touchstart', + topTransitionEnd: getVendorPrefixedEventName('transitionend') || 'transitionend', topVolumeChange: 'volumechange', topWaiting: 'waiting', topWheel: 'wheel', diff --git a/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js b/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js index 95072d607e866..f790e3fe9b442 100644 --- a/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js @@ -15,6 +15,7 @@ var EventConstants = require('EventConstants'); var EventListener = require('EventListener'); var EventPropagators = require('EventPropagators'); var ReactDOMComponentTree = require('ReactDOMComponentTree'); +var SyntheticAnimationEvent = require('SyntheticAnimationEvent'); var SyntheticClipboardEvent = require('SyntheticClipboardEvent'); var SyntheticEvent = require('SyntheticEvent'); var SyntheticFocusEvent = require('SyntheticFocusEvent'); @@ -22,6 +23,7 @@ var SyntheticKeyboardEvent = require('SyntheticKeyboardEvent'); var SyntheticMouseEvent = require('SyntheticMouseEvent'); var SyntheticDragEvent = require('SyntheticDragEvent'); var SyntheticTouchEvent = require('SyntheticTouchEvent'); +var SyntheticTransitionEvent = require('SyntheticTransitionEvent'); var SyntheticUIEvent = require('SyntheticUIEvent'); var SyntheticWheelEvent = require('SyntheticWheelEvent'); @@ -39,6 +41,24 @@ var eventTypes = { captured: keyOf({onAbortCapture: true}), }, }, + animationEnd: { + phasedRegistrationNames: { + bubbled: keyOf({onAnimationEnd: true}), + captured: keyOf({onAnimationEndCapture: true}), + }, + }, + animationIteration: { + phasedRegistrationNames: { + bubbled: keyOf({onAnimationIteration: true}), + captured: keyOf({onAnimationIterationCapture: true}), + }, + }, + animationStart: { + phasedRegistrationNames: { + bubbled: keyOf({onAnimationStart: true}), + captured: keyOf({onAnimationStartCapture: true}), + }, + }, blur: { phasedRegistrationNames: { bubbled: keyOf({onBlur: true}), @@ -365,6 +385,12 @@ var eventTypes = { captured: keyOf({onTouchStartCapture: true}), }, }, + transitionEnd: { + phasedRegistrationNames: { + bubbled: keyOf({onTransitionEnd: true}), + captured: keyOf({onTransitionEndCapture: true}), + }, + }, volumeChange: { phasedRegistrationNames: { bubbled: keyOf({onVolumeChange: true}), @@ -387,6 +413,9 @@ var eventTypes = { var topLevelEventsToDispatchConfig = { topAbort: eventTypes.abort, + topAnimationEnd: eventTypes.animationEnd, + topAnimationIteration: eventTypes.animationIteration, + topAnimationStart: eventTypes.animationStart, topBlur: eventTypes.blur, topCanPlay: eventTypes.canPlay, topCanPlayThrough: eventTypes.canPlayThrough, @@ -441,6 +470,7 @@ var topLevelEventsToDispatchConfig = { topTouchEnd: eventTypes.touchEnd, topTouchMove: eventTypes.touchMove, topTouchStart: eventTypes.touchStart, + topTransitionEnd: eventTypes.transitionEnd, topVolumeChange: eventTypes.volumeChange, topWaiting: eventTypes.waiting, topWheel: eventTypes.wheel, @@ -549,6 +579,14 @@ var SimpleEventPlugin = { case topLevelTypes.topTouchStart: EventConstructor = SyntheticTouchEvent; break; + case topLevelTypes.topAnimationEnd: + case topLevelTypes.topAnimationIteration: + case topLevelTypes.topAnimationStart: + EventConstructor = SyntheticAnimationEvent; + break; + case topLevelTypes.topTransitionEnd: + EventConstructor = SyntheticTransitionEvent; + break; case topLevelTypes.topScroll: EventConstructor = SyntheticUIEvent; break; diff --git a/src/renderers/dom/client/syntheticEvents/SyntheticAnimationEvent.js b/src/renderers/dom/client/syntheticEvents/SyntheticAnimationEvent.js new file mode 100644 index 0000000000000..17b6f368b1799 --- /dev/null +++ b/src/renderers/dom/client/syntheticEvents/SyntheticAnimationEvent.js @@ -0,0 +1,47 @@ +/** + * 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 SyntheticAnimationEvent + */ + +'use strict'; + +var SyntheticEvent = require('SyntheticEvent'); + +/** + * @interface Event + * @see http://www.w3.org/TR/css3-animations/#AnimationEvent-interface + * @see https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent + */ +var AnimationEventInterface = { + animationName: null, + elapsedTime: null, + pseudoElement: null, +}; + +/** + * @param {object} dispatchConfig Configuration used to dispatch this event. + * @param {string} dispatchMarker Marker identifying the event target. + * @param {object} nativeEvent Native browser event. + * @extends {SyntheticEvent} + */ +function SyntheticAnimationEvent( + dispatchConfig, + dispatchMarker, + nativeEvent, + nativeEventTarget +) { + return SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget); +} + +SyntheticEvent.augmentClass( + SyntheticAnimationEvent, + AnimationEventInterface +); + +module.exports = SyntheticAnimationEvent; diff --git a/src/renderers/dom/client/syntheticEvents/SyntheticTransitionEvent.js b/src/renderers/dom/client/syntheticEvents/SyntheticTransitionEvent.js new file mode 100644 index 0000000000000..982410f786971 --- /dev/null +++ b/src/renderers/dom/client/syntheticEvents/SyntheticTransitionEvent.js @@ -0,0 +1,47 @@ +/** + * 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 SyntheticTransitionEvent + */ + +'use strict'; + +var SyntheticEvent = require('SyntheticEvent'); + +/** + * @interface Event + * @see http://www.w3.org/TR/2009/WD-css3-transitions-20090320/#transition-events- + * @see https://developer.mozilla.org/en-US/docs/Web/API/TransitionEvent + */ +var TransitionEventInterface = { + propertyName: null, + elapsedTime: null, + pseudoElement: null, +}; + +/** + * @param {object} dispatchConfig Configuration used to dispatch this event. + * @param {string} dispatchMarker Marker identifying the event target. + * @param {object} nativeEvent Native browser event. + * @extends {SyntheticEvent} + */ +function SyntheticTransitionEvent( + dispatchConfig, + dispatchMarker, + nativeEvent, + nativeEventTarget +) { + return SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget); +} + +SyntheticEvent.augmentClass( + SyntheticTransitionEvent, + TransitionEventInterface +); + +module.exports = SyntheticTransitionEvent; diff --git a/src/renderers/dom/client/utils/getVendorPrefixedEventName.js b/src/renderers/dom/client/utils/getVendorPrefixedEventName.js new file mode 100644 index 0000000000000..25fa2aed8a5f4 --- /dev/null +++ b/src/renderers/dom/client/utils/getVendorPrefixedEventName.js @@ -0,0 +1,102 @@ +/** + * 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 getVendorPrefixedEventName + */ + +'use strict'; + +var ExecutionEnvironment = require('ExecutionEnvironment'); + +/** + * Generate a mapping of standard vendor prefixes using the defined style property and event name. + * + * @param {string} styleProp + * @param {string} eventName + * @returns {object} + */ +function makePrefixMap(styleProp, eventName) { + var prefixes = {}; + + prefixes[styleProp.toLowerCase()] = eventName.toLowerCase(); + prefixes['Webkit' + styleProp] = 'webkit' + eventName; + prefixes['Moz' + styleProp] = 'moz' + eventName; + prefixes['ms' + styleProp] = 'MS' + eventName; + prefixes['O' + styleProp] = 'o' + eventName.toLowerCase(); + + return prefixes; +} + +/** + * A list of event names to a configurable list of vendor prefixes. + */ +var vendorPrefixes = { + animationend: makePrefixMap('Animation', 'AnimationEnd'), + animationiteration: makePrefixMap('Animation', 'AnimationIteration'), + animationstart: makePrefixMap('Animation', 'AnimationStart'), + transitionend: makePrefixMap('Transition', 'TransitionEnd'), +}; + +/** + * Event names that have already been detected and prefixed (if applicable). + */ +var prefixedEventNames = {}; + +/** + * Element to check for prefixes on. + */ +var style = {}; + +/** + * Bootstrap if a DOM exists. + */ +if (ExecutionEnvironment.canUseDOM) { + style = document.createElement('div').style; + + // On some platforms, in particular some releases of Android 4.x, + // the un-prefixed "animation" and "transition" properties are defined on the + // style object but the events that fire will still be prefixed, so we need + // to check if the un-prefixed events are usable, and if not remove them from the map. + if (!('AnimationEvent' in window)) { + delete vendorPrefixes.animationend.animation; + delete vendorPrefixes.animationiteration.animation; + delete vendorPrefixes.animationstart.animation; + } + + // Same as above + if (!('TransitionEvent' in window)) { + delete vendorPrefixes.transitionend.transition; + } +} + +/** + * Attempts to determine the correct vendor prefixed event name. + * + * @param {string} eventName + * @returns {string} + */ +function getVendorPrefixedEventName(eventName) { + if (prefixedEventNames[eventName]) { + return prefixedEventNames[eventName]; + + } else if (!vendorPrefixes[eventName]) { + return eventName; + } + + var prefixMap = vendorPrefixes[eventName]; + + for (var styleProp in prefixMap) { + if (prefixMap.hasOwnProperty(styleProp) && styleProp in style) { + return prefixedEventNames[eventName] = prefixMap[styleProp]; + } + } + + return ''; +} + +module.exports = getVendorPrefixedEventName; diff --git a/src/renderers/shared/event/EventConstants.js b/src/renderers/shared/event/EventConstants.js index 5b1f9ca63af16..84a9e9d486ff2 100644 --- a/src/renderers/shared/event/EventConstants.js +++ b/src/renderers/shared/event/EventConstants.js @@ -20,6 +20,9 @@ var PropagationPhases = keyMirror({bubbled: null, captured: null}); */ var topLevelTypes = keyMirror({ topAbort: null, + topAnimationEnd: null, + topAnimationIteration: null, + topAnimationStart: null, topBlur: null, topCanPlay: null, topCanPlayThrough: null, @@ -80,6 +83,7 @@ var topLevelTypes = keyMirror({ topTouchEnd: null, topTouchMove: null, topTouchStart: null, + topTransitionEnd: null, topVolumeChange: null, topWaiting: null, topWheel: null,