UNPKG

react-cm-ui

Version:
352 lines (306 loc) 11.2 kB
import React from 'react'; import _ from 'lodash'; import ClassNames from 'classnames'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import domUtils from '../../utils/domUtils'; import Icon from '../../dataDisplay/icon'; class AccordionItem extends React.Component { constructor(props) { super(props); this.state = { isSelected: props.isSelected }; this._onAnimationComplete = this._onAnimationComplete.bind(this); this._onClick = this._onClick.bind(this); } componentDidUpdate(prevProps) { if (this.props.isSelected !== prevProps.isSelected) { this.setState({ isSelected: this.props.isSelected }); } } render() { const { children, className, style, subAccordion, summary, } = this.props; const { isSelected } = this.state; const containerClasses = ClassNames('accordion-item', className, { 'accordion-item-is-active': isSelected, 'accordion-item-sub-accordion': subAccordion, 'accordion-item-no-summary': summary === false, }); return ( <div className={containerClasses} onClick={this._onClick} style={style}> {children} </div> ); } _animationProps(el) { let a; const animations = { animation: 'animationend', OAnimation: 'oAnimationEnd', MozAnimation: 'animationend', WebkitAnimation: 'webkitAnimationEnd', }; for (a in animations) { if (el.style[a] !== undefined) { return animations[a]; } } } _onAnimationComplete() { const itemElement = ReactDOM.findDOMNode(this); const animationEvent = this._animationProps(itemElement); itemElement.removeEventListener(animationEvent, this._onAnimationComplete); const { scrollContainer, scrollContainerMarginHeight } = this.props; const scrollContainerEl = _.isString(scrollContainer) ? ReactDOM.findDOMNode(document.querySelector(scrollContainer)) : _.isObject(scrollContainer) ? scrollContainer : null; const containerHeight = scrollContainerEl ? scrollContainerEl.offsetHeight - (scrollContainerMarginHeight || 0) : window.innerHeight; const scrollPosition = domUtils.scrollPos(scrollContainerEl); const containerBottom = containerHeight + scrollPosition; const itemElHeight = itemElement.offsetHeight; const itemElTopYPosition = itemElement.offsetTop; const itemElBottomYPosition = itemElTopYPosition + itemElHeight; const belowFold = itemElBottomYPosition - (containerHeight + scrollPosition); if (containerBottom < itemElBottomYPosition) { domUtils.scrollTo(scrollPosition + belowFold, null, scrollContainerEl); } } _onClick() { const { isSelected } = this.state; if (!isSelected) { const itemElement = ReactDOM.findDOMNode(this); const animationEvent = this._animationProps(itemElement); itemElement.addEventListener(animationEvent, this._onAnimationComplete); } } } AccordionItem.propTypes = { className: PropTypes.string, isSelected: PropTypes.bool, scrollContainer: PropTypes.oneOfType([ PropTypes.shape({}), PropTypes.string, ]), scrollContainerMarginHeight: PropTypes.number, style: PropTypes.shape({}), subAccordion: PropTypes.bool, summary: PropTypes.bool, }; class AccordionSummary extends React.Component { constructor() { super(); this._onClick = this._onClick.bind(this); } render() { return ( <div className="accordion-item-summary" onClick={this._onClick}> {this.props.children} </div> ); } _onClick() { this.props.onClick(); } } AccordionSummary.propTypes = { onClick: PropTypes.func, }; class AccordionCheckbox extends React.Component { render() { return ( <div className="accordion-checkbox" style={{ flex: '0 1 auto', minWidth: '44px', }} > {this.props.children} </div> ); } } AccordionCheckbox.propTypes = { className: PropTypes.string, style: PropTypes.shape({}), }; class AccordionContent extends React.Component { render() { return ( <div className="accordion-item-content" style={this.props.style}> {this.props.children} </div> ); } } AccordionContent.propTypes = { style: PropTypes.shape({}), }; class Accordion extends React.Component { constructor(props) { super(props); let newSelected; if (props.exclusive === false) { newSelected = props.selected || []; } else { newSelected = _.isNil(props.selected) ? -1 : props.selected; } this.state = { selected: newSelected }; } componentDidUpdate(prevProps, prevState) { const { selected: prevSelectedProp } = prevProps; const { exclusive, selected: nextSelectedProp } = this.props; if (!_.isEqual(prevSelectedProp, nextSelectedProp)) { let nextSelectedState; if (exclusive === false) { nextSelectedState = nextSelectedProp || []; } else { nextSelectedState = _.isNil(nextSelectedProp) ? -1 : nextSelectedProp; } this.setState({ selected: nextSelectedState }); } } render() { const { basic, children, className, exclusive, id, inverse, scrollContainer, scrollContainerMarginHeight, style, } = this.props; const { selected } = this.state; const containerClasses = ClassNames('ui', 'accordion', className, { 'accordion-basic': basic, 'accordion-inverse': inverse, 'accordion-inclusive': exclusive === false, }); const convertedChildren = _.isArray(children) ? children : [children]; const items = _.map(convertedChildren, (child, index) => { const { children, onClick, style, subTitle, subAccordion, summary, title, } = child.props; const isSelected = exclusive === false ? _.includes(selected, index) : selected === index; if (title) { return ( <AccordionItem isSelected={isSelected} key={`accordion-item-${index}`} scrollContainer={scrollContainer} scrollContainerMarginHeight={scrollContainerMarginHeight} style={style} subAccordion={subAccordion} summary={summary} > <div className="accordion-item-title" onClick={this._onSummaryClick.bind(this, index, onClick)} > <span className="copy"> {title} {subTitle ? <span className="padding-left">{subTitle}</span> : null} </span> <Icon compact inverse={inverse} rotate={isSelected ? 135 : 0} title={isSelected ? 'Collapse' : 'Expand'} type="plus" /> </div> <AccordionContent> {children} </AccordionContent> </AccordionItem> ); } if (summary === false) { return ( <AccordionItem isSelected={isSelected} key={`accordion-item-${index}`} scrollContainer={scrollContainer} scrollContainerMarginHeight={scrollContainerMarginHeight} style={style} subAccordion={subAccordion} summary={summary} > <AccordionContent> {children} </AccordionContent> </AccordionItem> ); } return ( <AccordionItem isSelected={isSelected} key={`accordion-item-${index}`} scrollContainer={scrollContainer} scrollContainerMarginHeight={scrollContainerMarginHeight} style={style} subAccordion={subAccordion} summary={summary} > {React.Children.map(children, (c, i) => { if (i === 0) { return ( <AccordionSummary onClick={this._onSummaryClick.bind(this, index, c.props.onClick)}> {c.props.children} </AccordionSummary> ); } return c; })} </AccordionItem> ); }); return ( <div className={containerClasses} id={id} style={style} > {items} </div> ); } _onSummaryClick(index, onClick) { const { exclusive } = this.props; const { selected } = this.state; let newSelected; if (exclusive === false) { const isSelected = _.includes(selected, index); newSelected = isSelected ? _.pull(selected, index) : _.union(selected, [index]); } else { newSelected = selected === index ? -1 : index; } if (_.isFunction(onClick)) { onClick(newSelected); } this.setState({ selected: newSelected }); } } Accordion.Content = AccordionContent; Accordion.Checkbox = AccordionCheckbox; Accordion.Item = AccordionItem; Accordion.Summary = AccordionSummary; Accordion.propTypes = { basic: PropTypes.bool, className: PropTypes.string, exclusive: PropTypes.bool, id: PropTypes.string, inverse: PropTypes.bool, scrollContainer: PropTypes.oneOfType([ PropTypes.shape({}), PropTypes.string, ]), scrollContainerMarginHeight: PropTypes.number, selected: PropTypes.oneOfType([ PropTypes.array, PropTypes.number, ]), style: PropTypes.shape({}), }; export default Accordion;