Skip to content

Commit 750aaf9

Browse files
committed
feat(Dropdowns): basic dropdown, toggle, menu & menu items
1 parent 95ba591 commit 750aaf9

10 files changed

Lines changed: 607 additions & 1 deletion

lib/Dropdown.jsx

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React, { PropTypes } from 'react';
2+
import classNames from 'classnames';
3+
import omit from 'lodash.omit';
4+
5+
const propTypes = {
6+
disabled: PropTypes.bool,
7+
open: PropTypes.bool,
8+
tag: PropTypes.string
9+
};
10+
11+
const defaultProps = {
12+
open: false,
13+
tag: 'div'
14+
};
15+
16+
class Dropdown extends React.Component {
17+
constructor(props) {
18+
super(props);
19+
20+
this.state = {
21+
open: props.open
22+
};
23+
24+
this.closeDropdown = this.closeDropdown.bind(this);
25+
this.handleDocumentClick = this.handleDocumentClick.bind(this);
26+
this.handleContainerClick = this.handleContainerClick.bind(this);
27+
this.toggleDropdown = this.toggleDropdown.bind(this);
28+
}
29+
30+
componentWillUnmount() {
31+
this.removeDocumentEventListener();
32+
}
33+
34+
removeDocumentEventListener() {
35+
document.removeEventListener('click', this.handleDocumentClick);
36+
}
37+
38+
handleDocumentClick() {
39+
if (this.state.open) {
40+
this.removeDocumentEventListener();
41+
this.setState({
42+
open: !this.state.open
43+
});
44+
}
45+
}
46+
47+
handleContainerClick(e) {
48+
if (e.nativeEvent && e.nativeEvent.stopImmediatePropagation) {
49+
e.nativeEvent.stopImmediatePropagation();
50+
}
51+
}
52+
53+
toggleDropdown(e) {
54+
if (this.props.disabled) {
55+
return e.preventDefault();
56+
}
57+
58+
if (this.state.open) {
59+
document.removeEventListener('click', this.handleDocumentClick);
60+
} else {
61+
document.addEventListener('click', this.handleDocumentClick);
62+
}
63+
64+
this.setState({
65+
open: !this.state.open
66+
});
67+
}
68+
69+
closeDropdown() {
70+
if (this.state.open) {
71+
this.setState({
72+
open: false
73+
});
74+
}
75+
}
76+
77+
renderChildren() {
78+
return React.Children.map(React.Children.toArray(this.props.children), (child) => {
79+
if (React.isValidElement(child)) {
80+
return React.cloneElement(
81+
child,
82+
{
83+
isDropdownOpen: this.state.open,
84+
handleContainerClick: this.handleContainerClick,
85+
toggleDropdown: this.toggleDropdown
86+
}
87+
);
88+
}
89+
return child;
90+
});
91+
}
92+
93+
render() {
94+
const {
95+
className,
96+
'tag': TagName,
97+
...attributes
98+
} = omit(this.props, ['children', 'open']);
99+
100+
const classes = classNames(
101+
className,
102+
'dropdown',
103+
{ 'open': this.state.open }
104+
);
105+
106+
return (
107+
<TagName {...attributes}
108+
className={classes}>
109+
{this.renderChildren()}
110+
</TagName>
111+
);
112+
}
113+
}
114+
115+
Dropdown.propTypes = propTypes;
116+
Dropdown.defaultProps = defaultProps;
117+
118+
export default Dropdown;

lib/DropdownItem.jsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React, { PropTypes } from 'react';
2+
import classNames from 'classnames';
3+
import omit from 'lodash.omit';
4+
5+
const propTypes = {
6+
children: PropTypes.node,
7+
disabled: PropTypes.bool,
8+
divider: PropTypes.bool,
9+
El: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
10+
header: PropTypes.bool
11+
};
12+
13+
class DropdownItem extends React.Component {
14+
render() {
15+
let Tagname = 'button';
16+
const {
17+
className,
18+
children,
19+
divider,
20+
El,
21+
header,
22+
...props } = this.props;
23+
24+
const classes = classNames(
25+
className,
26+
{
27+
'dropdown-item': !divider && !header,
28+
'dropdown-header': header,
29+
'dropdown-divider': divider
30+
}
31+
);
32+
33+
if (El) {
34+
return (
35+
<El {...props}
36+
className={classes}
37+
onClick={this.onClick}>
38+
{children}
39+
</El>
40+
);
41+
}
42+
43+
if (header) {
44+
Tagname = 'h6';
45+
} else if (divider) {
46+
Tagname = 'div';
47+
}
48+
49+
return (
50+
<Tagname {...props} className={classes}>
51+
{children}
52+
</Tagname>
53+
);
54+
}
55+
}
56+
57+
DropdownItem.propTypes = propTypes;
58+
59+
export default DropdownItem;

lib/DropdownMenu.jsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, { PropTypes } from 'react';
2+
import classNames from 'classnames';
3+
import omit from 'lodash.omit';
4+
5+
const propTypes = {
6+
children: PropTypes.node.isRequired,
7+
handleContainerClick: PropTypes.func,
8+
isDropdownOpen: PropTypes.bool,
9+
onClick: PropTypes.func,
10+
right: PropTypes.bool,
11+
toggleDropdown: PropTypes.func
12+
};
13+
14+
class DropdownMenu extends React.Component {
15+
constructor(props) {
16+
super(props);
17+
18+
this.onClick = this.onClick.bind(this);
19+
}
20+
21+
onClick(e) {
22+
if (this.props.handleContainerClick) {
23+
this.props.handleContainerClick(e);
24+
}
25+
26+
if (this.props.onClick) {
27+
this.props.onClick(e);
28+
}
29+
}
30+
31+
render() {
32+
const { className, children, right, ...props } = omit(this.props, 'onClick');
33+
const classes = classNames(
34+
className,
35+
'dropdown-menu',
36+
{ 'dropdown-menu-right': right }
37+
);
38+
39+
return (
40+
<div {...props} className={classes} onClick={this.onClick}>
41+
{children}
42+
</div>
43+
);
44+
}
45+
}
46+
47+
DropdownMenu.propTypes = propTypes;
48+
49+
export default DropdownMenu;

lib/DropdownToggle.jsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, { PropTypes } from 'react';
2+
import classNames from 'classnames';
3+
4+
const propTypes = {
5+
caret: PropTypes.bool,
6+
children: PropTypes.node.isRequired,
7+
color: PropTypes.string,
8+
disabled: PropTypes.bool,
9+
handleContainerClick: PropTypes.func,
10+
isDropdownOpen: PropTypes.bool,
11+
onClick: PropTypes.func,
12+
toggleDropdown: PropTypes.func
13+
};
14+
15+
const defaultProps = {
16+
'data-toggle': 'dropdown',
17+
'aria-haspopup': true,
18+
color: 'secondary'
19+
};
20+
21+
class DropdownToggle extends React.Component {
22+
constructor(props) {
23+
super(props);
24+
25+
this.onClick = this.onClick.bind(this);
26+
}
27+
28+
onClick(e) {
29+
if (this.props.disabled) {
30+
return e.preventDefault();
31+
}
32+
33+
if (this.props.onClick) {
34+
this.props.onClick(e);
35+
}
36+
37+
if (this.props.toggleDropdown) {
38+
this.props.toggleDropdown(e);
39+
}
40+
}
41+
42+
render() {
43+
const { className, children, caret, color, ...props } = this.props;
44+
const classes = classNames(
45+
className,
46+
{ 'dropdown-toggle': caret }
47+
);
48+
const buttonClasses = classNames(
49+
classes,
50+
'btn',
51+
'btn-' + color
52+
);
53+
54+
if (React.isValidElement(children)) {
55+
return React.cloneElement(React.Children.only(children), {
56+
...props,
57+
className: classes,
58+
onClick: this.onClick,
59+
'aria-expanded': props.isDropdownOpen
60+
});
61+
}
62+
63+
return (
64+
<button {...props}
65+
className={buttonClasses}
66+
onClick={this.onClick}
67+
aria-expanded={props.isDropdownOpen}>
68+
{children}
69+
</button>
70+
);
71+
}
72+
}
73+
74+
DropdownToggle.propTypes = propTypes;
75+
DropdownToggle.defaultProps = defaultProps;
76+
77+
export default DropdownToggle;

lib/index.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import Button from './Button';
22
import ButtonGroup from './ButtonGroup';
33
import ButtonToolbar from './ButtonToolbar';
4+
import Dropdown from './Dropdown';
5+
import DropdownItem from './DropdownItem';
6+
import DropdownMenu from './DropdownMenu';
7+
import DropdownToggle from './DropdownToggle';
48

59
export {
610
Button,
711
ButtonGroup,
8-
ButtonToolbar
12+
ButtonToolbar,
13+
Dropdown,
14+
DropdownItem,
15+
DropdownMenu,
16+
DropdownToggle
917
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"homepage": "https://github.com/eddywashere/reactstrap#readme",
2929
"dependencies": {
3030
"classnames": "^2.2.3",
31+
"lodash.omit": "^4.1.0",
3132
"react": "^0.14.7",
3233
"react-dom": "^0.14.7"
3334
},

0 commit comments

Comments
 (0)