@shopify/polaris
Version:
Shopify’s product component library
97 lines (96 loc) • 4 kB
JavaScript
import React, { createContext, createRef, } from 'react';
import { read } from '@shopify/javascript-utilities/fastdom';
import { classNames } from '../../utilities/css';
import styles from './Collapsible.scss';
const ParentCollapsibleExpandingContext = createContext(false);
class Collapsible extends React.Component {
constructor() {
super(...arguments);
this.state = {
height: null,
animationState: 'idle',
// eslint-disable-next-line react/no-unused-state
open: this.props.open,
};
this.node = createRef();
this.heightNode = createRef();
this.handleTransitionEnd = (event) => {
const { target } = event;
if (target === this.node.current) {
this.setState({ animationState: 'idle', height: null });
}
};
}
static getDerivedStateFromProps({ open: willOpen }, { open, animationState: prevAnimationState }) {
let nextAnimationState = prevAnimationState;
if (open !== willOpen) {
nextAnimationState = 'measuring';
}
return {
animationState: nextAnimationState,
open: willOpen,
};
}
componentDidUpdate({ open: wasOpen }) {
const { animationState } = this.state;
const parentCollapsibleExpanding = this.context;
if (parentCollapsibleExpanding && animationState !== 'idle') {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
animationState: 'idle',
});
return;
}
read(() => {
const heightNode = this.heightNode.current;
switch (animationState) {
case 'idle':
break;
case 'measuring':
this.setState({
animationState: wasOpen ? 'closingStart' : 'openingStart',
height: wasOpen && heightNode ? heightNode.scrollHeight : 0,
});
break;
case 'closingStart':
this.setState({
animationState: 'closing',
height: 0,
});
break;
case 'openingStart':
this.setState({
animationState: 'opening',
height: heightNode ? heightNode.scrollHeight : 0,
});
}
});
}
render() {
const { id, open, children } = this.props;
const { animationState, height } = this.state;
const parentCollapsibleExpanding = this.context;
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 (<ParentCollapsibleExpandingContext.Provider value={parentCollapsibleExpanding || (open && animationState !== 'idle')}>
<div id={id} aria-hidden={!open} style={{ maxHeight: displayHeight }} className={wrapperClassName} ref={this.node} onTransitionEnd={this.handleTransitionEnd}>
<div ref={this.heightNode}>{content}</div>
</div>
</ParentCollapsibleExpandingContext.Provider>);
}
}
Collapsible.contextType = ParentCollapsibleExpandingContext;
function collapsibleHeight(open, animationState, height) {
if (animationState === 'idle' && open) {
return open ? 'none' : undefined;
}
if (animationState === 'measuring') {
return open ? undefined : 'none';
}
return `${height || 0}px`;
}
// Use named export once we work out why not casting this breaks web
// eslint-disable-next-line import/no-default-export
export default Collapsible;