Skip to content

Commit ebb60cc

Browse files
committed
Make target work like container
1 parent c40585b commit ebb60cc

File tree

4 files changed

+158
-162
lines changed

4 files changed

+158
-162
lines changed

src/Position.js

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1+
import classNames from 'classnames';
12
import React, { cloneElement } from 'react';
23
import ReactDOM from 'react-dom';
3-
import classNames from 'classnames';
4-
import ownerDocument from './utils/ownerDocument';
5-
import getContainer from './utils/getContainer';
6-
7-
import { calcOverlayPosition } from './utils/overlayPositionUtils';
8-
94
import mountable from 'react-prop-types/lib/mountable';
105

6+
import calculatePosition from './utils/calculatePosition';
7+
import getContainer from './utils/getContainer';
8+
import ownerDocument from './utils/ownerDocument';
9+
1110
/**
12-
* The Position component calculates the coordinates for its child, to
13-
* position it relative to a `target` component or node. Useful for creating callouts and tooltips,
14-
* the Position component injects a `style` props with `left` and `top` values for positioning your component.
11+
* The Position component calculates the coordinates for its child, to position
12+
* it relative to a `target` component or node. Useful for creating callouts
13+
* and tooltips, the Position component injects a `style` props with `left` and
14+
* `top` values for positioning your component.
1515
*
16-
* It also injects "arrow" `left`, and `top` values for styling callout arrows for giving your components
17-
* a sense of directionality.
16+
* It also injects "arrow" `left`, and `top` values for styling callout arrows
17+
* for giving your components a sense of directionality.
1818
*/
1919
class Position extends React.Component {
2020
constructor(props, context) {
@@ -32,7 +32,7 @@ class Position extends React.Component {
3232
}
3333

3434
componentDidMount() {
35-
this.updatePosition();
35+
this.updatePosition(this.getTarget());
3636
}
3737

3838
componentWillReceiveProps() {
@@ -42,16 +42,10 @@ class Position extends React.Component {
4242
componentDidUpdate(prevProps) {
4343
if (this._needsFlush) {
4444
this._needsFlush = false;
45-
this.updatePosition(prevProps.placement !== this.props.placement);
45+
this.maybeUpdatePosition(this.props.placement !== prevProps.placement);
4646
}
4747
}
4848

49-
componentWillUnmount() {
50-
// Probably not necessary, but just in case holding a reference to the
51-
// target causes problems somewhere.
52-
this._lastTarget = null;
53-
}
54-
5549
render() {
5650
const {children, className, ...props} = this.props;
5751
const {positionLeft, positionTop, ...arrowPosition} = this.state;
@@ -68,7 +62,8 @@ class Position extends React.Component {
6862
{
6963
...props,
7064
...arrowPosition,
71-
//do we need to also forward positionLeft and positionTop if they are set to style?
65+
// FIXME: Don't forward `positionLeft` and `positionTop` via both props
66+
// and `props.style`.
7267
positionLeft,
7368
positionTop,
7469
className: classNames(className, child.props.className),
@@ -81,27 +76,27 @@ class Position extends React.Component {
8176
);
8277
}
8378

84-
getTargetSafe() {
85-
if (!this.props.target) {
86-
return null;
87-
}
88-
89-
const target = this.props.target(this.props);
90-
if (!target) {
91-
// This is so we can just use === check below on all falsy targets.
92-
return null;
93-
}
94-
95-
return target;
79+
getTarget() {
80+
const { target } = this.props;
81+
const targetElement = typeof target === 'function' ? target() : target;
82+
return targetElement && ReactDOM.findDOMNode(targetElement) || null;
9683
}
9784

98-
updatePosition(placementChanged) {
99-
const target = this.getTargetSafe();
85+
maybeUpdatePosition(placementChanged) {
86+
const target = this.getTarget();
10087

101-
if (!this.props.shouldUpdatePosition && target === this._lastTarget && !placementChanged) {
88+
if (
89+
!this.props.shouldUpdatePosition &&
90+
target === this._lastTarget &&
91+
!placementChanged
92+
) {
10293
return;
10394
}
10495

96+
this.updatePosition(target);
97+
}
98+
99+
updatePosition(target) {
105100
this._lastTarget = target;
106101

107102
if (!target) {
@@ -116,9 +111,11 @@ class Position extends React.Component {
116111
}
117112

118113
const overlay = ReactDOM.findDOMNode(this);
119-
const container = getContainer(this.props.container, ownerDocument(this).body);
114+
const container = getContainer(
115+
this.props.container, ownerDocument(this).body
116+
);
120117

121-
this.setState(calcOverlayPosition(
118+
this.setState(calculatePosition(
122119
this.props.placement,
123120
overlay,
124121
target,
@@ -130,17 +127,18 @@ class Position extends React.Component {
130127

131128
Position.propTypes = {
132129
/**
133-
* Function mapping props to a DOM node the component is positioned next to
134-
*
130+
* A node, element, or function that returns either. The child will be
131+
* be positioned next to the `target` specified.
135132
*/
136-
target: React.PropTypes.func,
133+
target: React.PropTypes.oneOfType([
134+
mountable, React.PropTypes.func
135+
]),
137136

138137
/**
139138
* "offsetParent" of the component
140139
*/
141140
container: React.PropTypes.oneOfType([
142-
mountable,
143-
React.PropTypes.func
141+
mountable, React.PropTypes.func
144142
]),
145143
/**
146144
* Minimum spacing in pixels between container border and component border

src/utils/calculatePosition.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import getOffset from 'dom-helpers/query/offset';
2+
import getPosition from 'dom-helpers/query/position';
3+
import getScrollTop from 'dom-helpers/query/scrollTop';
4+
5+
import ownerDocument from './ownerDocument';
6+
7+
function getContainerDimensions(containerNode) {
8+
let width, height, scroll;
9+
10+
if (containerNode.tagName === 'BODY') {
11+
width = window.innerWidth;
12+
height = window.innerHeight;
13+
14+
scroll =
15+
getScrollTop(ownerDocument(containerNode).documentElement) ||
16+
getScrollTop(containerNode);
17+
} else {
18+
({ width, height } = getOffset(containerNode));
19+
scroll = getScrollTop(containerNode);
20+
}
21+
22+
return { width, height, scroll};
23+
}
24+
25+
function getTopDelta(top, overlayHeight, container, padding) {
26+
const containerDimensions = getContainerDimensions(container);
27+
const containerScroll = containerDimensions.scroll;
28+
const containerHeight = containerDimensions.height;
29+
30+
const topEdgeOffset = top - padding - containerScroll;
31+
const bottomEdgeOffset = top + padding - containerScroll + overlayHeight;
32+
33+
if (topEdgeOffset < 0) {
34+
return -topEdgeOffset;
35+
} else if (bottomEdgeOffset > containerHeight) {
36+
return containerHeight - bottomEdgeOffset;
37+
} else {
38+
return 0;
39+
}
40+
}
41+
42+
function getLeftDelta(left, overlayWidth, container, padding) {
43+
const containerDimensions = getContainerDimensions(container);
44+
const containerWidth = containerDimensions.width;
45+
46+
const leftEdgeOffset = left - padding;
47+
const rightEdgeOffset = left + padding + overlayWidth;
48+
49+
if (leftEdgeOffset < 0) {
50+
return -leftEdgeOffset;
51+
} else if (rightEdgeOffset > containerWidth) {
52+
return containerWidth - rightEdgeOffset;
53+
}
54+
55+
return 0;
56+
}
57+
58+
export default function calculatePosition(
59+
placement, overlayNode, target, container, padding
60+
) {
61+
const childOffset = container.tagName === 'BODY' ?
62+
getOffset(target) : getPosition(target, container);
63+
64+
const { height: overlayHeight, width: overlayWidth } =
65+
getOffset(overlayNode);
66+
67+
let positionLeft, positionTop, arrowOffsetLeft, arrowOffsetTop;
68+
69+
if (placement === 'left' || placement === 'right') {
70+
positionTop = childOffset.top + (childOffset.height - overlayHeight) / 2;
71+
72+
if (placement === 'left') {
73+
positionLeft = childOffset.left - overlayWidth;
74+
} else {
75+
positionLeft = childOffset.left + childOffset.width;
76+
}
77+
78+
const topDelta = getTopDelta(
79+
positionTop, overlayHeight, container, padding
80+
);
81+
82+
positionTop += topDelta;
83+
arrowOffsetTop = 50 * (1 - 2 * topDelta / overlayHeight) + '%';
84+
arrowOffsetLeft = void 0;
85+
86+
} else if (placement === 'top' || placement === 'bottom') {
87+
positionLeft = childOffset.left + (childOffset.width - overlayWidth) / 2;
88+
89+
if (placement === 'top') {
90+
positionTop = childOffset.top - overlayHeight;
91+
} else {
92+
positionTop = childOffset.top + childOffset.height;
93+
}
94+
95+
const leftDelta = getLeftDelta(
96+
positionLeft, overlayWidth, container, padding
97+
);
98+
99+
positionLeft += leftDelta;
100+
arrowOffsetLeft = 50 * (1 - 2 * leftDelta / overlayWidth) + '%';
101+
arrowOffsetTop = void 0;
102+
103+
} else {
104+
throw new Error(
105+
`calcOverlayPosition(): No such placement of "${placement}" found.`
106+
);
107+
}
108+
109+
return { positionLeft, positionTop, arrowOffsetLeft, arrowOffsetTop };
110+
}

src/utils/overlayPositionUtils.js

Lines changed: 0 additions & 111 deletions
This file was deleted.

0 commit comments

Comments
 (0)