From eb6d8b8ad36fcc5ddfcf520cd5cf7c1bef4ba2d3 Mon Sep 17 00:00:00 2001 From: Brad Richardson Date: Thu, 25 Aug 2016 09:57:48 -0400 Subject: [PATCH] feat(panel): Panel grouping Panels are now capable of being grouped together by any designation the user sees fit. Grouping allows the user to specify logic for a collection of panels. For instance, all popups in the header can be considered one group with a maximum of one popup open at a time. Dialogs within dialogs are another example of groups. This implementation prevents the panel from storing a ridiculous amount of data in memory or polluting the DOM with several panels. This helps performance and gives the user more flexibility about how to use panels. Fixes #8971 --- .../panel/demoBasicUsage/index.html | 2 +- src/components/panel/demoGroups/index.html | 46 +++ src/components/panel/demoGroups/script.js | 88 ++++++ .../panel/demoGroups/style.global.css | 77 +++++ src/components/panel/panel.js | 272 +++++++++++++++--- src/components/panel/panel.spec.js | 236 +++++++++++++-- 6 files changed, 661 insertions(+), 60 deletions(-) create mode 100644 src/components/panel/demoGroups/index.html create mode 100644 src/components/panel/demoGroups/script.js create mode 100644 src/components/panel/demoGroups/style.global.css diff --git a/src/components/panel/demoBasicUsage/index.html b/src/components/panel/demoBasicUsage/index.html index 70c1f4557d..8b24963328 100644 --- a/src/components/panel/demoBasicUsage/index.html +++ b/src/components/panel/demoBasicUsage/index.html @@ -1,4 +1,4 @@ -
+

A panel can be used to create dialogs, menus, and other overlays.

diff --git a/src/components/panel/demoGroups/index.html b/src/components/panel/demoGroups/index.html new file mode 100644 index 0000000000..253fb034e8 --- /dev/null +++ b/src/components/panel/demoGroups/index.html @@ -0,0 +1,46 @@ +
+ + +
+ + + +

Toolbar with grouped panels (Maximum open: 2)

+ + + + + + + +
+
+ + +

+ Panels can be added to a group. Groups are used to configure specific + behaviors on multiple panels. To add a panel to a group, use the + $mdPanel.newPanelGroup method, or simply add a group name + to the configuration object passed into the $mdPanel.create + method. +

+

+ Grouping allows for methods to be applied to several panels at once, i.e. + closing all panels within the toolbar group, or destroying all panels + within a dialog group. With the maxOpen property, you can + also limit the number of panels allowed open within a specific group. This + can be useful in limiting the number of menu panels allowed open at a + time, etc. +

+
+ +
diff --git a/src/components/panel/demoGroups/script.js b/src/components/panel/demoGroups/script.js new file mode 100644 index 0000000000..8961eeb70e --- /dev/null +++ b/src/components/panel/demoGroups/script.js @@ -0,0 +1,88 @@ +(function() { + 'use strict'; + + angular + .module('panelGroupsDemo', ['ngMaterial']) + .controller('PanelGroupsCtrl', PanelGroupsCtrl) + .controller('PanelMenuCtrl', PanelMenuCtrl); + + function PanelGroupsCtrl($mdPanel) { + this.settings = { + name: 'settings', + items: [ + 'Home', + 'About', + 'Contact' + ] + }; + this.favorite = { + name: 'favorite', + items: [ + 'Add to Favorites' + ] + }; + this.more = { + name: 'more', + items: [ + 'Account', + 'Sign Out' + ] + }; + + this.showToolbarMenu = function($event, menu) { + var template = '' + + ''; + + var position = $mdPanel.newPanelPosition() + .relativeTo($event.srcElement) + .addPanelPosition( + $mdPanel.xPosition.ALIGN_START, + $mdPanel.yPosition.BELOW + ); + + $mdPanel.newPanelGroup('toolbar', { + maxOpen: 2 + }); + + var config = { + id: 'toolbar_' + menu.name, + attachTo: angular.element(document.body), + controller: PanelMenuCtrl, + controllerAs: 'ctrl', + template: template, + position: position, + panelClass: 'menu-panel-container', + locals: { + items: menu.items + }, + openFrom: $event, + focusOnOpen: false, + zIndex: 100, + propagateContainerEvents: true, + groupName: 'toolbar' + }; + + $mdPanel.open(config); + } + } + + function PanelMenuCtrl(mdPanelRef) { + this.closeMenu = function() { + mdPanelRef && mdPanelRef.close(); + } + } +})(); diff --git a/src/components/panel/demoGroups/style.global.css b/src/components/panel/demoGroups/style.global.css new file mode 100644 index 0000000000..38f0af5e47 --- /dev/null +++ b/src/components/panel/demoGroups/style.global.css @@ -0,0 +1,77 @@ +.menu-panel-container { + pointer-events: auto; +} + +.menu-panel { + width: 256px; + background-color: #fff; + border-radius: 4px; +} + +.menu-panel .menu-divider { + width: 100%; + height: 1px; + min-height: 1px; + max-height: 1px; + margin-top: 4px; + margin-bottom: 4px; + background-color: rgba(0, 0, 0, 0.11); +} + +.menu-panel .menu-content { + display: flex; + flex-direction: column; + padding: 8px 0; + max-height: 305px; + overflow-y: auto; + min-width: 256px; +} + +.menu-panel .menu-item { + display: flex; + flex-direction: row; + min-height: 48px; + height: 48px; + align-content: center; + justify-content: flex-start; +} +.menu-panel .menu-item > * { + width: 100%; + margin: auto 0; + padding-left: 16px; + padding-right: 16px; +} +.menu-panel .menu-item > a.md-button { + padding-top: 5px; +} +.menu-panel .menu-item > .md-button { + display: inline-block; + border-radius: 0; + margin: auto 0; + font-size: 15px; + text-transform: none; + font-weight: 400; + height: 100%; + padding-left: 16px; + padding-right: 16px; + width: 100%; + text-align: left; +} +.menu-panel .menu-item > .md-button::-moz-focus-inner { + padding: 0; + border: 0; +} +.menu-panel .menu-item > .md-button md-icon { + margin: auto 16px auto 0; +} +.menu-panel .menu-item > .md-button p { + display: inline-block; + margin: auto; +} +.menu-panel .menu-item > .md-button span { + margin-top: auto; + margin-bottom: auto; +} +.menu-panel .menu-item > .md-button .md-ripple-container { + border-radius: inherit; +} diff --git a/src/components/panel/panel.js b/src/components/panel/panel.js index e2481a290d..dd6028c470 100644 --- a/src/components/panel/panel.js +++ b/src/components/panel/panel.js @@ -155,11 +155,14 @@ angular * on when the panel closes. This is commonly the element which triggered * the opening of the panel. If you do not use `origin`, you need to control * the focus manually. + * - `groupName` - `{string=}`: Name of a panel group. This group name is used + * for configuring the number of open panels and identifying specific + * behaviors for groups. For instance, all tooltips could be identified + * using the same groupName. * * @returns {!MdPanelRef} panelRef */ - /** * @ngdoc method * @name $mdPanel#open @@ -176,7 +179,6 @@ angular * to an instance of the panel. */ - /** * @ngdoc method * @name $mdPanel#newPanelPosition @@ -187,7 +189,6 @@ angular * @returns {!MdPanelPosition} panelPosition */ - /** * @ngdoc method * @name $mdPanel#newPanelAnimation @@ -198,6 +199,37 @@ angular * @returns {!MdPanelAnimation} panelAnimation */ +/** + * @ngdoc method + * @name $mdPanel#newPanelGroup + * @description + * Creates a panel group and adds it to a tracked list of panel groups. + * + * @param {string} groupName Name of the group to create. + * @param {!Object=} config Specific configuration object that may contain the + * following properties: + * + * - `maxOpen` - `{number=}`: The maximum number of panels that are allowed to + * be open within a defined panel group. + * + * @returns {!Object, + * openPanels: !Array, + * maxOpen: number}>} panelGroup + */ + +/** + * @ngdoc method + * @name $mdPanel#setGroupMaxOpen + * @description + * Sets the maximum number of panels in a group that can be opened at a given + * time. + * + * @param {string} groupName The name of the group to configure. + * @param {number} maxOpen The maximum number of panels that can be + * opened. Infinity can be passed in to remove the maxOpen limit. + */ + /***************************************************************************** * MdPanelRef * @@ -355,9 +387,28 @@ angular /** * @ngdoc method - * @name MdPanelRef#registerInterceptor + * @name MdPanelRef#addToGroup * @description + * Adds a panel to a group if the panel does not exist within the group already. + * A panel can only exist within a single group. * + * @param {string} groupName The name of the group to add the panel to. + */ + +/** + * @ngdoc method + * @name MdPanelRef#removeFromGroup + * @description + * Removes a panel from a group if the panel exists within that group. The group + * must be created ahead of time. + * + * @param {string} groupName The name of the group. + */ + +/** + * @ngdoc method + * @name MdPanelRef#registerInterceptor + * @description * Registers an interceptor with the panel. The callback should return a promise, * which will allow the action to continue when it gets resolved, or will * prevent an action if it is rejected. The interceptors are called sequentially @@ -374,7 +425,6 @@ angular * @ngdoc method * @name MdPanelRef#removeInterceptor * @description - * * Removes a registered interceptor. * * @param {string} type Type of interceptor to be removed. @@ -386,7 +436,6 @@ angular * @ngdoc method * @name MdPanelRef#removeAllInterceptors * @description - * * Removes all interceptors. If a type is supplied, only the * interceptors of that type will be cleared. * @@ -762,9 +811,20 @@ function MdPanelService($rootElement, $rootScope, $injector, $window) { /** @private @const */ this._$window = $window; + /** @private @const */ + this._$mdUtil = this._$injector.get('$mdUtil'); + /** @private {!Object} */ this._trackedPanels = {}; + /** + * @private {!Object, + * openPanels: !Array, + * maxOpen: number}>} + */ + this._groups = Object.create(null); + /** * Default animations that can be used within the panel. * @type {enum} @@ -807,9 +867,9 @@ MdPanelService.prototype.create = function(config) { return this._trackedPanels[config.id]; } - // If no ID is set within the passed-in config, then create an arbitrary ID. this._config = { - id: config.id || 'panel_' + this._$injector.get('$mdUtil').nextUid(), + // If no ID is set within the passed-in config, then create an arbitrary ID. + id: config.id || 'panel_' + this._$mdUtil.nextUid(), scope: this._$rootScope.$new(true), attachTo: this._$rootElement }; @@ -819,6 +879,10 @@ MdPanelService.prototype.create = function(config) { this._trackedPanels[config.id] = panelRef; this._config.scope.$on('$destroy', angular.bind(panelRef, panelRef.detach)); + if (this._config.groupName) { + panelRef.addToGroup(this._config.groupName); + } + return panelRef; }; @@ -856,6 +920,78 @@ MdPanelService.prototype.newPanelAnimation = function() { }; +/** + * Creates a panel group and adds it to a tracked list of panel groups. + * @param groupName {string} Name of the group to create. + * @param config {!Object=} Specific configuration object that may contain the + * following properties: + * + * - `maxOpen` - `{number=}`: The maximum number of panels that are allowed + * open within a defined panel group. + * + * @returns {!Object, + * openPanels: !Array, + * maxOpen: number}>} panelGroup + */ +MdPanelService.prototype.newPanelGroup = function(groupName, config) { + if (!this._groups[groupName]) { + config = config || {}; + var group = { + panels: [], + openPanels: [], + maxOpen: config.maxOpen > 0 ? config.maxOpen : Infinity + }; + this._groups[groupName] = group; + } + return this._groups[groupName]; +}; + + +/** + * Sets the maximum number of panels in a group that can be opened at a given + * time. + * @param {string} groupName The name of the group to configure. + * @param {number} maxOpen The maximum number of panels that can be + * opened. Infinity can be passed in to remove the maxOpen limit. + */ +MdPanelService.prototype.setGroupMaxOpen = function(groupName, maxOpen) { + if (this._groups[groupName]) { + this._groups[groupName].maxOpen = maxOpen; + } else { + throw new Error('mdPanel: Group does not exist yet. Call newPanelGroup().'); + } +}; + + +/** + * Determines if the current number of open panels within a group exceeds the + * limit of allowed open panels. + * @param {string} groupName The name of the group to check. + * @returns {boolean} true if open count does exceed maxOpen and false if not. + * @private + */ +MdPanelService.prototype._openCountExceedsMaxOpen = function(groupName) { + if (groupName && this._groups[groupName]) { + var group = this._groups[groupName]; + return group.maxOpen > 0 && group.openPanels.length > group.maxOpen; + } + return false; +}; + + +/** + * Closes the first open panel within a specific group. + * @param {string} groupName The name of the group. + * @private + */ +MdPanelService.prototype._closeFirstOpenedPanel = function(groupName) { + if (groupName) { + this._groups[groupName].openPanels[0].close(); + } +}; + + /** * Wraps the users template in two elements, md-panel-outer-wrapper, which * covers the entire attachTo element, and md-panel, which contains only the @@ -985,9 +1121,15 @@ MdPanelRef.prototype.open = function() { return this._$q(function(resolve, reject) { var done = self._done(resolve, self); var show = self._simpleBind(self.show, self); + var checkGroupMaxOpen = function() { + if (self._$mdPanel._openCountExceedsMaxOpen(self.config.groupName)) { + self._$mdPanel._closeFirstOpenedPanel(self.config.groupName); + } + }; self.attach() .then(show) + .then(checkGroupMaxOpen) .then(done) .catch(reject); }); @@ -1031,9 +1173,9 @@ MdPanelRef.prototype.attach = function() { var done = self._done(resolve, self); var onDomAdded = self.config['onDomAdded'] || angular.noop; var addListeners = function(response) { - self.isAttached = true; - self._addEventListeners(); - return response; + self.isAttached = true; + self._addEventListeners(); + return response; }; self._$q.all([ @@ -1101,6 +1243,9 @@ MdPanelRef.prototype.detach = function() { * Destroys the panel. The Panel cannot be opened again after this. */ MdPanelRef.prototype.destroy = function() { + if (this.config.groupName) { + this.removeFromGroup(this.config.groupName); + } this.config.scope.$destroy(); this.config.locals = null; this._interceptors = null; @@ -1115,7 +1260,7 @@ MdPanelRef.prototype.destroy = function() { MdPanelRef.prototype.show = function() { if (!this.panelContainer) { return this._$q(function(resolve, reject) { - reject('Panel does not exist yet. Call open() or attach().'); + reject('mdPanel: Panel does not exist yet. Call open() or attach().'); }); } @@ -1132,11 +1277,18 @@ MdPanelRef.prototype.show = function() { return this._$q(function(resolve, reject) { var done = self._done(resolve, self); var onOpenComplete = self.config['onOpenComplete'] || angular.noop; + var addToGroupOpen = function() { + if (self.config.groupName) { + var group = self._$mdPanel._groups[self.config.groupName]; + group.openPanels.push(self); + } + }; self._$q.all([ self._backdropRef ? self._backdropRef.show() : self, animatePromise().then(function() { self._focusOnOpen(); }, reject) ]).then(onOpenComplete) + .then(addToGroupOpen) .then(done) .catch(reject); }); @@ -1151,7 +1303,7 @@ MdPanelRef.prototype.show = function() { MdPanelRef.prototype.hide = function() { if (!this.panelContainer) { return this._$q(function(resolve, reject) { - reject('Panel does not exist yet. Call open() or attach().'); + reject('mdPanel: Panel does not exist yet. Call open() or attach().'); }); } @@ -1164,7 +1316,18 @@ MdPanelRef.prototype.hide = function() { return this._$q(function(resolve, reject) { var done = self._done(resolve, self); var onRemoving = self.config['onRemoving'] || angular.noop; - + var hidePanel = function() { + self.panelContainer.addClass(MD_PANEL_HIDDEN); + }; + var removeFromGroupOpen = function() { + if (self.config.groupName) { + var group = self._$mdPanel._groups[self.config.groupName]; + var index = group.openPanels.indexOf(self); + if (index > -1) { + group.openPanels.splice(index, 1); + } + } + }; var focusOnOrigin = function() { var origin = self.config['origin']; if (origin) { @@ -1172,15 +1335,12 @@ MdPanelRef.prototype.hide = function() { } }; - var hidePanel = function() { - self.panelContainer.addClass(MD_PANEL_HIDDEN); - }; - self._$q.all([ self._backdropRef ? self._backdropRef.hide() : self, self._animateClose() .then(onRemoving) .then(hidePanel) + .then(removeFromGroupOpen) .then(focusOnOrigin) .catch(reject) ]).then(done, reject); @@ -1201,13 +1361,14 @@ MdPanelRef.prototype.hide = function() { */ MdPanelRef.prototype.addClass = function(newClass, toElement) { this._$log.warn( - 'The addClass method is in the process of being deprecated. ' + + 'mdPanel: The addClass method is in the process of being deprecated. ' + 'Full deprecation is scheduled for the Angular Material 1.2 release. ' + 'To achieve the same results, use the panelContainer or panelEl ' + 'JQLite elements that are referenced in MdPanelRef.'); if (!this.panelContainer) { - throw new Error('Panel does not exist yet. Call open() or attach().'); + throw new Error( + 'mdPanel: Panel does not exist yet. Call open() or attach().'); } if (!toElement && !this.panelContainer.hasClass(newClass)) { @@ -1231,13 +1392,14 @@ MdPanelRef.prototype.addClass = function(newClass, toElement) { */ MdPanelRef.prototype.removeClass = function(oldClass, fromElement) { this._$log.warn( - 'The removeClass method is in the process of being deprecated. ' + + 'mdPanel: The removeClass method is in the process of being deprecated. ' + 'Full deprecation is scheduled for the Angular Material 1.2 release. ' + 'To achieve the same results, use the panelContainer or panelEl ' + 'JQLite elements that are referenced in MdPanelRef.'); if (!this.panelContainer) { - throw new Error('Panel does not exist yet. Call open() or attach().'); + throw new Error( + 'mdPanel: Panel does not exist yet. Call open() or attach().'); } if (!fromElement && this.panelContainer.hasClass(oldClass)) { @@ -1261,13 +1423,14 @@ MdPanelRef.prototype.removeClass = function(oldClass, fromElement) { */ MdPanelRef.prototype.toggleClass = function(toggleClass, onElement) { this._$log.warn( - 'The toggleClass method is in the process of being deprecated. ' + + 'mdPanel: The toggleClass method is in the process of being deprecated. ' + 'Full deprecation is scheduled for the Angular Material 1.2 release. ' + 'To achieve the same results, use the panelContainer or panelEl ' + 'JQLite elements that are referenced in MdPanelRef.'); if (!this.panelContainer) { - throw new Error('Panel does not exist yet. Call open() or attach().'); + throw new Error( + 'mdPanel: Panel does not exist yet. Call open() or attach().'); } if (!onElement) { @@ -1382,7 +1545,8 @@ MdPanelRef.prototype._addStyles = function() { */ MdPanelRef.prototype.updatePosition = function(position) { if (!this.panelContainer) { - throw new Error('Panel does not exist yet. Call open() or attach().'); + throw new Error( + 'mdPanel: Panel does not exist yet. Call open() or attach().'); } this.config['position'] = position; @@ -1662,7 +1826,8 @@ MdPanelRef.prototype._animateOpen = function() { var done = self._done(resolve, self); var warnAndOpen = function() { self._$log.warn( - 'MdPanel Animations failed. Showing panel without animating.'); + 'mdPanel: MdPanel Animations failed. ' + + 'Showing panel without animating.'); done(); }; @@ -1694,7 +1859,8 @@ MdPanelRef.prototype._animateClose = function() { }; var warnAndClose = function() { self._$log.warn( - 'MdPanel Animations failed. Hiding panel without animating.'); + 'mdPanel: MdPanel Animations failed. ' + + 'Hiding panel without animating.'); done(); }; @@ -1830,6 +1996,44 @@ MdPanelRef.prototype._done = function(callback, self) { }; +/** + * Adds a panel to a group if the panel does not exist within the group already. + * A panel can only exist within a single group. + * @param {string} groupName The name of the group. + */ +MdPanelRef.prototype.addToGroup = function(groupName) { + if (!this._$mdPanel._groups[groupName]) { + this._$mdPanel.newPanelGroup(groupName); + } + + var group = this._$mdPanel._groups[groupName]; + var index = group.panels.indexOf(this); + + if (index < 0) { + group.panels.push(this); + } +}; + + +/** + * Removes a panel from a group if the panel exists within that group. The group + * must be created ahead of time. + * @param {string} groupName The name of the group. + */ +MdPanelRef.prototype.removeFromGroup = function(groupName) { + if (!this._$mdPanel._groups[groupName]) { + throw new Error('mdPanel: The group ' + groupName + ' does not exist.'); + } + + var group = this._$mdPanel._groups[groupName]; + var index = group.panels.indexOf(this); + + if (index > -1) { + group.panels.splice(index, 1); + } +}; + + /***************************************************************************** * MdPanelPosition * *****************************************************************************/ @@ -1961,7 +2165,7 @@ MdPanelPosition.prototype._setPosition = function(position, value) { var positions = Object.keys(MdPanelPosition.absPosition).join() .toLowerCase(); - throw new Error('Position must be one of ' + positions + '.'); + throw new Error('mdPanel: Position must be one of ' + positions + '.'); } this['_' + position] = angular.isString(value) ? value : '0'; @@ -2098,8 +2302,8 @@ MdPanelPosition.prototype.relativeTo = function(element) { */ MdPanelPosition.prototype.addPanelPosition = function(xPosition, yPosition) { if (!this._relativeToEl) { - throw new Error('addPanelPosition can only be used with relative ' + - 'positioning. Set relativeTo first.'); + throw new Error('mdPanel: addPanelPosition can only be used with ' + + 'relative positioning. Set relativeTo first.'); } this._validateXPosition(xPosition); @@ -2134,8 +2338,8 @@ MdPanelPosition.prototype._validateYPosition = function(yPosition) { } } - throw new Error('Panel y position only accepts the following values:\n' + - positionValues.join(' | ')); + throw new Error('mdPanel: Panel y position only accepts the following ' + + 'values:\n' + positionValues.join(' | ')); }; @@ -2159,8 +2363,8 @@ MdPanelPosition.prototype._validateXPosition = function(xPosition) { } } - throw new Error('Panel x Position only accepts the following values:\n' + - positionValues.join(' | ')); + throw new Error('mdPanel: Panel x Position only accepts the following ' + + 'values:\n' + positionValues.join(' | ')); }; diff --git a/src/components/panel/panel.spec.js b/src/components/panel/panel.spec.js index 025f4e334f..69ac476394 100644 --- a/src/components/panel/panel.spec.js +++ b/src/components/panel/panel.spec.js @@ -1059,6 +1059,169 @@ describe('$mdPanel', function() { }); }); + describe('grouping logic:', function() { + it('should create a group using the newPanelGroup method', function() { + $mdPanel.newPanelGroup('test'); + + expect($mdPanel._groups['test']).toExist(); + }); + + it('should create a group using the config option groupName when the ' + + 'group hasn\'t been created yet', function() { + var config = { + groupName: 'test' + }; + var panel = $mdPanel.create(config); + + expect($mdPanel._groups['test']).toExist(); + }); + + it('should only create a group once', function() { + var config = { + groupName: 'test' + }; + var panel = $mdPanel.create(config); + + expect(getNumberOfGroups()).toEqual(1); + + $mdPanel.newPanelGroup('test'); + + expect(getNumberOfGroups()).toEqual(1); + }); + + it('should not create a group using the config option when the group is ' + + 'already defined', function() { + $mdPanel.newPanelGroup('test'); + + expect(getNumberOfGroups()).toEqual(1); + + var config = { + groupName: 'test' + }; + var panel = $mdPanel.create(config); + + expect(getNumberOfGroups()).toEqual(1); + }); + + it('should add a panel to a group using the addToGroup method', function() { + $mdPanel.newPanelGroup('test'); + var panel = $mdPanel.create(DEFAULT_CONFIG); + + panel.addToGroup('test'); + expect(getGroupPanels('test')).toContain(panel); + }); + + it('should add a panel to a group using the config option groupName', + function() { + $mdPanel.newPanelGroup('test'); + + var config = { + groupName: 'test' + }; + + var panel = $mdPanel.create(config); + expect(getGroupPanels('test')).toContain(panel); + }); + + it('should remove a panel from a group using the removeFromGroup method', + function() { + $mdPanel.newPanelGroup('test'); + + var config = { + groupName: 'test' + }; + + var panel = $mdPanel.create(config); + + panel.removeFromGroup('test'); + expect(getGroupPanels('test')).not.toContain(panel); + }); + + it('should remove a panel from a group on panel destroy', function() { + $mdPanel.newPanelGroup('test'); + + var config = { + groupName: 'test' + }; + + var panel = $mdPanel.create(config); + + panel.destroy(); + expect(getGroupPanels('test')).not.toContain(panel); + }); + + it('should set the maximum number of panels allowed open within a group ' + + 'using the newPanelGroup option', function() { + $mdPanel.newPanelGroup('test', { + maxOpen: 1 + }); + + expect(getGroupMaxOpen('test')).toEqual(1); + }); + + it('should set the maximum number of panels allowed open within a group ' + + 'using the setGroupMaxOpen method', function() { + $mdPanel.newPanelGroup('test'); + $mdPanel.setGroupMaxOpen('test', 1); + + expect(getGroupMaxOpen('test')).toEqual(1); + }); + + it('should throw if trying to set maxOpen on a group that doesn\'t exist', + function() { + var expression = function() { + $mdPanel.setGroupMaxOpen('test', 1); + }; + + expect(expression).toThrow(); + }); + + it('should update open panels when a panel is closed', function() { + $mdPanel.newPanelGroup('test'); + + var config = { + groupName: 'test' + }; + + openPanel(config); + flushPanel(); + expect(getGroupOpenPanels('test')).toContain(panelRef); + + closePanel(); + flushPanel(); + expect(getGroupOpenPanels('test')).not.toContain(panelRef); + }); + + it('should close the first open panel when more than the maximum number ' + + 'of panels is opened', function() { + $mdPanel.newPanelGroup('test', { + maxOpen: 2 + }); + + var config = { + groupName: 'test' + }; + + var panel1 = $mdPanel.create(config); + var panel2 = $mdPanel.create(config); + var panel3 = $mdPanel.create(config); + + panel1.open(); + flushPanel(); + expect(panel1.isAttached).toEqual(true); + + panel2.open(); + panel3.open(); + flushPanel(); + expect(panel1.isAttached).toEqual(false); + expect(panel2.isAttached).toEqual(true); + expect(panel3.isAttached).toEqual(true); + + panel2.close(); + panel3.close(); + }); + }); + describe('component logic: ', function() { it('should allow templateUrl to specify content', function() { var htmlContent = 'Puppies and Unicorns'; @@ -2239,8 +2402,7 @@ describe('$mdPanel', function() { spyOn(obj, 'callback'); panelRef.registerInterceptor(interceptorTypes.CLOSE, obj.callback); - panelRef._callInterceptors(interceptorTypes.CLOSE); - flushPanel(); + callInterceptors('CLOSE'); expect(obj.callback).toHaveBeenCalledWith(panelRef); }); @@ -2254,9 +2416,8 @@ describe('$mdPanel', function() { panelRef.registerInterceptor(interceptorTypes.CLOSE, makePromise(1)); panelRef.registerInterceptor(interceptorTypes.CLOSE, makePromise(2)); panelRef.registerInterceptor(interceptorTypes.CLOSE, makePromise(3)); - - panelRef._callInterceptors(interceptorTypes.CLOSE).then(obj.callback); - flushPanel(); + callInterceptors('CLOSE').then(obj.callback); + $rootScope.$apply(); expect(results).toEqual([3, 2, 1]); expect(obj.callback).toHaveBeenCalled(); @@ -2280,8 +2441,8 @@ describe('$mdPanel', function() { panelRef.registerInterceptor(interceptorTypes.CLOSE, makePromise(2)); panelRef.registerInterceptor(interceptorTypes.CLOSE, makePromise(3)); - panelRef._callInterceptors(interceptorTypes.CLOSE).catch(obj.callback); - flushPanel(); + callInterceptors('CLOSE').catch(obj.callback); + $rootScope.$apply(); expect(results).toEqual([3, 2]); expect(obj.callback).toHaveBeenCalled(); @@ -2308,8 +2469,8 @@ describe('$mdPanel', function() { return $q.resolve(); }); - panelRef._callInterceptors(interceptorTypes.CLOSE).catch(obj.callback); - flushPanel(); + callInterceptors('CLOSE').catch(obj.callback); + $rootScope.$apply(); expect(obj.callback).toHaveBeenCalled(); }); @@ -2319,8 +2480,8 @@ describe('$mdPanel', function() { spyOn(obj, 'callback'); - panelRef._callInterceptors(interceptorTypes.CLOSE).then(obj.callback); - flushPanel(); + callInterceptors('CLOSE').then(obj.callback); + $rootScope.$apply(); expect(obj.callback).toHaveBeenCalled(); }); @@ -2331,8 +2492,8 @@ describe('$mdPanel', function() { spyOn(obj, 'callback'); panelRef.registerInterceptor(interceptorTypes.CLOSE, obj.callback); - panelRef._callInterceptors(interceptorTypes.CLOSE); - flushPanel(); + + callInterceptors('CLOSE'); expect(obj.callback).toHaveBeenCalledTimes(1); @@ -2355,17 +2516,16 @@ describe('$mdPanel', function() { panelRef.registerInterceptor(interceptorTypes.CLOSE, obj.callback); panelRef.registerInterceptor('onOpen', obj.otherCallback); - panelRef._callInterceptors(interceptorTypes.CLOSE); - panelRef._callInterceptors('onOpen'); - flushPanel(); + callInterceptors('CLOSE'); + callInterceptors('onOpen'); expect(obj.callback).toHaveBeenCalledTimes(1); expect(obj.otherCallback).toHaveBeenCalledTimes(1); panelRef.removeAllInterceptors(); - panelRef._callInterceptors(interceptorTypes.CLOSE); - panelRef._callInterceptors('onOpen'); - flushPanel(); + + callInterceptors('CLOSE'); + callInterceptors('onOpen'); expect(obj.callback).toHaveBeenCalledTimes(1); expect(obj.otherCallback).toHaveBeenCalledTimes(1); @@ -2383,17 +2543,16 @@ describe('$mdPanel', function() { panelRef.registerInterceptor(interceptorTypes.CLOSE, obj.callback); panelRef.registerInterceptor('onOpen', obj.otherCallback); - panelRef._callInterceptors(interceptorTypes.CLOSE); - panelRef._callInterceptors('onOpen'); - flushPanel(); + callInterceptors('CLOSE'); + callInterceptors('onOpen'); expect(obj.callback).toHaveBeenCalledTimes(1); expect(obj.otherCallback).toHaveBeenCalledTimes(1); panelRef.removeAllInterceptors(interceptorTypes.CLOSE); - panelRef._callInterceptors(interceptorTypes.CLOSE); - panelRef._callInterceptors('onOpen'); - flushPanel(); + + callInterceptors('CLOSE'); + callInterceptors('onOpen'); expect(obj.callback).toHaveBeenCalledTimes(1); expect(obj.otherCallback).toHaveBeenCalledTimes(2); @@ -2520,4 +2679,31 @@ describe('$mdPanel', function() { $rootScope.$apply(); $material.flushOutstandingAnimations(); } + + function callInterceptors(type) { + if (panelRef) { + var promise = panelRef._callInterceptors( + $mdPanel.interceptorTypes[type] || type + ); + + flushPanel(); + return promise; + } + } + + function getNumberOfGroups() { + return Object.keys($mdPanel._groups).length; + } + + function getGroupPanels(groupName) { + return $mdPanel._groups[groupName].panels; + } + + function getGroupOpenPanels(groupName) { + return $mdPanel._groups[groupName].openPanels; + } + + function getGroupMaxOpen(groupName) { + return $mdPanel._groups[groupName].maxOpen; + } });