Skip to content

Commit

Permalink
[added] Collapse Component, replaces CollapsibleMixin
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed Jul 14, 2015
1 parent 3a0b4da commit 0503507
Show file tree
Hide file tree
Showing 6 changed files with 513 additions and 64 deletions.
28 changes: 28 additions & 0 deletions docs/examples/Collapse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class Example extends React.Component {
constructor(...args){
super(...args);

this.state = {};
}

render(){

return (
<div>
<Button onClick={ ()=> this.setState({ open: !this.state.open })}>
click
</Button>
<Collapse in={this.state.open}>
<div>
<Well>
Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid.
Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident.
</Well>
</div>
</Collapse>
</div>
);
}
}

React.render(<Example/>, mountNode);
200 changes: 200 additions & 0 deletions src/Collapse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*eslint-disable react/prop-types */
'use strict';
import React from 'react';
import Transition from './Transition';
import domUtils from './utils/domUtils';
import createChainedFunction from './utils/createChainedFunction';

let capitalize = str => str[0].toUpperCase() + str.substr(1);

// reading a dimension prop will cause the browser to recalculate,
// which will let our animations work
let triggerBrowserReflow = node => node.offsetHeight; //eslint-disable-line no-unused-expressions

const MARGINS = {
height: ['marginTop', 'marginBottom'],
width: ['marginLeft', 'marginRight']
};

function getDimensionValue(dimension, elem){
let value = elem[`offset${capitalize(dimension)}`];
let computedStyles = domUtils.getComputedStyles(elem);
let margins = MARGINS[dimension];

return (value +
parseInt(computedStyles[margins[0]], 10) +
parseInt(computedStyles[margins[1]], 10)
);
}

class Collapse extends React.Component {

constructor(props, context){
super(props, context);

this.onEnterListener = this.handleEnter.bind(this);
this.onEnteringListener = this.handleEntering.bind(this);
this.onEnteredListener = this.handleEntered.bind(this);
this.onExitListener = this.handleExit.bind(this);
this.onExitingListener = this.handleExiting.bind(this);
}

render() {
let enter = createChainedFunction(this.onEnterListener, this.props.onEnter);
let entering = createChainedFunction(this.onEnteringListener, this.props.onEntering);
let entered = createChainedFunction(this.onEnteredListener, this.props.onEntered);
let exit = createChainedFunction(this.onExitListener, this.props.onExit);
let exiting = createChainedFunction(this.onExitingListener, this.props.onExiting);

return (
<Transition
ref='transition'
{...this.props}
aria-expanded={this.props.in}
className={this._dimension() === 'width' ? 'width' : ''}
exitedClassName='collapse'
exitingClassName='collapsing'
enteredClassName='collapse in'
enteringClassName='collapsing'
onEnter={enter}
onEntering={entering}
onEntered={entered}
onExit={exit}
onExiting={exiting}
onExited={this.props.onExited}
>
{ this.props.children }
</Transition>
);
}

/* -- Expanding -- */
handleEnter(elem){
let dimension = this._dimension();
elem.style[dimension] = '0';
}

handleEntering(elem){
let dimension = this._dimension();

elem.style[dimension] = this._getScrollDimensionValue(elem, dimension);
}

handleEntered(elem){
let dimension = this._dimension();
elem.style[dimension] = null;
}

/* -- Collapsing -- */
handleExit(elem){
let dimension = this._dimension();

elem.style[dimension] = this.props.getDimensionValue(dimension, elem) + 'px';
}

handleExiting(elem){
let dimension = this._dimension();

triggerBrowserReflow(elem);
elem.style[dimension] = '0';
}

_dimension(){
return typeof this.props.dimension === 'function'
? this.props.dimension()
: this.props.dimension;
}

//for testing
_getTransitionInstance(){
return this.refs.transition;
}

_getScrollDimensionValue(elem, dimension){
return elem[`scroll${capitalize(dimension)}`] + 'px';
}
}

Collapse.propTypes = {
/**
* Collapse the Component in or out.
*/
in: React.PropTypes.bool,

/**
* Provide the durration of the animation in milliseconds, used to ensure that finishing callbacks are fired even if the
* original browser transition end events are canceled.
*/
duration: React.PropTypes.number,

/**
* Specifies the dimension used when collapsing.
*
* _Note: Bootstrap only partially supports this!
* You will need to supply your own css animation for the `.width` css class._
*/
dimension: React.PropTypes.oneOfType([
React.PropTypes.oneOf(['height', 'width']),
React.PropTypes.func
]),

/**
* A function that returns the height or width of the animating DOM node. Allows for providing some custom logic how much
* Collapse component should animation in its specified dimension.
*
* `getDimensionValue` is called with the current dimension prop value and the DOM node.
*/
getDimensionValue: React.PropTypes.func,

/**
* A Callback fired before the component starts to expand.
*/
onEnter: React.PropTypes.func,

/**
* A Callback fired immediately after the component starts to expand.
*/
onEntering: React.PropTypes.func,

/**
* A Callback fired after the component has expanded.
*/
onEntered: React.PropTypes.func,

/**
* A Callback fired before the component starts to collapse.
*/
onExit: React.PropTypes.func,

/**
* A Callback fired immediately after the component starts to collapse.
*/
onExiting: React.PropTypes.func,

/**
* A Callback fired after the component has collapsed.
*/
onExited: React.PropTypes.func,

/**
* Specify whether the transitioning component should be unmounted (removed from the DOM) once the exit animation finishes.
*/
unmountOnExit: React.PropTypes.bool,

/**
* Specify whether the component should collapse or expand when it mounts.
*/
transitionAppear: React.PropTypes.bool
};

Collapse.defaultProps = {
in: false,
duration: 300,
dimension: 'height',
transitionAppear: false,
unmountOnExit: false,
getDimensionValue
};

export default Collapse;

54 changes: 30 additions & 24 deletions src/CollapsibleNav.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React, { cloneElement } from 'react';
import BootstrapMixin from './BootstrapMixin';
import CollapsibleMixin from './CollapsibleMixin';
import Collapse from './Collapse';
import classNames from 'classnames';
import domUtils from './utils/domUtils';

import ValidComponentChildren from './utils/ValidComponentChildren';
import createChainedFunction from './utils/createChainedFunction';

const CollapsibleNav = React.createClass({
mixins: [BootstrapMixin, CollapsibleMixin],
mixins: [BootstrapMixin],

propTypes: {
onSelect: React.PropTypes.func,
Expand All @@ -19,41 +18,48 @@ const CollapsibleNav = React.createClass({
eventKey: React.PropTypes.any
},

getCollapsibleDOMNode() {
return React.findDOMNode(this);
},

getCollapsibleDimensionValue() {
let height = 0;
let nodes = this.refs;
for (let key in nodes) {
if (nodes.hasOwnProperty(key)) {
// getCollapsibleDimensionValue() {
// let height = 0;
// let nodes = this.refs;
// for (let key in nodes) {
// if (nodes.hasOwnProperty(key)) {

let n = React.findDOMNode(nodes[key]);
let h = n.offsetHeight;
let computedStyles = domUtils.getComputedStyles(n);
// let n = React.findDOMNode(nodes[key]);
// let h = n.offsetHeight;
// let computedStyles = domUtils.getComputedStyles(n);

height += (h +
parseInt(computedStyles.marginTop, 10) +
parseInt(computedStyles.marginBottom, 10)
);
}
}
return height;
},
// height += (h +
// parseInt(computedStyles.marginTop, 10) +
// parseInt(computedStyles.marginBottom, 10)
// );
// }
// }
// return height;
// },

render() {
/*
* this.props.collapsible is set in NavBar when an eventKey is supplied.
*/
const classes = this.props.collapsible ? this.getCollapsibleClassSet('navbar-collapse') : null;
const classes = this.props.collapsible ? 'navbar-collapse' : null;
const renderChildren = this.props.collapsible ? this.renderCollapsibleNavChildren : this.renderChildren;

return (
let nav = (
<div eventKey={this.props.eventKey} className={classNames(this.props.className, classes)} >
{ValidComponentChildren.map(this.props.children, renderChildren)}
</div>
);

if ( this.props.collapsible ){
return (
<Collapse in={this.props.expanded}>
{ nav }
</Collapse>
);
} else {
return nav;
}
},

getChildActiveProp(child) {
Expand Down
30 changes: 10 additions & 20 deletions src/Nav.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React, { cloneElement } from 'react';
import BootstrapMixin from './BootstrapMixin';
import CollapsibleMixin from './CollapsibleMixin';
import Collapse from './Collapse';
import classNames from 'classnames';
import domUtils from './utils/domUtils';

import ValidComponentChildren from './utils/ValidComponentChildren';
import createChainedFunction from './utils/createChainedFunction';

const Nav = React.createClass({
mixins: [BootstrapMixin, CollapsibleMixin],
mixins: [BootstrapMixin],

propTypes: {
activeHref: React.PropTypes.string,
Expand All @@ -27,33 +26,24 @@ const Nav = React.createClass({

getDefaultProps() {
return {
bsClass: 'nav'
bsClass: 'nav',
expanded: true
};
},

getCollapsibleDOMNode() {
return React.findDOMNode(this);
},

getCollapsibleDimensionValue() {
let node = React.findDOMNode(this.refs.ul);
let height = node.offsetHeight;
let computedStyles = domUtils.getComputedStyles(node);

return height + parseInt(computedStyles.marginTop, 10) + parseInt(computedStyles.marginBottom, 10);
},

render() {
const classes = this.props.collapsible ? this.getCollapsibleClassSet('navbar-collapse') : null;
const classes = this.props.collapsible ? 'navbar-collapse' : null;

if (this.props.navbar && !this.props.collapsible) {
return (this.renderUl());
}

return (
<nav {...this.props} className={classNames(this.props.className, classes)}>
{ this.renderUl() }
</nav>
<Collapse in={this.props.expanded}>
<nav {...this.props} className={classNames(this.props.className, classes)}>
{this.renderUl()}
</nav>
</Collapse>
);
},

Expand Down

0 comments on commit 0503507

Please sign in to comment.