Skip to content

Commit

Permalink
Merge pull request #102 from react-bootstrap/target
Browse files Browse the repository at this point in the history
Make `target` work like `container`
  • Loading branch information
jquense committed Jul 13, 2016
2 parents c40585b + ebb60cc commit e32f1c5
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 162 deletions.
82 changes: 40 additions & 42 deletions src/Position.js
@@ -1,20 +1,20 @@
import classNames from 'classnames';
import React, { cloneElement } from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import ownerDocument from './utils/ownerDocument';
import getContainer from './utils/getContainer';

import { calcOverlayPosition } from './utils/overlayPositionUtils';

import mountable from 'react-prop-types/lib/mountable';

import calculatePosition from './utils/calculatePosition';
import getContainer from './utils/getContainer';
import ownerDocument from './utils/ownerDocument';

/**
* The Position component calculates the coordinates for its child, to
* position it relative to a `target` component or node. Useful for creating callouts and tooltips,
* the Position component injects a `style` props with `left` and `top` values for positioning your component.
* The Position component calculates the coordinates for its child, to position
* it relative to a `target` component or node. Useful for creating callouts
* and tooltips, the Position component injects a `style` props with `left` and
* `top` values for positioning your component.
*
* It also injects "arrow" `left`, and `top` values for styling callout arrows for giving your components
* a sense of directionality.
* It also injects "arrow" `left`, and `top` values for styling callout arrows
* for giving your components a sense of directionality.
*/
class Position extends React.Component {
constructor(props, context) {
Expand All @@ -32,7 +32,7 @@ class Position extends React.Component {
}

componentDidMount() {
this.updatePosition();
this.updatePosition(this.getTarget());
}

componentWillReceiveProps() {
Expand All @@ -42,16 +42,10 @@ class Position extends React.Component {
componentDidUpdate(prevProps) {
if (this._needsFlush) {
this._needsFlush = false;
this.updatePosition(prevProps.placement !== this.props.placement);
this.maybeUpdatePosition(this.props.placement !== prevProps.placement);
}
}

componentWillUnmount() {
// Probably not necessary, but just in case holding a reference to the
// target causes problems somewhere.
this._lastTarget = null;
}

render() {
const {children, className, ...props} = this.props;
const {positionLeft, positionTop, ...arrowPosition} = this.state;
Expand All @@ -68,7 +62,8 @@ class Position extends React.Component {
{
...props,
...arrowPosition,
//do we need to also forward positionLeft and positionTop if they are set to style?
// FIXME: Don't forward `positionLeft` and `positionTop` via both props
// and `props.style`.
positionLeft,
positionTop,
className: classNames(className, child.props.className),
Expand All @@ -81,27 +76,27 @@ class Position extends React.Component {
);
}

getTargetSafe() {
if (!this.props.target) {
return null;
}

const target = this.props.target(this.props);
if (!target) {
// This is so we can just use === check below on all falsy targets.
return null;
}

return target;
getTarget() {
const { target } = this.props;
const targetElement = typeof target === 'function' ? target() : target;
return targetElement && ReactDOM.findDOMNode(targetElement) || null;
}

updatePosition(placementChanged) {
const target = this.getTargetSafe();
maybeUpdatePosition(placementChanged) {
const target = this.getTarget();

if (!this.props.shouldUpdatePosition && target === this._lastTarget && !placementChanged) {
if (
!this.props.shouldUpdatePosition &&
target === this._lastTarget &&
!placementChanged
) {
return;
}

this.updatePosition(target);
}

updatePosition(target) {
this._lastTarget = target;

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

const overlay = ReactDOM.findDOMNode(this);
const container = getContainer(this.props.container, ownerDocument(this).body);
const container = getContainer(
this.props.container, ownerDocument(this).body
);

this.setState(calcOverlayPosition(
this.setState(calculatePosition(
this.props.placement,
overlay,
target,
Expand All @@ -130,17 +127,18 @@ class Position extends React.Component {

Position.propTypes = {
/**
* Function mapping props to a DOM node the component is positioned next to
*
* A node, element, or function that returns either. The child will be
* be positioned next to the `target` specified.
*/
target: React.PropTypes.func,
target: React.PropTypes.oneOfType([
mountable, React.PropTypes.func
]),

/**
* "offsetParent" of the component
*/
container: React.PropTypes.oneOfType([
mountable,
React.PropTypes.func
mountable, React.PropTypes.func
]),
/**
* Minimum spacing in pixels between container border and component border
Expand Down
110 changes: 110 additions & 0 deletions src/utils/calculatePosition.js
@@ -0,0 +1,110 @@
import getOffset from 'dom-helpers/query/offset';
import getPosition from 'dom-helpers/query/position';
import getScrollTop from 'dom-helpers/query/scrollTop';

import ownerDocument from './ownerDocument';

function getContainerDimensions(containerNode) {
let width, height, scroll;

if (containerNode.tagName === 'BODY') {
width = window.innerWidth;
height = window.innerHeight;

scroll =
getScrollTop(ownerDocument(containerNode).documentElement) ||
getScrollTop(containerNode);
} else {
({ width, height } = getOffset(containerNode));
scroll = getScrollTop(containerNode);
}

return { width, height, scroll};
}

function getTopDelta(top, overlayHeight, container, padding) {
const containerDimensions = getContainerDimensions(container);
const containerScroll = containerDimensions.scroll;
const containerHeight = containerDimensions.height;

const topEdgeOffset = top - padding - containerScroll;
const bottomEdgeOffset = top + padding - containerScroll + overlayHeight;

if (topEdgeOffset < 0) {
return -topEdgeOffset;
} else if (bottomEdgeOffset > containerHeight) {
return containerHeight - bottomEdgeOffset;
} else {
return 0;
}
}

function getLeftDelta(left, overlayWidth, container, padding) {
const containerDimensions = getContainerDimensions(container);
const containerWidth = containerDimensions.width;

const leftEdgeOffset = left - padding;
const rightEdgeOffset = left + padding + overlayWidth;

if (leftEdgeOffset < 0) {
return -leftEdgeOffset;
} else if (rightEdgeOffset > containerWidth) {
return containerWidth - rightEdgeOffset;
}

return 0;
}

export default function calculatePosition(
placement, overlayNode, target, container, padding
) {
const childOffset = container.tagName === 'BODY' ?
getOffset(target) : getPosition(target, container);

const { height: overlayHeight, width: overlayWidth } =
getOffset(overlayNode);

let positionLeft, positionTop, arrowOffsetLeft, arrowOffsetTop;

if (placement === 'left' || placement === 'right') {
positionTop = childOffset.top + (childOffset.height - overlayHeight) / 2;

if (placement === 'left') {
positionLeft = childOffset.left - overlayWidth;
} else {
positionLeft = childOffset.left + childOffset.width;
}

const topDelta = getTopDelta(
positionTop, overlayHeight, container, padding
);

positionTop += topDelta;
arrowOffsetTop = 50 * (1 - 2 * topDelta / overlayHeight) + '%';
arrowOffsetLeft = void 0;

} else if (placement === 'top' || placement === 'bottom') {
positionLeft = childOffset.left + (childOffset.width - overlayWidth) / 2;

if (placement === 'top') {
positionTop = childOffset.top - overlayHeight;
} else {
positionTop = childOffset.top + childOffset.height;
}

const leftDelta = getLeftDelta(
positionLeft, overlayWidth, container, padding
);

positionLeft += leftDelta;
arrowOffsetLeft = 50 * (1 - 2 * leftDelta / overlayWidth) + '%';
arrowOffsetTop = void 0;

} else {
throw new Error(
`calcOverlayPosition(): No such placement of "${placement}" found.`
);
}

return { positionLeft, positionTop, arrowOffsetLeft, arrowOffsetTop };
}
111 changes: 0 additions & 111 deletions src/utils/overlayPositionUtils.js

This file was deleted.

0 comments on commit e32f1c5

Please sign in to comment.