Skip to content
This repository has been archived by the owner on May 29, 2019. It is now read-only.

feat(collapse): add possibility to collapse horizontally #6010

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 47 additions & 19 deletions src/collapse/collapse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the same CSS is in both if blocks - that can be combined.

Copy link
Contributor

@wesleycho wesleycho Jun 16, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is different CSS - note the inversion of order of values

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is this good ? I can't combine because that's not the same value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, it's fine

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gah! sorry. i totally missed that.

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() {
Expand All @@ -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);
}
});
Expand All @@ -46,7 +74,7 @@ angular.module('ui.bootstrap.collapse', [])
function expandDone() {
Copy link
Contributor

@wesleycho wesleycho Jun 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modify function so we can DRY this up, i.e.

function expandDone(prop) {
  return function() {
    var css = {};
    css[prop] = 'auto';
    ...
  };
}

Or just pass the desired object wholesale

element.removeClass('collapsing')
.addClass('collapse')
.css({height: 'auto'});
.css(css);
expandedExpr(scope);
}

Expand All @@ -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')
Expand All @@ -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);
Expand Down
8 changes: 7 additions & 1 deletion src/collapse/docs/demo.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<div ng-controller="CollapseDemoCtrl">
<button type="button" class="btn btn-default" ng-click="isCollapsed = !isCollapsed">Toggle collapse</button>
<button type="button" class="btn btn-default" ng-click="isCollapsed = !isCollapsed">Toggle collapse Vertically</button>
<hr>
<div uib-collapse="isCollapsed">
<div class="well well-lg">Some content</div>
</div>

<button type="button" class="btn btn-default" ng-click="isCollapsedHorizontal = !isCollapsedHorizontal">Toggle collapse Horizontally</button>
<hr>
<div uib-collapse="isCollapsedHorizontal" horizontal>
<div class="well well-lg">Some content</div>
</div>
</div>
1 change: 1 addition & 0 deletions src/collapse/docs/demo.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
angular.module('ui.bootstrap.demo').controller('CollapseDemoCtrl', function ($scope) {
$scope.isCollapsed = false;
$scope.isCollapsedHorizontal = false;
});
4 changes: 4 additions & 0 deletions src/collapse/docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,8 @@
<i class="glyphicon glyphicon-eye-open"></i>
_(Default: `false`)_ -
Whether the element should be collapsed or not.

* `horizontal`
<small class="badge">$</small> -
An optional attribute that permit to collapse horizontally.

255 changes: 255 additions & 0 deletions src/collapse/test/collapseHorizontally.spec.js
Original file line number Diff line number Diff line change
@@ -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(
'<div uib-collapse="isCollapsed" '
+ 'expanding="expanding()" '
+ 'expanded="expanded()" '
+ 'collapsing="collapsing()" '
+ 'collapsed="collapsed()" '
+ 'horizontal>'
+ 'Some Content</div>');
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);
});
});

});