react-collapsible
Version:
React component to wrap content in Collapsible element with trigger to open and close.
259 lines (232 loc) • 7.33 kB
JavaScript
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Collapsible extends Component {
constructor(props) {
super(props)
// Bind class methods
this.handleTriggerClick = this.handleTriggerClick.bind(this);
this.handleTransitionEnd = this.handleTransitionEnd.bind(this);
this.continueOpenCollapsible = this.continueOpenCollapsible.bind(this);
// Defaults the dropdown to be closed
if (props.open) {
this.state = {
isClosed: false,
shouldSwitchAutoOnNextCycle: false,
height: 'auto',
transition: 'none',
hasBeenOpened: true,
overflow: props.overflowWhenOpen,
inTransition: false,
}
} else {
this.state = {
isClosed: true,
shouldSwitchAutoOnNextCycle: false,
height: 0,
transition: `height ${props.transitionTime}ms ${props.easing}`,
hasBeenOpened: false,
overflow: 'hidden',
inTransition: false,
}
}
}
componentDidUpdate(prevProps, prevState) {
if(this.state.shouldOpenOnNextCycle){
this.continueOpenCollapsible();
}
if (prevState.height === 'auto' && this.state.shouldSwitchAutoOnNextCycle === true) {
window.setTimeout(() => { // Set small timeout to ensure a true re-render
this.setState({
height: 0,
overflow: 'hidden',
isClosed: true,
shouldSwitchAutoOnNextCycle: false,
});
}, 50);
}
// If there has been a change in the open prop (controlled by accordion)
if (prevProps.open !== this.props.open) {
if(this.props.open === true) {
this.openCollapsible();
this.props.onOpening();
} else {
this.closeCollapsible();
this.props.onClosing();
}
}
}
closeCollapsible() {
this.setState({
shouldSwitchAutoOnNextCycle: true,
height: this.refs.inner.offsetHeight,
transition: `height ${this.props.transitionTime}ms ${this.props.easing}`,
inTransition: true,
});
}
openCollapsible() {
this.setState({
inTransition: true,
shouldOpenOnNextCycle: true,
});
}
continueOpenCollapsible() {
this.setState({
height: this.refs.inner.offsetHeight,
transition: `height ${this.props.transitionTime}ms ${this.props.easing}`,
isClosed: false,
hasBeenOpened: true,
inTransition: true,
shouldOpenOnNextCycle: false,
});
}
handleTriggerClick(event) {
event.preventDefault();
if (this.props.triggerDisabled) {
return
}
if (this.props.handleTriggerClick) {
this.props.handleTriggerClick(this.props.accordionPosition);
} else {
if (this.state.isClosed === true) {
this.openCollapsible();
this.props.onOpening();
} else {
this.closeCollapsible();
this.props.onClosing();
}
}
}
renderNonClickableTriggerElement() {
if (this.props.triggerSibling && typeof this.props.triggerSibling === 'string') {
return (
<span className={`${this.props.classParentString}__trigger-sibling`}>{this.props.triggerSibling}</span>
)
} else if(this.props.triggerSibling) {
return <this.props.triggerSibling />
}
return null;
}
handleTransitionEnd() {
// Switch to height auto to make the container responsive
if (!this.state.isClosed) {
this.setState({ height: 'auto', overflow: this.props.overflowWhenOpen, inTransition: false });
this.props.onOpen();
} else {
this.setState({ inTransition: false });
this.props.onClose();
}
}
render() {
var dropdownStyle = {
height: this.state.height,
WebkitTransition: this.state.transition,
msTransition: this.state.transition,
transition: this.state.transition,
overflow: this.state.overflow,
}
var openClass = this.state.isClosed ? 'is-closed' : 'is-open';
var disabledClass = this.props.triggerDisabled ? 'is-disabled' : '';
//If user wants different text when tray is open
var trigger = (this.state.isClosed === false) && (this.props.triggerWhenOpen !== undefined)
? this.props.triggerWhenOpen
: this.props.trigger;
// Don't render children until the first opening of the Collapsible if lazy rendering is enabled
var children = this.props.lazyRender
&& !this.state.hasBeenOpened
&& this.state.isClosed
&& !this.state.inTransition ? null : this.props.children;
// Construct CSS classes strings
const triggerClassString = `${this.props.classParentString}__trigger ${openClass} ${disabledClass} ${
this.state.isClosed ? this.props.triggerClassName : this.props.triggerOpenedClassName
}`;
const parentClassString = `${this.props.classParentString} ${
this.state.isClosed ? this.props.className : this.props.openedClassName
}`;
const outerClassString = `${this.props.classParentString}__contentOuter ${this.props.contentOuterClassName}`;
const innerClassString = `${this.props.classParentString}__contentInner ${this.props.contentInnerClassName}`;
return(
<div className={parentClassString.trim()}>
<span
className={triggerClassString.trim()}
onClick={this.handleTriggerClick}>
{trigger}
</span>
{this.renderNonClickableTriggerElement()}
<div
className={outerClassString.trim()}
ref="outer"
style={dropdownStyle}
onTransitionEnd={this.handleTransitionEnd}
>
<div
className={innerClassString.trim()}
ref="inner"
>
{children}
</div>
</div>
</div>
);
}
}
Collapsible.propTypes = {
transitionTime: PropTypes.number,
easing: PropTypes.string,
open: PropTypes.bool,
classParentString: PropTypes.string,
openedClassName: PropTypes.string,
triggerClassName: PropTypes.string,
triggerOpenedClassName: PropTypes.string,
contentOuterClassName: PropTypes.string,
contentInnerClassName: PropTypes.string,
accordionPosition: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
handleTriggerClick: PropTypes.func,
onOpen: PropTypes.func,
onClose: PropTypes.func,
onOpening: PropTypes.func,
onClosing: PropTypes.func,
trigger: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element
]),
triggerWhenOpen:PropTypes.oneOfType([
PropTypes.string,
PropTypes.element
]),
triggerDisabled: PropTypes.bool,
lazyRender: PropTypes.bool,
overflowWhenOpen: PropTypes.oneOf([
'hidden',
'visible',
'auto',
'scroll',
'inherit',
'initial',
'unset',
]),
triggerSibling: PropTypes.oneOfType([
PropTypes.element,
PropTypes.func,
]),
}
Collapsible.defaultProps = {
transitionTime: 400,
easing: 'linear',
open: false,
classParentString: 'Collapsible',
triggerDisabled: false,
lazyRender: false,
overflowWhenOpen: 'hidden',
openedClassName: '',
triggerClassName: '',
triggerOpenedClassName: '',
contentOuterClassName: '',
contentInnerClassName: '',
className: '',
triggerSibling: null,
onOpen: () => {},
onClose: () => {},
onOpening: () => {},
onClosing: () => {},
};
export default Collapsible;