/ common / components / ui / DropdownShell.tsx
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  }