Skip to content

Commit 87596e4

Browse files
committed
feat(Dropdowns): pass more methods to children
1 parent 96627ef commit 87596e4

5 files changed

Lines changed: 160 additions & 44 deletions

File tree

lib/Dropdown.jsx

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,26 @@ class Dropdown extends React.Component {
2323
open: props.open
2424
};
2525

26+
this.openDropdown = this.openDropdown.bind(this);
2627
this.closeDropdown = this.closeDropdown.bind(this);
2728
this.handleDocumentClick = this.handleDocumentClick.bind(this);
2829
this.handleContainerClick = this.handleContainerClick.bind(this);
2930
this.toggleDropdown = this.toggleDropdown.bind(this);
3031
}
3132

32-
componentWillUnmount() {
33-
this.removeDocumentEventListener();
33+
componentDidMount() {
34+
if (this.state.open) {
35+
this.openDropdown();
36+
}
3437
}
3538

36-
removeDocumentEventListener() {
37-
document.removeEventListener('click', this.handleDocumentClick);
39+
componentWillUnmount() {
40+
this.closeDropdown();
3841
}
3942

4043
handleDocumentClick() {
4144
if (this.state.open) {
42-
this.removeDocumentEventListener();
43-
this.setState({
44-
open: !this.state.open
45-
});
45+
this.closeDropdown();
4646
}
4747
}
4848

@@ -58,22 +58,24 @@ class Dropdown extends React.Component {
5858
}
5959

6060
if (this.state.open) {
61-
document.removeEventListener('click', this.handleDocumentClick);
61+
this.closeDropdown();
6262
} else {
63-
document.addEventListener('click', this.handleDocumentClick);
63+
this.openDropdown();
6464
}
65+
}
6566

67+
closeDropdown() {
6668
this.setState({
67-
open: !this.state.open
69+
open: false
6870
});
71+
document.removeEventListener('click', this.handleDocumentClick);
6972
}
7073

71-
closeDropdown() {
72-
if (this.state.open) {
73-
this.setState({
74-
open: false
75-
});
76-
}
74+
openDropdown() {
75+
this.setState({
76+
open: true
77+
});
78+
document.addEventListener('click', this.handleDocumentClick);
7779
}
7880

7981
renderChildren() {
@@ -82,9 +84,11 @@ class Dropdown extends React.Component {
8284
return React.cloneElement(
8385
child,
8486
{
85-
isDropdownOpen: this.state.open,
87+
closeDropdown: this.closeDropdown,
8688
handleContainerClick: this.handleContainerClick,
87-
toggleDropdown: this.toggleDropdown
89+
isDropdownOpen: this.state.open,
90+
openDropdown: this.openDropdown,
91+
toggleDropdown: this.toggleDropdown,
8892
}
8993
);
9094
}

lib/DropdownItem.jsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,46 @@ import omit from 'lodash.omit';
44

55
const propTypes = {
66
children: PropTypes.node,
7+
closeDropdown: PropTypes.func,
78
disabled: PropTypes.bool,
89
divider: PropTypes.bool,
910
El: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
10-
header: PropTypes.bool
11+
handleContainerClick: PropTypes.func,
12+
header: PropTypes.bool,
13+
isDropdownOpen: PropTypes.bool,
14+
toggleDropdown: PropTypes.func
1115
};
1216

1317
class DropdownItem extends React.Component {
18+
constructor(props) {
19+
super(props);
20+
21+
this.onClick = this.onClick.bind(this);
22+
this.onClose = this.onClose.bind(this);
23+
}
24+
25+
onClick(e) {
26+
if (this.props.disabled || this.props.header || this.props.divider) {
27+
return e.preventDefault();
28+
}
29+
30+
if (this.props.onClick) {
31+
this.props.onClick(e);
32+
}
33+
34+
this.onClose();
35+
}
36+
37+
onClose(e) {
38+
if (this.props.onClose) {
39+
this.props.onClose(e);
40+
}
41+
42+
if(this.props.closeDropdown) {
43+
this.props.closeDropdown();
44+
}
45+
}
46+
1447
render() {
1548
let Tagname = 'button';
1649
const {
@@ -24,6 +57,7 @@ class DropdownItem extends React.Component {
2457
const classes = classNames(
2558
className,
2659
{
60+
'disabled': props.disabled,
2761
'dropdown-item': !divider && !header,
2862
'dropdown-header': header,
2963
'dropdown-divider': divider
@@ -34,6 +68,7 @@ class DropdownItem extends React.Component {
3468
return (
3569
<El {...props}
3670
className={classes}
71+
onClose={this.onClose}
3772
onClick={this.onClick}>
3873
{children}
3974
</El>
@@ -47,7 +82,10 @@ class DropdownItem extends React.Component {
4782
}
4883

4984
return (
50-
<Tagname {...props} className={classes}>
85+
<Tagname {...props}
86+
className={classes}
87+
onClose={this.onClose}
88+
onClick={this.onClick}>
5189
{children}
5290
</Tagname>
5391
);

lib/DropdownMenu.jsx

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

55
const propTypes = {
66
children: PropTypes.node.isRequired,
7+
closeDropdown: PropTypes.func,
78
handleContainerClick: PropTypes.func,
89
isDropdownOpen: PropTypes.bool,
910
onClick: PropTypes.func,
11+
openDropdown: PropTypes.func,
1012
right: PropTypes.bool,
1113
toggleDropdown: PropTypes.func
1214
};
@@ -28,8 +30,26 @@ class DropdownMenu extends React.Component {
2830
}
2931
}
3032

33+
renderChildren() {
34+
return React.Children.map(React.Children.toArray(this.props.children), (child) => {
35+
if (React.isValidElement(child)) {
36+
return React.cloneElement(
37+
child,
38+
{
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
44+
}
45+
);
46+
}
47+
return child;
48+
});
49+
}
50+
3151
render() {
32-
const { className, children, right, ...props } = omit(this.props, 'onClick');
52+
const { className, right, ...props } = omit(this.props, 'onClick', 'children');
3353
const classes = classNames(
3454
className,
3555
'dropdown-menu',
@@ -38,7 +58,7 @@ class DropdownMenu extends React.Component {
3858

3959
return (
4060
<div {...props} className={classes} onClick={this.onClick}>
41-
{children}
61+
{this.renderChildren()}
4262
</div>
4363
);
4464
}

test/Dropdown.spec.js

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ describe('Dropdown', () => {
9393
});
9494

9595
describe('componentWillUnmount', () => {
96-
it('should removeDocumentEventListener', () => {
96+
it('should call closeDropdown', () => {
9797
const wrapper = mount(
9898
<Dropdown>
9999
<DropdownToggle>Toggle</DropdownToggle>
@@ -102,44 +102,30 @@ describe('Dropdown', () => {
102102
const instance = wrapper.instance();
103103

104104
spyOn(instance, 'componentWillUnmount').and.callThrough();
105-
spyOn(instance, 'removeDocumentEventListener').and.callThrough();
105+
spyOn(instance, 'closeDropdown').and.callThrough();
106106

107107
expect(instance.componentWillUnmount).not.toHaveBeenCalled();
108-
expect(instance.removeDocumentEventListener).not.toHaveBeenCalled();
108+
expect(instance.closeDropdown).not.toHaveBeenCalled();
109109

110110
wrapper.unmount();
111111

112112
expect(instance.componentWillUnmount).toHaveBeenCalled();
113-
expect(instance.removeDocumentEventListener).toHaveBeenCalled();
113+
expect(instance.closeDropdown).toHaveBeenCalled();
114114
});
115115
});
116116

117117
describe('handleDocumentClick', () => {
118-
it('should call removeEventListener when open', () => {
118+
it('should call closeDropdown', () => {
119119
const wrapper = mount(
120120
<Dropdown open>
121121
<DropdownToggle>Toggle</DropdownToggle>
122122
</Dropdown>
123123
);
124124
const instance = wrapper.instance();
125-
spyOn(instance, 'removeDocumentEventListener').and.callThrough();
125+
spyOn(instance, 'closeDropdown').and.callThrough();
126126
instance.handleDocumentClick();
127127

128-
expect(instance.removeDocumentEventListener).toHaveBeenCalled();
129-
expect(wrapper.state('open')).toBe(false);
130-
});
131-
132-
it('should not call removeEventListener when closed', () => {
133-
const wrapper = mount(
134-
<Dropdown>
135-
<DropdownToggle>Toggle</DropdownToggle>
136-
</Dropdown>
137-
);
138-
const instance = wrapper.instance();
139-
spyOn(instance, 'removeDocumentEventListener');
140-
instance.handleDocumentClick();
141-
142-
expect(instance.removeDocumentEventListener).not.toHaveBeenCalled();
128+
expect(instance.closeDropdown).toHaveBeenCalled();
143129
expect(wrapper.state('open')).toBe(false);
144130
});
145131
});

test/DropdownItem.spec.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,72 @@ describe('DropdownItem', () => {
3838
expect(wrapper.find('.dropdown-divider').length).toBe(1);
3939
});
4040
});
41+
42+
describe('onClick', () => {
43+
it('should not be called when disabled', () => {
44+
const e = { preventDefault: jasmine.createSpy('preventDefault') };
45+
const wrapper = mount(<DropdownItem disabled>Item</DropdownItem>);
46+
const instance = wrapper.instance();
47+
48+
instance.onClick(e);
49+
expect(e.preventDefault).toHaveBeenCalled();
50+
});
51+
52+
it('should not be called when divider is set', () => {
53+
const e = { preventDefault: jasmine.createSpy('preventDefault') };
54+
const wrapper = mount(<DropdownItem divider/>);
55+
const instance = wrapper.instance();
56+
57+
instance.onClick(e);
58+
expect(e.preventDefault).toHaveBeenCalled();
59+
});
60+
61+
it('should not be called when header item', () => {
62+
const e = { preventDefault: jasmine.createSpy('preventDefault') };
63+
const wrapper = mount(<DropdownItem header>Header</DropdownItem>);
64+
const instance = wrapper.instance();
65+
66+
instance.onClick(e);
67+
expect(e.preventDefault).toHaveBeenCalled();
68+
});
69+
70+
it('should be called not disabled, heading, or divider', () => {
71+
const e = { preventDefault: jasmine.createSpy('preventDefault') };
72+
const onClick = jasmine.createSpy('onClick');
73+
const wrapper = mount(<DropdownItem onClick={onClick.bind(this)}>Click me</DropdownItem>);
74+
const instance = wrapper.instance();
75+
76+
instance.onClick(e);
77+
expect(onClick).toHaveBeenCalled();
78+
});
79+
80+
it('should call onClose', () => {
81+
const wrapper = mount(<DropdownItem>Click me</DropdownItem>);
82+
const instance = wrapper.instance();
83+
spyOn(instance, 'onClose');
84+
85+
instance.onClick({});
86+
expect(instance.onClose).toHaveBeenCalled();
87+
});
88+
});
89+
90+
describe('onClose', () => {
91+
it('should call props.onClose', () => {
92+
const onClose = jasmine.createSpy('onClose');
93+
const wrapper = mount(<DropdownItem onClose={onClose.bind(this)}>Click me</DropdownItem>);
94+
const instance = wrapper.instance();
95+
96+
instance.onClose({});
97+
expect(onClose).toHaveBeenCalled();
98+
});
99+
100+
it('should call props.closeDropdown', () => {
101+
const closeDropdown = jasmine.createSpy('closeDropdown');
102+
const wrapper = mount(<DropdownItem closeDropdown={closeDropdown.bind(this)}>Click me</DropdownItem>);
103+
const instance = wrapper.instance();
104+
105+
instance.onClose({});
106+
expect(closeDropdown).toHaveBeenCalled();
107+
});
108+
});
41109
});

0 commit comments

Comments
 (0)