@momentum-ui/react
Version:
Cisco Momentum UI framework for ReactJs applications
1,066 lines (945 loc) • 38.4 kB
JavaScript
/** @component event-overlay */
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import omit from 'lodash/omit';
import FocusLock from 'react-focus-lock';
const defaultDims = {
offsetTop: 0,
bottom: 0,
center: 0,
height: 0,
left: 0,
middle: 0,
right: 0,
top: 0,
width: 0,
};
function eventPath(evt) {
let path = (evt.composedPath && evt.composedPath()) || evt.path,
target = evt.target;
if (path != null) {
// Safari doesn't include Window, and it should.
path = path.indexOf(window) < 0 ? path.concat([window]) : path;
return path;
}
if (target === window) {
return [window];
}
function getParents(node, memo) {
memo = memo || [];
let parentNode = node !== undefined ? node.parentNode : false;
if (!parentNode) {
return memo;
} else {
return getParents(parentNode, memo.concat([parentNode]));
}
}
return [target].concat(getParents(target)).concat([window]);
}
class EventOverlay extends React.Component {
static getDerivedStateFromProps({ isOpen }, state) {
return {
...state,
isOpen: isOpen,
};
}
state = {
absoluteParent: null,
containerParent: null,
isOpen: false,
scrollParent: null,
transformParent: null,
visibleDirection: this.props.direction,
};
componentDidMount = () => {
this.props.isOpen && this.forceUpdate();
this.addHandlers();
};
componentDidUpdate = (prevProps, prevState) => {
const { direction } = this.props;
const { isOpen } = this.state;
if ((isOpen && prevState.isOpen !== isOpen) || prevProps.direction !== direction) {
return this.forceUpdate(() => this.isVisible());
} else if (!isOpen && prevState.isOpen !== isOpen) {
this.focusOnAnchorNode();
}
};
componentWillUnmount = () => {
this.removeHandlers();
};
addHandlers = () => {
const {
absoluteParentID,
allowClickAway,
boundingParentID,
checkOverflow,
closeOnClick,
isContained,
scrollParentID,
transformParentID,
} = this.props;
this.handleResize = this.isVisible;
this.handleScroll = this.isVisible;
const element = ReactDOM.findDOMNode(this.container);
const elementParent = element && element.parentElement;
const elementParents = elementParent && this.findParents(elementParent);
let scrollParent;
if (allowClickAway) {
document.addEventListener('click', this.handleAllowClickAway, true);
document.addEventListener('keydown', this.handleKeyDown, false);
}
closeOnClick && document.addEventListener('click', this.handleCloseOnClick, false);
window.addEventListener('resize', this.handleResize, false);
document.addEventListener('scroll', this.handleScroll, false);
if (scrollParentID) {
scrollParent = document.getElementById(scrollParentID);
scrollParent && scrollParent.addEventListener('scroll', this.handleScroll, false);
}
if (checkOverflow) {
scrollParent =
!scrollParent &&
elementParents &&
this.findScrollParent(elementParents, ['overflow', 'overflow-y', 'overflow-x']);
scrollParent && scrollParent.addEventListener('scroll', this.handleScroll, false);
}
const transformParent = transformParentID
? document.getElementById(transformParentID)
: elementParents && this.findTransformParent(elementParents, ['transform'], 1);
const absoluteParent = absoluteParentID
? document.getElementById(absoluteParentID)
: elementParents && this.findAbsoluteParent(elementParents, ['position'], 1);
const containerParent =
(isContained && document.getElementById(boundingParentID)) || scrollParent;
this.observer = new MutationObserver(this.isVisible);
this.observer.observe(document.body, {
attributes: false,
characterData: false,
childList: true,
subtree: true,
attributeOldValue: false,
characterDataOldValue: false,
});
this.setState(
{
absoluteParent,
containerParent,
scrollParent,
transformParent,
},
() => this.isVisible()
);
};
findOverflow = (node, searchProps) => {
return searchProps.reduce((agg, prop) => {
let overflowElement = window.getComputedStyle(ReactDOM.findDOMNode(node))[prop];
return !overflowElement || agg.includes(overflowElement) ? agg : (agg += overflowElement);
}, '');
};
findParents = (ele, tempParentArr = []) => {
return !ele.parentElement
? tempParentArr
: this.findParents(ele.parentElement, tempParentArr.concat(ele));
};
findAbsoluteParent = (elementParents, searchProps, startIndex) => {
let absoluteElement;
let idx = startIndex;
while (!absoluteElement && elementParents[idx]) {
let currentAbsoluteElement = this.findOverflow(elementParents[idx], searchProps);
if (/(absolute)/.test(currentAbsoluteElement)) {
return (absoluteElement = elementParents[idx]);
}
idx++;
}
return absoluteElement ? absoluteElement : null;
};
findScrollParent = (elementParents, searchProps) => {
let overflowElement = null;
let idx = 1;
while (!overflowElement && elementParents[idx]) {
let currentOverflowElement = this.findOverflow(elementParents[idx], searchProps);
if (/(auto|scroll|hidden)/.test(currentOverflowElement)) {
return (overflowElement = elementParents[idx]);
}
idx++;
}
return overflowElement ? overflowElement : null;
};
findTransformParent = (elementParents, searchProps, startIndex) => {
let transformElement = null;
let idx = startIndex;
while (!transformElement && elementParents[idx]) {
let potentialTransformElement = this.findOverflow(elementParents[idx], ['will-change']);
let currentTransformElement = this.findOverflow(elementParents[idx], searchProps);
if (/(transform)/.test(potentialTransformElement) || currentTransformElement !== 'none') {
return (transformElement = elementParents[idx]);
}
idx++;
}
return transformElement ? transformElement : null;
};
focusOnAnchorNode = () => {
const { anchorNode } = this.props;
const domAnchorNode =
anchorNode &&
(anchorNode.props ? anchorNode.props.onClick : false) &&
ReactDOM.findDOMNode(anchorNode);
domAnchorNode && domAnchorNode.focus();
};
getAnchorPosition = node => {
const { transformParent } = this.state;
const rect = node.getBoundingClientRect();
const transformParentDims = transformParent && this.getElementPosition(transformParent);
const parentRect = transformParentDims || defaultDims;
const anchorPosition = {
top: rect.top - parentRect.top,
left: rect.left - parentRect.left,
width: node.offsetWidth,
height: node.offsetHeight,
};
anchorPosition.right =
(rect.right || anchorPosition.left + anchorPosition.width) - parentRect.left;
anchorPosition.bottom =
(rect.bottom || anchorPosition.top + anchorPosition.height) - parentRect.top;
anchorPosition.middle = anchorPosition.left + (anchorPosition.right - anchorPosition.left) / 2;
anchorPosition.center = anchorPosition.top + (anchorPosition.bottom - anchorPosition.top) / 2;
return anchorPosition;
};
getAbsoluteAnchorPosition = (node, absoluteParentDims) => {
const { scrollParent } = this.state;
const rect = node.getBoundingClientRect();
const parentRect = absoluteParentDims;
const scrollAdjust = (scrollParent && scrollParent.scrollTop) || 0;
const anchorPosition = {
top: absoluteParentDims.offsetTop
? absoluteParentDims.offsetTop + node.offsetTop - scrollAdjust
: rect.top - parentRect.top,
left: absoluteParentDims.offsetLeft
? absoluteParentDims.offsetLeft - node.offsetLeft
: rect.left - parentRect.left,
width: node.offsetWidth,
height: node.offsetHeight,
};
anchorPosition.right =
(rect.right || anchorPosition.left + anchorPosition.width) - parentRect.left;
anchorPosition.bottom = anchorPosition.top + anchorPosition.height;
anchorPosition.middle = anchorPosition.left + (anchorPosition.right - anchorPosition.left) / 2;
anchorPosition.center = anchorPosition.top + (anchorPosition.bottom - anchorPosition.top) / 2;
return anchorPosition;
};
getElementPosition = element => {
const elementRect = element.getBoundingClientRect();
return {
offsetTop: element.offsetTop,
offsetLeft: element.offsetLeft,
bottom: elementRect.bottom,
top: elementRect.top,
left: elementRect.left,
height: elementRect.height,
width: elementRect.width,
hasAbsParent:
element.offsetTop !== elementRect.top || element.offsetLeft !== elementRect.left,
};
};
getOrigin = () => {
const side = this.state.visibleDirection.split('-')[0];
const alignment = this.props.direction.split('-')[1];
const origin = {
anchor: {},
target: {},
};
if (side === 'top' || side === 'bottom') {
origin.anchor.vertical = side;
origin.anchor.horizontal = alignment === 'center' ? 'middle' : alignment;
origin.target.vertical = side === 'top' ? 'bottom' : 'top';
origin.target.horizontal = alignment === 'center' ? 'middle' : alignment;
}
if (side === 'left' || side === 'right') {
origin.anchor.vertical = alignment;
origin.anchor.horizontal = side;
origin.target.vertical = alignment;
origin.target.horizontal = side === 'left' ? 'right' : 'left';
}
return origin;
};
getTargetPosition = targetNode => {
return {
top: 0,
center: targetNode.offsetHeight / 2,
bottom: targetNode.offsetHeight,
left: 0,
middle: targetNode.offsetWidth / 2,
right: targetNode.offsetWidth,
};
};
handleAllowClickAway = e => {
if (!this.props.isOpen) return;
const eventTarget = eventPath(e)[0];
const anchorNode = ReactDOM.findDOMNode(this.props.anchorNode);
return (
this.container &&
!anchorNode.contains(eventTarget) &&
!ReactDOM.findDOMNode(this.container).contains(eventTarget) &&
this.handleClickAway(e)
);
};
handleClickAway = e => {
const { close } = this.props;
this.focusOnAnchorNode();
close && close(e);
};
handleCloseOnClick = e => {
if (!this.props.isOpen) return;
const eventTarget = eventPath(e)[0];
const { closeOnClick } = this.props;
return (
closeOnClick &&
this.container &&
ReactDOM.findDOMNode(this.container).contains(eventTarget) &&
this.handleClickAway(e)
);
};
handleKeyDown = e => {
const {isOpen, disableCloseOnEnterOrSpaceKey} = this.props;
if (!isOpen) return;
if (e.keyCode === 27) return this.handleClickAway(e);
if (!disableCloseOnEnterOrSpaceKey || (disableCloseOnEnterOrSpaceKey && e.which !== 13 && e.which !== 32)) {
const eventTarget = eventPath(e)[0];
const anchorNode = ReactDOM.findDOMNode(this.props.anchorNode);
return (
this.container &&
anchorNode &&
!anchorNode.contains(eventTarget) &&
!ReactDOM.findDOMNode(this.container).contains(eventTarget) &&
this.handleClickAway(e)
);
}
};
isVisible = () => {
const { anchorNode, direction, isOpen, isDynamic } = this.props;
if (!isOpen) return;
if (!isDynamic) return this.setPlacement();
const anchorElement = ReactDOM.findDOMNode(anchorNode);
const element = ReactDOM.findDOMNode(this.container);
const side = direction.split('-')[0];
const alignment = direction.split('-')[1];
const anchorDims = anchorElement && anchorElement.getBoundingClientRect();
const elementBoundingRect = element.getBoundingClientRect();
const elementParent = element.parentElement;
['top', 'bottom'].includes(side)
? this.setVerticalClass(alignment, anchorDims, elementBoundingRect, elementParent)
: this.setHorizontalClass(alignment, anchorDims, elementBoundingRect, elementParent);
};
removeHandlers = () => {
const { scrollParent } = this.state;
document.removeEventListener('click', this.handleAllowClickAway, true);
document.removeEventListener('click', this.handleCloseOnClick, false);
document.removeEventListener('keydown', this.handleKeyDown, false);
window.removeEventListener('resize', this.handleResize, false);
document.removeEventListener('scroll', this.handleScroll, false);
scrollParent && scrollParent.removeEventListener('scroll', this.handleScroll, false);
this.observer && this.observer.disconnect() && this.observer.takeRecords();
};
setArrowPlacement = (anchor, container) => {
const arrow = this.arrow;
const { targetOffset } = this.props;
const { visibleDirection } = this.state;
const side = visibleDirection.split('-')[0];
const verticalOffset = targetOffset.vertical || 0;
const horizontalOffset = targetOffset.horizontal || 0;
const isAnchorWider = anchor.width > container.right;
const isAnchorTaller = anchor.height > container.bottom;
const arrowLeft =
isAnchorWider && !visibleDirection.includes('center')
? visibleDirection.includes('left')
? container.middle + anchor.left
: anchor.right - container.middle
: anchor.middle;
const arrowTop =
isAnchorTaller && !visibleDirection.includes('center')
? visibleDirection.includes('top')
? container.center + anchor.top
: anchor.bottom - container.center
: anchor.center;
switch (side) {
case 'top':
arrow.style.left = `${arrowLeft}px`;
arrow.style.top = `${anchor.top - verticalOffset}px`;
break;
case 'bottom':
arrow.style.left = `${arrowLeft}px`;
arrow.style.top = `${anchor.bottom + verticalOffset}px`;
break;
case 'left':
arrow.style.left = `${anchor.left - horizontalOffset}px`;
arrow.style.top = `${arrowTop}px`;
break;
case 'right':
arrow.style.left = `${anchor.right + horizontalOffset}px`;
arrow.style.top = `${arrowTop}px`;
break;
}
};
setBoundingBox = (side, targetNode, anchorPosition) => {
const { checkOverflow, isContained, maxHeight, maxWidth, showArrow, targetOffset } = this.props;
const { absoluteParent, scrollParent, transformParent } = this.state;
const arrowDims = showArrow && ReactDOM.findDOMNode(this.arrow).getBoundingClientRect();
const checkVertical = isContained === 'vertical';
const checkHorizontal = isContained === 'horizontal';
const element = ReactDOM.findDOMNode(this.container);
const documentScrollTop = document.documentElement.scrollTop;
const documentBottom = document.documentElement.scrollHeight;
const windowBottom = window.pageXOffset + window.innerHeight;
const documentRight = Math.max(document.documentElement.offsetWidth, document.documentElement.clientWidth);
const arrowHeight = (arrowDims && arrowDims.height) || 0;
const arrowWidth = (arrowDims && arrowDims.width) || 0;
const offsetHeight = targetOffset.vertical || 0;
const offsetWidth = targetOffset.horizontal || 0;
const elementDims = element.getBoundingClientRect();
const elementVerticalHeight = elementDims.height + offsetHeight;
const elementVerticalWidth = elementDims.width + offsetWidth;
const getAvailableTopSpace = top =>
top + anchorPosition.top - (this.elementHeight + arrowHeight);
const scrollParentDimsv2 = this.setBoundingContainer(scrollParent);
const scrollParentDims = scrollParent ? scrollParent.getBoundingClientRect() : defaultDims;
const absoluteParentDims = absoluteParent && this.getElementPosition(absoluteParent);
const transformParentDims = transformParent && this.getElementPosition(transformParent);
const scrollParentScrollTop = (scrollParent && scrollParent.offsetTop) || 0;
if (targetNode && targetNode.style && !targetNode.style.bottom && elementVerticalHeight) {
this.elementHeight = elementVerticalHeight;
this.elementBottom = elementDims.bottom;
}
if (targetNode && targetNode.style && !targetNode.style.right && elementVerticalWidth) {
this.elementWidth = elementVerticalWidth;
this.elementLeft = elementDims.left;
this.elementRight = elementDims.right;
}
switch (side) {
case 'top':
if (!scrollParent && !transformParentDims) {
if (!checkHorizontal) {
targetNode.style.bottom = `${windowBottom -
anchorPosition.top +
arrowHeight +
offsetHeight}px`;
if (getAvailableTopSpace(documentScrollTop) < 0) {
targetNode.style.top = `${arrowHeight - documentScrollTop}px`;
}
}
if (!checkVertical) {
if (elementDims.right > documentRight || this.elementWidth > documentRight) {
targetNode.style.right = '0px';
if (this.elementWidth < documentRight) {
targetNode.style.left = `${documentRight - this.elementWidth}px`;
}
}
if (this.elementLeft < 0) {
targetNode.style.left = '0px';
}
}
} else {
if (transformParentDims) {
targetNode.style.bottom = `${transformParentDims.height -
anchorPosition.top +
arrowHeight +
offsetHeight}px`;
if (anchorPosition.top - scrollParentScrollTop - this.elementHeight - arrowHeight < 0) {
targetNode.style.top = `${scrollParentScrollTop + arrowHeight}px`;
targetNode.style.maxHeight = `${maxHeight || transformParentDims.height}px`;
}
if (!checkVertical) {
if (
this.elementWidth > transformParentDims.width ||
this.elementRight > transformParentDims.right
) {
targetNode.style.right = `${0}px`;
if (this.elementWidth > transformParentDims.width) {
targetNode.style.left = `0px`;
} else {
targetNode.style.left = `${this.elementWidth}px`;
}
}
if (this.elementLeft < transformParentDims.left) {
targetNode.style.left = `${0}px`;
}
}
if (
arrowDims &&
(arrowDims.top - (scrollParent ? scrollParentDims.top : transformParentDims.top) <
0 ||
arrowDims.bottom + 1 >
(scrollParent ? scrollParentDims.bottom : transformParentDims.bottom))
) {
this.arrow.style.visibility = 'hidden';
} else if (arrowDims) {
this.arrow.style.visibility = 'visible';
}
} else {
targetNode.style.bottom = `${windowBottom -
anchorPosition.top +
arrowHeight +
offsetHeight}px`;
if (!checkHorizontal) {
if (
anchorPosition.top - scrollParentDimsv2.top - this.elementHeight - arrowHeight <
0
) {
targetNode.style.top = `${scrollParentDimsv2.top + arrowHeight}px`;
targetNode.style.maxHeight = `${maxHeight || scrollParentDimsv2.height}px`;
}
}
if (!checkVertical) {
if (
this.elementWidth > scrollParentDimsv2.width ||
this.elementRight > scrollParentDimsv2.right
) {
targetNode.style.right = `${documentRight - scrollParentDimsv2.right}px`;
}
if (this.elementLeft < scrollParentDimsv2.left) {
targetNode.style.left = `${scrollParentDimsv2.left}px`;
}
}
if (
arrowDims &&
(arrowDims.top < scrollParentDims.top ||
arrowDims.bottom + 1 > scrollParentDims.bottom)
) {
this.arrow.style.visibility = 'hidden';
} else if (arrowDims) {
this.arrow.style.visibility = 'visible';
}
}
}
break;
case 'bottom':
if (!scrollParentDims.bottom && !transformParentDims) {
if (
this.elementHeight + arrowHeight + anchorPosition.bottom + documentScrollTop >
documentBottom
) {
targetNode.style.bottom = `${documentScrollTop + windowBottom - documentBottom}px`;
}
if (elementDims.right >= documentRight || this.elementWidth > documentRight) {
targetNode.style.right = '0px';
if (this.elementWidth < documentRight) {
targetNode.style.left = 'inherit';
}
}
if (elementDims.left < 0) {
targetNode.style.left = '0px';
}
} else if (scrollParentDims.bottom && !transformParentDims) {
targetNode.style.bottom = 'auto';
if (anchorPosition.bottom + arrowHeight - scrollParentDims.top < 0) {
targetNode.style.top = `${scrollParentDims.top - arrowHeight}px`;
}
if (this.elementHeight + arrowHeight + anchorPosition.bottom > scrollParentDims.bottom) {
targetNode.style.bottom = `${windowBottom - scrollParentDims.bottom}px`;
targetNode.style.maxHeight = `${maxHeight || scrollParentDims.height}px`;
}
if (
this.elementWidth > scrollParentDims.width ||
this.elementRight > scrollParentDims.right
) {
targetNode.style.right = `${documentRight - scrollParentDims.right}px`;
}
if (this.elementLeft < scrollParentDims.left) {
targetNode.style.left = `${scrollParentDims.left}px`;
}
if (
arrowDims &&
(arrowDims.top < scrollParentDims.top || arrowDims.bottom + 1 > scrollParentDims.bottom)
) {
this.arrow.style.visibility = 'hidden';
} else if (arrowDims) {
this.arrow.style.visibility = 'visible';
}
} else {
if (
anchorPosition.bottom + arrowHeight + offsetHeight <
scrollParentDims.top - transformParentDims.top
) {
targetNode.style.top = `${scrollParentDims.top -
transformParentDims.top -
arrowHeight}px`;
}
if (
this.elementHeight + arrowHeight + anchorPosition.bottom >
transformParentDims.height + ((absoluteParentDims && absoluteParentDims.offsetTop) || 0)
) {
targetNode.style.bottom = `0px`;
}
if (this.elementLeft < transformParentDims.left) {
targetNode.style.left = `0px`;
}
if (
this.elementWidth > transformParentDims.width ||
this.elementRight > transformParentDims.right
) {
targetNode.style.right = `0px`;
if (this.elementWidth > transformParentDims.width) {
targetNode.style.left = `0px`;
} else {
targetNode.style.left = `${transformParentDims.width - this.elementWidth}px`;
}
}
if (
arrowDims &&
(arrowDims.top < (checkOverflow ? scrollParentDims.top : transformParentDims.top) ||
arrowDims.bottom + 1 >
(checkOverflow ? scrollParentDims.bottom : transformParentDims.bottom))
) {
this.arrow.style.visibility = 'hidden';
} else if (arrowDims) {
this.arrow.style.visibility = 'visible';
}
}
break;
case 'left':
if (!scrollParentDims.left && !transformParentDims) {
targetNode.style.right = `${documentRight -
anchorPosition.left +
arrowWidth +
offsetWidth}px`;
if ( elementDims.left - arrowWidth < 0 ) {
targetNode.style.left = `${arrowWidth}px`;
} else {
targetNode.style.left = 'inherit';
}
if (getAvailableTopSpace(documentScrollTop) < 0) {
targetNode.style.top = `${-documentScrollTop}px`;
}
if (
this.elementHeight + arrowHeight + anchorPosition.bottom + documentScrollTop >
documentBottom
) {
targetNode.style.bottom = `${documentScrollTop + windowBottom - documentBottom}px`;
}
} else if (scrollParentDims.left && !transformParentDims) {
if (anchorPosition.left - scrollParentDims.left < this.elementWidth + arrowWidth) {
targetNode.style.left = `${scrollParentDims.left + arrowWidth}px`;
targetNode.style.right = `${documentRight -
anchorPosition.left +
arrowWidth +
offsetWidth}px`;
targetNode.style.maxWidth = `${maxWidth || scrollParentDims.width}px`;
}
if (anchorPosition.top - scrollParentDims.top - this.elementHeight < 0) {
targetNode.style.top = `${scrollParentDims.top}px`;
}
if (this.elementHeight + anchorPosition.bottom > scrollParentDims.bottom) {
targetNode.style.bottom = `${windowBottom - scrollParentDims.bottom}px`;
}
if (
arrowDims &&
(arrowDims.top < scrollParentDims.top || arrowDims.bottom > scrollParentDims.bottom)
) {
this.arrow.style.visibility = 'hidden';
} else if (arrowDims) {
this.arrow.style.visibility = 'visible';
}
}
break;
case 'right':
if (!scrollParentDims.right && !transformParentDims) {
if (arrowWidth + offsetWidth + elementDims.width + anchorPosition.right > documentRight) {
targetNode.style.right = '0px';
}
if (getAvailableTopSpace(documentScrollTop) < 0) {
targetNode.style.top = `${-documentScrollTop}px`;
}
if (
this.elementHeight + arrowHeight + anchorPosition.bottom + documentScrollTop >
documentBottom
) {
targetNode.style.bottom = `${documentScrollTop + windowBottom - documentBottom}px`;
}
} else if (scrollParentDims.right && !transformParentDims) {
if (anchorPosition.right + this.elementWidth + arrowWidth > scrollParentDims.right) {
targetNode.style.left = `${anchorPosition.right + offsetWidth}px`;
targetNode.style.right = transformParentDims
? `${scrollParentDims.width}px`
: `${documentRight - scrollParentDims.right}px`;
targetNode.style.maxWidth = `${maxWidth || scrollParentDims.width}px`;
}
if (anchorPosition.top - scrollParentDims.top - this.elementHeight < 0) {
targetNode.style.top = `${scrollParentDims.top}px`;
}
if (this.elementHeight + anchorPosition.bottom > scrollParentDims.bottom) {
targetNode.style.bottom = `${windowBottom - scrollParentDims.bottom}px`;
}
if (
arrowDims &&
(arrowDims.top < scrollParentDims.top || arrowDims.bottom > scrollParentDims.bottom)
) {
this.arrow.style.visibility = 'hidden';
} else if (arrowDims) {
this.arrow.style.visibility = 'visible';
}
}
break;
}
};
setBoundingContainer = containerNode => {
const { boundingParentID, isContained } = this.props;
const { containerParent } = this.state;
const containerNodeDims =
(containerNode && containerNode.getBoundingClientRect()) || defaultDims;
const containerParentDims =
(containerParent && containerParent.getBoundingClientRect()) || defaultDims;
const checkVertical = isContained === true || isContained === 'vertical';
const checkHorizontal = isContained === true || isContained === 'horizontal';
return {
bottom:
checkVertical && boundingParentID ? containerParentDims.bottom : containerNodeDims.bottom,
center: 0,
height:
checkVertical && boundingParentID ? containerParentDims.height : containerNodeDims.height,
left: checkHorizontal && boundingParentID ? containerParentDims.left : containerNodeDims.left,
middle: 0,
right:
checkHorizontal && boundingParentID ? containerParentDims.right : containerNodeDims.right,
top: checkVertical && boundingParentID ? containerParentDims.top : containerNodeDims.top,
width:
checkHorizontal && boundingParentID ? containerParentDims.width : containerNodeDims.width,
};
};
setHorizontalClass = (alignment, anchor, elementBoundingRect, elementParent) => {
const { showArrow, checkOverflow, targetOffset, scrollParentID } = this.props;
const windowRight = window.pageYOffset + window.innerWidth;
const elementWidth = elementBoundingRect.width;
const anchorRight = anchor.right;
const arrowWidth = showArrow
? ReactDOM.findDOMNode(this.arrow).getBoundingClientRect().width
: 0;
const offsetWidth = targetOffset.horizontal || 0;
const totalWidth = anchorRight + elementWidth + arrowWidth + offsetWidth;
const elementParents = this.findParents(elementParent);
const scrollParent = scrollParentID
? React.findDOMNode(scrollParentID)
: this.findScrollParent(elementParents, ['overflow', 'overflow-x']);
const parentRight =
(checkOverflow &&
!!scrollParent.getBoundingClientRect &&
scrollParent.getBoundingClientRect().right) ||
windowRight;
return totalWidth < parentRight && totalWidth < windowRight
? this.setState({ visibleDirection: `right-${alignment}` }, () => this.setPlacement())
: this.setState({ visibleDirection: `left-${alignment}` }, () => this.setPlacement());
};
setPlacement = () => {
const { anchorNode, isOpen, isContained, showArrow, targetOffset } = this.props;
const { visibleDirection, absoluteParent, transformParent } = this.state;
if (!isOpen) return;
const anchorElement = ReactDOM.findDOMNode(anchorNode);
const side = visibleDirection.split('-')[0];
const targetNode = this.container;
const verticalOffset = targetOffset.vertical || 0;
const horizontalOffset = targetOffset.horizontal || 0;
const absoluteParentDims = absoluteParent && this.getElementPosition(absoluteParent);
if (!targetNode || !anchorElement) return;
anchorElement.link = this.state.id;
const anchorPosition =
!!transformParent && absoluteParentDims && absoluteParentDims.hasAbsParent
? this.getAbsoluteAnchorPosition(anchorElement, absoluteParentDims)
: this.getAnchorPosition(anchorElement);
const targetPosition = this.getTargetPosition(targetNode);
const origin = this.getOrigin();
const anchorOrigin = origin.anchor;
const targetOrigin = origin.target;
const targetNodePosition = {
top:
anchorPosition[anchorOrigin.vertical] -
targetPosition[targetOrigin.vertical] +
(side === 'top' ? -verticalOffset : verticalOffset),
left:
anchorPosition[anchorOrigin.horizontal] -
targetPosition[targetOrigin.horizontal] +
(side === 'left' ? -horizontalOffset : horizontalOffset),
};
targetNode.style.top = `${targetNodePosition.top}px`;
targetNode.style.left = `${targetNodePosition.left}px`;
showArrow && this.setArrowPlacement(anchorPosition, targetPosition);
isContained && this.setBoundingBox(side, targetNode, anchorPosition);
};
setVerticalClass = (alignment, anchor, elementBoundingRect, elementParent) => {
const { showArrow, checkOverflow, targetOffset, scrollParentID } = this.props;
const windowBottom = window.pageXOffset + window.innerHeight;
const elementHeight = elementBoundingRect.height;
const anchorBottom = anchor.bottom;
const arrowHeight = showArrow
? ReactDOM.findDOMNode(this.arrow).getBoundingClientRect().height
: 0;
const offsetHeight = targetOffset.vertical || 0;
const totalHeight = anchorBottom + elementHeight + arrowHeight + offsetHeight;
const elementParents = this.findParents(elementParent);
const scrollParent = scrollParentID
? React.findDOMNode(scrollParentID)
: this.findScrollParent(elementParents, ['overflow', 'overflow-x']);
const parentBottom =
(checkOverflow &&
!!scrollParent.getBoundingClientRect &&
scrollParent.getBoundingClientRect().bottom) ||
windowBottom;
return totalHeight < parentBottom && totalHeight < windowBottom
? this.setState({ visibleDirection: `bottom-${alignment}` }, () => this.setPlacement())
: this.setState({ visibleDirection: `top-${alignment}` }, () => this.setPlacement());
};
render() {
const {
children,
className,
focusLockProps,
isOpen,
maxHeight,
maxWidth,
portalNode,
shouldLockFocus,
showArrow,
style,
...props
} = this.props;
const side = this.state.visibleDirection.split('-')[0];
const otherProps = omit({ ...props }, [
'absoluteParentID',
'allowClickAway',
'anchorNode',
'boundingParentID',
'checkOverflow',
'close',
'closeOnClick',
'direction',
'disableCloseOnEnterOrSpaceKey',
'isDynamic',
'isContained',
'scrollParentID',
'targetOffset',
'transformParentID',
]);
const contentNodes = isOpen && (
<div
className={
'md-event-overlay' +
`${(showArrow && ` md-event-overlay--arrow`) || ''}` +
`${(side && ` md-event-overlay--${side}`) || ''}` +
`${(className && ` ${className}`) || ''}`
}
>
{showArrow && <div ref={ref => (this.arrow = ref)} className="md-event-overlay__arrow" />}
<div
className="md-event-overlay__children"
ref={ref => (this.container = ref)}
style={{
...(maxWidth && { maxWidth: `${maxWidth}px` }),
...(maxHeight && { maxHeight: `${maxHeight}px` }),
...style,
}}
{...otherProps}
>
{children}
</div>
</div>
);
const withFocusLock = content =>
shouldLockFocus ? <FocusLock {...focusLockProps}>{content}</FocusLock> : content;
const withPortal = content =>
portalNode ? ReactDOM.createPortal(content, portalNode) : content;
return withFocusLock(withPortal(contentNodes));
}
}
EventOverlay.defaultProps = {
absoluteParentID: null,
allowClickAway: true,
anchorNode: null,
boundingParentID: null,
children: null,
checkOverflow: false,
className: '',
close: null,
direction: 'bottom-left',
disableCloseOnEnterOrSpaceKey: false,
focusLockProps: null,
isContained: '',
isDynamic: false,
isOpen: false,
maxHeight: null,
maxWidth: null,
portalNode: null,
scrollParentID: null,
shouldLockFocus: false,
showArrow: false,
style: null,
targetOffset: {
horizontal: 0,
vertical: 0,
},
transformParentID: null,
};
EventOverlay.propTypes = {
/** @prop Set the id of the absoluteParent | null */
absoluteParentID: PropTypes.string,
/** @prop Allows user to click outside of EventOverlay | true */
allowClickAway: PropTypes.bool,
/** @prop Node which serves as basis of dom positioning | null */
anchorNode: PropTypes.object,
/** @prop Set the id of the boundingParent | null */
boundingParentID: PropTypes.string,
/** @prop Set to determine if dom ancestors have overflow property | false */
checkOverflow: PropTypes.bool,
/** @prop Children nodes to render inside the EventOverlay | null */
children: PropTypes.node,
/** @prop Optional css class string | '' */
className: PropTypes.string,
/** @prop Function to close EventOverlay | null */
close: PropTypes.func,
/** @prop Determines if the EventOverlay should close when clicked on | true */
closeOnClick: PropTypes.bool,
/** @prop Sets the direction in which the EventOverlay extends | 'bottom-left' */
direction: PropTypes.oneOf([
'top-center',
'left-center',
'right-center',
'bottom-center',
'top-left',
'top-right',
'bottom-left',
'bottom-right',
'left-top',
'left-bottom',
'right-top',
'right-bottom',
]),
/** @prop Prevent closing on ENTER or SPACE keydown event | false */
disableCloseOnEnterOrSpaceKey: PropTypes.bool,
/** @prop Props to be passed to focus lock component | null */
focusLockProps: PropTypes.object,
/** @prop Determines if the overlay is contained in bounding ancestor | '' */
isContained: PropTypes.oneOf([true, false, 'horizontal', 'vertical', 'both', '']),
/** @prop When true, will flip children based on space available (does not work with isContained) | false */
isDynamic: PropTypes.bool,
/** @prop Sets the visibility of the EventOverlay | false */
isOpen: PropTypes.bool,
/** @prop Sets the max height of the EventOverlay | null */
maxHeight: PropTypes.number,
/** @prop Sets the max width of the EventOverlay | null */
maxWidth: PropTypes.number,
/** @prop Node/ReactElement where overlay should be appended using ReactDOM portal | null */
portalNode: PropTypes.oneOfType([PropTypes.node, PropTypes.element]),
/** @prop Set the id of the scrollParent | null */
scrollParentID: PropTypes.string,
/** @prop Determines if focus should be locked to overlay | false */
shouldLockFocus: PropTypes.bool,
/** @prop Determines if the EventOverlay should show the open/close arrow | false */
showArrow: PropTypes.bool,
/** @prop Optional css styling | null */
style: PropTypes.object,
/** @prop Sets the target offset from anchorNode | { horizontal: 0, vertical: 0 } */
targetOffset: PropTypes.shape({
horizontal: PropTypes.number,
vertical: PropTypes.number,
}),
/** @prop Set the id of the transformParent | null */
transformParentID: PropTypes.string,
};
EventOverlay.displayName = 'EventOverlay';
export default EventOverlay;