react-collapse
Version:
Component-wrapper for collapse animation for elements with variable (and dynamic) height
181 lines (136 loc) • 4.48 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
export class Collapse extends React.Component {
static propTypes = {
theme: PropTypes.shape({
collapse: PropTypes.string,
content: PropTypes.string
}),
isOpened: PropTypes.bool.isRequired,
initialStyle: PropTypes.shape({
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
overflow: PropTypes.string
}),
onRest: PropTypes.func,
onWork: PropTypes.func,
checkTimeout: PropTypes.number,
children: PropTypes.node.isRequired
};
static defaultProps = {
theme: {
collapse: 'ReactCollapse--collapse',
content: 'ReactCollapse--content'
},
initialStyle: undefined,
onRest: undefined,
onWork: undefined,
checkTimeout: 50
};
timeout = undefined;
container = undefined;
content = undefined;
constructor(props) {
super(props);
if (props.initialStyle) {
this.initialStyle = props.initialStyle;
} else {
this.initialStyle = props.isOpened
? {height: 'auto', overflow: 'initial'}
: {height: '0px', overflow: 'hidden'};
}
}
componentDidMount() {
this.onResize();
}
shouldComponentUpdate(nextProps) {
const {theme, isOpened, children} = this.props;
return children !== nextProps.children
|| isOpened !== nextProps.isOpened
|| Object.keys(theme).some(c => theme[c] !== nextProps.theme[c]);
}
getSnapshotBeforeUpdate() {
if (!this.container || !this.content) {
return null;
}
if (this.container.style.height === 'auto') {
const {clientHeight: contentHeight} = this.content;
this.container.style.height = `${contentHeight}px`;
}
return null;
}
componentDidUpdate() {
this.onResize();
}
componentWillUnmount() {
clearTimeout(this.timeout);
}
onResize = () => {
clearTimeout(this.timeout);
if (!this.container || !this.content) {
return;
}
const {isOpened, checkTimeout} = this.props;
const containerHeight = Math.floor(this.container.clientHeight);
const contentHeight = Math.floor(this.content.clientHeight);
const isFullyOpened = isOpened && Math.abs(contentHeight - containerHeight) <= 1;
const isFullyClosed = !isOpened && Math.abs(containerHeight) <= 1;
if (isFullyOpened || isFullyClosed) {
this.onRest({isFullyOpened, isFullyClosed, isOpened, containerHeight, contentHeight});
} else {
this.onWork({isFullyOpened, isFullyClosed, isOpened, containerHeight, contentHeight});
this.timeout = setTimeout(() => this.onResize(), checkTimeout);
}
};
onRest = ({isFullyOpened, isFullyClosed, isOpened, containerHeight, contentHeight}) => {
if (!this.container || !this.content) {
return;
}
const hasOpened = isOpened && this.container.style.height === `${contentHeight}px`;
const hasClosed = !isOpened && this.container.style.height === '0px';
if (hasOpened || hasClosed) {
this.container.style.overflow = isOpened ? 'initial' : 'hidden';
this.container.style.height = isOpened ? 'auto' : '0px';
const {onRest} = this.props;
if (onRest) {
onRest({isFullyOpened, isFullyClosed, isOpened, containerHeight, contentHeight});
}
}
};
onWork = ({isFullyOpened, isFullyClosed, isOpened, containerHeight, contentHeight}) => {
if (!this.container || !this.content) {
return;
}
const isOpenining = isOpened && this.container.style.height === `${contentHeight}px`;
const isClosing = !isOpened && this.container.style.height === '0px';
if (isOpenining || isClosing) {
// No need to do any work
return;
}
this.container.style.overflow = 'hidden';
this.container.style.height = isOpened ? `${contentHeight}px` : '0px';
const {onWork} = this.props;
if (onWork) {
onWork({isFullyOpened, isFullyClosed, isOpened, containerHeight, contentHeight});
}
};
onRefContainer = container => {
this.container = container;
};
onRefContent = content => {
this.content = content;
};
render() {
const {theme, children, isOpened} = this.props;
return (
<div
ref={this.onRefContainer}
className={theme.collapse}
style={this.initialStyle}
aria-hidden={!isOpened}>
<div ref={this.onRefContent} className={theme.content}>
{children}
</div>
</div>
);
}
}