Skip to content

Commit

Permalink
[fixed] CollapsableMixin fixed size
Browse files Browse the repository at this point in the history
* Fixes expand/collapse animation for stand-alone panel
* Fixes #399, where the panel would stay a fixed size
* Added basic CollapsableParagraph example
* Added aria-expanded attributes to Panel

Fixes #399
  • Loading branch information
joemcbride committed Mar 9, 2015
1 parent f7808bd commit de6f7dd
Show file tree
Hide file tree
Showing 8 changed files with 424 additions and 86 deletions.
37 changes: 37 additions & 0 deletions docs/examples/CollapsableParagraph.js
@@ -0,0 +1,37 @@
var CollapsableParagraph = React.createClass({
mixins: [CollapsableMixin],

getCollapsableDOMNode: function(){
return this.refs.panel.getDOMNode();
},

getCollapsableDimensionValue: function(){
return this.refs.panel.getDOMNode().scrollHeight;
},

onHandleToggle: function(e){
e.preventDefault();
this.setState({expanded:!this.state.expanded});
},

render: function(){
var styles = this.getCollapsableClassSet();
var text = this.isExpanded() ? 'Hide' : 'Show';
return (
<div>
<Button onClick={this.onHandleToggle}>{text} Content</Button>
<div ref="panel" className={classSet(styles)}>
{this.props.children}
</div>
</div>
);
}
});

var panelInstance = (
<CollapsableParagraph>
Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS.
</CollapsableParagraph>
);

React.render(panelInstance, mountNode);
4 changes: 4 additions & 0 deletions docs/src/ComponentsPage.js
Expand Up @@ -207,6 +207,10 @@ var ComponentsPage = React.createClass({
<h3 id="panels-accordion">Accordions</h3>
<p><code>&lt;Accordion /&gt;</code> aliases <code>&lt;PanelGroup accordion /&gt;</code>.</p>
<ReactPlayground codeText={fs.readFileSync(__dirname + '/../examples/PanelGroupAccordion.js', 'utf8')} />

<h3 id="panels-collapsable">Collapsable Mixin</h3>
<p><code>CollapsableMixin</code> can be used to create your own components with collapse functionality.</p>
<ReactPlayground codeText={fs.readFileSync(__dirname + '/../examples/CollapsableParagraph.js', 'utf8')} />
</div>

<div className="bs-docs-section">
Expand Down
1 change: 1 addition & 0 deletions docs/src/ReactPlayground.js
Expand Up @@ -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');
Expand Down
166 changes: 107 additions & 59 deletions 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) {
Expand Down
54 changes: 30 additions & 24 deletions src/Panel.jsx
Expand Up @@ -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,
Expand All @@ -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 () {
Expand Down Expand Up @@ -69,7 +70,11 @@ var Panel = React.createClass({

renderCollapsableBody: function () {
return (
<div className={classSet(this.getCollapsableClassSet('panel-collapse'))} id={this.props.id} ref="panel">
<div
className={classSet(this.getCollapsableClassSet('panel-collapse'))}
id={this.props.id}
ref="panel"
aria-expanded={this.isExpanded() ? 'true' : 'false'}>
{this.renderBody()}
</div>
);
Expand All @@ -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};
Expand All @@ -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)) {
Expand All @@ -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 () {
Expand Down Expand Up @@ -168,6 +173,7 @@ var Panel = React.createClass({
<a
href={'#' + (this.props.id || '')}
className={this.isExpanded() ? null : 'collapsed'}
aria-expanded={this.isExpanded() ? 'true' : 'false'}
onClick={this.handleSelect}>
{header}
</a>
Expand Down
4 changes: 3 additions & 1 deletion src/PanelGroup.jsx
Expand Up @@ -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);
Expand Down

0 comments on commit de6f7dd

Please sign in to comment.