Skip to content

Commit bc66aec

Browse files
committed
feat(Popover): add component
1 parent 7282225 commit bc66aec

3 files changed

Lines changed: 207 additions & 0 deletions

File tree

lib/Popover.jsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, { PropTypes } from 'react';
2+
import TetherContent from './TetherContent';
3+
import { getTetherAttachments, tetherAttachements } from './utils';
4+
5+
const propTypes = {
6+
placement: React.PropTypes.oneOf(tetherAttachements),
7+
target: PropTypes.string.isRequired,
8+
isOpen: PropTypes.bool
9+
};
10+
11+
const defaultProps = {
12+
isOpen: false,
13+
placement: 'bottom',
14+
toggle: () => {}
15+
};
16+
17+
const defaultTetherConfig = {
18+
classPrefix: 'bs-tether',
19+
classes: { element: 'popover', enabled: 'open' },
20+
constraints: [
21+
{ to: 'scrollParent', attachment: 'together none' },
22+
{ to: 'window', attachment: 'together none' }
23+
]
24+
};
25+
26+
class Popover extends React.Component {
27+
constructor(props) {
28+
super(props);
29+
30+
this.getTetherConfig = this.getTetherConfig.bind(this);
31+
}
32+
33+
getTetherConfig() {
34+
const attachments = getTetherAttachments(this.props.placement);
35+
return {
36+
...defaultTetherConfig,
37+
...attachments,
38+
target: '#' + this.props.target,
39+
...this.props.tether
40+
};
41+
}
42+
43+
render() {
44+
if (!this.props.isOpen) {
45+
return null;
46+
}
47+
48+
let tetherConfig = this.getTetherConfig();
49+
50+
return (
51+
<TetherContent
52+
arrow="popover"
53+
tether={tetherConfig}
54+
isOpen={this.props.isOpen}
55+
toggle={this.props.toggle}>
56+
<div className="popover-inner">
57+
{this.props.children}
58+
</div>
59+
</TetherContent>
60+
);
61+
}
62+
}
63+
64+
Popover.propTypes = propTypes;
65+
Popover.defaultProps = defaultProps;
66+
67+
export default Popover;

lib/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import DropdownItem from './DropdownItem';
77
import DropdownMenu from './DropdownMenu';
88
import DropdownToggle from './DropdownToggle';
99
import Label from './Label';
10+
import Popover from './Popover';
1011
import PopoverTitle from './PopoverTitle';
1112
import PopoverContent from './PopoverContent';
1213
import TetherContent from './TetherContent';
@@ -22,6 +23,7 @@ export {
2223
DropdownMenu,
2324
DropdownToggle,
2425
Label,
26+
Popover,
2527
PopoverContent,
2628
PopoverTitle,
2729
TetherContent,

test/Popover.spec.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/* eslint react/no-multi-comp: 0, react/prop-types: 0 */
2+
import React from 'react';
3+
import { mount } from 'enzyme';
4+
import { Popover, PopoverTitle, PopoverContent, TetherContent } from '../lib';
5+
6+
describe('Popover', () => {
7+
let element;
8+
let isOpen;
9+
let toggle;
10+
let placement;
11+
12+
beforeEach(() => {
13+
element = document.createElement('div');
14+
isOpen = false;
15+
toggle = () => { isOpen = !isOpen; };
16+
placement = 'top';
17+
18+
element.setAttribute('id', 'popover-target');
19+
20+
document.body.appendChild(element);
21+
});
22+
23+
afterEach(() => {
24+
document.body.removeChild(element);
25+
element = null;
26+
});
27+
28+
it('should render inner TetherContent when isOpen', () => {
29+
isOpen = true;
30+
const wrapper = mount(
31+
<Popover isOpen={isOpen} toggle={toggle} placement={placement} target="popover-target">
32+
<PopoverTitle>Title</PopoverTitle>
33+
<PopoverContent>Content</PopoverContent>
34+
</Popover>
35+
);
36+
37+
expect(wrapper.find(TetherContent).length).toBe(1);
38+
expect(document.getElementsByClassName('popover').length).toBe(1);
39+
expect(document.getElementsByClassName('popover-inner').length).toBe(1);
40+
expect(document.getElementsByClassName('popover-title').length).toBe(1);
41+
expect(document.getElementsByClassName('popover-content').length).toBe(1);
42+
wrapper.unmount();
43+
});
44+
45+
it('should not render inner TetherContent when not isOpen', () => {
46+
const wrapper = mount(
47+
<Popover isOpen={isOpen} toggle={toggle} placement={placement} target="popover-target">
48+
<PopoverTitle>Title</PopoverTitle>
49+
<PopoverContent>Content</PopoverContent>
50+
</Popover>
51+
);
52+
53+
expect(wrapper.find(TetherContent).length).toBe(0);
54+
expect(document.getElementsByClassName('popover').length).toBe(0);
55+
expect(document.getElementsByClassName('popover-inner').length).toBe(0);
56+
expect(document.getElementsByClassName('popover-title').length).toBe(0);
57+
expect(document.getElementsByClassName('popover-content').length).toBe(0);
58+
wrapper.unmount();
59+
});
60+
61+
it('should be able to show the popover', () => {
62+
const wrapper = mount(
63+
<Popover isOpen={isOpen} toggle={toggle} placement={placement} target="popover-target">
64+
<PopoverTitle>Title</PopoverTitle>
65+
<PopoverContent>Content</PopoverContent>
66+
</Popover>
67+
);
68+
69+
expect(isOpen).toBe(false);
70+
71+
expect(wrapper.find(TetherContent).length).toBe(0);
72+
expect(document.getElementsByClassName('popover').length).toBe(0);
73+
expect(document.getElementsByClassName('popover-inner').length).toBe(0);
74+
expect(document.getElementsByClassName('popover-title').length).toBe(0);
75+
expect(document.getElementsByClassName('popover-content').length).toBe(0);
76+
77+
toggle();
78+
wrapper.setProps({
79+
isOpen: isOpen
80+
});
81+
82+
expect(isOpen).toBe(true);
83+
expect(wrapper.find(TetherContent).length).toBe(1);
84+
expect(document.getElementsByClassName('popover').length).toBe(1);
85+
expect(document.getElementsByClassName('popover-inner').length).toBe(1);
86+
expect(document.getElementsByClassName('popover-title').length).toBe(1);
87+
expect(document.getElementsByClassName('popover-content').length).toBe(1);
88+
89+
wrapper.unmount();
90+
});
91+
92+
it('should be able to hide the popover', () => {
93+
isOpen = true;
94+
const wrapper = mount(
95+
<Popover isOpen={isOpen} toggle={toggle} placement={placement} target="popover-target">
96+
<PopoverTitle>Title</PopoverTitle>
97+
<PopoverContent>Content</PopoverContent>
98+
</Popover>
99+
);
100+
101+
expect(isOpen).toBe(true);
102+
expect(wrapper.find(TetherContent).length).toBe(1);
103+
expect(document.getElementsByClassName('popover').length).toBe(1);
104+
expect(document.getElementsByClassName('popover-inner').length).toBe(1);
105+
expect(document.getElementsByClassName('popover-title').length).toBe(1);
106+
expect(document.getElementsByClassName('popover-content').length).toBe(1);
107+
108+
toggle();
109+
wrapper.setProps({
110+
isOpen: isOpen
111+
});
112+
113+
expect(isOpen).toBe(false);
114+
expect(wrapper.find(TetherContent).length).toBe(0);
115+
expect(document.getElementsByClassName('popover').length).toBe(0);
116+
expect(document.getElementsByClassName('popover-inner').length).toBe(0);
117+
expect(document.getElementsByClassName('popover-title').length).toBe(0);
118+
expect(document.getElementsByClassName('popover-content').length).toBe(0);
119+
120+
wrapper.unmount();
121+
});
122+
123+
it('default toggle prop does nothing', () => {
124+
const wrapper = mount(
125+
<Popover isOpen={isOpen} placement={placement} target="popover-target">
126+
<PopoverTitle>Title</PopoverTitle>
127+
<PopoverContent>Content</PopoverContent>
128+
</Popover>
129+
);
130+
const instance = wrapper.instance();
131+
132+
expect(isOpen).toBe(false);
133+
instance.props.toggle();
134+
expect(isOpen).toBe(false);
135+
136+
wrapper.unmount();
137+
});
138+
});

0 commit comments

Comments
 (0)