@wordpress/components
Version:
UI components for WordPress.
355 lines (319 loc) • 9.85 kB
JavaScript
/**
* WordPress dependencies
*/
import { isRTL } from '@wordpress/i18n';
/**
* Module constants
*/
const HEIGHT_OFFSET = 10; // used by the arrow and a bit of empty space
/**
* Utility used to compute the popover position over the xAxis
*
* @param {Object} anchorRect Anchor Rect.
* @param {Object} contentSize Content Size.
* @param {string} xAxis Desired xAxis.
* @param {string} corner Desired corner.
* @param {boolean} stickyBoundaryElement The boundary element to use when
* switching between sticky and normal
* position.
* @param {string} chosenYAxis yAxis to be used.
* @param {Element} boundaryElement Boundary element.
* @param {boolean} forcePosition Don't adjust position based on anchor.
*
* @return {Object} Popover xAxis position and constraints.
*/
export function computePopoverXAxisPosition(
anchorRect,
contentSize,
xAxis,
corner,
stickyBoundaryElement,
chosenYAxis,
boundaryElement,
forcePosition
) {
const { width } = contentSize;
// Correct xAxis for RTL support
if ( xAxis === 'left' && isRTL() ) {
xAxis = 'right';
} else if ( xAxis === 'right' && isRTL() ) {
xAxis = 'left';
}
if ( corner === 'left' && isRTL() ) {
corner = 'right';
} else if ( corner === 'right' && isRTL() ) {
corner = 'left';
}
// x axis alignment choices
const anchorMidPoint = Math.round( anchorRect.left + anchorRect.width / 2 );
const centerAlignment = {
popoverLeft: anchorMidPoint,
contentWidth:
( anchorMidPoint - width / 2 > 0 ? width / 2 : anchorMidPoint ) +
( anchorMidPoint + width / 2 > window.innerWidth
? window.innerWidth - anchorMidPoint
: width / 2 ),
};
let leftAlignmentX = anchorRect.left;
if ( corner === 'right' ) {
leftAlignmentX = anchorRect.right;
} else if ( chosenYAxis !== 'middle' ) {
leftAlignmentX = anchorMidPoint;
}
let rightAlignmentX = anchorRect.right;
if ( corner === 'left' ) {
rightAlignmentX = anchorRect.left;
} else if ( chosenYAxis !== 'middle' ) {
rightAlignmentX = anchorMidPoint;
}
const leftAlignment = {
popoverLeft: leftAlignmentX,
contentWidth: leftAlignmentX - width > 0 ? width : leftAlignmentX,
};
const rightAlignment = {
popoverLeft: rightAlignmentX,
contentWidth:
rightAlignmentX + width > window.innerWidth
? window.innerWidth - rightAlignmentX
: width,
};
// Choosing the x axis
let chosenXAxis = xAxis;
let contentWidth = null;
if ( ! stickyBoundaryElement && ! forcePosition ) {
if ( xAxis === 'center' && centerAlignment.contentWidth === width ) {
chosenXAxis = 'center';
} else if ( xAxis === 'left' && leftAlignment.contentWidth === width ) {
chosenXAxis = 'left';
} else if (
xAxis === 'right' &&
rightAlignment.contentWidth === width
) {
chosenXAxis = 'right';
} else {
chosenXAxis =
leftAlignment.contentWidth > rightAlignment.contentWidth
? 'left'
: 'right';
const chosenWidth =
chosenXAxis === 'left'
? leftAlignment.contentWidth
: rightAlignment.contentWidth;
// Limit width of the content to the viewport width
if ( width > window.innerWidth ) {
contentWidth = window.innerWidth;
}
// If we can't find any alignment options that could fit
// our content, then let's fallback to the center of the viewport.
if ( chosenWidth !== width ) {
chosenXAxis = 'center';
centerAlignment.popoverLeft = window.innerWidth / 2;
}
}
}
let popoverLeft;
if ( chosenXAxis === 'center' ) {
popoverLeft = centerAlignment.popoverLeft;
} else if ( chosenXAxis === 'left' ) {
popoverLeft = leftAlignment.popoverLeft;
} else {
popoverLeft = rightAlignment.popoverLeft;
}
if ( boundaryElement ) {
const boundaryRect = boundaryElement.getBoundingClientRect();
popoverLeft = Math.min( popoverLeft, boundaryRect.right - width );
// Avoid the popover being position beyond the left boundary if the
// direction is left to right.
if ( ! isRTL() ) {
popoverLeft = Math.max( popoverLeft, 0 );
}
}
return {
xAxis: chosenXAxis,
popoverLeft,
contentWidth,
};
}
/**
* Utility used to compute the popover position over the yAxis
*
* @param {Object} anchorRect Anchor Rect.
* @param {Object} contentSize Content Size.
* @param {string} yAxis Desired yAxis.
* @param {string} corner Desired corner.
* @param {boolean} stickyBoundaryElement The boundary element to use when
* switching between sticky and normal
* position.
* @param {Element} anchorRef The anchor element.
* @param {Element} relativeOffsetTop If applicable, top offset of the
* relative positioned parent container.
* @param {boolean} forcePosition Don't adjust position based on anchor.
*
* @return {Object} Popover xAxis position and constraints.
*/
export function computePopoverYAxisPosition(
anchorRect,
contentSize,
yAxis,
corner,
stickyBoundaryElement,
anchorRef,
relativeOffsetTop,
forcePosition
) {
const { height } = contentSize;
if ( stickyBoundaryElement ) {
const stickyRect = stickyBoundaryElement.getBoundingClientRect();
const stickyPosition = stickyRect.top + height - relativeOffsetTop;
if ( anchorRect.top <= stickyPosition ) {
return {
yAxis,
popoverTop: Math.min( anchorRect.bottom, stickyPosition ),
};
}
}
// y axis alignment choices
let anchorMidPoint = anchorRect.top + anchorRect.height / 2;
if ( corner === 'bottom' ) {
anchorMidPoint = anchorRect.bottom;
} else if ( corner === 'top' ) {
anchorMidPoint = anchorRect.top;
}
const middleAlignment = {
popoverTop: anchorMidPoint,
contentHeight:
( anchorMidPoint - height / 2 > 0 ? height / 2 : anchorMidPoint ) +
( anchorMidPoint + height / 2 > window.innerHeight
? window.innerHeight - anchorMidPoint
: height / 2 ),
};
const topAlignment = {
popoverTop: anchorRect.top,
contentHeight:
anchorRect.top - HEIGHT_OFFSET - height > 0
? height
: anchorRect.top - HEIGHT_OFFSET,
};
const bottomAlignment = {
popoverTop: anchorRect.bottom,
contentHeight:
anchorRect.bottom + HEIGHT_OFFSET + height > window.innerHeight
? window.innerHeight - HEIGHT_OFFSET - anchorRect.bottom
: height,
};
// Choosing the y axis
let chosenYAxis = yAxis;
let contentHeight = null;
if ( ! stickyBoundaryElement && ! forcePosition ) {
if ( yAxis === 'middle' && middleAlignment.contentHeight === height ) {
chosenYAxis = 'middle';
} else if ( yAxis === 'top' && topAlignment.contentHeight === height ) {
chosenYAxis = 'top';
} else if (
yAxis === 'bottom' &&
bottomAlignment.contentHeight === height
) {
chosenYAxis = 'bottom';
} else {
chosenYAxis =
topAlignment.contentHeight > bottomAlignment.contentHeight
? 'top'
: 'bottom';
const chosenHeight =
chosenYAxis === 'top'
? topAlignment.contentHeight
: bottomAlignment.contentHeight;
contentHeight = chosenHeight !== height ? chosenHeight : null;
}
}
let popoverTop;
if ( chosenYAxis === 'middle' ) {
popoverTop = middleAlignment.popoverTop;
} else if ( chosenYAxis === 'top' ) {
popoverTop = topAlignment.popoverTop;
} else {
popoverTop = bottomAlignment.popoverTop;
}
return {
yAxis: chosenYAxis,
popoverTop,
contentHeight,
};
}
/**
* Utility used to compute the popover position and the content max width/height
* for a popover given its anchor rect and its content size.
*
* @param {Object} anchorRect Anchor Rect.
* @param {Object} contentSize Content Size.
* @param {string} position Position.
* @param {boolean} stickyBoundaryElement The boundary element to use when
* switching between sticky and normal
* position.
* @param {Element} anchorRef The anchor element.
* @param {number} relativeOffsetTop If applicable, top offset of the
* relative positioned parent container.
* @param {Element} boundaryElement Boundary element.
* @param {boolean} forcePosition Don't adjust position based on anchor.
*
* @return {Object} Popover position and constraints.
*/
export function computePopoverPosition(
anchorRect,
contentSize,
position = 'top',
stickyBoundaryElement,
anchorRef,
relativeOffsetTop,
boundaryElement,
forcePosition
) {
const [ yAxis, xAxis = 'center', corner ] = position.split( ' ' );
const yAxisPosition = computePopoverYAxisPosition(
anchorRect,
contentSize,
yAxis,
corner,
stickyBoundaryElement,
anchorRef,
relativeOffsetTop,
forcePosition
);
const xAxisPosition = computePopoverXAxisPosition(
anchorRect,
contentSize,
xAxis,
corner,
stickyBoundaryElement,
yAxisPosition.yAxis,
boundaryElement,
forcePosition
);
return {
...xAxisPosition,
...yAxisPosition,
};
}
/**
* Offsets the given rect by the position of the iframe that contains the element.
* If the owner document is not in an iframe then it returns with the original rect.
*
* @param {DOMRect} rect bounds of the element
* @param {Document} ownerDocument document of the element
*
* @return {DOMRect} offsetted bounds
*/
export function offsetIframe( rect, ownerDocument ) {
const { defaultView } = ownerDocument;
const { frameElement } = defaultView;
if ( ! frameElement ) {
return rect;
}
const iframeRect = frameElement.getBoundingClientRect();
return new defaultView.DOMRect(
rect.left + iframeRect.left,
rect.top + iframeRect.top,
rect.width,
rect.height
);
}