Skip to content
This repository was archived by the owner on Dec 5, 2024. It is now read-only.

Commit 1bd24d8

Browse files
GudahttFezVrasta
authored andcommitted
feat: Allow to pass target directly to Popper component (#92)
The Popper target previously was assumed to be made available via context from the Manager component. Instead it can now be passed in directly via props. This change required making the popover manager in the context optional. Closes #17
1 parent 8bbc40f commit 1bd24d8

File tree

6 files changed

+202
-4
lines changed

6 files changed

+202
-4
lines changed

README.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const PopperExample = () => (
3939
)
4040
```
4141

42-
## Usage w/ child function
42+
## Usage with child function
4343

4444
This is a useful way to interact with custom components. Just make sure you pass down the refs properly.
4545

@@ -77,6 +77,49 @@ const PopperExample = () => (
7777
)
7878
```
7979

80+
## Usage without Manager
81+
82+
It's generally easiest to let the `Manager` and `Target` components handle passing the target DOM element to the `Popper` component. However, you can pass a target [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or a [referenceObject](https://popper.js.org/popper-documentation.html#referenceObject) directly into `Popper` if you need to.
83+
84+
Handling DOM Elements from React can be complicated. The `Manager` and `Target` components handle these complexities for you, so their use is strongly recommended when using DOM Elements.
85+
86+
```js
87+
import { PureComonent } from 'react'
88+
import { Popper, Arrow } from 'react-popper'
89+
90+
class StandaloneExample extends PureComponent {
91+
state = {
92+
isOpen: false,
93+
}
94+
95+
handleClick() = () => {
96+
this.setState(prevState => ({
97+
isOpen: !prevState.isOpen
98+
}))
99+
}
100+
101+
render() {
102+
return (
103+
<div>
104+
<div
105+
ref={(div) => this.target = div}
106+
style={{ width: 120, height: 120, background: '#b4da55' }}
107+
onClick={this.handleClick}
108+
>
109+
Click {this.state.isOpen ? 'to hide' : 'to show'} popper
110+
</div>
111+
{this.state.isOpen && (
112+
<Popper className="popper" target={this.target}>
113+
Popper Content
114+
<Arrow className="popper__arrow"/>
115+
</Popper>
116+
)}
117+
</div>
118+
)
119+
}
120+
}
121+
```
122+
80123
## `Shared Props`
81124

82125
`Target`, `Popper`, and `Arrow` all share the following props
@@ -124,11 +167,12 @@ A `Target`'s child may be one of the following:
124167

125168
Your popper that gets attached to the `Target` component.
126169

127-
Each `Popper` must be wrapped in a `Manager`, and each `Manager` can wrap multiple `Popper` components.
170+
Each `Popper` must either be wrapped in a `Manager`, or passed a `target` prop directly. Each `Manager` can wrap multiple `Popper` components.
128171

129172
#### `placement`: PropTypes.oneOf(Popper.placements)
130173
#### `eventsEnabled`: PropTypes.bool
131174
#### `modifiers`: PropTypes.object
175+
#### `target`: PropTypes.oneOfType([PropTypes.instanceOf(Element), Popper.referenceObject])
132176

133177
Passes respective options to a new [Popper instance](https://github.com/FezVrasta/popper.js/blob/master/docs/_includes/popper-documentation.md#new-popperreference-popper-options). As for `onCreate` and `onUpdate`, these callbacks were intentionally left out in favor of using the [component lifecycle methods](https://facebook.github.io/react/docs/react-component.html#the-component-lifecycle). If you have a good use case for these please feel free to file and issue and I will consider adding them in.
134178

example/index.jsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import ReactDOM from 'react-dom'
44
import MultipleExample from './multiple'
55
import AnimatedExample from './animated'
66
import ModifiersExample from './modifiers'
7+
import ToggleableExample from './toggleable'
8+
import StandaloneExample from './standalone'
9+
import StandaloneObjectExample from './standaloneObject'
710

811
import './main.css'
912

@@ -18,6 +21,15 @@ const App = () => (
1821
<div style={{ marginBottom: 200 }}>
1922
<ModifiersExample />
2023
</div>
24+
<div style={{ marginBottom: 200 }}>
25+
<ToggleableExample />
26+
</div>
27+
<div style={{ marginBottom: 200 }}>
28+
<StandaloneExample />
29+
</div>
30+
<div style={{ marginBottom: 200 }}>
31+
<StandaloneObjectExample />
32+
</div>
2133
</div>
2234
)
2335

example/standalone.jsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React, { PureComponent } from 'react'
2+
import { Popper, Arrow } from '../src/react-popper'
3+
4+
class StandaloneExample extends PureComponent {
5+
state = {
6+
isOpen: false,
7+
}
8+
9+
handleClick = () => {
10+
this.setState(prevState => ({
11+
isOpen: !prevState.isOpen,
12+
}))
13+
}
14+
15+
render() {
16+
return (
17+
<div>
18+
<h2>Standalone Popper Example</h2>
19+
<div
20+
ref={div => (this.target = div)}
21+
style={{ width: 120, height: 120, background: '#b4da55' }}
22+
onClick={this.handleClick}
23+
>
24+
Click {this.state.isOpen ? 'to hide' : 'to show'} popper
25+
</div>
26+
{this.state.isOpen && (
27+
<Popper className="popper" target={this.target}>
28+
Popper Content for Standalone example
29+
<Arrow className="popper__arrow" />
30+
</Popper>
31+
)}
32+
</div>
33+
)
34+
}
35+
}
36+
37+
export default StandaloneExample

example/standaloneObject.jsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React, { PureComponent } from 'react'
2+
import { Popper, Arrow } from '../src/react-popper'
3+
4+
class StandaloneObjectExample extends PureComponent {
5+
state = {
6+
isOpen: false,
7+
}
8+
9+
handleClick = () => {
10+
this.setState(prevState => ({
11+
isOpen: !prevState.isOpen,
12+
}))
13+
}
14+
15+
render() {
16+
const reference = {
17+
getBoundingClientRect: () => ({
18+
top: 10,
19+
left: 100,
20+
right: 150,
21+
bottom: 90,
22+
width: 50,
23+
height: 80,
24+
}),
25+
clientWidth: 50,
26+
clientHeight: 80,
27+
}
28+
return (
29+
<div>
30+
<h2>Standalone referenceObject Popper Example</h2>
31+
<div
32+
style={{ width: 120, height: 120, background: '#b4da55' }}
33+
onClick={this.handleClick}
34+
>
35+
Click {this.state.isOpen ? 'to hide' : 'to show'} popper
36+
</div>
37+
{this.state.isOpen && (
38+
<Popper className="popper" target={reference}>
39+
Popper Content for Standalone example
40+
<Arrow className="popper__arrow" />
41+
</Popper>
42+
)}
43+
</div>
44+
)
45+
}
46+
}
47+
48+
export default StandaloneObjectExample

example/toggleable.jsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React, { PureComponent } from 'react'
2+
import { Manager, Target, Popper, Arrow } from '../src/react-popper'
3+
4+
class ToggleableExample extends PureComponent {
5+
state = {
6+
isOpen: false,
7+
}
8+
9+
handleClick = () => {
10+
this.setState(prevState => ({
11+
isOpen: !prevState.isOpen,
12+
}))
13+
}
14+
15+
render() {
16+
return (
17+
<div>
18+
<h2>Toggleable Popper Example</h2>
19+
<Manager>
20+
<Target
21+
style={{ width: 120, height: 120, background: '#b4da55' }}
22+
onClick={this.handleClick}
23+
>
24+
Click {this.state.isOpen ? 'to hide' : 'to show'} popper
25+
</Target>
26+
{this.state.isOpen && (
27+
<Popper className="popper">
28+
Popper Content for Toggleable Example
29+
<Arrow className="popper__arrow" />
30+
</Popper>
31+
)}
32+
</Manager>
33+
</div>
34+
)
35+
}
36+
}
37+
38+
export default ToggleableExample

src/Popper.jsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import PopperJS from 'popper.js'
44

55
class Popper extends Component {
66
static contextTypes = {
7-
popperManager: PropTypes.object.isRequired,
7+
popperManager: PropTypes.object,
88
}
99

1010
static childContextTypes = {
@@ -18,6 +18,14 @@ class Popper extends Component {
1818
eventsEnabled: PropTypes.bool,
1919
modifiers: PropTypes.object,
2020
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
21+
target: PropTypes.oneOfType([
22+
PropTypes.instanceOf(Element),
23+
PropTypes.shape({
24+
getBoundingClientRect: PropTypes.func.isRequired,
25+
clientWidth: PropTypes.number.isRequired,
26+
clientHeight: PropTypes.number.isRequired,
27+
}),
28+
]),
2129
}
2230

2331
static defaultProps = {
@@ -41,7 +49,8 @@ class Popper extends Component {
4149
componentDidUpdate(lastProps) {
4250
if (
4351
lastProps.placement !== this.props.placement ||
44-
lastProps.eventsEnabled !== this.props.eventsEnabled
52+
lastProps.eventsEnabled !== this.props.eventsEnabled ||
53+
lastProps.target !== this.props.target
4554
) {
4655
this._destroyPopper()
4756
this._createPopper()
@@ -60,6 +69,16 @@ class Popper extends Component {
6069
}
6170

6271
_getTargetNode = () => {
72+
if (this.props.target) {
73+
return this.props.target
74+
} else if (
75+
!this.context.popperManager ||
76+
!this.context.popperManager.getTargetNode()
77+
) {
78+
throw new Error(
79+
'Target missing. Popper must be given a target from the Popper Manager, or as a prop.',
80+
)
81+
}
6382
return this.context.popperManager.getTargetNode()
6483
}
6584

0 commit comments

Comments
 (0)