Skip to content

Commit 16c0f86

Browse files
committed
feat(Dropdowns): add TetherContent support
Removes internal component state management and requires .isOpen & .toggle. This helps support state via props and composition.
1 parent 8114f61 commit 16c0f86

9 files changed

Lines changed: 366 additions & 220 deletions

lib/Dropdown.jsx

Lines changed: 101 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,35 @@
11
import React, { PropTypes } from 'react';
2+
import ReactDOM from 'react-dom';
23
import classNames from 'classnames';
34
import omit from 'lodash.omit';
5+
import TetherContent from './TetherContent';
6+
import DropdownMenu from './DropdownMenu';
7+
import DropdownToggle from './DropdownToggle';
48

59
const propTypes = {
610
disabled: PropTypes.bool,
711
dropup: PropTypes.bool,
812
group: PropTypes.bool,
9-
open: PropTypes.bool,
10-
tag: PropTypes.string
13+
isOpen: PropTypes.bool,
14+
tag: PropTypes.string,
15+
tether: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
16+
toggle: PropTypes.func
1117
};
1218

1319
const defaultProps = {
1420
open: false,
1521
tag: 'div'
1622
};
1723

24+
const defaultTetherConfig = {
25+
classPrefix: 'bs-tether',
26+
classes: { element: 'dropdown', enabled: 'open' },
27+
constraints: [
28+
{ to: 'scrollParent', attachment: 'together none' },
29+
{ to: 'window', attachment: 'together none' }
30+
]
31+
};
32+
1833
class Dropdown extends React.Component {
1934
constructor(props) {
2035
super(props);
@@ -23,72 +38,113 @@ class Dropdown extends React.Component {
2338
open: props.open
2439
};
2540

26-
this.openDropdown = this.openDropdown.bind(this);
27-
this.closeDropdown = this.closeDropdown.bind(this);
41+
this.addEvents = this.addEvents.bind(this);
42+
this.getTetherConfig = this.getTetherConfig.bind(this);
2843
this.handleDocumentClick = this.handleDocumentClick.bind(this);
29-
this.handleContainerClick = this.handleContainerClick.bind(this);
30-
this.toggleDropdown = this.toggleDropdown.bind(this);
44+
this.removeEvents = this.removeEvents.bind(this);
45+
this.toggle = this.toggle.bind(this);
3146
}
3247

3348
componentDidMount() {
34-
if (this.state.open) {
35-
this.openDropdown();
49+
this.handleProps();
50+
}
51+
52+
componentDidUpdate(prevProps) {
53+
if (this.props.isOpen !== prevProps.isOpen) {
54+
this.handleProps();
3655
}
3756
}
3857

3958
componentWillUnmount() {
40-
this.closeDropdown();
59+
this.removeEvents();
60+
}
61+
62+
getTetherConfig(childProps) {
63+
const target = () => this._target;
64+
let vElementAttach = 'top';
65+
let hElementAttach = 'left';
66+
let vTargetAttach = 'bottom';
67+
let hTargetAttach = 'left';
68+
69+
if (childProps.right) {
70+
hElementAttach = 'right';
71+
hTargetAttach = 'right';
72+
}
73+
74+
if (childProps.dropup) {
75+
vElementAttach = 'bottom';
76+
vTargetAttach = 'top';
77+
}
78+
79+
return {
80+
...defaultTetherConfig,
81+
attachment: vElementAttach + ' ' + hElementAttach,
82+
targetAttachment: vTargetAttach + ' ' + hTargetAttach,
83+
target,
84+
...this.props.tether
85+
};
4186
}
4287

43-
handleDocumentClick() {
44-
this.closeDropdown();
88+
addEvents() {
89+
document.addEventListener('click', this.handleDocumentClick);
4590
}
4691

47-
handleContainerClick(e) {
48-
if (e.nativeEvent && e.nativeEvent.stopImmediatePropagation) {
49-
e.nativeEvent.stopImmediatePropagation();
92+
removeEvents() {
93+
document.removeEventListener('click', this.handleDocumentClick);
94+
}
95+
96+
handleDocumentClick(e) {
97+
const container = ReactDOM.findDOMNode(this);
98+
99+
if (container.contains(e.target) && container !== e.target) {
100+
return;
50101
}
102+
103+
this.toggle();
51104
}
52105

53-
toggleDropdown(e) {
54-
if (this.props.disabled) {
55-
return e.preventDefault();
106+
handleProps() {
107+
if (this.props.tether) {
108+
return;
56109
}
57110

58-
if (this.state.open) {
59-
this.closeDropdown();
111+
if (this.props.isOpen) {
112+
this.addEvents();
60113
} else {
61-
this.openDropdown();
114+
this.removeEvents();
62115
}
63116
}
64117

65-
closeDropdown() {
66-
this.setState({
67-
open: false
68-
});
69-
document.removeEventListener('click', this.handleDocumentClick);
70-
}
118+
toggle(e) {
119+
if (this.props.disabled) {
120+
return e && e.preventDefault();
121+
}
71122

72-
openDropdown() {
73-
this.setState({
74-
open: true
75-
});
76-
document.addEventListener('click', this.handleDocumentClick);
123+
this.props.toggle();
77124
}
78125

79126
renderChildren() {
127+
let props = omit(this.props, ['children', 'className', 'id']);
128+
props.toggle = this.toggle;
129+
80130
return React.Children.map(React.Children.toArray(this.props.children), (child) => {
81131
if (React.isValidElement(child)) {
82-
return React.cloneElement(
83-
child,
84-
{
85-
closeDropdown: this.closeDropdown,
86-
handleContainerClick: this.handleContainerClick,
87-
isDropdownOpen: this.state.open,
88-
openDropdown: this.openDropdown,
89-
toggleDropdown: this.toggleDropdown,
90-
}
91-
);
132+
if (child.type === DropdownToggle) {
133+
return React.cloneElement(child, {
134+
...props,
135+
ref: (c) => this._target = c
136+
});
137+
} else if (child.type === DropdownMenu && !props.isOpen) {
138+
// don't bother with hidden content
139+
return null;
140+
} else if (child.type === DropdownMenu && props.tether) {
141+
let tetherConfig = this.getTetherConfig(child.props);
142+
143+
return (
144+
<TetherContent {...props} tether={tetherConfig}>{child}</TetherContent>
145+
);
146+
}
147+
return React.cloneElement(child, props);
92148
}
93149
return child;
94150
});
@@ -101,13 +157,14 @@ class Dropdown extends React.Component {
101157
group,
102158
'tag': TagName,
103159
...attributes
104-
} = omit(this.props, ['children', 'open']);
160+
} = omit(this.props, ['children', 'isOpen']);
105161

106162
const classes = classNames(
107163
className,
108-
group ? 'btn-group' : 'dropdown',
109164
{
110-
open: this.state.open,
165+
'btn-group': group,
166+
dropdown: !group,
167+
open: this.props.isOpen,
111168
dropup: dropup
112169
}
113170
);

lib/DropdownItem.jsx

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,20 @@ import omit from 'lodash.omit';
44

55
const propTypes = {
66
children: PropTypes.node,
7-
closeDropdown: PropTypes.func,
87
disabled: PropTypes.bool,
98
divider: PropTypes.bool,
109
El: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
11-
handleContainerClick: PropTypes.func,
1210
header: PropTypes.bool,
13-
isDropdownOpen: PropTypes.bool,
14-
toggleDropdown: PropTypes.func
11+
isOpen: PropTypes.bool,
12+
toggle: PropTypes.func
1513
};
1614

1715
class DropdownItem extends React.Component {
1816
constructor(props) {
1917
super(props);
2018

2119
this.onClick = this.onClick.bind(this);
22-
this.onClose = this.onClose.bind(this);
20+
this.getTabIndex = this.getTabIndex.bind(this);
2321
}
2422

2523
onClick(e) {
@@ -31,21 +29,20 @@ class DropdownItem extends React.Component {
3129
this.props.onClick(e);
3230
}
3331

34-
this.onClose();
32+
this.props.toggle();
3533
}
3634

37-
onClose(e) {
38-
if (this.props.onClose) {
39-
this.props.onClose(e);
35+
getTabIndex() {
36+
if (this.props.disabled || this.props.header || this.props.divider) {
37+
return '-1';
4038
}
4139

42-
if(this.props.closeDropdown) {
43-
this.props.closeDropdown();
44-
}
40+
return '0';
4541
}
4642

4743
render() {
4844
let Tagname = 'button';
45+
const tabIndex = this.getTabIndex();
4946
const {
5047
className,
5148
children,
@@ -67,8 +64,8 @@ class DropdownItem extends React.Component {
6764
if (El) {
6865
return (
6966
<El {...props}
67+
tabIndex={tabIndex}
7068
className={classes}
71-
onClose={this.onClose}
7269
onClick={this.onClick}>
7370
{children}
7471
</El>
@@ -83,8 +80,8 @@ class DropdownItem extends React.Component {
8380

8481
return (
8582
<Tagname {...props}
83+
tabIndex={tabIndex}
8684
className={classes}
87-
onClose={this.onClose}
8885
onClick={this.onClick}>
8986
{children}
9087
</Tagname>

lib/DropdownMenu.jsx

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,30 @@ import omit from 'lodash.omit';
44

55
const propTypes = {
66
children: PropTypes.node.isRequired,
7-
closeDropdown: PropTypes.func,
8-
handleContainerClick: PropTypes.func,
9-
isDropdownOpen: PropTypes.bool,
10-
onClick: PropTypes.func,
11-
openDropdown: PropTypes.func,
7+
isOpen: PropTypes.bool,
128
right: PropTypes.bool,
13-
toggleDropdown: PropTypes.func
9+
toggle: PropTypes.func
1410
};
1511

1612
class DropdownMenu extends React.Component {
1713
constructor(props) {
1814
super(props);
19-
20-
this.onClick = this.onClick.bind(this);
2115
}
2216

23-
onClick(e) {
24-
if (this.props.handleContainerClick) {
25-
this.props.handleContainerClick(e);
26-
}
27-
28-
if (this.props.onClick) {
29-
this.props.onClick(e);
30-
}
31-
}
17+
// onClick(e) {
18+
// if (this.props.onClick) {
19+
// this.props.onClick(e);
20+
// }
21+
// }
3222

3323
renderChildren() {
3424
return React.Children.map(React.Children.toArray(this.props.children), (child) => {
3525
if (React.isValidElement(child)) {
3626
return React.cloneElement(
3727
child,
3828
{
39-
closeDropdown: this.props.closeDropdown,
40-
handleContainerClick: this.props.handleContainerClick,
41-
isDropdownOpen: this.props.isDropdownOpen,
42-
openDropdown: this.props.openDropdown,
43-
toggleDropdown: this.props.toggleDropdown
29+
isOpen: this.props.isOpen,
30+
toggle: this.props.toggle
4431
}
4532
);
4633
}
@@ -49,15 +36,19 @@ class DropdownMenu extends React.Component {
4936
}
5037

5138
render() {
52-
const { className, right, ...props } = omit(this.props, 'onClick', 'children');
39+
const { className, right, ...props } = omit(this.props, 'children');
5340
const classes = classNames(
5441
className,
5542
'dropdown-menu',
5643
{ 'dropdown-menu-right': right }
5744
);
5845

46+
if (!this.props.isOpen) {
47+
return null;
48+
}
49+
5950
return (
60-
<div {...props} className={classes} onClick={this.onClick}>
51+
<div {...props} tabIndex="-1" aria-hidden="false" role="menu" className={classes}>
6152
{this.renderChildren()}
6253
</div>
6354
);

0 commit comments

Comments
 (0)