From c1215aa300e8506d30fbeaf2c608b07058c46e3b Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Thu, 31 Jul 2014 14:43:28 -0500 Subject: [PATCH] feat(popover): created popovers --- demos/service/popover/index.html | 76 +++++++ demos/service/popover/index.js | 33 +++ demos/service/popover/test.scenario.js | 41 ++++ js/angular/directive/popover.js | 16 ++ js/angular/directive/popoverView.js | 10 + js/angular/service/modal.js | 31 +-- js/angular/service/popover.js | 134 +++++++++++ js/angular/service/position.js | 94 ++++++++ scss/_popover.scss | 152 +++++++++++++ scss/_variables.scss | 21 ++ scss/ionic.scss | 1 + test/html/popover.html | 175 +++++++++++++++ test/unit/angular/service/popover.unit.js | 257 ++++++++++++++++++++++ 13 files changed, 1028 insertions(+), 13 deletions(-) create mode 100644 demos/service/popover/index.html create mode 100644 demos/service/popover/index.js create mode 100644 demos/service/popover/test.scenario.js create mode 100644 js/angular/directive/popover.js create mode 100644 js/angular/directive/popoverView.js create mode 100644 js/angular/service/popover.js create mode 100644 js/angular/service/position.js create mode 100644 scss/_popover.scss create mode 100644 test/html/popover.html create mode 100644 test/unit/angular/service/popover.unit.js diff --git a/demos/service/popover/index.html b/demos/service/popover/index.html new file mode 100644 index 00000000000..2b3ac7085ad --- /dev/null +++ b/demos/service/popover/index.html @@ -0,0 +1,76 @@ +--- +name: popover +component: $ionicPopover +--- + + +
+ +
+

Popover

+
+ + +
+
+ + +

+ + + +

+
+ + + + + diff --git a/demos/service/popover/index.js b/demos/service/popover/index.js new file mode 100644 index 00000000000..1164d8097c0 --- /dev/null +++ b/demos/service/popover/index.js @@ -0,0 +1,33 @@ +--- +name: popover +component: $ionicPopover +--- + +angular.module('popover', ['ionic']) + +.controller('HeaderCtrl', function($scope, $ionicPopover) { + + $scope.openPopover = function($event) { + $scope.popover.show($event); + }; + $ionicPopover.fromTemplateUrl('popover.html', function(popover) { + $scope.popover = popover; + }); + + $scope.openPopover2 = function($event) { + $scope.popover2.show($event); + }; + $ionicPopover.fromTemplateUrl('popover2.html', function(popover) { + $scope.popover2 = popover; + }); +}) + +.controller('PlatformCtrl', function($scope, $ionicPopover) { + + $scope.setPlatform = function(p) { + document.body.classList.remove('platform-ios'); + document.body.classList.remove('platform-android'); + document.body.classList.add('platform-' + p); + }; + +}); diff --git a/demos/service/popover/test.scenario.js b/demos/service/popover/test.scenario.js new file mode 100644 index 00000000000..8d6e61038e9 --- /dev/null +++ b/demos/service/popover/test.scenario.js @@ -0,0 +1,41 @@ +--- +name: popover +component: $ionicPopover +--- + +it('should open left side ios popover', function(){ + element(by.css('#ios')).click(); + element(by.css('#icon-btn')).click(); +}); + +it('should close ios popover when clicking backdrop', function(){ + element(by.css('.popover-backdrop.active')).click(); +}); + +it('should open middle ios popover', function(){ + element(by.css('#mid-btn')).click(); +}); + +it('should open right ios popover', function(){ + element(by.css('.popover-backdrop.active')).click(); + element(by.css('#right-btn')).click(); +}); + +it('should open left side android popover', function(){ + element(by.css('.popover-backdrop.active')).click(); + element(by.css('#android')).click(); + element(by.css('#icon-btn')).click(); +}); + +it('should close android popover when clicking backdrop', function(){ + element(by.css('.popover-backdrop.active')).click(); +}); + +it('should open middle android popover', function(){ + element(by.css('#mid-btn')).click(); +}); + +it('should open right android popover', function(){ + element(by.css('.popover-backdrop.active')).click(); + element(by.css('#right-btn')).click(); +}); diff --git a/js/angular/directive/popover.js b/js/angular/directive/popover.js new file mode 100644 index 00000000000..5cb47c78041 --- /dev/null +++ b/js/angular/directive/popover.js @@ -0,0 +1,16 @@ +/* + * We don't document the ionPopover directive, we instead document + * the $ionicPopover service + */ +IonicModule +.directive('ionPopover', [function() { + return { + restrict: 'E', + transclude: true, + replace: true, + controller: [function(){}], + template: '
' + + '
' + + '
' + }; +}]); diff --git a/js/angular/directive/popoverView.js b/js/angular/directive/popoverView.js new file mode 100644 index 00000000000..4df9f6a9792 --- /dev/null +++ b/js/angular/directive/popoverView.js @@ -0,0 +1,10 @@ +IonicModule +.directive('ionPopoverView', function() { + return { + restrict: 'E', + compile: function(element) { + element.append( angular.element('
') ); + element.addClass('popover'); + } + }; +}); diff --git a/js/angular/service/modal.js b/js/angular/service/modal.js index 83f09327d37..6e2863b7793 100644 --- a/js/angular/service/modal.js +++ b/js/angular/service/modal.js @@ -112,11 +112,11 @@ function($rootScope, $document, $compile, $timeout, $ionicPlatform, $ionicTempla * @description Show this modal instance. * @returns {promise} A promise which is resolved when the modal is finished animating in. */ - show: function() { + show: function(target) { var self = this; if(self.scope.$$destroyed) { - $log.error('Cannot call modal.show() after remove(). Please create a new modal instance using $ionicModal.'); + $log.error('Cannot call ' + self.viewType + '.show() after remove(). Please create a new ' + self.viewType + ' instance.'); return; } @@ -124,15 +124,18 @@ function($rootScope, $document, $compile, $timeout, $ionicPlatform, $ionicTempla self.el.classList.remove('hide'); $timeout(function(){ - $document[0].body.classList.add('modal-open'); + $document[0].body.classList.add(self.viewType + '-open'); }, 400); - if(!self.el.parentElement) { modalEl.addClass(self.animation); $document[0].body.appendChild(self.el); } + if(target && self.positionView) { + self.positionView(target, modalEl); + } + modalEl.addClass('ng-enter active') .removeClass('ng-leave ng-leave-active'); @@ -149,7 +152,7 @@ function($rootScope, $document, $compile, $timeout, $ionicPlatform, $ionicTempla $timeout(function(){ modalEl.addClass('ng-enter-active'); ionic.trigger('resize'); - self.scope.$parent && self.scope.$parent.$broadcast('modal.shown', self); + self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.shown', self); self.el.classList.add('active'); }, 20); @@ -183,15 +186,15 @@ function($rootScope, $document, $compile, $timeout, $ionicPlatform, $ionicTempla self.$el.off('click'); self._isShown = false; - self.scope.$parent && self.scope.$parent.$broadcast('modal.hidden', self); + self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.hidden', self); self._deregisterBackButton && self._deregisterBackButton(); ionic.views.Modal.prototype.hide.call(self); return $timeout(function(){ - $document[0].body.classList.remove('modal-open'); + $document[0].body.classList.remove(self.viewType + '-open'); self.el.classList.add('hide'); - }, 500); + }, self.hideDelay || 500); }, /** @@ -202,7 +205,7 @@ function($rootScope, $document, $compile, $timeout, $ionicPlatform, $ionicTempla */ remove: function() { var self = this; - self.scope.$parent && self.scope.$parent.$broadcast('modal.removed', self); + self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.removed', self); return self.hide().then(function() { self.scope.$destroy(); @@ -224,6 +227,8 @@ function($rootScope, $document, $compile, $timeout, $ionicPlatform, $ionicTempla // Create a new scope for the modal var scope = options.scope && options.scope.$new() || $rootScope.$new(true); + options.viewType = options.viewType || 'modal'; + extend(scope, { $hasHeader: false, $hasSubheader: false, @@ -234,19 +239,19 @@ function($rootScope, $document, $compile, $timeout, $ionicPlatform, $ionicTempla }); // Compile the template - var element = $compile('' + templateString + '')(scope); + var element = $compile('' + templateString + '')(scope); options.$el = element; options.el = element[0]; - options.modalEl = options.el.querySelector('.modal'); + options.modalEl = options.el.querySelector('.' + options.viewType); var modal = new ModalView(options); modal.scope = scope; - // If this wasn't a defined scope, we can assign 'modal' to the isolated scope + // If this wasn't a defined scope, we can assign the viewType to the isolated scope // we created if(!options.scope) { - scope.modal = modal; + scope[ options.viewType ] = modal; } return modal; diff --git a/js/angular/service/popover.js b/js/angular/service/popover.js new file mode 100644 index 00000000000..3043d2399c2 --- /dev/null +++ b/js/angular/service/popover.js @@ -0,0 +1,134 @@ +/** + * @ngdoc service + * @name $ionicPopover + * @module ionic + * @description + * + * The Popover is a view that floats above an app’s content. Popovers provide an + * easy way to present or gather information from the user and are + * commonly used in the following situations: + * + * - Show more info about the current view + * - Select a commonly used tool or configuration + * - Present a list of actions to perform inside one of your views + * + * Put the content of the popover inside of an `` element. + * + * Note: a popover will broadcast 'popover.shown', 'popover.hidden', and 'popover.removed' events from its originating + * scope, passing in itself as an event argument. Both the popover.removed and popover.hidden events are + * called when the popover is removed. + * + * @usage + * ```html + * + * ``` + * ```js + * angular.module('testApp', ['ionic']) + * .controller('MyController', function($scope, $ionicPopover) { + * $ionicPopover.fromTemplateUrl('my-popover.html', { + * scope: $scope, + * }).then(function(popover) { + * $scope.popover = popover; + * }); + * $scope.openPopover = function() { + * $scope.popover.show(); + * }; + * $scope.closePopover = function() { + * $scope.popover.hide(); + * }; + * //Cleanup the popover when we're done with it! + * $scope.$on('$destroy', function() { + * $scope.popover.remove(); + * }); + * // Execute action on hide popover + * $scope.$on('popover.hidden', function() { + * // Execute action + * }); + * // Execute action on remove popover + * $scope.$on('popover.removed', function() { + * // Execute action + * }); + * }); + * ``` + */ +IonicModule +.factory('$ionicPopover', ['$ionicModal', '$ionicPosition', '$document', +function($ionicModal, $ionicPosition, $document) { + + var POPOVER_BODY_PADDING = 6; + + var POPOVER_OPTIONS = { + viewType: 'popover', + hideDelay: 1, + animation: 'none', + positionView: positionView + }; + + function positionView(target, popoverEle) { + var targetEle = angular.element(target.target || target); + var buttonOffset = $ionicPosition.offset( targetEle ); + var popoverWidth = popoverEle.prop('offsetWidth'); + var bodyWidth = $document[0].body.clientWidth; + var bodyHeight = $document[0].body.clientHeight; + + var popoverCSS = { + top: buttonOffset.top + buttonOffset.height, + left: buttonOffset.left + buttonOffset.width / 2 - popoverWidth / 2 + }; + + if(popoverCSS.left < POPOVER_BODY_PADDING) { + popoverCSS.left = POPOVER_BODY_PADDING; + } else if(popoverCSS.left + popoverWidth + POPOVER_BODY_PADDING > bodyWidth) { + popoverCSS.left = bodyWidth - popoverWidth - POPOVER_BODY_PADDING; + } + + var arrowEle = popoverEle[0].querySelector('.popover-arrow'); + angular.element(arrowEle).css({ + left: (buttonOffset.left - popoverCSS.left) + (buttonOffset.width / 2) - (arrowEle.offsetWidth / 2) + 'px' + }); + + popoverEle.css({ + top: popoverCSS.top + 'px', + left: popoverCSS.left + 'px', + marginLeft: '0', + opacity: '1' + }); + + } + + return { + /** + * @ngdoc method + * @name $ionicPopover#fromTemplate + * @param {string} templateString The template string to use as the popovers's + * content. + * @param {object} options Options to be passed to the initialize method. + * @returns {object} An instance of an {@link ionic.controller:ionicModal} + * controller ($ionicPopover is built on top of $ionicModal). + */ + fromTemplate: function(templateString, options) { + return $ionicModal.fromTemplate(templateString, ionic.Utils.extend(options || {}, POPOVER_OPTIONS) ); + }, + /** + * @ngdoc method + * @name $ionicPopover#fromTemplateUrl + * @param {string} templateUrl The url to load the template from. + * @param {object} options Options to be passed to the initialize method. + * @returns {promise} A promise that will be resolved with an instance of + * an {@link ionic.controller:ionicModal} controller ($ionicPopover is built on top of $ionicModal). + */ + fromTemplateUrl: function(url, options, _) { + return $ionicModal.fromTemplateUrl(url, options, ionic.Utils.extend(options || {}, POPOVER_OPTIONS) ); + } + }; + +}]); diff --git a/js/angular/service/position.js b/js/angular/service/position.js new file mode 100644 index 00000000000..56278f6bc66 --- /dev/null +++ b/js/angular/service/position.js @@ -0,0 +1,94 @@ +/** + * @ngdoc service + * @name $ionicPosition + * @module ionic + * @description + * A set of utility methods that can be use to retrieve position of DOM elements. + * It is meant to be used where we need to absolute-position DOM elements in + * relation to other, existing elements (this is the case for tooltips, popovers, etc.). + * + * Adapted from [ui.bootstrap.position](https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js), + * [License](https://github.com/angular-ui/bootstrap/blob/master/LICENSE) + */ +IonicModule +.factory('$ionicPosition', ['$document', '$window', function ($document, $window) { + + function getStyle(el, cssprop) { + if (el.currentStyle) { //IE + return el.currentStyle[cssprop]; + } else if ($window.getComputedStyle) { + return $window.getComputedStyle(el)[cssprop]; + } + // finally try and get inline style + return el.style[cssprop]; + } + + /** + * Checks if a given element is statically positioned + * @param element - raw DOM element + */ + function isStaticPositioned(element) { + return (getStyle(element, 'position') || 'static' ) === 'static'; + } + + /** + * returns the closest, non-statically positioned parentOffset of a given element + * @param element + */ + var parentOffsetEl = function (element) { + var docDomEl = $document[0]; + var offsetParent = element.offsetParent || docDomEl; + while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || docDomEl; + }; + + return { + /** + * @ngdoc method + * @name $ionicPosition#position + * @description Get the current coordinates of the element, relative to the offset parent. + * Read-only equivalent of [jQuery's position function](http://api.jquery.com/position/) + * @param {element} element The element to get the position of. + * @returns {object} Returns an object containing the properties top, left, width and height. + */ + position: function (element) { + var elBCR = this.offset(element); + var offsetParentBCR = { top: 0, left: 0 }; + var offsetParentEl = parentOffsetEl(element[0]); + if (offsetParentEl != $document[0]) { + offsetParentBCR = this.offset(angular.element(offsetParentEl)); + offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; + offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; + } + + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: elBCR.top - offsetParentBCR.top, + left: elBCR.left - offsetParentBCR.left + }; + }, + + /** + * @ngdoc method + * @name $ionicPosition#offset + * @description Get the current coordinates of the element, relative to the document. + * Read-only equivalent of [jQuery's offset function](http://api.jquery.com/offset/) + * @param {element} element The element to get the offset of. + * @returns {object} Returns an object containing the properties top, left, width and height. + */ + offset: function (element) { + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), + left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) + }; + } + + }; +}]); diff --git a/scss/_popover.scss b/scss/_popover.scss new file mode 100644 index 00000000000..c648a20f38d --- /dev/null +++ b/scss/_popover.scss @@ -0,0 +1,152 @@ + +/** + * Popovers + * -------------------------------------------------- + * Popovers are independent views which float over content + */ + +.popover-backdrop { + position: fixed; + top: 0; + left: 0; + z-index: $z-index-popover; + width: 100%; + height: 100%; + background-color: $popover-backdrop-bg-inactive; + + &.active { + background-color: $popover-backdrop-bg-active; + } +} + +.popover { + position: absolute; + top: 25%; + left: 50%; + z-index: $z-index-popover; + display: block; + margin-left: -$popover-width / 2; + margin-top: 12px; + height: $popover-height; + width: $popover-width; + background-color: $popover-bg-color; + box-shadow: $popover-box-shadow; + opacity: 0; + + .item:first-child { + border-top: 0; + } + + .item:last-child { + border-bottom: 0; + } +} + + +// Set popover border-radius +.popover, +.popover .bar-header { + border-radius: $popover-border-radius; +} +.popover .scroll-content { + z-index: 1; + margin: 2px 0; +} +.popover .bar-header { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.popover .has-header { + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.popover-arrow { + display: none; +} + + +// iOS Popover +.platform-ios { + + .popover { + box-shadow: $popover-box-shadow-ios; + } + .popover, + .popover .bar-header { + border-radius: $popover-border-radius-ios; + } + .popover .scroll-content { + margin: 8px 0; + border-radius: $popover-border-radius-ios; + } + .popover .scroll-content.has-header { + margin-top: 0; + } + .popover-arrow { + position: absolute; + top: -17px; + left: 43%; + display: block; + width: 30px; + height: 19px; + overflow: hidden; + + &:after { + position: absolute; + top: 12px; + left: 5px; + width: 20px; + height: 20px; + background-color: $popover-bg-color; + border-radius: 3px; + content: ''; + @include rotate(-45deg); + } + } +} + + +// Android Popover +.platform-android { + .popover { + box-shadow: $popover-box-shadow-android; + margin-top: -32px; + + .item { + border-color: #fafafa; + background-color: #fafafa; + color: #4d4d4d; + } + } + + .popover-backdrop, + .popover-backdrop.active { + background-color: transparent; + } +} + + +// disable clicks on all but the popover +.popover-open { + pointer-events: none; + + .popover, + .popover-backdrop { + pointer-events: auto; + } + // prevent clicks on popover when loading overlay is active though + &.loading-active { + .popover, + .popover-backdrop { + pointer-events: none; + } + } +} + + +// wider popover on larger viewports +@media (min-width: $popover-large-break-point) { + .popover { + width: $popover-large-width; + } +} diff --git a/scss/_variables.scss b/scss/_variables.scss index 48fa8c02ad2..78103c7f9d3 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -562,6 +562,26 @@ $modal-inset-mode-left: 20% !default; $modal-inset-mode-min-height: 240px !default; +// Popovers +// ------------------------------- + +$popover-bg-color: $light !default; +$popover-backdrop-bg-active: rgba(0,0,0,0.1) !default; +$popover-backdrop-bg-inactive: rgba(0,0,0,0) !default; +$popover-width: 220px !default; +$popover-height: 280px !default; +$popover-large-break-point: 680px !default; +$popover-large-width: 360px !default; + +$popover-box-shadow: 0 1px 3px rgba(0,0,0,0.4) !default; +$popover-border-radius: 2px !default; + +$popover-box-shadow-ios: 0 0 40px rgba(0,0,0,0.08) !default; +$popover-border-radius-ios: 10px !default; + +$popover-box-shadow-android: 0 2px 6px rgba(0,0,0,0.35) !default; + + // Grids // ------------------------------- @@ -675,6 +695,7 @@ $z-index-menu-scroll-content: 10 !default; $z-index-modal: 10 !default; $z-index-pane: 1 !default; $z-index-popup: 12 !default; +$z-index-popover: 10 !default; $z-index-scroll-bar: 9999 !default; $z-index-scroll-content-false: 11 !default; $z-index-slider-pager: 1 !default; diff --git a/scss/ionic.scss b/scss/ionic.scss index 5bc9f3191bf..5a426a4ecbd 100644 --- a/scss/ionic.scss +++ b/scss/ionic.scss @@ -20,6 +20,7 @@ "tabs", "menu", "modal", + "popover", "popup", "loading", "items", diff --git a/test/html/popover.html b/test/html/popover.html new file mode 100644 index 00000000000..12aece77997 --- /dev/null +++ b/test/html/popover.html @@ -0,0 +1,175 @@ + + + + + Popover + + + + + + + + + + +
+ +
+

Popover

+
+ + + +
+
+ + + Content? Yes, content. + + + + + + + + + + + + + + + + + diff --git a/test/unit/angular/service/popover.unit.js b/test/unit/angular/service/popover.unit.js new file mode 100644 index 00000000000..867173d102b --- /dev/null +++ b/test/unit/angular/service/popover.unit.js @@ -0,0 +1,257 @@ +describe('Ionic Popover', function() { + var popover, q, timeout, ionicPlatform, rootScope; + + beforeEach(module('ionic')); + + beforeEach(inject(function($ionicPopover, $q, $templateCache, $timeout, $ionicPlatform, $rootScope) { + q = $q; + popover = $ionicPopover; + timeout = $timeout; + ionicPlatform = $ionicPlatform; + rootScope = $rootScope; + + $templateCache.put('popover.html', '
'); + })); + + it('Should set popover instance properties', function() { + var template = '
'; + var instance = popover.fromTemplate(template); + expect(instance.viewType).toEqual('popover'); + expect(instance.hideDelay).toEqual(1); + expect(instance.animation).toEqual('none'); + }); + + it('Should show for static template', function() { + var template = ''; + var instance = popover.fromTemplate(template); + var target = document.createElement('button'); + instance.show(target); + expect(instance.el.classList.contains('popover-backdrop')).toBe(true); + expect(instance.modalEl.classList.contains('popover')).toBe(true); + expect(instance.modalEl.querySelector('.popover-arrow')).toBeDefined(); + }); + + it('Should show for dynamic template', function() { + var template = '
'; + + var done = false; + + var instance = popover.fromTemplateUrl('popover.html', function(instance) { + done = true; + instance.show(); + expect(instance.el.classList.contains('popover-backdrop')).toBe(true); + expect(instance.modalEl.classList.contains('popover')).toBe(true); + expect(instance.modalEl.classList.contains('active')).toBe(true); + }); + + timeout.flush(); + expect(done).toBe(true); + }); + + it('should set isShown on show/hide', function() { + var instance = popover.fromTemplate('
hello
'); + expect(instance.isShown()).toBe(false); + instance.show(); + expect(instance.isShown()).toBe(true); + instance.hide(); + expect(instance.isShown()).toBe(false); + }); + + it('should trigger a resize event', function() { + var instance = popover.fromTemplate('
hello
'); + spyOn(ionic, 'trigger'); + instance.show(); + timeout.flush(); + expect(ionic.trigger).toHaveBeenCalledWith('resize'); + }); + + it('should set isShown on remove', function() { + var instance = popover.fromTemplate('
hello
'); + expect(instance.isShown()).toBe(false); + instance.show(); + expect(instance.isShown()).toBe(true); + instance.remove(); + expect(instance.isShown()).toBe(false); + }); + + it('show & remove should add .popover-open to body', inject(function() { + var instance = popover.fromTemplate('
hi
'); + instance.show(); + timeout.flush(); + expect(angular.element(document.body).hasClass('popover-open')).toBe(true); + instance.remove(); + timeout.flush(); + expect(angular.element(document.body).hasClass('popover-open')).toBe(false); + })); + + it('show & hide should add .model-open body', inject(function() { + var instance = popover.fromTemplate('
hi
'); + instance.show(); + timeout.flush(); + expect(angular.element(document.body).hasClass('popover-open')).toBe(true); + instance.hide(); + timeout.flush(); + expect(angular.element(document.body).hasClass('popover-open')).toBe(false); + })); + + it('should animate leave and destroy scope on remove', inject(function($animate) { + var instance = popover.fromTemplate('
'); + spyOn($animate, 'leave').andCallFake(function(el, cb) { cb(); }); + spyOn(instance.scope, '$destroy'); + instance.remove(); + timeout.flush(); + expect(instance.scope.$destroy).toHaveBeenCalled(); + })); + + it('Should close on hardware back button by default', inject(function($ionicPlatform) { + var template = '
'; + var instance = popover.fromTemplate(template); + spyOn($ionicPlatform, 'registerBackButtonAction').andCallThrough(); + instance.show(); + + timeout.flush(); + expect(instance.isShown()).toBe(true); + expect($ionicPlatform.registerBackButtonAction).toHaveBeenCalled(); + + ionicPlatform.hardwareBackButtonClick(); + + expect(instance.isShown()).toBe(false); + })); + + it('should not close on hardware back button if option', inject(function($ionicPlatform) { + var template = '
'; + var instance = popover.fromTemplate(template, { + hardwareBackButtonClose: false + }); + spyOn($ionicPlatform, 'registerBackButtonAction').andCallThrough(); + instance.show(); + timeout.flush(); + expect($ionicPlatform.registerBackButtonAction).toHaveBeenCalledWith(jasmine.any(Function), PLATFORM_BACK_BUTTON_PRIORITY_MODAL); + + ionicPlatform.hardwareBackButtonClick(); + + expect(instance.isShown()).toBe(true); + })); + + it('should call _deregisterBackButton on hide', function() { + var template = '
'; + var instance = popover.fromTemplate(template); + instance.show(); + timeout.flush(); + spyOn(instance, '_deregisterBackButton'); + instance.hide(); + expect(instance._deregisterBackButton).toHaveBeenCalled(); + }); + + it('should close popover on backdrop click after animate is done', function() { + var template = '
'; + var instance = popover.fromTemplate(template); + spyOn(instance, 'hide'); + instance.show(); + timeout.flush(); + instance.$el.triggerHandler('click'); + expect(instance.hide).toHaveBeenCalled(); + }); + + it('should not close popover on backdrop click if options.backdropClickToClose', function() { + var template = '
'; + var instance = popover.fromTemplate(template, { backdropClickToClose: false }); + spyOn(instance, 'hide'); + instance.show(); + timeout.flush(); + instance.$el.triggerHandler('click'); + expect(instance.hide).not.toHaveBeenCalled(); + }); + + it('should not close popover on backdrop click if target is not backdrop', function() { + var template = '
'; + var instance = popover.fromTemplate(template); + spyOn(instance, 'hide'); + instance.show(); + timeout.flush(); + ionic.trigger('click', { target: instance.el.firstElementChild }, true); + expect(instance.hide).not.toHaveBeenCalled(); + }); + + it('should not close popover on backdrop click until animation is done', function() { + var template = '
'; + var instance = popover.fromTemplate(template); + spyOn(instance, 'hide'); + instance.show(); + instance.$el.triggerHandler('click'); + expect(instance.hide).not.toHaveBeenCalled(); + timeout.flush(); + instance.$el.triggerHandler('click'); + expect(instance.hide).toHaveBeenCalled(); + }); + + it('should remove click listener on hide', function() { + var template = '
'; + var instance = popover.fromTemplate(template); + spyOn(instance.$el, 'off'); + instance.hide(); + expect(instance.$el.off).toHaveBeenCalledWith('click'); + }); + + it('should broadcast "popover.shown" on show with self', function() { + var template = '
'; + var instance = popover.fromTemplate(template, {}); + spyOn(instance.scope.$parent, '$broadcast').andCallThrough(); + instance.show(); + timeout.flush(); + expect(instance.scope.$parent.$broadcast).toHaveBeenCalledWith('popover.shown', instance); + }); + + it('should broadcast "popover.hidden" on hide with self', function() { + var template = '
'; + var instance = popover.fromTemplate(template, {}); + spyOn(instance.scope.$parent, '$broadcast'); + instance.hide(); + expect(instance.scope.$parent.$broadcast).toHaveBeenCalledWith('popover.hidden', instance); + }); + + it('should broadcast "popover.removed" on remove', inject(function($animate) { + var template = '
'; + var instance = popover.fromTemplate(template, {}); + var broadcastedModal; + var done = false; + + //By the time instance.remove() is done, our scope will be destroyed. so we have to save the popover + //it gives us + spyOn(instance.scope.$parent, '$broadcast').andCallThrough(); + spyOn(instance.scope, '$destroy'); + + instance.remove(); + expect(instance.scope.$parent.$broadcast).toHaveBeenCalledWith('popover.removed', instance); + timeout.flush(); + })); + + it('show should return a promise resolved on hide', function() { + var template = '
'; + var instance = popover.fromTemplate(template, {}); + var done = false; + + instance.hide().then(function() { + done = true; + }); + expect(instance.el.classList.contains('hide')).toBe(false); + timeout.flush(); + expect(instance.el.classList.contains('hide')).toBe(true); + expect(done).toBe(true); + }); + + it('show should return a promise resolved on remove', function() { + var template = '
'; + var instance = popover.fromTemplate(template, {}); + var done = false; + + instance.remove().then(function() { + done = true; + }); + spyOn(instance.scope, '$destroy'); + timeout.flush(); + expect(instance.scope.$destroy).toHaveBeenCalled(); + expect(done).toBe(true); + }); + +});