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