Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

fix(mdDialog): if targetEvent element is hidden when dialog closes, shrink to the element's original position #3555

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions src/components/dialog/dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
155 changes: 155 additions & 0 deletions src/components/dialog/dialog.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<div>');
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('<div>');
$mdDialog.show({
template: '<md-dialog>',
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('<div>');
$mdDialog.show({
template: '<md-dialog>',
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('<div>');
$mdDialog.show({
template: '<md-dialog>',
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);

Expand Down