diff --git a/src/collapse/collapse.js b/src/collapse/collapse.js
index cafd93a3ea..5f3a84006f 100644
--- a/src/collapse/collapse.js
+++ b/src/collapse/collapse.js
@@ -5,16 +5,44 @@ angular.module('ui.bootstrap.collapse', [])
return {
link: function(scope, element, attrs) {
var expandingExpr = $parse(attrs.expanding),
- expandedExpr = $parse(attrs.expanded),
- collapsingExpr = $parse(attrs.collapsing),
- collapsedExpr = $parse(attrs.collapsed);
+ expandedExpr = $parse(attrs.expanded),
+ collapsingExpr = $parse(attrs.collapsing),
+ collapsedExpr = $parse(attrs.collapsed),
+ horizontal = false,
+ css = {},
+ cssTo = {};
- if (!scope.$eval(attrs.uibCollapse)) {
- element.addClass('in')
- .addClass('collapse')
- .attr('aria-expanded', true)
- .attr('aria-hidden', false)
- .css({height: 'auto'});
+ init();
+
+ function init() {
+ horizontal = !!('horizontal' in attrs);
+ if (horizontal) {
+ css = {
+ width: 'auto',
+ height: 'inherit'
+ };
+ cssTo = {width: '0'};
+ } else {
+ css = {
+ width: 'inherit',
+ height: 'auto'
+ };
+ cssTo = {height: '0'};
+ }
+ if (!scope.$eval(attrs.uibCollapse)) {
+ element.addClass('in')
+ .addClass('collapse')
+ .attr('aria-expanded', true)
+ .attr('aria-hidden', false)
+ .css(css);
+ }
+ }
+
+ function getScrollFromElement(element) {
+ if (horizontal) {
+ return {width: element.scrollWidth + 'px'};
+ }
+ return {height: element.scrollHeight + 'px'};
}
function expand() {
@@ -33,11 +61,11 @@ angular.module('ui.bootstrap.collapse', [])
$animateCss(element, {
addClass: 'in',
easing: 'ease',
- to: { height: element[0].scrollHeight + 'px' }
+ to: getScrollFromElement(element[0])
}).start()['finally'](expandDone);
} else {
$animate.addClass(element, 'in', {
- to: { height: element[0].scrollHeight + 'px' }
+ to: getScrollFromElement(element[0])
}).then(expandDone);
}
});
@@ -46,7 +74,7 @@ angular.module('ui.bootstrap.collapse', [])
function expandDone() {
element.removeClass('collapsing')
.addClass('collapse')
- .css({height: 'auto'});
+ .css(css);
expandedExpr(scope);
}
@@ -58,10 +86,10 @@ angular.module('ui.bootstrap.collapse', [])
$q.resolve(collapsingExpr(scope))
.then(function() {
element
- // IMPORTANT: The height must be set before adding "collapsing" class.
- // Otherwise, the browser attempts to animate from height 0 (in
- // collapsing class) to the given height here.
- .css({height: element[0].scrollHeight + 'px'})
+ // IMPORTANT: The width must be set before adding "collapsing" class.
+ // Otherwise, the browser attempts to animate from width 0 (in
+ // collapsing class) to the given width here.
+ .css(getScrollFromElement(element[0]))
// initially all panel collapse have the collapse class, this removal
// prevents the animation from jumping to collapsed state
.removeClass('collapse')
@@ -72,18 +100,18 @@ angular.module('ui.bootstrap.collapse', [])
if ($animateCss) {
$animateCss(element, {
removeClass: 'in',
- to: {height: '0'}
+ to: cssTo
}).start()['finally'](collapseDone);
} else {
$animate.removeClass(element, 'in', {
- to: {height: '0'}
+ to: cssTo
}).then(collapseDone);
}
});
}
function collapseDone() {
- element.css({height: '0'}); // Required so that collapse works when animation is disabled
+ element.css(cssTo); // Required so that collapse works when animation is disabled
element.removeClass('collapsing')
.addClass('collapse');
collapsedExpr(scope);
diff --git a/src/collapse/docs/demo.html b/src/collapse/docs/demo.html
index 462bda3ba0..5367fc275e 100644
--- a/src/collapse/docs/demo.html
+++ b/src/collapse/docs/demo.html
@@ -1,7 +1,13 @@
-
+
+
+
+
+
diff --git a/src/collapse/docs/demo.js b/src/collapse/docs/demo.js
index 897eecaf58..b838cb2117 100644
--- a/src/collapse/docs/demo.js
+++ b/src/collapse/docs/demo.js
@@ -1,3 +1,4 @@
angular.module('ui.bootstrap.demo').controller('CollapseDemoCtrl', function ($scope) {
$scope.isCollapsed = false;
+ $scope.isCollapsedHorizontal = false;
});
diff --git a/src/collapse/docs/readme.md b/src/collapse/docs/readme.md
index 60237b9952..80612c95e9 100644
--- a/src/collapse/docs/readme.md
+++ b/src/collapse/docs/readme.md
@@ -27,4 +27,8 @@
_(Default: `false`)_ -
Whether the element should be collapsed or not.
+
+* `horizontal`
+ $ -
+ An optional attribute that permit to collapse horizontally.
diff --git a/src/collapse/test/collapseHorizontally.spec.js b/src/collapse/test/collapseHorizontally.spec.js
new file mode 100644
index 0000000000..e5b22bdb56
--- /dev/null
+++ b/src/collapse/test/collapseHorizontally.spec.js
@@ -0,0 +1,255 @@
+describe('collapse directive', function() {
+ var elementH, compileFnH, scope, $compile, $animate, $q;
+
+ beforeEach(module('ui.bootstrap.collapse'));
+ beforeEach(module('ngAnimateMock'));
+ beforeEach(inject(function(_$rootScope_, _$compile_, _$animate_, _$q_) {
+ scope = _$rootScope_;
+ $compile = _$compile_;
+ $animate = _$animate_;
+ $q = _$q_;
+ }));
+
+ beforeEach(function() {
+ elementH = angular.element(
+ ''
+ + 'Some Content
');
+ compileFnH = $compile(elementH);
+ angular.element(document.body).append(elementH);
+ });
+
+ afterEach(function() {
+ elementH.remove();
+ });
+
+ function initCallbacks() {
+ scope.collapsing = jasmine.createSpy('scope.collapsing');
+ scope.collapsed = jasmine.createSpy('scope.collapsed');
+ scope.expanding = jasmine.createSpy('scope.expanding');
+ scope.expanded = jasmine.createSpy('scope.expanded');
+ }
+
+ function assertCallbacks(expected) {
+ ['collapsing', 'collapsed', 'expanding', 'expanded'].forEach(function(cbName) {
+ if (expected[cbName]) {
+ expect(scope[cbName]).toHaveBeenCalled();
+ } else {
+ expect(scope[cbName]).not.toHaveBeenCalled();
+ }
+ });
+ }
+
+ it('should be hidden on initialization if isCollapsed = true', function() {
+ initCallbacks();
+ scope.isCollapsed = true;
+ compileFnH(scope);
+ scope.$digest();
+ expect(elementH.width()).toBe(0);
+ assertCallbacks({ collapsed: true });
+ });
+
+ it('should not trigger any animation on initialization if isCollapsed = true', function() {
+ var wrapperFn = function() {
+ $animate.flush();
+ };
+
+ scope.isCollapsed = true;
+ compileFnH(scope);
+ scope.$digest();
+
+ expect(wrapperFn).toThrowError(/No pending animations ready to be closed or flushed/);
+ });
+
+ it('should collapse if isCollapsed = true on subsequent use', function() {
+ scope.isCollapsed = false;
+ compileFnH(scope);
+ scope.$digest();
+ initCallbacks();
+ scope.isCollapsed = true;
+ scope.$digest();
+ $animate.flush();
+ expect(elementH.width()).toBe(0);
+ assertCallbacks({ collapsing: true, collapsed: true });
+ });
+
+ it('should show after toggled from collapsed', function() {
+ initCallbacks();
+ scope.isCollapsed = true;
+ compileFnH(scope);
+ scope.$digest();
+ expect(elementH.width()).toBe(0);
+ assertCallbacks({ collapsed: true });
+ scope.collapsed.calls.reset();
+
+ scope.isCollapsed = false;
+ scope.$digest();
+ $animate.flush();
+ expect(elementH.width()).not.toBe(0);
+ assertCallbacks({ expanding: true, expanded: true });
+ });
+
+ it('should not trigger any animation on initialization if isCollapsed = false', function() {
+ var wrapperFn = function() {
+ $animate.flush();
+ };
+
+ scope.isCollapsed = false;
+ compileFnH(scope);
+ scope.$digest();
+
+ expect(wrapperFn).toThrowError(/No pending animations ready to be closed or flushed/);
+ });
+
+ it('should expand if isCollapsed = false on subsequent use', function() {
+ scope.isCollapsed = false;
+ compileFnH(scope);
+ scope.$digest();
+ scope.isCollapsed = true;
+ scope.$digest();
+ $animate.flush();
+ initCallbacks();
+ scope.isCollapsed = false;
+ scope.$digest();
+ $animate.flush();
+ expect(elementH.width()).not.toBe(0);
+ assertCallbacks({ expanding: true, expanded: true });
+ });
+
+ it('should collapse if isCollapsed = true on subsequent uses', function() {
+ scope.isCollapsed = false;
+ compileFnH(scope);
+ scope.$digest();
+ scope.isCollapsed = true;
+ scope.$digest();
+ $animate.flush();
+ scope.isCollapsed = false;
+ scope.$digest();
+ $animate.flush();
+ initCallbacks();
+ scope.isCollapsed = true;
+ scope.$digest();
+ $animate.flush();
+ expect(elementH.width()).toBe(0);
+ assertCallbacks({ collapsing: true, collapsed: true });
+ });
+
+ it('should change aria-expanded attribute', function() {
+ scope.isCollapsed = false;
+ compileFnH(scope);
+ scope.$digest();
+ expect(elementH.attr('aria-expanded')).toBe('true');
+
+ scope.isCollapsed = true;
+ scope.$digest();
+ $animate.flush();
+ expect(elementH.attr('aria-expanded')).toBe('false');
+ });
+
+ it('should change aria-hidden attribute', function() {
+ scope.isCollapsed = false;
+ compileFnH(scope);
+ scope.$digest();
+ expect(elementH.attr('aria-hidden')).toBe('false');
+
+ scope.isCollapsed = true;
+ scope.$digest();
+ $animate.flush();
+ expect(elementH.attr('aria-hidden')).toBe('true');
+ });
+
+ describe('expanding callback returning a promise', function() {
+ var defer, collapsedWidth;
+
+ beforeEach(function() {
+ defer = $q.defer();
+
+ scope.isCollapsed = true;
+ scope.expanding = function() {
+ return defer.promise;
+ };
+ compileFnH(scope);
+ scope.$digest();
+ collapsedWidth = elementH.width();
+
+ // set flag to expand ...
+ scope.isCollapsed = false;
+ scope.$digest();
+
+ // ... shouldn't expand yet ...
+ expect(elementH.attr('aria-expanded')).not.toBe('true');
+ expect(elementH.width()).toBe(collapsedWidth);
+ });
+
+ it('should wait for it to resolve before animating', function() {
+ defer.resolve();
+
+ // should now expand
+ scope.$digest();
+ $animate.flush();
+
+ expect(elementH.attr('aria-expanded')).toBe('true');
+ expect(elementH.width()).toBeGreaterThan(collapsedWidth);
+ });
+
+ it('should not animate if it rejects', function() {
+ defer.reject();
+
+ // should NOT expand
+ scope.$digest();
+
+ expect(elementH.attr('aria-expanded')).not.toBe('true');
+ expect(elementH.width()).toBe(collapsedWidth);
+ });
+ });
+
+ describe('collapsing callback returning a promise', function() {
+ var defer, expandedWidth;
+
+ beforeEach(function() {
+ defer = $q.defer();
+ scope.isCollapsed = false;
+ scope.collapsing = function() {
+ return defer.promise;
+ };
+ compileFnH(scope);
+ scope.$digest();
+
+ expandedWidth = elementH.width();
+
+ // set flag to collapse ...
+ scope.isCollapsed = true;
+ scope.$digest();
+
+ // ... but it shouldn't collapse yet ...
+ expect(elementH.attr('aria-expanded')).not.toBe('false');
+ expect(elementH.width()).toBe(expandedWidth);
+ });
+
+ it('should wait for it to resolve before animating', function() {
+ defer.resolve();
+
+ // should now collapse
+ scope.$digest();
+ $animate.flush();
+
+ expect(elementH.attr('aria-expanded')).toBe('false');
+ expect(elementH.width()).toBeLessThan(expandedWidth);
+ });
+
+ it('should not animate if it rejects', function() {
+ defer.reject();
+
+ // should NOT collapse
+ scope.$digest();
+
+ expect(elementH.attr('aria-expanded')).not.toBe('false');
+ expect(elementH.width()).toBe(expandedWidth);
+ });
+ });
+
+});