diff --git a/src/components/dialog/dialog.spec.js b/src/components/dialog/dialog.spec.js index 24772be9d24..ae3d70c7671 100644 --- a/src/components/dialog/dialog.spec.js +++ b/src/components/dialog/dialog.spec.js @@ -1607,6 +1607,8 @@ describe('$mdDialog', function() { document.body.appendChild(parent); $mdDialog.show({template: template, parent: parent}); + runAnimation(); + $rootScope.$apply(); // It should add two focus traps to the document around the dialog content. diff --git a/src/components/menu/js/menuServiceProvider.js b/src/components/menu/js/menuServiceProvider.js index 6520460f050..80c6a7df962 100644 --- a/src/components/menu/js/menuServiceProvider.js +++ b/src/components/menu/js/menuServiceProvider.js @@ -34,7 +34,7 @@ function MenuProvider($$interimElementProvider) { disableParentScroll: true, skipCompile: true, preserveScope: true, - skipHide: true, + multiple: true, themable: true }; diff --git a/src/core/services/interimElement/interimElement.js b/src/core/services/interimElement/interimElement.js index b5feff58a8b..e8d48703aa6 100644 --- a/src/core/services/interimElement/interimElement.js +++ b/src/core/services/interimElement/interimElement.js @@ -257,15 +257,20 @@ function InterimElementProvider() { * A service used to control inserting and removing an element into the DOM. * */ - var service, stack = []; + + var service; + + var showPromises = []; // Promises for the interim's which are currently opening. + var hidePromises = []; // Promises for the interim's which are currently hiding. + var showingInterims = []; // Interim elements which are currently showing up. // Publish instance $$interimElement service; // ... used as $mdDialog, $mdToast, $mdMenu, and $mdSelect return service = { show: show, - hide: hide, - cancel: cancel, + hide: waitForInterim(hide), + cancel: waitForInterim(cancel), destroy : destroy, $injector_: $injector }; @@ -286,26 +291,35 @@ function InterimElementProvider() { function show(options) { options = options || {}; var interimElement = new InterimElement(options || {}); + // When an interim element is currently showing, we have to cancel it. // Just hiding it, will resolve the InterimElement's promise, the promise should be // rejected instead. - var hideExisting = !options.skipHide && stack.length ? service.cancel() : $q.when(true); - - // This hide()s only the current interim element before showing the next, new one - // NOTE: this is not reversible (e.g. interim elements are not stackable) + var hideAction = options.multiple ? $q.resolve() : $q.all(showPromises); + + if (!options.multiple) { + // Wait for all opening interim's to finish their transition. + hideAction = hideAction.then(function() { + // Wait for all closing and showing interim's to be completely closed. + var promiseArray = hidePromises.concat(showingInterims.map(service.cancel)); + return $q.all(promiseArray); + }); + } - hideExisting.finally(function() { + var showAction = hideAction.then(function() { - stack.push(interimElement); - interimElement + return interimElement .show() - .catch(function( reason ) { - //$log.error("InterimElement.show() error: " + reason ); - return reason; + .catch(function(reason) { return reason; }) + .finally(function() { + showPromises.splice(showPromises.indexOf(showAction), 1); + showingInterims.push(interimElement); }); }); + showPromises.push(showAction); + // Return a promise that will be resolved when the interim // element is hidden or cancelled... @@ -325,27 +339,30 @@ function InterimElementProvider() { * */ function hide(reason, options) { - if ( !stack.length ) return $q.when(reason); options = options || {}; if (options.closeAll) { - var promise = $q.all(stack.reverse().map(closeElement)); - stack = []; - return promise; + // We have to make a shallow copy of the array, because otherwise the map will break. + return $q.all(showingInterims.slice().reverse().map(closeElement)); } else if (options.closeTo !== undefined) { - return $q.all(stack.splice(options.closeTo).map(closeElement)); - } else { - var interim = stack.pop(); - return closeElement(interim); + return $q.all(showingInterims.slice(options.closeTo).map(closeElement)); } + // Hide the latest showing interim element. + return closeElement(showingInterims.pop()); + function closeElement(interim) { - interim + + var hideAction = interim .remove(reason, false, options || { }) - .catch(function( reason ) { - //$log.error("InterimElement.hide() error: " + reason ); - return reason; + .catch(function(reason) { return reason; }) + .finally(function() { + hidePromises.splice(hidePromises.indexOf(hideAction), 1); }); + + showingInterims.splice(showingInterims.indexOf(interim), 1); + hidePromises.push(hideAction); + return interim.deferred.promise; } } @@ -363,46 +380,76 @@ function InterimElementProvider() { * */ function cancel(reason, options) { - var interim = stack.pop(); - if ( !interim ) return $q.when(reason); - - interim - .remove(reason, true, options || { }) - .catch(function( reason ) { - //$log.error("InterimElement.cancel() error: " + reason ); - return reason; + var interim = showingInterims.pop(); + if (!interim) { + return $q.when(reason); + } + + var cancelAction = interim + .remove(reason, true, options || {}) + .catch(function(reason) { return reason; }) + .finally(function() { + hidePromises.splice(hidePromises.indexOf(cancelAction), 1); }); + hidePromises.push(cancelAction); + // Since Angular 1.6.7, promises will be logged to $exceptionHandler when the promise // is not handling the rejection. We create a pseudo catch handler, which will prevent the // promise from being logged to the $exceptionHandler. return interim.deferred.promise.catch(angular.noop); } + /** + * Creates a function to wait for at least one interim element to be available. + * @param callbackFn Function to be used as callback + * @returns {Function} + */ + function waitForInterim(callbackFn) { + return function() { + var fnArguments = arguments; + + if (!showingInterims.length) { + // When there are still interim's opening, then wait for the first interim element to + // finish its open animation. + if (showPromises.length) { + return showPromises[0].finally(function () { + return callbackFn.apply(service, fnArguments); + }); + } + + return $q.when("No interim elements currently showing up."); + } + + return callbackFn.apply(service, fnArguments); + }; + } + /* * Special method to quick-remove the interim element without animations * Note: interim elements are in "interim containers" */ - function destroy(target) { - var interim = !target ? stack.shift() : null; - var cntr = angular.element(target).length ? angular.element(target)[0].parentNode : null; - - if (cntr) { - // Try to find the interim element in the stack which corresponds to the supplied DOM element. - var filtered = stack.filter(function(entry) { - var currNode = entry.options.element[0]; - return (currNode === cntr); - }); + function destroy(targetEl) { + var interim = !targetEl ? showingInterims.shift() : null; - // Note: this function might be called when the element already has been removed, in which - // case we won't find any matches. That's ok. - if (filtered.length > 0) { - interim = filtered[0]; - stack.splice(stack.indexOf(interim), 1); - } + var parentEl = angular.element(targetEl).length && angular.element(targetEl)[0].parentNode; + + if (parentEl) { + // Try to find the interim in the stack which corresponds to the supplied DOM element. + var filtered = showingInterims.filter(function(entry) { + return entry.options.element[0] === parentEl; + }); + + // Note: This function might be called when the element already has been removed, + // in which case we won't find any matches. + if (filtered.length) { + interim = filtered[0]; + showingInterims.splice(showingInterims.indexOf(interim), 1); + } } - return interim ? interim.remove(SHOW_CANCELLED, false, {'$destroy':true}) : $q.when(SHOW_CANCELLED); + return interim ? interim.remove(SHOW_CANCELLED, false, { '$destroy': true }) : + $q.when(SHOW_CANCELLED); } /* diff --git a/src/core/services/interimElement/interimElement.spec.js b/src/core/services/interimElement/interimElement.spec.js index 05200a83212..f85a5843483 100644 --- a/src/core/services/interimElement/interimElement.spec.js +++ b/src/core/services/interimElement/interimElement.spec.js @@ -342,6 +342,80 @@ describe('$$interimElement service', function() { })); + it('should show multiple interim elements', function() { + var showCount = 0; + + showInterim(); + expect(showCount).toBe(1); + + showInterim(); + expect(showCount).toBe(2); + + function showInterim() { + Service.show({ + template: '
First Interim
', + onShow: function() { + showCount++; + }, + onRemove: function() { + showCount--; + }, + multiple: true + }); + } + }); + + + it('should not show multiple interim elements by default', function() { + var showCount = 0; + + showInterim(); + expect(showCount).toBe(1); + + showInterim(); + expect(showCount).toBe(1); + + function showInterim() { + Service.show({ + template: '
First Interim
', + onShow: function() { + showCount++; + }, + onRemove: function() { + showCount--; + } + }); + } + }); + + it('should cancel a previous interim after a second shows up', inject(function($q, $timeout) { + var hidePromise = $q.defer(); + var isShown = false; + + Service.show({ + template: '
First Interim
', + onRemove: function() { + return hidePromise.promise; + } + }); + + // Once we show the second interim, the first interim should be cancelled and new interim + // will successfully show up after the first interim hides completely. + Service.show({ + template: '
Second Interim
', + onShow: function() { + isShown = true; + } + }); + + expect(isShown).toBe(false); + + hidePromise.resolve(); + $timeout.flush(); + + expect(isShown).toBe(true); + })); + it('should cancel a previous shown interim element', inject(function() { var isCancelled = false;