Skip to content

Make target work like container #102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 13, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 40 additions & 42 deletions src/Position.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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.

Loading