react-conventions
Version:
An open source set of React components that implement Ambassador's Design and UX patterns.
154 lines (135 loc) • 4.52 kB
JavaScript
import React from 'react'
import ReactDOM from 'react-dom'
import enhanceWithClickOutside from 'react-click-outside'
import Button from '../Button/Button'
import style from './style.scss'
import classNames from 'classnames/bind'
import Immutable from 'immutable'
export class Dropdown extends React.Component {
constructor(props) {
super(props)
}
static defaultProps = {
isOpened: false
}
static propTypes = {
/**
* A callback function to be called when dropdown isOpen state changes.
*/
changeCallback: React.PropTypes.func,
/**
* Whether the dropdown is visible.
*/
isOpened: React.PropTypes.bool,
/**
* Optional styles to add to the button.
*/
optClass: React.PropTypes.string,
/**
* An element to pass as a target (number, string, node).
*/
trigger: React.PropTypes.oneOfType([
React.PropTypes.number,
React.PropTypes.string,
React.PropTypes.node
]),
/**
* Optional array of items used in a dropdown list
*/
listItems: React.PropTypes.array
}
state = {
isOpened: this.props.isOpened,
listItems: this.props.listItems ? Immutable.fromJS(this.props.listItems) : Immutable.fromJS([]),
clickedItem: null
}
componentWillMount = () => {
if (this.props.isOpened) {
this.setState({isOpened: true})
}
}
componentWillReceiveProps = (nextProps) => {
if (nextProps.isOpened !== this.state.isOpened) {
this.setState({isOpened: !!nextProps.isOpened})
}
if (nextProps.listItems && !Immutable.is(Immutable.fromJS(nextProps.listItems), this.state.listItems)) {
this.setState({listItems: Immutable.fromJS(nextProps.listItems)})
}
}
toggleDropdown = (e) => {
e.preventDefault()
this.setState({isOpened: !this.state.isOpened}, () => {
if (typeof this.props.changeCallback === 'function') {
this.props.changeCallback(this.state.isOpened)
}
})
}
handleClickOutside = () => {
if (!this.state.isOpened) return
this.setState({isOpened: false, confirmationOverlayOpen: false, clickedItem: null}, () => {
if (typeof this.props.changeCallback === 'function') {
this.props.changeCallback(this.state.isOpened)
}
})
}
listItemCallback = (item) => {
this.setState({isOpened: false, confirmationOverlayOpen: false, clickedItem: null})
if (typeof item.callback === 'function') {
item.callback(item.name)
}
}
handleConfirmation = (confirm) => {
if (confirm) {
this.listItemCallback(this.state.clickedItem)
}
else {
this.setState({ confirmationOverlayOpen: false, clickedItem: null })
}
}
handleItemClick = (item) => {
if (item.callbackConfirmation) {
this.setState({ confirmationOverlayOpen: true, clickedItem: item })
}
else {
this.listItemCallback(item)
}
}
render() {
const cx = classNames.bind(style)
const isOpenedClass = this.state.isOpened ? style['is-opened'] : null
const dropdownClasses = cx(style['dropdown-component'], this.props.optClass, isOpenedClass)
const dropdownWrapperClasses = cx(style['dropdown-wrapper'], (this.props.listItems ? style['dropdown-wrapper-flush'] : null))
const listItems = this.state.listItems.toJS()
const listItemNodes = listItems instanceof Array
? listItems.map((item, index) =>
<li key={index} onClick={this.handleItemClick.bind(this, item)}>{item.name}</li>
)
: []
return (
<div className={dropdownClasses}>
<span className={style.trigger} onClick={this.toggleDropdown}>{this.props.trigger}</span>
<div className={dropdownWrapperClasses}>
{
listItemNodes.length > 0 && !this.state.confirmationOverlayOpen
? <ul className={style['list-wrapper']}>
{listItemNodes}
</ul>
: this.props.children
}
{
this.state.confirmationOverlayOpen
? <div className={style.overlay}>
<span>Are you sure?</span>
<div className={style['button-wrapper']}>
<Button onClick={this.handleConfirmation.bind(this, false)} optClass='danger-alt'>Cancel</Button>
<Button onClick={this.handleConfirmation.bind(this, true)}>Yes</Button>
</div>
</div>
: null
}
</div>
</div>
)
}
}
export default enhanceWithClickOutside(Dropdown)