Skip to content

Commit fd59d37

Browse files
committed
feat(popper): add container prop to popper
1 parent 627a73e commit fd59d37

9 files changed

Lines changed: 171 additions & 109 deletions

File tree

docs/lib/Components/PopoversPage.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,17 @@ export default class PopoversPage extends React.Component {
2626
<pre>
2727
<PrismCode className="language-jsx">
2828
{`Popover.propTypes = {
29-
isOpen: PropTypes.bool,
3029
// boolean to control the state of the popover
31-
toggle: PropTypes.func,
30+
isOpen: PropTypes.bool,
3231
// callback for toggling isOpen in the controlling component
32+
toggle: PropTypes.func,
3333
target: PropTypes.oneOfType([
3434
PropTypes.string,
3535
PropTypes.func,
3636
DOMElement, // instanceof Element (https://developer.mozilla.org/en-US/docs/Web/API/Element)
3737
]).isRequired,
38+
// Where to inject the popper DOM node, default to body
39+
container: PropTypes.oneOfType([PropTypes.string, PropTypes.func, DOMElement]),
3840
disabled: PropTypes.bool,
3941
placementPrefix: PropTypes.string,
4042
delay: PropTypes.oneOfType([

docs/lib/Components/TooltipsPage.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,26 @@ export default class TooltipsPage extends React.Component {
3030
<pre>
3131
<PrismCode className="language-jsx">
3232
{`Tooltip.propTypes = {
33-
isOpen: PropTypes.bool,
3433
// boolean to control the state of the tooltip
35-
toggle: PropTypes.func,
34+
isOpen: PropTypes.bool,
3635
// callback for toggling isOpen in the controlling component
36+
toggle: PropTypes.func,
37+
// target element or element ID, popover is attached to this element
3738
target: PropTypes.oneOfType([
3839
PropTypes.string,
39-
PropTypes.object
40+
PropTypes.func,
41+
DOMElement, // instanceof Element (https://developer.mozilla.org/en-US/docs/Web/API/Element)
4042
]).isRequired,
41-
// target element or element ID, popover is attached to this element
43+
// Where to inject the popper DOM node, default to body
44+
container: PropTypes.oneOfType([PropTypes.string, PropTypes.func, DOMElement]),
45+
// optionally override show/hide delays - default { show: 0, hide: 250 }
4246
delay: PropTypes.oneOfType([
4347
PropTypes.shape({ show: PropTypes.number, hide: PropTypes.number }),
4448
PropTypes.number
4549
]),
46-
// optionally override show/hide delays - default { show: 0, hide: 250 }
47-
autohide: PropTypes.bool,
4850
// optionally hide tooltip when hovering over tooltip content - default true
51+
autohide: PropTypes.bool,
52+
// convenience attachments for popover
4953
placement: PropTypes.oneOf([
5054
'auto',
5155
'auto-start',
@@ -63,8 +67,6 @@ export default class TooltipsPage extends React.Component {
6367
'left-start',
6468
'left-end',
6569
])
66-
// convenience attachments for popover
67-
// examples http://github.hubspot.com/tooltip/docs/welcome/
6870
}`}
6971
</PrismCode>
7072
</pre>

docs/lib/examples/CustomDropdown.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export default class Example extends React.Component {
2424
tag="span"
2525
onClick={this.toggle}
2626
data-toggle="dropdown"
27-
aria-haspopup="true"
2827
aria-expanded={this.state.dropdownOpen}
2928
>
3029
Custom Dropdown Content

src/Dropdown.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class Dropdown extends React.Component {
120120
dropup: dropup
121121
}
122122
), cssModule);
123-
return <Manager {...attrs} className={classes}>{}</Manager>;
123+
return <Manager {...attrs} className={classes} />;
124124
}
125125
}
126126

src/DropdownToggle.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class DropdownToggle extends React.Component {
5656
}
5757

5858
render() {
59-
const { className, color, cssModule, caret, split, nav, tag, ...props } = this.props;
59+
const { className, color, cssModule, caret, split, nav, tag, 'aria-haspopup': ariaHaspopup, ...props } = this.props;
6060
const ariaLabel = props['aria-label'] || 'Toggle Dropdown';
6161
const classes = mapToCssModules(classNames(
6262
className,
@@ -87,7 +87,7 @@ class DropdownToggle extends React.Component {
8787
className={classes}
8888
component={Tag}
8989
onClick={this.onClick}
90-
aria-haspopup="true"
90+
aria-haspopup={ariaHaspopup ? 'true' : 'false'}
9191
aria-expanded={this.context.isOpen}
9292
children={children}
9393
/>

src/PopperContent.js

Lines changed: 103 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3+
import ReactDOM from 'react-dom';
34
import classNames from 'classnames';
4-
import { Arrow, Manager, Popper as ReactPopper } from 'react-popper';
5-
import PopperTargetHelper from './PopperTargetHelper';
6-
import { DOMElement, mapToCssModules } from './utils';
5+
import { Arrow, Popper as ReactPopper } from 'react-popper';
6+
import { getTarget, DOMElement, mapToCssModules } from './utils';
77

88
const propTypes = {
99
children: PropTypes.node.isRequired,
@@ -13,11 +13,10 @@ const propTypes = {
1313
tag: PropTypes.string,
1414
isOpen: PropTypes.bool.isRequired,
1515
cssModule: PropTypes.object,
16-
wrapTag: PropTypes.string,
17-
wrapClassName: PropTypes.string,
1816
offset: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
1917
fallbackPlacement: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
2018
flip: PropTypes.bool,
19+
container: PropTypes.oneOfType([PropTypes.string, PropTypes.func, DOMElement]),
2120
target: PropTypes.oneOfType([PropTypes.string, PropTypes.func, DOMElement]).isRequired,
2221
};
2322

@@ -26,26 +25,105 @@ const defaultProps = {
2625
isOpen: false,
2726
offset: 0,
2827
fallbackPlacement: 'flip',
29-
wrapTag: 'span',
3028
flip: true,
29+
container: 'body',
30+
};
31+
32+
const childContextTypes = {
33+
popperManager: PropTypes.object.isRequired,
3134
};
3235

3336
class PopperContent extends React.Component {
3437
constructor(props) {
3538
super(props);
3639

3740
this.handlePlacementChange = this.handlePlacementChange.bind(this);
41+
this.setTargetNode = this.setTargetNode.bind(this);
42+
this.getTargetNode = this.getTargetNode.bind(this);
3843
this.state = {};
3944
}
4045

46+
getChildContext() {
47+
return {
48+
popperManager: {
49+
setTargetNode: this.setTargetNode,
50+
getTargetNode: this.getTargetNode,
51+
},
52+
};
53+
}
54+
55+
componentDidMount() {
56+
this.handleProps();
57+
}
58+
59+
componentDidUpdate(prevProps) {
60+
if (this.props.isOpen !== prevProps.isOpen) {
61+
this.handleProps();
62+
} else if (this._element) {
63+
// rerender
64+
this.renderIntoSubtree();
65+
}
66+
}
67+
68+
componentWillUnmount() {
69+
this.hide();
70+
}
71+
72+
setTargetNode(node) {
73+
this.targetNode = node;
74+
}
75+
76+
getTargetNode() {
77+
return this.targetNode;
78+
}
79+
80+
getContainerNode() {
81+
return getTarget(this.props.container);
82+
}
83+
4184
handlePlacementChange(data) {
4285
if (this.state.placement !== data.placement) {
4386
this.setState({ placement: data.placement });
4487
}
4588
return data;
4689
}
4790

48-
render() {
91+
handleProps() {
92+
if (this.props.container !== 'inline') {
93+
if (this.props.isOpen) {
94+
this.show();
95+
} else {
96+
this.hide();
97+
}
98+
}
99+
}
100+
101+
hide() {
102+
if (this._element) {
103+
this.getContainerNode().removeChild(this._element);
104+
ReactDOM.unmountComponentAtNode(this._element);
105+
this._element = null;
106+
}
107+
}
108+
109+
show() {
110+
this._element = document.createElement('div');
111+
this.getContainerNode().appendChild(this._element);
112+
this.renderIntoSubtree();
113+
if (this._element.childNodes && this._element.childNodes[0] && this._element.childNodes[0].focus) {
114+
this._element.childNodes[0].focus();
115+
}
116+
}
117+
118+
renderIntoSubtree() {
119+
ReactDOM.unstable_renderSubtreeIntoContainer(
120+
this,
121+
this.renderChildren(),
122+
this._element
123+
);
124+
}
125+
126+
renderChildren() {
49127
const {
50128
cssModule,
51129
children,
@@ -56,13 +134,12 @@ class PopperContent extends React.Component {
56134
fallbackPlacement,
57135
placementPrefix,
58136
className,
59-
wrapTag,
60-
wrapClassName,
61137
tag,
62-
...attrs } = this.props;
138+
container,
139+
...attrs
140+
} = this.props;
63141
const arrowClassName = mapToCssModules('arrow', cssModule);
64142
const placement = (this.state.placement || attrs.placement).split('-')[0];
65-
const managerClass = mapToCssModules(wrapClassName, this.props.cssModule);
66143
const popperClassName = mapToCssModules(classNames(
67144
className,
68145
placementPrefix ? `${placementPrefix}-${placement}` : placement
@@ -79,18 +156,26 @@ class PopperContent extends React.Component {
79156
};
80157

81158
return (
82-
<Manager tag={wrapTag} className={managerClass}>
83-
<PopperTargetHelper target={target} />
84-
{isOpen && <ReactPopper modifiers={modifiers} {...attrs} component={tag} className={popperClassName}>
85-
{children}
86-
<Arrow className={arrowClassName} />
87-
</ReactPopper>}
88-
</Manager>
159+
<ReactPopper modifiers={modifiers} {...attrs} component={tag} className={popperClassName}>
160+
{children}
161+
<Arrow className={arrowClassName} />
162+
</ReactPopper>
89163
);
90164
}
165+
166+
render() {
167+
this.setTargetNode(getTarget(this.props.target));
168+
169+
if (this.props.container === 'inline') {
170+
return this.props.isOpen ? this.renderChildren() : null;
171+
}
172+
173+
return null;
174+
}
91175
}
92176

93177
PopperContent.propTypes = propTypes;
94178
PopperContent.defaultProps = defaultProps;
179+
PopperContent.childContextTypes = childContextTypes;
95180

96181
export default PopperContent;

src/__tests__/Popover.spec.js

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ describe('Popover', () => {
4242
</Popover>
4343
);
4444

45-
expect(wrapper.find('.popover').length).toBe(1);
46-
expect(wrapper.find('.popover-inner').length).toBe(1);
47-
expect(wrapper.find('.popover-header').length).toBe(1);
48-
expect(wrapper.find('.popover-body').length).toBe(1);
45+
expect(document.getElementsByClassName('popover').length).toBe(1);
46+
expect(document.getElementsByClassName('popover-inner').length).toBe(1);
47+
expect(document.getElementsByClassName('popover-header').length).toBe(1);
48+
expect(document.getElementsByClassName('popover-body').length).toBe(1);
4949
wrapper.unmount();
5050
});
5151

@@ -57,10 +57,10 @@ describe('Popover', () => {
5757
</Popover>
5858
);
5959

60-
expect(wrapper.find('.popover').length).toBe(0);
61-
expect(wrapper.find('.popover-inner').length).toBe(0);
62-
expect(wrapper.find('.popover-header').length).toBe(0);
63-
expect(wrapper.find('.popover-body').length).toBe(0);
60+
expect(document.getElementsByClassName('popover').length).toBe(0);
61+
expect(document.getElementsByClassName('popover-inner').length).toBe(0);
62+
expect(document.getElementsByClassName('popover-header').length).toBe(0);
63+
expect(document.getElementsByClassName('popover-body').length).toBe(0);
6464
wrapper.unmount();
6565
});
6666

@@ -74,23 +74,23 @@ describe('Popover', () => {
7474

7575
expect(isOpen).toBe(false);
7676

77-
expect(wrapper.find('.popover.show').length).toBe(0);
78-
expect(wrapper.find('.popover').length).toBe(0);
79-
expect(wrapper.find('.popover-inner').length).toBe(0);
80-
expect(wrapper.find('.popover-header').length).toBe(0);
81-
expect(wrapper.find('.popover-body').length).toBe(0);
77+
expect(document.getElementsByClassName('show').length).toBe(0);
78+
expect(document.getElementsByClassName('popover').length).toBe(0);
79+
expect(document.getElementsByClassName('popover-inner').length).toBe(0);
80+
expect(document.getElementsByClassName('popover-header').length).toBe(0);
81+
expect(document.getElementsByClassName('popover-body').length).toBe(0);
8282

8383
toggle();
8484
wrapper.setProps({
8585
isOpen: isOpen
8686
});
8787

8888
expect(isOpen).toBe(true);
89-
expect(wrapper.find('.popover.show').length).toBe(1);
90-
expect(wrapper.find('.popover').length).toBe(1);
91-
expect(wrapper.find('.popover-inner').length).toBe(1);
92-
expect(wrapper.find('.popover-header').length).toBe(1);
93-
expect(wrapper.find('.popover-body').length).toBe(1);
89+
expect(document.getElementsByClassName('show').length).toBe(1);
90+
expect(document.getElementsByClassName('popover').length).toBe(1);
91+
expect(document.getElementsByClassName('popover-inner').length).toBe(1);
92+
expect(document.getElementsByClassName('popover-header').length).toBe(1);
93+
expect(document.getElementsByClassName('popover-body').length).toBe(1);
9494

9595
wrapper.unmount();
9696
});
@@ -105,21 +105,21 @@ describe('Popover', () => {
105105
);
106106

107107
expect(isOpen).toBe(true);
108-
expect(wrapper.find('.popover').length).toBe(1);
109-
expect(wrapper.find('.popover-inner').length).toBe(1);
110-
expect(wrapper.find('.popover-header').length).toBe(1);
111-
expect(wrapper.find('.popover-body').length).toBe(1);
108+
expect(document.getElementsByClassName('popover').length).toBe(1);
109+
expect(document.getElementsByClassName('popover-inner').length).toBe(1);
110+
expect(document.getElementsByClassName('popover-header').length).toBe(1);
111+
expect(document.getElementsByClassName('popover-body').length).toBe(1);
112112

113113
toggle();
114114
wrapper.setProps({
115115
isOpen: isOpen
116116
});
117117

118118
expect(isOpen).toBe(false);
119-
expect(wrapper.find('.popover').length).toBe(0);
120-
expect(wrapper.find('.popover-inner').length).toBe(0);
121-
expect(wrapper.find('.popover-header').length).toBe(0);
122-
expect(wrapper.find('.popover-body').length).toBe(0);
119+
expect(document.getElementsByClassName('popover').length).toBe(0);
120+
expect(document.getElementsByClassName('popover-inner').length).toBe(0);
121+
expect(document.getElementsByClassName('popover-header').length).toBe(0);
122+
expect(document.getElementsByClassName('popover-body').length).toBe(0);
123123

124124
wrapper.unmount();
125125
});
@@ -148,7 +148,7 @@ describe('Popover', () => {
148148
</Popover>
149149
);
150150

151-
expect(wrapper.find('.popover-inner').hasClass('popover-special')).toBe(true);
151+
expect(document.getElementsByClassName('popover-inner')[0].className.indexOf('popover-special') > -1).toBe(true);
152152

153153
wrapper.unmount();
154154
});

0 commit comments

Comments
 (0)