diff --git a/src/components/dialog/dialog.js b/src/components/dialog/dialog.js index 10bac0de38f..bbfe50909e7 100644 --- a/src/components/dialog/dialog.js +++ b/src/components/dialog/dialog.js @@ -481,6 +481,13 @@ function MdDialogProvider($$interimElementProvider) { options.restoreScroll = $mdUtil.disableScrollAround(element); } + if (options.popInTarget && options.popInTarget.length) { + // Compute and save the target element's bounding rect, so that if the + // element is hidden when the dialog closes, we can shrink the dialog + // back to the same position it expanded from. + options.popInTargetRect = options.popInTarget[0].getBoundingClientRect(); + } + return dialogPopIn( element, options.parent, @@ -551,7 +558,8 @@ function MdDialogProvider($$interimElementProvider) { return dialogPopOut( element, options.parent, - options.popInTarget && options.popInTarget.length && options.popInTarget + options.popInTarget && options.popInTarget.length && options.popInTarget, + options.popInTargetRect ).then(function() { element.remove(); options.popInTarget && options.popInTarget.focus(); @@ -644,18 +652,27 @@ function MdDialogProvider($$interimElementProvider) { return $mdUtil.transitionEndPromise(dialogEl); } - function dialogPopOut(container, parentElement, clickElement) { + function dialogPopOut(container, parentElement, clickElement, initialClickRect) { var dialogEl = container.find('md-dialog'); dialogEl.addClass('transition-out').removeClass('transition-in'); - transformToClickElement(dialogEl, clickElement); + transformToClickElement(dialogEl, clickElement, initialClickRect); return $mdUtil.transitionEndPromise(dialogEl); } - function transformToClickElement(dialogEl, clickElement) { + function isPositiveSizeClientRect(rect) { + return (rect && rect.width > 0 && rect.height > 0); + } + + function transformToClickElement(dialogEl, clickElement, initialClickRect) { if (clickElement) { var clickRect = clickElement[0].getBoundingClientRect(); + // If the event target element has zero size, it has probably been hidden. + // Use its initial position if available. + if (!isPositiveSizeClientRect(clickRect) && isPositiveSizeClientRect(initialClickRect)) { + clickRect = initialClickRect; + } var dialogRect = dialogEl[0].getBoundingClientRect(); var scaleX = Math.min(0.5, clickRect.width / dialogRect.width); diff --git a/src/components/dialog/dialog.spec.js b/src/components/dialog/dialog.spec.js index 104556e8d4b..2947211433c 100644 --- a/src/components/dialog/dialog.spec.js +++ b/src/components/dialog/dialog.spec.js @@ -413,6 +413,161 @@ describe('$mdDialog', function() { expect($document.activeElement).toBe(undefined); })); + /** + * Verifies that an element has the expected CSS for its transform property. + * Works by creating a new element, setting the expected CSS on that + * element, and comparing to the element being tested. This convoluted + * approach is needed because if jQuery is installed it can rewrite + * 'translate3d' values to equivalent 'matrix' values, for example turning + * 'translate3d(240px, 120px, 0px) scale(0.5, 0.5)' into + * 'matrix(0.5, 0, 0, 0.5, 240, 120)'. + */ + var verifyTransformCss = function(element, transformAttr, expectedCss) { + var testDiv = angular.element('
'); + testDiv.css(transformAttr, expectedCss); + expect(element.css(transformAttr)).toBe(testDiv.css(transformAttr)); + }; + + it('should expand from and shrink to targetEvent element', inject(function($mdDialog, $rootScope, $timeout, $mdConstant) { + // Create a targetEvent parameter pointing to a fake element with a + // defined bounding rectangle. + var fakeEvent = { + target: { + getBoundingClientRect: function() { + return {top: 100, left: 200, bottom: 140, right: 280, height: 40, width: 80}; + } + } + }; + var parent = angular.element('
'); + $mdDialog.show({ + template: '', + parent: parent, + targetEvent: fakeEvent, + clickOutsideToClose: true + }); + $rootScope.$apply(); + + var container = angular.element(parent[0].querySelector('.md-dialog-container')); + var dialog = parent.find('md-dialog'); + + dialog.triggerHandler('transitionend'); + $rootScope.$apply(); + + // The dialog's bounding rectangle is always zero size and position in + // these tests, so the target of the CSS transform should be the midpoint + // of the targetEvent element's bounding rect. + verifyTransformCss(dialog, $mdConstant.CSS.TRANSFORM, + 'translate3d(240px, 120px, 0px) scale(0.5, 0.5)'); + + // Clear the animation CSS so we can be sure it gets reset. + dialog.css($mdConstant.CSS.TRANSFORM, ''); + + // When the dialog is closed (here by an outside click), the animation + // should shrink to the same point it expanded from. + container.triggerHandler({ + type: 'click', + target: container[0] + }); + $timeout.flush(); + + verifyTransformCss(dialog, $mdConstant.CSS.TRANSFORM, + 'translate3d(240px, 120px, 0px) scale(0.5, 0.5)'); + })); + + it('should shrink to updated targetEvent element location', inject(function($mdDialog, $rootScope, $timeout, $mdConstant) { + // Create a targetEvent parameter pointing to a fake element with a + // defined bounding rectangle. + var fakeEvent = { + target: { + getBoundingClientRect: function() { + return {top: 100, left: 200, bottom: 140, right: 280, height: 40, width: 80}; + } + } + }; + + var parent = angular.element('
'); + $mdDialog.show({ + template: '', + parent: parent, + targetEvent: fakeEvent, + clickOutsideToClose: true + }); + $rootScope.$apply(); + + var container = angular.element(parent[0].querySelector('.md-dialog-container')); + var dialog = parent.find('md-dialog'); + + dialog.triggerHandler('transitionend'); + $rootScope.$apply(); + + verifyTransformCss(dialog, $mdConstant.CSS.TRANSFORM, + 'translate3d(240px, 120px, 0px) scale(0.5, 0.5)'); + + // Simulate the event target element moving on the page. When the dialog + // is closed, it should animate to the new midpoint. + fakeEvent.target.getBoundingClientRect = function() { + return {top: 300, left: 400, bottom: 360, right: 500, height: 60, width: 100}; + }; + container.triggerHandler({ + type: 'click', + target: container[0] + }); + $timeout.flush(); + + verifyTransformCss(dialog, $mdConstant.CSS.TRANSFORM, + 'translate3d(450px, 330px, 0px) scale(0.5, 0.5)'); + })); + + it('should shrink to original targetEvent element location if element is hidden', inject(function($mdDialog, $rootScope, $timeout, $mdConstant) { + // Create a targetEvent parameter pointing to a fake element with a + // defined bounding rectangle. + var fakeEvent = { + target: { + getBoundingClientRect: function() { + return {top: 100, left: 200, bottom: 140, right: 280, height: 40, width: 80}; + } + } + }; + + var parent = angular.element('
'); + $mdDialog.show({ + template: '', + parent: parent, + targetEvent: fakeEvent, + clickOutsideToClose: true + }); + $rootScope.$apply(); + + var container = angular.element(parent[0].querySelector('.md-dialog-container')); + var dialog = parent.find('md-dialog'); + + $timeout.flush(); + verifyTransformCss(dialog, $mdConstant.CSS.TRANSFORM, + 'translate3d(240px, 120px, 0px) scale(0.5, 0.5)'); + + dialog.triggerHandler('transitionend'); + $rootScope.$apply(); + + // Clear the animation CSS so we can be sure it gets reset. + dialog.css($mdConstant.CSS.TRANSFORM, ''); + + // Simulate the event target element being hidden, which would cause + // getBoundingClientRect() to return a rect with zero position and size. + // When the dialog is closed, the animation should shrink to the point + // it originally expanded from. + fakeEvent.target.getBoundingClientRect = function() { + return {top: 0, left: 0, bottom: 0, right: 0, height: 0, width: 0}; + }; + container.triggerHandler({ + type: 'click', + target: container[0] + }); + $timeout.flush(); + + verifyTransformCss(dialog, $mdConstant.CSS.TRANSFORM, + 'translate3d(240px, 120px, 0px) scale(0.5, 0.5)'); + })); + it('should focus the last `md-button` in md-actions open if no `.dialog-close`', inject(function($mdDialog, $rootScope, $document, $timeout, $mdConstant) { jasmine.mockElementFocus(this);