diff --git a/docs/src/ReactPlayground.js b/docs/src/ReactPlayground.js
index 7ff19f1b02..8a23add10d 100644
--- a/docs/src/ReactPlayground.js
+++ b/docs/src/ReactPlayground.js
@@ -8,6 +8,7 @@ var Badge = require('../../lib/Badge');
var Button = require('../../lib/Button');
var ButtonGroup = require('../../lib/ButtonGroup');
var ButtonToolbar = require('../../lib/ButtonToolbar');
+var CollapsableMixin = require('../../lib/CollapsableMixin');
var Carousel = require('../../lib/Carousel');
var CarouselItem = require('../../lib/CarouselItem');
var Col = require('../../lib/Col');
diff --git a/src/CollapsableMixin.js b/src/CollapsableMixin.js
index 6cd877de33..201ed69fe4 100644
--- a/src/CollapsableMixin.js
+++ b/src/CollapsableMixin.js
@@ -1,101 +1,149 @@
var React = require('react');
-var TransitionEvents = require('./utils/TransitionEvents');
+var TransitionEvents = require('react/lib/ReactTransitionEvents');
var CollapsableMixin = {
propTypes: {
- collapsable: React.PropTypes.bool,
defaultExpanded: React.PropTypes.bool,
expanded: React.PropTypes.bool
},
- getInitialState: function () {
+ getInitialState: function(){
+ var defaultExpanded = this.props.defaultExpanded != null ?
+ this.props.defaultExpanded :
+ this.props.expanded != null ?
+ this.props.expanded :
+ false;
+
return {
- expanded: this.props.defaultExpanded != null ? this.props.defaultExpanded : null,
+ expanded: defaultExpanded,
collapsing: false
};
},
- handleTransitionEnd: function () {
- this._collapseEnd = true;
- this.setState({
- collapsing: false
- });
- },
-
- componentWillReceiveProps: function (newProps) {
- if (this.props.collapsable && newProps.expanded !== this.props.expanded) {
- this._collapseEnd = false;
- this.setState({
- collapsing: true
- });
+ componentWillUpdate: function(nextProps, nextState){
+ var willExpanded = nextProps.expanded != null ? nextProps.expanded : nextState.expanded;
+ if (willExpanded === this.isExpanded()) {
+ return;
}
- },
- _addEndTransitionListener: function () {
+ // if the expanded state is being toggled, ensure node has a dimension value
+ // this is needed for the animation to work and needs to be set before
+ // the collapsing class is applied (after collapsing is applied the in class
+ // is removed and the node's dimension will be wrong)
+
var node = this.getCollapsableDOMNode();
+ var dimension = this.dimension();
+ var value = '0';
- if (node) {
- TransitionEvents.addEndEventListener(
- node,
- this.handleTransitionEnd
- );
+ if(!willExpanded){
+ value = this.getCollapsableDimensionValue();
}
+
+ node.style[dimension] = value + 'px';
+
+ this._afterWillUpdate();
},
- _removeEndTransitionListener: function () {
- var node = this.getCollapsableDOMNode();
+ componentDidUpdate: function(prevProps, prevState){
+ // check if expanded is being toggled; if so, set collapsing
+ this._checkToggleCollapsing(prevProps, prevState);
- if (node) {
- TransitionEvents.removeEndEventListener(
- node,
- this.handleTransitionEnd
- );
- }
+ // check if collapsing was turned on; if so, start animation
+ this._checkStartAnimation();
+ },
+
+ // helps enable test stubs
+ _afterWillUpdate: function(){
},
- componentDidMount: function () {
- this._afterRender();
+ _checkStartAnimation: function(){
+ if(!this.state.collapsing) {
+ return;
+ }
+
+ var node = this.getCollapsableDOMNode();
+ var dimension = this.dimension();
+ var value = this.getCollapsableDimensionValue();
+
+ // setting the dimension here starts the transition animation
+ var result;
+ if(this.isExpanded()) {
+ result = value + 'px';
+ } else {
+ result = '0px';
+ }
+ node.style[dimension] = result;
},
- componentWillUnmount: function () {
- this._removeEndTransitionListener();
+ _checkToggleCollapsing: function(prevProps, prevState){
+ var wasExpanded = prevProps.expanded != null ? prevProps.expanded : prevState.expanded;
+ var isExpanded = this.isExpanded();
+ if(wasExpanded !== isExpanded){
+ if(wasExpanded) {
+ this._handleCollapse();
+ } else {
+ this._handleExpand();
+ }
+ }
},
- componentWillUpdate: function (nextProps) {
- var dimension = (typeof this.getCollapsableDimension === 'function') ?
- this.getCollapsableDimension() : 'height';
+ _handleExpand: function(){
var node = this.getCollapsableDOMNode();
+ var dimension = this.dimension();
+
+ var complete = (function (){
+ this._removeEndEventListener(node, complete);
+ // remove dimension value - this ensures the collapsable item can grow
+ // in dimension after initial display (such as an image loading)
+ node.style[dimension] = '';
+ this.setState({
+ collapsing:false
+ });
+ }).bind(this);
+
+ this._addEndEventListener(node, complete);
- this._removeEndTransitionListener();
+ this.setState({
+ collapsing: true
+ });
},
- componentDidUpdate: function (prevProps, prevState) {
- this._afterRender();
+ _handleCollapse: function(){
+ var node = this.getCollapsableDOMNode();
+
+ var complete = (function (){
+ this._removeEndEventListener(node, complete);
+ this.setState({
+ collapsing: false
+ });
+ }).bind(this);
+
+ this._addEndEventListener(node, complete);
+
+ this.setState({
+ collapsing: true
+ });
},
- _afterRender: function () {
- if (!this.props.collapsable) {
- return;
- }
+ // helps enable test stubs
+ _addEndEventListener: function(node, complete){
+ TransitionEvents.addEndEventListener(node, complete);
+ },
- this._addEndTransitionListener();
- setTimeout(this._updateDimensionAfterRender, 0);
+ // helps enable test stubs
+ _removeEndEventListener: function(node, complete){
+ TransitionEvents.removeEndEventListener(node, complete);
},
- _updateDimensionAfterRender: function () {
- var node = this.getCollapsableDOMNode();
- if (node) {
- var dimension = (typeof this.getCollapsableDimension === 'function') ?
- this.getCollapsableDimension() : 'height';
- node.style[dimension] = this.isExpanded() ?
- this.getCollapsableDimensionValue() + 'px' : '0px';
- }
+ dimension: function(){
+ return (typeof this.getCollapsableDimension === 'function') ?
+ this.getCollapsableDimension() :
+ 'height';
},
- isExpanded: function () {
- return (this.props.expanded != null) ?
- this.props.expanded : this.state.expanded;
+ isExpanded: function(){
+ return this.props.expanded != null ? this.props.expanded : this.state.expanded;
},
getCollapsableClassSet: function (className) {
diff --git a/src/Panel.jsx b/src/Panel.jsx
index 703ba75bed..36ed087612 100644
--- a/src/Panel.jsx
+++ b/src/Panel.jsx
@@ -10,6 +10,7 @@ var Panel = React.createClass({
mixins: [BootstrapMixin, CollapsableMixin],
propTypes: {
+ collapsable: React.PropTypes.bool,
onSelect: React.PropTypes.func,
header: React.PropTypes.node,
footer: React.PropTypes.node,
@@ -23,22 +24,22 @@ var Panel = React.createClass({
};
},
- handleSelect: function (e) {
+ handleSelect: function(e){
+ e.selected = true;
+
if (this.props.onSelect) {
- this._isChanging = true;
- this.props.onSelect(this.props.eventKey);
- this._isChanging = false;
+ this.props.onSelect(e, this.props.eventKey);
+ } else {
+ e.preventDefault();
}
- e.preventDefault();
-
- this.setState({
- expanded: !this.state.expanded
- });
+ if (e.selected) {
+ this.handleToggle();
+ }
},
- shouldComponentUpdate: function () {
- return !this._isChanging;
+ handleToggle: function(){
+ this.setState({expanded:!this.state.expanded});
},
getCollapsableDimensionValue: function () {
@@ -69,7 +70,11 @@ var Panel = React.createClass({
renderCollapsableBody: function () {
return (
-
+
{this.renderBody()}
);
@@ -78,6 +83,7 @@ var Panel = React.createClass({
renderBody: function () {
var allChildren = this.props.children;
var bodyElements = [];
+ var panelBodyChildren = [];
function getProps() {
return {key: bodyElements.length};
@@ -95,24 +101,23 @@ var Panel = React.createClass({
);
}
+ function maybeRenderPanelBody () {
+ if (panelBodyChildren.length === 0) {
+ return;
+ }
+
+ addPanelBody(panelBodyChildren);
+ panelBodyChildren = [];
+ }
+
// Handle edge cases where we should not iterate through children.
- if (!Array.isArray(allChildren) || allChildren.length == 0) {
+ if (!Array.isArray(allChildren) || allChildren.length === 0) {
if (this.shouldRenderFill(allChildren)) {
addPanelChild(allChildren);
} else {
addPanelBody(allChildren);
}
} else {
- var panelBodyChildren = [];
-
- function maybeRenderPanelBody () {
- if (panelBodyChildren.length == 0) {
- return;
- }
-
- addPanelBody(panelBodyChildren);
- panelBodyChildren = [];
- }
allChildren.forEach(function(child) {
if (this.shouldRenderFill(child)) {
@@ -132,7 +137,7 @@ var Panel = React.createClass({
},
shouldRenderFill: function (child) {
- return React.isValidElement(child) && child.props.fill != null
+ return React.isValidElement(child) && child.props.fill != null;
},
renderHeading: function () {
@@ -168,6 +173,7 @@ var Panel = React.createClass({
{header}
diff --git a/src/PanelGroup.jsx b/src/PanelGroup.jsx
index eb177b4a4d..1c0d4b1b07 100644
--- a/src/PanelGroup.jsx
+++ b/src/PanelGroup.jsx
@@ -66,7 +66,9 @@ var PanelGroup = React.createClass({
return !this._isChanging;
},
- handleSelect: function (key) {
+ handleSelect: function (e, key) {
+ e.preventDefault();
+
if (this.props.onSelect) {
this._isChanging = true;
this.props.onSelect(key);
diff --git a/test/CollapsableMixinSpec.jsx b/test/CollapsableMixinSpec.jsx
new file mode 100644
index 0000000000..1c613ea800
--- /dev/null
+++ b/test/CollapsableMixinSpec.jsx
@@ -0,0 +1,220 @@
+/*global describe, it, assert */
+
+var React = require('react');
+var ReactTestUtils = require('react/lib/ReactTestUtils');
+var CollapsableMixin = require('../lib/CollapsableMixin');
+var classSet = require('../lib/utils/classSet');
+
+describe('CollapsableMixin', function () {
+
+ var Component, instance;
+
+ beforeEach(function(){
+ Component = React.createClass({
+ mixins: [CollapsableMixin],
+
+ getCollapsableDOMNode: function(){
+ return this.refs.panel.getDOMNode();
+ },
+
+ getCollapsableDimensionValue: function(){
+ return 15;
+ },
+
+ render: function(){
+ var styles = this.getCollapsableClassSet();
+ return (
+
+
+ {this.props.children}
+
+
+ );
+ }
+ });
+ });
+
+ describe('getInitialState', function(){
+ it('Should check defaultExpanded', function () {
+ instance = ReactTestUtils.renderIntoDocument(
+
Panel content
+ );
+ var state = instance.getInitialState();
+ assert.ok(state.expanded === true);
+ });
+
+ it('Should default collapsing to false', function () {
+ instance = ReactTestUtils.renderIntoDocument(
+
Panel content
+ );
+ var state = instance.getInitialState();
+ assert.ok(state.collapsing === false);
+ });
+ });
+
+ describe('collapsed', function(){
+ it('Should have collapse class', function () {
+ instance = ReactTestUtils.renderIntoDocument(
+
Panel content
+ );
+ assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'collapse'));
+ });
+ });
+
+ describe('from collapsed to expanded', function(){
+ beforeEach(function(){
+ instance = ReactTestUtils.renderIntoDocument(
+
Panel content
+ );
+ });
+
+ it('Should have collapsing class', function () {
+ instance.setProps({expanded:true});
+ var node = instance.getCollapsableDOMNode();
+ assert.equal(node.className, 'collapsing');
+ });
+
+ it('Should set initial 0px height', function () {
+ var node = instance.getCollapsableDOMNode();
+ assert.equal(node.style['height'], '');
+
+ instance._afterWillUpdate = function(){
+ assert.equal(node.style['height'], '0px');
+ };
+
+ instance.setProps({expanded:true});
+ });
+
+ it('Should set transition to height', function () {
+ var node = instance.getCollapsableDOMNode();
+ assert.equal(node.style['height'], '');
+
+ instance.setProps({expanded:true});
+ assert.equal(node.style['height'], '15px');
+ });
+
+ it('Should transition from collapsing to not collapsing', function (done) {
+ instance._addEndEventListener = function(node, complete){
+ setTimeout(function(){
+ complete();
+ assert.ok(!instance.state.collapsing);
+ done();
+ }, 100);
+ };
+ instance.setProps({expanded:true});
+ assert.ok(instance.state.collapsing);
+ });
+
+ it('Should clear height after transition complete', function (done) {
+ var node = instance.getCollapsableDOMNode();
+
+ instance._addEndEventListener = function(node, complete){
+ setTimeout(function(){
+ complete();
+ assert.equal(node.style['height'], '');
+ done();
+ }, 100);
+ };
+
+ assert.equal(node.style['height'], '');
+ instance.setProps({expanded:true});
+ assert.equal(node.style['height'], '15px');
+ });
+ });
+
+ describe('from expanded to collapsed', function(){
+ beforeEach(function(){
+ instance = ReactTestUtils.renderIntoDocument(
+
Panel content
+ );
+ });
+
+ it('Should have collapsing class', function () {
+ instance.setProps({expanded:false});
+ var node = instance.getCollapsableDOMNode();
+ assert.equal(node.className, 'collapsing');
+ });
+
+ it('Should set initial height', function () {
+ var node = instance.getCollapsableDOMNode();
+
+ instance._afterWillUpdate = function(){
+ assert.equal(node.style['height'], '15px');
+ };
+
+ assert.equal(node.style['height'], '');
+ instance.setProps({expanded:false});
+ });
+
+ it('Should set transition to height', function () {
+ var node = instance.getCollapsableDOMNode();
+ assert.equal(node.style['height'], '');
+
+ instance.setProps({expanded:false});
+ assert.equal(node.style['height'], '0px');
+ });
+
+ it('Should transition from collapsing to not collapsing', function (done) {
+ instance._addEndEventListener = function(node, complete){
+ setTimeout(function(){
+ complete();
+ assert.ok(!instance.state.collapsing);
+ done();
+ }, 100);
+ };
+ instance.setProps({expanded:false});
+ assert.ok(instance.state.collapsing);
+ });
+
+ it('Should have 0px height after transition complete', function (done) {
+ var node = instance.getCollapsableDOMNode();
+
+ instance._addEndEventListener = function(node, complete){
+ setTimeout(function(){
+ complete();
+ assert.ok(node.style['height'] === '0px');
+ done();
+ }, 100);
+ };
+
+ assert.equal(node.style['height'], '');
+ instance.setProps({expanded:false});
+ assert.equal(node.style['height'], '0px');
+ });
+ });
+
+ describe('expanded', function(){
+ it('Should have collapse and in class', function () {
+ instance = ReactTestUtils.renderIntoDocument(
+
Panel content
+ );
+ assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'collapse in'));
+ });
+
+ it('Should have collapse and in class with defaultExpanded', function () {
+ instance = ReactTestUtils.renderIntoDocument(
+
Panel content
+ );
+ assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'collapse in'));
+ });
+ });
+
+ describe('dimension', function(){
+ beforeEach(function(){
+ instance = ReactTestUtils.renderIntoDocument(
+
Panel content
+ );
+ });
+
+ it('Defaults to height', function(){
+ assert.equal(instance.dimension(), 'height');
+ });
+
+ it('Uses getCollapsableDimension if exists', function(){
+ instance.getCollapsableDimension = function(){
+ return 'whatevs';
+ };
+ assert.equal(instance.dimension(), 'whatevs');
+ });
+ });
+});
diff --git a/test/PanelSpec.jsx b/test/PanelSpec.jsx
index 37a93c38d0..a761242204 100644
--- a/test/PanelSpec.jsx
+++ b/test/PanelSpec.jsx
@@ -113,8 +113,28 @@ describe('Panel', function () {
assert.ok(anchor.className.match(/\bcollapsed\b/));
});
+ it('Should be aria-expanded=true', function () {
+ var instance = ReactTestUtils.renderIntoDocument(
+
Panel content
+ );
+ var collapse = instance.getDOMNode().querySelector('.panel-collapse');
+ var anchor = instance.getDOMNode().querySelector('.panel-title a');
+ assert.equal(collapse.getAttribute('aria-expanded'), 'true');
+ assert.equal(anchor.getAttribute('aria-expanded'), 'true');
+ });
+
+ it('Should be aria-expanded=false', function () {
+ var instance = ReactTestUtils.renderIntoDocument(
+
Panel content
+ );
+ var collapse = instance.getDOMNode().querySelector('.panel-collapse');
+ var anchor = instance.getDOMNode().querySelector('.panel-title a');
+ assert.equal(collapse.getAttribute('aria-expanded'), 'false');
+ assert.equal(anchor.getAttribute('aria-expanded'), 'false');
+ });
+
it('Should call onSelect handler', function (done) {
- function handleSelect (key) {
+ function handleSelect (e, key) {
assert.equal(key, '1');
done();
}
@@ -174,4 +194,4 @@ describe('Panel', function () {
assert.equal(children[0].nodeName, 'TABLE');
assert.notOk(children[0].className.match(/\bpanel-body\b/));
});
-});
\ No newline at end of file
+});