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);
+ });
+
+});