DropdownShell.tsx
1 import React, { Component } from 'react'; 2 import classnames from 'classnames'; 3 4 interface Props { 5 ariaLabel: string; 6 disabled?: boolean; 7 size?: string; 8 color?: string; 9 renderLabel(): any; 10 renderOptions(): any; 11 } 12 13 interface State { 14 expanded: boolean; 15 } 16 17 export default class DropdownComponent extends Component<Props, State> { 18 public static defaultProps = { 19 color: 'default', 20 size: 'sm' 21 }; 22 23 public state: State = { 24 expanded: false 25 }; 26 27 private dropdown: HTMLElement | null; 28 29 public componentDidMount() { 30 document.addEventListener('click', this.clickOffHandler); 31 } 32 33 public componentWillUnmount() { 34 document.removeEventListener('click', this.clickOffHandler); 35 } 36 37 public render() { 38 const { ariaLabel, color, disabled, size, renderOptions, renderLabel } = this.props; 39 const { expanded } = this.state; 40 const toggleClasses = classnames(['dropdown-toggle', 'btn', `btn-${color}`, `btn-${size}`]); 41 42 return ( 43 <span 44 className={`dropdown ${expanded || disabled ? 'open' : ''}`} 45 ref={el => (this.dropdown = el)} 46 > 47 <a 48 tabIndex={0} 49 aria-haspopup="true" 50 aria-expanded={expanded} 51 aria-label={ariaLabel} 52 className={toggleClasses} 53 onClick={this.toggle} 54 > 55 {renderLabel()} 56 {!disabled && <i className="caret" />} 57 </a> 58 {expanded && !disabled && renderOptions()} 59 </span> 60 ); 61 } 62 63 public toggle = () => { 64 this.setState({ expanded: !this.state.expanded }); 65 }; 66 67 public open = () => { 68 this.setState({ expanded: true }); 69 }; 70 71 public close = () => { 72 this.setState({ expanded: false }); 73 }; 74 75 private clickOffHandler = (ev: MouseEvent) => { 76 // Only calculate if dropdown is open & we have the ref 77 if (!this.state.expanded || !this.dropdown) { 78 return; 79 } 80 81 // If it's an element that's not inside of the dropdown, close it up 82 if ( 83 this.dropdown !== ev.target && 84 ev.target instanceof HTMLElement && 85 !this.dropdown.contains(ev.target) 86 ) { 87 this.setState({ expanded: false }); 88 } 89 }; 90 }