@hackplan/polaris
Version:
Shopify’s product component library
95 lines (94 loc) • 3.51 kB
JavaScript
import React from 'react';
import { addEventListener, removeEventListener, } from '@shopify/javascript-utilities/events';
import { read } from '@shopify/javascript-utilities/fastdom';
import { classNames } from '../../utilities/css';
import { withAppProvider } from '../AppProvider';
import styles from './Collapsible.scss';
export class Collapsible extends React.Component {
constructor() {
super(...arguments);
this.state = {
height: null,
animationState: 'idle',
};
this.node = null;
this.heightNode = null;
this.bindNode = (node) => {
this.node = node;
};
this.bindHeightNode = (node) => {
this.heightNode = node;
};
this.handleTransitionEnd = (event) => {
const { target } = event;
if (target === this.node) {
this.setState({ animationState: 'idle', height: null });
}
};
}
componentWillReceiveProps({ open: willOpen }) {
const { open } = this.props;
if (open !== willOpen) {
this.setState({ animationState: 'measuring' });
}
}
componentDidUpdate({ open: wasOpen }) {
const { animationState } = this.state;
read(() => {
switch (animationState) {
case 'idle':
break;
case 'measuring':
this.setState({
animationState: wasOpen ? 'closingStart' : 'openingStart',
height: wasOpen && this.heightNode ? this.heightNode.scrollHeight : 0,
});
break;
case 'closingStart':
this.setState({
animationState: 'closing',
height: 0,
});
break;
case 'openingStart':
this.setState({
animationState: 'opening',
height: this.heightNode ? this.heightNode.scrollHeight : 0,
});
}
});
}
componentDidMount() {
if (this.node == null) {
return;
}
addEventListener(this.node, 'transitionend', this.handleTransitionEnd);
}
componentWillUnmount() {
if (this.node == null) {
return;
}
removeEventListener(this.node, 'transitionend', this.handleTransitionEnd);
}
render() {
const { id, open, children } = this.props;
const { animationState, height } = this.state;
const animating = animationState !== 'idle';
const wrapperClassName = classNames(styles.Collapsible, open && styles.open, animating && styles.animating, !animating && open && styles.fullyOpen);
const displayHeight = collapsibleHeight(open, animationState, height);
const content = animating || open ? children : null;
return (<div id={id} aria-hidden={!open} style={{ height: displayHeight }} className={wrapperClassName} ref={this.bindNode}>
<div ref={this.bindHeightNode}>{content}</div>
</div>);
}
}
function collapsibleHeight(open, animationState, height) {
if (animationState === 'idle' && open) {
return open ? 'auto' : undefined;
}
if (animationState === 'measuring') {
return open ? undefined : 'auto';
}
return `${height || 0}px`;
}
export default withAppProvider()(Collapsible);