diff --git a/src/components/panel/demoPanelAnimations/index.html b/src/components/panel/demoPanelAnimations/index.html new file mode 100644 index 00000000000..5142dc17f16 --- /dev/null +++ b/src/components/panel/demoPanelAnimations/index.html @@ -0,0 +1,39 @@ +
+

Animations

+
+
+ OpenFrom: + + Button + Top/Left Corner + Bottom Center + +
+ +
+ CloseTo: + + Button + Top/Left Corner + Bottom Center + +
+ +
+ AnimationType: + + None + Slide + Scale + Fade + Custom + +
+
+ +
+ + Dialog + +
+
diff --git a/src/components/panel/demoPanelAnimations/panel.tmpl.html b/src/components/panel/demoPanelAnimations/panel.tmpl.html new file mode 100644 index 00000000000..9ef08394ca8 --- /dev/null +++ b/src/components/panel/demoPanelAnimations/panel.tmpl.html @@ -0,0 +1,26 @@ +
+ +
+

Surprise!

+
+
+ +
+

+ You hit the secret button. Here's a donut: +

+ +
+ Delicious donut +
+
+ +
+ + Nothing + + + Close + +
+
diff --git a/src/components/panel/demoPanelAnimations/script.js b/src/components/panel/demoPanelAnimations/script.js new file mode 100644 index 00000000000..3bebabdb607 --- /dev/null +++ b/src/components/panel/demoPanelAnimations/script.js @@ -0,0 +1,94 @@ +angular.module('panelAnimationsDemo', ['ngMaterial']) + .controller('AnimationCtrl', AnimationCtrl) + .controller('DialogCtrl', DialogCtrl); + + +function AnimationCtrl($mdPanel) { + this._mdPanel = $mdPanel; + this.openFrom = 'button'; + this.closeTo = 'button'; + this.animationType = 'none'; +} + + +AnimationCtrl.prototype.showDialog = function() { + var position = this._mdPanel.newPanelPosition() + .absolute() + .right() + .top(); + + var animation = this._mdPanel.newPanelAnimation(); + + switch(this.openFrom) { + case 'button': + animation.openFrom('.animation-target'); + break; + case 'corner': + animation.openFrom({top:0, left:0}); + break; + case 'bottom': + animation.openFrom({ + top: document.documentElement.clientHeight, + left: document.documentElement.clientWidth / 2 - 250 + }); + } + switch(this.closeTo) { + case 'button': + animation.closeTo('.animation-target'); + break; + case 'corner': + animation.closeTo({top:0, left:0}); + break; + case 'bottom': + animation.closeTo({ + top: document.documentElement.clientHeight, + left: document.documentElement.clientWidth / 2 - 250 + }); + } + switch(this.animationType) { + case 'custom': + animation.withAnimation({ + open: 'demo-dialog-custom-animation-open', + close: 'demo-dialog-custom-animation-close' + }); + break; + case 'slide': + animation.withAnimation(this._mdPanel.animation.SLIDE); + break; + case 'scale': + animation.withAnimation(this._mdPanel.animation.SCALE); + break; + case 'fade': + animation.withAnimation(this._mdPanel.animation.FADE); + break; + case 'none': + animation = undefined; + break; + } + + var config = { + animation: animation, + attachTo: angular.element(document.querySelector('.demo-md-panel-animation')), + controller: DialogCtrl, + controllerAs: 'ctrl', + templateUrl: 'panel.tmpl.html', + locals: { + closeFn: angular.bind(this, this.closeDialog) + }, + panelClass: 'demo-dialog-example', + position: position, + trapFocus: true, + zIndex: 150 + }; + + this._panelRef = this._mdPanel.open(config); +}; + + +AnimationCtrl.prototype.closeDialog = function() { + this._panelRef && this._panelRef.close(); +}; + + +// Necessary to pass locals to the dialog template. +function DialogCtrl() { } diff --git a/src/components/panel/demoPanelAnimations/style.css b/src/components/panel/demoPanelAnimations/style.css new file mode 100644 index 00000000000..451cd203fdc --- /dev/null +++ b/src/components/panel/demoPanelAnimations/style.css @@ -0,0 +1,36 @@ +.demo-md-panel { + min-height: 500px; +} + +.demo-dialog-example { + background: white; + border-radius: 4px; + box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2), + 0 13px 19px 2px rgba(0, 0, 0, 0.14), + 0 5px 24px 4px rgba(0, 0, 0, 0.12); + width: 500px; +} + +.demo-dialog-content { + padding: 0 15px; + width: 100%; +} + +.demo-dialog-content img { + height: 300px; + margin: auto; +} + +.demo-dialog-button { + width: 100%; +} + +.demo-dialog-custom-animation-open { + transition: all 1s linear, opacity 1ms; + transform: rotate(390deg); +} + +.demo-dialog-custom-animation-close { + transition: all 1s linear, opacity 1ms; + transform: rotate(0deg); +} diff --git a/src/components/panel/panel.js b/src/components/panel/panel.js index c1e613e3ca6..f0fe1b93d2b 100644 --- a/src/components/panel/panel.js +++ b/src/components/panel/panel.js @@ -486,7 +486,7 @@ angular * var panelAnimation = new MdPanelAnimation() * .openFrom(myButtonEl) * .closeTo('.my-button') - * .withAnimation(''); + * .withAnimation(MdPanelPosition.animation.SCALE); * * $mdPanel.create({ * animation: panelAnimation @@ -502,7 +502,7 @@ angular * is used to determine the bounds. When passed a click event, the location * of the click will be used as the position to start the animation. * - * @param {string|!Element|!Event|!{top: number, left: number}} + * @param {string|!Element|!Event|{top: number, left: number}} * @returns {MdPanelAnimation} */ @@ -514,11 +514,33 @@ angular * query selector, DOM element, or a Rect object that is used to determine * the bounds. * - * @param {string|!Element|!{top: number, left: number}} + * @param {string|!Element|{top: number, left: number}} + * @returns {MdPanelAnimation} + */ + +/** + * @ngdoc method + * @name MdPanelAnimation#withAnimation + * @description + * Specifies the animation class. + * + * There are several default animations that can be used: + * (MdPanelPosition.animation) + * SLIDE: The panel slides in and out from the specified + * elements. It will not fade in or out. + * SCALE: The panel scales in and out. Slide and fade are + * included in this animation. + * FADE: The panel fades in and out. + * + * Custom classes will by default fade in and out unless + * "transition: opacity 1ms" is added to the to custom class. + * + * @param {string|{open: string, close: string}} cssClass * @returns {MdPanelAnimation} */ + /***************************************************************************** * IMPLEMENTATION * *****************************************************************************/ @@ -562,6 +584,9 @@ function MdPanelService($rootElement, $rootScope, $injector) { /** @private {!angular.$injector} */ this._$injector = $injector; + + /** @type {enum} */ + this.animation = MdPanelAnimation.animation; } @@ -717,6 +742,9 @@ function MdPanelRef(config, $injector) { /** @private {!angular.JQLite|undefined} */ this._bottomFocusTrap; + + /** @private {!angular.$q.Promise|undefined} */ + this._reverseAnimation; } @@ -762,8 +790,8 @@ MdPanelRef.prototype.close = function() { return this._$q(function(resolve, reject) { self.hide().then(function () { self.detach().then(function() { - // TODO(ErinCoughlan) - Add destroy. This will make the code here different - // than just calling this.detach(). + // TODO(ErinCoughlan) - Add destroy. This will make the code here + // different than just calling this.detach(). resolve(self); }, reject); }, reject); @@ -815,7 +843,8 @@ MdPanelRef.prototype.detach = function() { self.hide().then(function() { self._removeEventListener(); - // Remove the focus traps that we added earlier for keeping focus within the panel. + // Remove the focus traps that we added earlier for keeping focus within + // the panel. if (self._topFocusTrap && self._topFocusTrap.parentNode) { self._topFocusTrap.parentNode.removeChild(self._topFocusTrap); } @@ -854,15 +883,11 @@ MdPanelRef.prototype.show = function() { var self = this; this._showPromise = this._$q(function(resolve, reject) { - try { - // TODO(KarenParker): Add show animation. - self.removeClass(MD_PANEL_HIDDEN); - // TODO(KarenParker): Chain this with animation when available. + self.removeClass(MD_PANEL_HIDDEN); + self._animateOpen().then(function() { self.focusOnOpen(); resolve(self); - } catch (e) { - reject(e.message); - } + }, reject); }); return this._showPromise; @@ -889,14 +914,10 @@ MdPanelRef.prototype.hide = function() { var self = this; this._hidePromise = this._$q(function(resolve, reject) { - try { - // TODO(KarenParker): Add hide animation. + self._animateClose().then(function() { self.addClass(MD_PANEL_HIDDEN); - // TODO(KarenParker): Chain this with animation when available. resolve(self); - } catch (e) { - reject(e.message); - } + }, reject); }); return this._hidePromise; @@ -953,7 +974,7 @@ MdPanelRef.prototype.toggleClass = function(toggleClass) { * Focuses on the panel or the first focus target. */ MdPanelRef.prototype.focusOnOpen = function() { - if (this._config.focusOnOpen) { + if (this._config['focusOnOpen']) { // Wait a digest to guarantee md-autofocus has finished adding the class // _md-autofocus, otherwise the focusable element isn't available to focus. var self = this; @@ -978,8 +999,9 @@ MdPanelRef.prototype._createPanel = function() { return this._$q(function(resolve, reject) { self._$mdCompiler.compile(self._config) .then(function(compileData) { - self._panelContainer = compileData.link(self._config.scope); - angular.element(self._config.attachTo).append(self._panelContainer); + self._panelContainer = compileData.link(self._config['scope']); + angular.element(self._config['attachTo']).append( + self._panelContainer); self._panelEl = angular.element( self._panelContainer[0].querySelector('.md-panel')); @@ -1047,7 +1069,7 @@ MdPanelRef.prototype._addEventListeners = function() { * @private */ MdPanelRef.prototype._removeEventListener = function() { - this._removeListeners.forEach(function(removeFn) { + this._removeListeners && this._removeListeners.forEach(function(removeFn) { removeFn(); }); this._removeListeners = null; @@ -1059,8 +1081,8 @@ MdPanelRef.prototype._removeEventListener = function() { * @private */ MdPanelRef.prototype._configureEscapeToClose = function() { - if (this._config.escapeToClose) { - var parentTarget = this._config.attachTo; + if (this._config['escapeToClose']) { + var parentTarget = this._config['attachTo']; var self = this; var keyHandlerFn = function (ev) { @@ -1090,7 +1112,7 @@ MdPanelRef.prototype._configureEscapeToClose = function() { * @private */ MdPanelRef.prototype._configureClickOutsideToClose = function() { - if (this._config.clickOutsideToClose) { + if (this._config['clickOutsideToClose']) { var target = this._panelContainer; var sourceElem; @@ -1136,7 +1158,7 @@ MdPanelRef.prototype._configureClickOutsideToClose = function() { MdPanelRef.prototype._configureTrapFocus = function() { // Focus doesn't remain instead of the panel without this. this._panelEl.attr('tabIndex', '-1'); - if (this._config.trapFocus) { + if (this._config['trapFocus']) { var element = this._panelEl; // Set up elements before and after the panel to capture focus and // redirect back into the panel. @@ -1160,6 +1182,46 @@ MdPanelRef.prototype._configureTrapFocus = function() { }; +/** + * Animate the panel opening. + * @returns {!angular.$q.Promise} + * @private + */ +MdPanelRef.prototype._animateOpen = function() { + this.addClass('md-panel-is-showing'); + var animationConfig = this._config['animation']; + if (!animationConfig) { + this.addClass('_md-panel-shown'); + return this._$q.resolve(); + } + + return animationConfig.animateOpen(this._panelEl, + this._$mdUtil.dom.animator); +}; + + +/** + * Animate the panel closing. + * @returns {!angular.$q.Promise} + * @private + */ +MdPanelRef.prototype._animateClose = function() { + var animationConfig = this._config['animation']; + if (!animationConfig) { + this.removeClass('md-panel-is-showing'); + this.removeClass('_md-panel-shown'); + return this._$q.resolve(); + } + + var self = this; + return this._$q(function(resolve, reject) { + animationConfig.animateClose(self._$q).then(function(){ + self.removeClass('md-panel-is-showing'); + resolve(self); + }, reject); + }); +}; + /***************************************************************************** * MdPanelPosition * *****************************************************************************/ @@ -1497,7 +1559,7 @@ MdPanelPosition.prototype._calculatePanelPosition = function(panelEl) { * var panelAnimation = new MdPanelAnimation() * .openFrom(myButtonEl) * .closeTo('.my-button') - * .withAnimation(''); + * .withAnimation($mdPanel.animation.SCALE); * * $mdPanel.create({ * animation: panelAnimation @@ -1506,29 +1568,34 @@ MdPanelPosition.prototype._calculatePanelPosition = function(panelEl) { * @final @constructor */ function MdPanelAnimation() { - /** @private {!{element: !angular.JQLite|undefined, bounds: !DOMRect}} */ - this._openFrom = {}; + /** + * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}| + * undefined} + */ + this._openFrom; - /** @private {!{element: !angular.JQLite|undefined, bounds: !DOMRect}} */ - this._closeTo = {}; -} + /** + * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}| + * undefined} + */ + this._closeTo; + /** @private {string|{open: string, close: string} */ + this._animationClass = ''; -/** - * Gets the boundingClientRect for the opening animation. - * @returns {!{element: !angular.JQLite|undefined, bounds: !DOMRect}} - */ -MdPanelAnimation.prototype.getOpenFrom = function() { - return this._openFrom; -}; + /** @private {!angular.$q.Promise|undefined} **/ + this._reverseAnimation; +} /** - * Gets the boundingClientRect for the closing animation. - * @returns {!{element: !angular.JQLite|undefined, bounds: !DOMRect}} + * Possible default animations. + * @enum {string} */ -MdPanelAnimation.prototype.getCloseTo = function() { - return this._closeTo; +MdPanelAnimation.animation = { + SLIDE: 'md-panel-animate-slide', + SCALE: 'md-panel-animate-scale', + FADE: 'md-panel-animate-fade' }; @@ -1545,7 +1612,7 @@ MdPanelAnimation.prototype.openFrom = function(openFrom) { // Check if 'openFrom' is an Event. openFrom = openFrom.target ? openFrom.target : openFrom; - this._openFrom = this.getPanelAnimationTarget(openFrom); + this._openFrom = this._getPanelAnimationTarget(openFrom); if (!this._closeTo) { this._closeTo = this._openFrom; @@ -1563,7 +1630,7 @@ MdPanelAnimation.prototype.openFrom = function(openFrom) { * @returns {MdPanelAnimation} */ MdPanelAnimation.prototype.closeTo = function(closeTo) { - this._closeTo = this.getPanelAnimationTarget(closeTo); + this._closeTo = this._getPanelAnimationTarget(closeTo); return this; }; @@ -1571,44 +1638,169 @@ MdPanelAnimation.prototype.closeTo = function(closeTo) { /** * Returns the element and bounds for the animation target. * @param {string|!Element|{top: number, left: number}} location - * @returns {!{element: !angular.JQLite|undefined, bounds: !DOMRect}} + * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}} + * @private */ -MdPanelAnimation.prototype.getPanelAnimationTarget = function(location) { +MdPanelAnimation.prototype._getPanelAnimationTarget = function(location) { if (angular.isDefined(location.top) || angular.isDefined(location.left)) { return { element: undefined, bounds: { top: location.top || 0, - left: location.left || 0, - height: 1, - width: 1 + left: location.left || 0 } }; } else { - return this._getBoundingClientRect(this._getElement(location)); + return this._getBoundingClientRect(getElement(location)); } -} +}; /** - * TODO(KarenParker): Switch and update the shared version of this method that - * others are currently building. - * Gets the element from the string|element. - * @param {string|!Element} el - * @returns {!angular.JQLite} + * Specifies the animation class. + * + * There are several default animations that can be used: + * (MdPanelAnimation.animation) + * SLIDE: The panel slides in and out from the specified + * elements. + * SCALE: The panel scales in and out. + * FADE: The panel fades in and out. + * + * @param {string|{open: string, close: string}} cssClass + * @returns {MdPanelAnimation} + */ + +MdPanelAnimation.prototype.withAnimation = function(cssClass) { + this._animationClass = cssClass; + return this; +}; + + +/** + * Animate the panel open. + * @param {!angular.JQLite} panelEl + * @param animator + * @returns {!angular.$q.Promise} + */ +MdPanelAnimation.prototype.animateOpen = function(panelEl, animator) { + this._fixBounds(panelEl); + var animationOptions = {}; + var reverseAnimationOptions = {}; + var openFrom = animator.toTransformCss(""); + var openTo = animator.toTransformCss(""); + var closeFrom = animator.toTransformCss(""); + var closeTo = animator.toTransformCss(""); + + switch (this._animationClass) { + case MdPanelAnimation.animation.SLIDE: + animationOptions = { + transitionInClass: '_md-panel-animate-slide-in _md-panel-shown', + transitionOutClass: '_md-panel-animate-slide-out' + }; + reverseAnimationOptions = { + transitionOutClass: '_md-panel-animate-slide-in _md-panel-shown', + transitionInClass: '_md-panel-animate-slide-out' + }; + openFrom = animator.toTransformCss(animator.calculateSlideToOrigin( + panelEl, this._openFrom) || ""); + closeTo = animator.toTransformCss(animator.calculateSlideToOrigin( + panelEl, this._closeTo)); + break; + case MdPanelAnimation.animation.SCALE: + animationOptions = { + transitionInClass: '_md-panel-animate-scale-in _md-panel-shown', + transitionOutClass: '_md-panel-animate-scale-out' + }; + reverseAnimationOptions = { + transitionOutClass: '_md-panel-animate-scale-in _md-panel-shown', + transitionInClass: '_md-panel-animate-scale-out' + }; + openFrom = animator.toTransformCss(animator.calculateZoomToOrigin( + panelEl, this._openFrom) || ""); + closeTo = animator.toTransformCss(animator.calculateZoomToOrigin( + panelEl, this._closeTo)); + break; + case MdPanelAnimation.animation.FADE: + animationOptions = { + transitionInClass: '_md-panel-animate-fade-in _md-panel-shown', + transitionOutClass: '_md-panel-animate-fade-out' + }; + reverseAnimationOptions = { + transitionOutClass: '_md-panel-animate-fade-in _md-panel-shown', + transitionInClass: '_md-panel-animate-fade-out' + }; + break; + default: + if (angular.isString(this._animationClass)) { + animationOptions = { + transitionInClass: this._animationClass + ' _md-panel-shown' + }; + } else { + animationOptions = { + transitionInClass: this._animationClass['open'] + ' _md-panel-shown' + }; + reverseAnimationOptions = { + transitionInClass: this._animationClass['close'] + }; + } + } + + var self = this; + return animator + .translate3d(panelEl, openFrom, openTo, animationOptions) + .then(function () { + self._reverseAnimation = function () { + return animator + .translate3d(panelEl, closeFrom, closeTo, + reverseAnimationOptions); + + }; + }); +}; + + +/** + * Animate the panel close. + * @param $q + * @returns {!angular.$q.Promise} + */ +MdPanelAnimation.prototype.animateClose = function($q) { + if (this._reverseAnimation) { + return this._reverseAnimation(); + } + return $q.reject('No panel close animation. ' + + 'Have you called MdPanelAnimation.animateOpen()?'); +}; + + +/** + * Set the height and width to match the panel if not provided. + * @param {!angular.JQLite} panelEl * @private */ -MdPanelAnimation.prototype._getElement = function(el) { - var queryResult = angular.isString(el) ? - document.querySelector(el) : el; - return angular.element(queryResult); +MdPanelAnimation.prototype._fixBounds = function(panelEl) { + var panelWidth = panelEl[0].offsetWidth; + var panelHeight = panelEl[0].offsetHeight; + + if (this._openFrom.bounds.height == null) { + this._openFrom.bounds.height = panelHeight; + } + if (this._openFrom.bounds.width == null) { + this._openFrom.bounds.width = panelWidth; + } + if (this._closeTo.bounds.height == null) { + this._closeTo.bounds.height = panelHeight; + } + if (this._closeTo.bounds.width == null) { + this._closeTo.bounds.width = panelWidth; + } }; /** * Identify the bounding RECT for the target element. * @param {!angular.JQLite} element - * @returns {!{element: !angular.JQLite|undefined, bounds: !DOMRect}} + * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}} * @private */ MdPanelAnimation.prototype._getBoundingClientRect = function(element) { diff --git a/src/components/panel/panel.scss b/src/components/panel/panel.scss index f5891838b8b..3e13ebc8ead 100644 --- a/src/components/panel/panel.scss +++ b/src/components/panel/panel.scss @@ -6,11 +6,7 @@ width: 100%; } -.md-panel { - overflow: auto; -} - -.md-panel-hidden { +._md-panel-hidden { display: none; } @@ -22,3 +18,51 @@ position: fixed; top: 0; } + +// Only used when no animations are present. +._md-panel-shown .md-panel { + opacity: 1; + transition: none; +} + +.md-panel { + opacity: 0; + overflow: auto; + + &._md-panel-shown { + // Only used when custom animations are present. + // Overridden by the default animations. + opacity: 1; + transition: none; + + &._md-panel-animate-slide-in { + opacity: 1; + transition: $swift-ease-out, opacity 1ms; + } + + &._md-panel-animate-scale-in { + opacity: 1; + transition: $swift-ease-out; + } + + &._md-panel-animate-fade-in { + opacity: 1; + transition: all 0.4s linear; + } + } + + &._md-panel-animate-slide-out { + opacity: 1; + transition: $swift-ease-out, opacity 1ms; + } + + &._md-panel-animate-scale-out { + opacity: 0; + transition: $swift-ease-out; + } + + &._md-panel-animate-fade-out { + opacity: 0; + transition: all 0.4s linear; + } +} diff --git a/src/components/panel/panel.spec.js b/src/components/panel/panel.spec.js index 3110dc42dce..3da1c67b672 100644 --- a/src/components/panel/panel.spec.js +++ b/src/components/panel/panel.spec.js @@ -1,5 +1,5 @@ describe('$mdPanel', function() { - var $mdPanel, $rootScope, $rootEl, $templateCache, $q; + var $mdPanel, $rootScope, $rootEl, $templateCache, $q, $material; var panelRef; var attachedElements = []; var PANEL_WRAPPER_CLASS = '.md-panel-outer-wrapper'; @@ -20,6 +20,7 @@ describe('$mdPanel', function() { $rootEl = $injector.get('$rootElement'); $templateCache = $injector.get('$templateCache'); $q = $injector.get('$q'); + $material = $injector.get('$material'); }; beforeEach(function() { @@ -937,27 +938,55 @@ describe('$mdPanel', function() { myButton = angular.element(document.querySelector('button')); }); + it('should animate with default slide', function() { + var config = DEFAULT_CONFIG; + config['animation'] = mdPanelAnimation.openFrom('button') + .withAnimation('md-panel-animate-slide'); + + openPanel(); + // If animation dies, panel doesn't unhide. + expect(panelRef._panelContainer).not.toHaveClass(HIDDEN_CLASS); + + closePanel(); + // If animation dies, panel doesn't hide. + expect(panelRef._panelContainer).toHaveClass(HIDDEN_CLASS); + }); + + it('should animate with custom class', function() { + var config = DEFAULT_CONFIG; + config['animation'] = mdPanelAnimation.openFrom('button') + .withAnimation('myClass'); + + openPanel(); + // If animation dies, panel doesn't unhide. + expect(panelRef._panelContainer).not.toHaveClass(HIDDEN_CLASS); + + closePanel(); + // If animation dies, panel doesn't hide. + expect(panelRef._panelContainer).toHaveClass(HIDDEN_CLASS); + }); + describe('should determine openFrom when', function() { it('provided a selector', function() { var animation = mdPanelAnimation.openFrom('button'); - expect(animation.getOpenFrom().element[0]).toEqual(myButton[0]); - expect(animation.getOpenFrom().bounds).toEqual(myButton[0].getBoundingClientRect()); + expect(animation._openFrom.element[0]).toEqual(myButton[0]); + expect(animation._openFrom.bounds).toEqual(myButton[0].getBoundingClientRect()); }); it('provided an element', function() { var animation = mdPanelAnimation.openFrom(myButton[0]); - expect(animation.getOpenFrom().element[0]).toEqual(myButton[0]); - expect(animation.getOpenFrom().bounds).toEqual(myButton[0].getBoundingClientRect()); + expect(animation._openFrom.element[0]).toEqual(myButton[0]); + expect(animation._openFrom.bounds).toEqual(myButton[0].getBoundingClientRect()); }); it('provided an event', function() { var myEvent = { type: 'click', target: myButton}; var animation = mdPanelAnimation.openFrom(myEvent); - expect(animation.getOpenFrom().element[0]).toEqual(myButton[0]); - expect(animation.getOpenFrom().bounds).toEqual(myButton[0].getBoundingClientRect()); + expect(animation._openFrom.element[0]).toEqual(myButton[0]); + expect(animation._openFrom.bounds).toEqual(myButton[0].getBoundingClientRect()); }); @@ -966,8 +995,8 @@ describe('$mdPanel', function() { var inputRect = {top: rect.top, left: rect.left}; var animation = mdPanelAnimation.openFrom(inputRect); - expect(animation.getOpenFrom().element).toBeUndefined(); - expect(animation.getOpenFrom().bounds).toEqual(angular.extend(inputRect, {height: 1, width: 1})); + expect(animation._openFrom.element).toBeUndefined(); + expect(animation._openFrom.bounds).toEqual(inputRect); }); }); @@ -975,15 +1004,15 @@ describe('$mdPanel', function() { it('provided a selector', function() { var animation = mdPanelAnimation.closeTo('button'); - expect(animation.getCloseTo().element).toEqual(myButton); - expect(animation.getCloseTo().bounds).toEqual(myButton[0].getBoundingClientRect()); + expect(animation._closeTo.element).toEqual(myButton); + expect(animation._closeTo.bounds).toEqual(myButton[0].getBoundingClientRect()); }); it('provided an element', function() { var animation = mdPanelAnimation.closeTo(myButton[0]); - expect(animation.getCloseTo().element).toEqual(myButton); - expect(animation.getCloseTo().bounds).toEqual(myButton[0].getBoundingClientRect()); + expect(animation._closeTo.element).toEqual(myButton); + expect(animation._closeTo.bounds).toEqual(myButton[0].getBoundingClientRect()); }); it('provided a bounding rect', function() { @@ -991,8 +1020,8 @@ describe('$mdPanel', function() { var inputRect = {top: rect.top, left: rect.left}; var animation = mdPanelAnimation.closeTo(inputRect); - expect(animation.getCloseTo().element).toBeUndefined(); - expect(animation.getCloseTo().bounds).toEqual(angular.extend(inputRect, {height: 1, width: 1})); + expect(animation._closeTo.element).toBeUndefined(); + expect(animation._closeTo.bounds).toEqual(inputRect); }); }); }); @@ -1027,6 +1056,7 @@ describe('$mdPanel', function() { // isn't always necessary, but is better to have here twice than sprinkled // through the tests. $rootScope.$apply(); + $material.flushOutstandingAnimations(); } /** @@ -1035,15 +1065,18 @@ describe('$mdPanel', function() { function closePanel() { panelRef && panelRef.close(); $rootScope.$apply(); + $material.flushOutstandingAnimations(); } function showPanel() { panelRef && panelRef.show(HIDDEN_CLASS); $rootScope.$apply(); + $material.flushOutstandingAnimations(); } function hidePanel() { panelRef && panelRef.hide(HIDDEN_CLASS); $rootScope.$apply(); + $material.flushOutstandingAnimations(); } }); diff --git a/src/core/util/animation/animate.js b/src/core/util/animation/animate.js index 833d65b9509..63578d7197c 100644 --- a/src/core/util/animation/animate.js +++ b/src/core/util/animation/animate.js @@ -24,7 +24,8 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { return $animateCss(target,{ from:from, to:to, - addClass:options.transitionInClass + addClass:options.transitionInClass, + removeClass:options.transitionOutClass }) .start() .then(function(){ @@ -43,73 +44,57 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { }).start(); } - }, + }, /** * Listen for transitionEnd event (with optional timeout) * Announce completion or failure via promise handlers */ waitTransitionEnd: function (element, opts) { - var TIMEOUT = 3000; // fallback is 3 secs + var TIMEOUT = 3000; // fallback is 3 secs - return $q(function(resolve, reject){ - opts = opts || { }; + return $q(function(resolve, reject){ + opts = opts || { }; - var timer = $timeout(finished, opts.timeout || TIMEOUT); - element.on($mdConstant.CSS.TRANSITIONEND, finished); + var timer = $timeout(finished, opts.timeout || TIMEOUT); + element.on($mdConstant.CSS.TRANSITIONEND, finished); - /** - * Upon timeout or transitionEnd, reject or resolve (respectively) this promise. - * NOTE: Make sure this transitionEnd didn't bubble up from a child - */ - function finished(ev) { - if ( ev && ev.target !== element[0]) return; + /** + * Upon timeout or transitionEnd, reject or resolve (respectively) this promise. + * NOTE: Make sure this transitionEnd didn't bubble up from a child + */ + function finished(ev) { + if ( ev && ev.target !== element[0]) return; - if ( ev ) $timeout.cancel(timer); - element.off($mdConstant.CSS.TRANSITIONEND, finished); + if ( ev ) $timeout.cancel(timer); + element.off($mdConstant.CSS.TRANSITIONEND, finished); - // Never reject since ngAnimate may cause timeouts due missed transitionEnd events - resolve(); + // Never reject since ngAnimate may cause timeouts due missed transitionEnd events + resolve(); - } + } - }); - }, + }); + }, - /** - * Calculate the zoom transform from dialog to origin. - * - * We use this to set the dialog position immediately; - * then the md-transition-in actually translates back to - * `translate3d(0,0,0) scale(1.0)`... - * - * NOTE: all values are rounded to the nearest integer - */ - calculateZoomToOrigin: function (element, originator) { + calculateTransformValues: function (element, originator) { var origin = originator.element; var bounds = originator.bounds; - var zoomTemplate = "translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )"; - var buildZoom = angular.bind(null, $mdUtil.supplant, zoomTemplate); - var zoomStyle = buildZoom({centerX: 0, centerY: 0, scaleX: 0.5, scaleY: 0.5}); - if (origin || bounds) { var originBnds = origin ? self.clientRect(origin) || currentBounds() : self.copyRect(bounds); var dialogRect = self.copyRect(element[0].getBoundingClientRect()); var dialogCenterPt = self.centerPointFor(dialogRect); var originCenterPt = self.centerPointFor(originBnds); - // Build the transform to zoom from the dialog center to the origin center - - zoomStyle = buildZoom({ + return { centerX: originCenterPt.x - dialogCenterPt.x, centerY: originCenterPt.y - dialogCenterPt.y, - scaleX: Math.round(100 * Math.min(0.5, originBnds.width / dialogRect.width))/100, - scaleY: Math.round(100 * Math.min(0.5, originBnds.height / dialogRect.height))/100 - }); + scaleX: Math.round(100 * Math.min(0.5, originBnds.width / dialogRect.width)) / 100, + scaleY: Math.round(100 * Math.min(0.5, originBnds.height / dialogRect.height)) / 100 + }; } - - return zoomStyle; + return {centerX: 0, centerY: 0, scaleX: 0.5, scaleY: 0.5}; /** * This is a fallback if the origin information is no longer valid, then the @@ -123,6 +108,33 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { } }, + /** + * Calculate the zoom transform from dialog to origin. + * + * We use this to set the dialog position immediately; + * then the md-transition-in actually translates back to + * `translate3d(0,0,0) scale(1.0)`... + * + * NOTE: all values are rounded to the nearest integer + */ + calculateZoomToOrigin: function (element, originator) { + var zoomTemplate = "translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )"; + var buildZoom = angular.bind(null, $mdUtil.supplant, zoomTemplate); + + return buildZoom(self.calculateTransformValues(element, originator)); + }, + + /** + * Calculate the slide transform from panel to origin. + * NOTE: all values are rounded to the nearest integer + */ + calculateSlideToOrigin: function (element, originator) { + var slideTemplate = "translate3d( {centerX}px, {centerY}px, 0 )"; + var buildSlide = angular.bind(null, $mdUtil.supplant, slideTemplate); + + return buildSlide(self.calculateTransformValues(element, originator)); + }, + /** * Enhance raw values to represent valid css stylings... */