fix(modal): Improve slide-up/down animations, remove flickers

This commit is contained in:
Adam Bradley
2014-03-04 16:44:12 -06:00
parent 1b2cd5c51c
commit b593cf1be6
6 changed files with 97 additions and 122 deletions

View File

@@ -10,63 +10,72 @@ angular.module('ionic.service.modal', ['ionic.service.templateLoad', 'ionic.serv
// Show the modal
show: function() {
var self = this;
var element = angular.element(this.el);
var element = angular.element(self.el);
document.body.classList.add('modal-open');
self._isShown = true;
if(!element.parent().length) {
element.addClass(this.animation);
$animate.enter(element, angular.element($document[0].body), null, function() {
});
ionic.views.Modal.prototype.show.call(self);
} else {
$animate.addClass(element, this.animation, function() {
});
self.el.classList.add(self.animation);
$document[0].body.appendChild(self.el);
}
if(!this.didInitEvents) {
var onHardwareBackButton = function() {
self.hide();
};
element.addClass('ng-enter active');
element.removeClass('ng-leave ng-leave-active');
self.scope.$on('$destroy', function() {
$ionicPlatform.offHardwareBackButton(onHardwareBackButton);
});
$timeout(function(){
element.addClass('ng-enter-active');
// Support Android back button to close
$ionicPlatform.onHardwareBackButton(onHardwareBackButton);
if(!self.didInitEvents) {
var onHardwareBackButton = function() {
self.hide();
};
this.didInitEvents = true;
}
self.scope.$on('$destroy', function() {
$ionicPlatform.offHardwareBackButton(onHardwareBackButton);
});
this.scope.$parent.$broadcast('modal.shown', this);
// Support Android back button to close
$ionicPlatform.onHardwareBackButton(onHardwareBackButton);
self.didInitEvents = true;
}
self.scope.$parent.$broadcast('modal.shown');
}, 20);
},
// Hide the modal
hide: function() {
this._isShown = false;
var element = angular.element(this.el);
$animate.removeClass(element, this.animation, function() {
onHideModal(element[0]);
});
element.addClass('ng-leave');
$timeout(function(){
element.addClass('ng-leave-active');
element.removeClass('ng-enter ng-enter-active active');
}, 20);
$timeout(function(){
document.body.classList.remove('modal-open');
}, 400);
ionic.views.Modal.prototype.hide.call(this);
this.scope.$parent.$broadcast('modal.hidden', this);
this.scope.$parent.$broadcast('modal.hidden');
},
// Remove and destroy the modal scope
remove: function() {
var self = this,
element = angular.element(this.el);
this._isShown = false;
$animate.leave(angular.element(this.el), function() {
onHideModal(element[0]);
self.scope.$parent.$broadcast('modal.removed', self);
var self = this;
self.hide();
self.scope.$parent.$broadcast('modal.removed');
$timeout(function(){
self.scope.$destroy();
});
}, 500);
},
isShown: function() {
@@ -74,10 +83,6 @@ angular.module('ionic.service.modal', ['ionic.service.templateLoad', 'ionic.serv
}
});
function onHideModal(element) {
document.body.classList.remove('modal-open');
}
var createModal = function(templateString, options) {
// Create a new scope for the modal
var scope = options.scope && options.scope.$new() || $rootScope.$new(true);
@@ -116,6 +121,6 @@ angular.module('ionic.service.modal', ['ionic.service.templateLoad', 'ionic.serv
cb ? cb(modal) : null;
return modal;
});
},
}
};
}]);

View File

@@ -7,6 +7,7 @@
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="stylesheet" href="../../../../dist/css/ionic.css">
<script src="../../../../dist/js/ionic.bundle.js"></script>
<script src="dom-trace.js"></script>
</head>
<body>
<ion-pane ng-controller="AppCtrl">
@@ -23,7 +24,7 @@
<script id="modal.html" type="text/ng-template">
<div class="modal" ng-controller="ModalCtrl">
<header class="bar bar-header bar-positive">
<header class="bar bar-header bar-assertive">
<h1 class="title">New Contact</h1>
<button class="button button-clear button-primary" ng-click="close()">Cancel</button>
</header>
@@ -42,8 +43,8 @@
<span class="input-label">Email</span>
<input type="text" placeholder="">
</label>
<button class="button button-full button-positive" ng-click="closeModal()">Create</button>
<button class="button button-full button-positive" ng-click="openActionSheet()">ActionSheet</button>
<button class="button button-block button-positive" ng-click="closeModal()">Create</button>
<button class="button button-block button-positive" ng-click="openActionSheet()">ActionSheet</button>
</div>
</div>
</ion-content>
@@ -127,6 +128,8 @@
};
});
domTrace.observe();
</script>
</body>
</html>

View File

@@ -53,25 +53,21 @@ describe('Ionic Modal', function() {
expect(m.isShown()).toBe(false);
});
it('show & remove should add .model-open to body', inject(function($animate) {
it('show & remove should add .model-open to body', inject(function() {
var m = modal.fromTemplate('<div class="modal">hi</div>');
m.show();
expect(angular.element(document.body).hasClass('modal-open')).toBe(true);
spyOn($animate, 'leave').andCallFake(function(el, cb) {
cb();
});
m.remove();
timeout.flush();
expect(angular.element(document.body).hasClass('modal-open')).toBe(false);
}));
it('show & hide should add .model-open body', inject(function($animate) {
it('show & hide should add .model-open body', inject(function() {
var m = modal.fromTemplate('<div class="modal">hi</div>');
m.show();
expect(angular.element(document.body).hasClass('modal-open')).toBe(true);
spyOn($animate, 'removeClass').andCallFake(function(el, cls, cb) {
cb();
});
m.hide();
timeout.flush();
expect(angular.element(document.body).hasClass('modal-open')).toBe(false);
}));
@@ -80,7 +76,7 @@ describe('Ionic Modal', function() {
spyOn($animate, 'leave').andCallFake(function(el, cb) { cb(); });
spyOn(m.scope, '$destroy');
m.remove();
expect($animate.leave).toHaveBeenCalled();
timeout.flush();
expect(m.scope.$destroy).toHaveBeenCalled();
}));
@@ -89,7 +85,7 @@ describe('Ionic Modal', function() {
var modalInstance = modal.fromTemplate(template);
modalInstance.show();
//timeout.flush();
timeout.flush();
expect(modalInstance.el.classList.contains('active')).toBe(true);
@@ -97,6 +93,7 @@ describe('Ionic Modal', function() {
target: document
});
timeout.flush();
expect(modalInstance.el.classList.contains('active')).toBe(false);
});
@@ -105,14 +102,15 @@ describe('Ionic Modal', function() {
var m = modal.fromTemplate(template, {});
spyOn(m.scope.$parent, '$broadcast');
m.show();
expect(m.scope.$parent.$broadcast).toHaveBeenCalledWith('modal.shown', m);
timeout.flush();
expect(m.scope.$parent.$broadcast).toHaveBeenCalledWith('modal.shown');
});
it('should broadcast "modal.hidden" on hide', function() {
var template = '<div class="modal"></div>';
var m = modal.fromTemplate(template, {});
spyOn(m.scope.$parent, '$broadcast');
m.hide();
expect(m.scope.$parent.$broadcast).toHaveBeenCalledWith('modal.hidden', m);
expect(m.scope.$parent.$broadcast).toHaveBeenCalledWith('modal.hidden');
});
it('should broadcast "modal.removed" on remove', inject(function($animate) {
var template = '<div class="modal"></div>';
@@ -125,9 +123,9 @@ describe('Ionic Modal', function() {
spyOn(m.scope.$parent, '$broadcast').andCallFake(function(e, modal) {
broadcastedModal = modal;
});
spyOn($animate, 'leave').andCallFake(function(el, cb) { cb(); });
m.remove();
expect(broadcastedModal).toBe(m);
expect(m.scope.$parent.$broadcast).toHaveBeenCalledWith('modal.removed');
timeout.flush();
}));
});

View File

@@ -16,19 +16,15 @@
show: function() {
var self = this;
this.el.classList.add('active');
if(this.focusFirstInput) {
if(self.focusFirstInput) {
// Let any animations run first
window.setTimeout(function() {
var input = self.el.querySelector('input, textarea');
input && input.focus && input.focus();
}, this.focusFirstDelay);
}, self.focusFirstDelay);
}
},
hide: function() {
this.el.classList.remove('active');
// Unfocus all elements
if(this.unfocusOnHide) {
var inputs = this.el.querySelectorAll('input, textarea');

View File

@@ -17,7 +17,7 @@ $slide-in-up-function: cubic-bezier(.1, .7, .1, 1);
@-webkit-keyframes slideInUp {
0% {
@include translate3d(0, 100%, 0);
opacity: 0;
opacity: 1;
}
100% {
@include translate3d(0, 0, 0);
@@ -27,7 +27,7 @@ $slide-in-up-function: cubic-bezier(.1, .7, .1, 1);
@-moz-keyframes slideInUp {
0% {
@include translate3d(0, 100%, 0);
opacity: 0;
opacity: 1;
}
100% {
@include translate3d(0, 0, 0);
@@ -41,7 +41,7 @@ $slide-in-up-function: cubic-bezier(.1, .7, .1, 1);
}
100% {
@include translate3d(0, 100%, 0);
opacity: 0;
opacity: 1;
}
}
@-moz-keyframes slideOutUp {
@@ -51,7 +51,7 @@ $slide-in-up-function: cubic-bezier(.1, .7, .1, 1);
}
100% {
@include translate3d(0, 100%, 0);
opacity: 0;
opacity: 1;
}
}
@@ -160,14 +160,14 @@ $slide-in-up-function: cubic-bezier(.1, .7, .1, 1);
// Spin
// -------------------------------
@-webkit-keyframes spin {
100% { -webkit-transform: rotate(360deg); }
@-webkit-keyframes spin {
100% { -webkit-transform: rotate(360deg); }
}
@-moz-keyframes spin {
100% { -moz-transform: rotate(360deg); }
@-moz-keyframes spin {
100% { -moz-transform: rotate(360deg); }
}
@keyframes spin {
100% { transform: rotate(360deg); }
@keyframes spin {
100% { transform: rotate(360deg); }
}
.no-animation {
@@ -201,7 +201,7 @@ $slide-in-up-function: cubic-bezier(.1, .7, .1, 1);
*/
.slide-left-right {
> .ng-enter, &.ng-enter,
> .ng-enter, &.ng-enter,
> .ng-leave, &.ng-leave {
@include transition(all ease-in-out $transition-duration);
position: absolute;
@@ -249,14 +249,14 @@ $slide-in-up-function: cubic-bezier(.1, .7, .1, 1);
/**
* iOS7 style slide left to right
* iOS7 style slide left to right
* --------------------------------------------------
*/
$ios7-timing-function: ease-in-out;
$ios7-transition-duration: 250ms;
.slide-left-right-ios7 {
> .ng-enter, &.ng-enter,
> .ng-enter, &.ng-enter,
> .ng-leave, &.ng-leave {
@include transition(all $ios7-timing-function $ios7-transition-duration);
position: absolute;
@@ -280,7 +280,7 @@ $ios7-transition-duration: 250ms;
/* OLD content ACTIVELY sliding OUT to the LEFT */
@include translate3d(-15%, 0, 0);
}
&.reverse {
> .ng-enter, &.ng-enter, > .ng-leave, &.ng-leave {
@include transition(all $ios7-timing-function $ios7-transition-duration);
@@ -420,54 +420,29 @@ $ios7-transition-duration: 250ms;
@include animation-name(slideOutToRight);
}
/**
* Slide up from the bottom, used for modals
* --------------------------------------------------
*/
.slide-in-up {
// Start it down low
@include translate3d(0, 100%, 0);
}
.slide-in-up.ng-enter,
.slide-in-up .ng-enter {
@include transition(all $slide-in-up-function 400ms);
}
.slide-in-up.ng-enter-active,
.slide-in-up .ng-enter-active {
@include translate3d(0, 0, 0);
opacity: 1;
&.ng-enter, .ng-enter {
// Start it down low
@include translate3d(0, 100%, 0);
@include animation-duration(400ms);
@include animation-timing-function($slide-in-up-function);
@include animation-fill-mode(both);
// Start hidden
opacity: 0;
}
&.ng-enter-active, .ng-enter-active {
@include animation-name(slideInUp);
}
&.ng-leave, .ng-leave {
@include animation-duration(400ms);
@include animation-timing-function($slide-in-up-function);
@include animation-fill-mode(both);
}
&.ng-leave-active, .ng-leave {
@include animation-name(slideOutUp);
}
}
.slide-in-up-add {
@include animation-duration(400ms);
@include animation-timing-function($slide-in-up-function);
@include animation-fill-mode(forwards);
}
.slide-in-up-add-active {
@include animation-name(slideInUp);
.slide-in-up.ng-leave,
.slide-in-up .ng-leave {
@include transition(all ease-in-out 250ms);
}
.slide-in-up-remove {
@include animation-duration(400ms);
@include animation-timing-function($slide-in-up-function);
@include animation-fill-mode(forwards);
}
.slide-in-up-remove-active {
@include animation-name(slideOutUp);
}
.fade-in {
@include animation(fadeOut 0.3s);
@@ -492,7 +467,7 @@ $ios7-transition-duration: 250ms;
*/
$nav-title-slide-ios7-delay: 250ms;
.nav-title-slide-ios7 {
> .ng-enter, &.ng-enter,
> .ng-enter, &.ng-enter,
> .ng-leave, &.ng-leave {
@include transition(all $nav-title-slide-ios7-delay);
@include transition-timing-function($ios7-timing-function);
@@ -512,7 +487,7 @@ $nav-title-slide-ios7-delay: 250ms;
}
&.reverse {
> .ng-enter, &.ng-enter,
> .ng-enter, &.ng-enter,
> .ng-leave, &.ng-leave {
@include transition(all ease-in-out $transition-duration);
opacity: 1;

View File

@@ -6,9 +6,8 @@
*/
.modal {
@include translate3d(0, 100%, 0);
position: fixed;
top: 0;
position: absolute;
top: 0;
z-index: $z-index-modal;
overflow: hidden;
@@ -16,7 +15,6 @@
width: 100%;
background-color: $modal-bg-color;
opacity: 0;
// Active modal
&.active {
@@ -30,4 +28,4 @@
.modal {
pointer-events: auto;
}
}
}