Skip to content

Instantly share code, notes, and snippets.

@willhoney7
Created August 31, 2018 21:55
Show Gist options
  • Save willhoney7/e39ef6f2598964094f7b97c4d9396db0 to your computer and use it in GitHub Desktop.
Save willhoney7/e39ef6f2598964094f7b97c4d9396db0 to your computer and use it in GitHub Desktop.
Popover component can be used for Dropdowns/Tooltips/etc
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import _throttle from 'lodash/throttle';
let portalsRoot = document.getElementById('portals-root');
const validate = value => (value !== undefined && value < 0 ? 0 : value);
class Popover extends Component {
static propTypes = {
at: PropTypes.instanceOf(Element),
onClose: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
right: PropTypes.bool,
above: PropTypes.bool,
overlap: PropTypes.bool,
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func])
};
constructor(props) {
super(props);
this.el = document.createElement('div');
if (!portalsRoot) portalsRoot = document.getElementById('portals-root');
}
appendToDom = () => {
portalsRoot.appendChild(this.el);
this.addListeners();
};
removeFromDom = () => {
if (this.el.parentNode !== null) {
portalsRoot.removeChild(this.el);
this.removeListeners();
}
};
componentWillUnmount() {
this.removeFromDom();
}
addListeners = () => {
this.listenForResize();
this.listenForEscape();
};
removeListeners = () => {
this.stopListeningForResize();
this.stopListeningForEscape();
};
// resizing
listenForResize = () => {
window.addEventListener('resize', this.handleResize);
this.listener = setInterval(this.handleResize, 250);
};
stopListeningForResize = () => {
window.removeEventListener('resize', this.handleResize);
clearInterval(this.listener);
};
handleResize = _throttle(() => {
if (this.props && this.props.isOpen) this.forceUpdate();
}, 100);
// escape to remove
listenForEscape = () => {
window.addEventListener('keydown', this.handleKeypress);
};
stopListeningForEscape = () => {
window.removeEventListener('keydown', this.handleKeypress);
};
handleKeypress = e => {
if (e.keyCode === 27) {
this.props.onClose();
}
};
componentWillUpdate(nextProps) {
if (nextProps.isOpen && !this.props.isOpen) {
// opening
this.appendToDom();
} else if (!nextProps.isOpen && this.props.isOpen) {
// closing
this.removeFromDom();
}
}
getContentPosition = () => {
if (this.props.at) {
const { right: alignRight, overlap, above, at } = this.props;
const { top, bottom, left, right, height } = at.getBoundingClientRect();
const windowWidth = window.innerWidth < 992 ? 992 : window.innerWidth;
let rightPosition = alignRight
? windowWidth - (window.pageXOffset || 0) - right
: undefined;
let leftPosition = !alignRight ? left + window.pageXOffset : undefined;
let topPosition = !above
? top + (window.pageYOffset || 0) + (overlap ? 0 : height + 5)
: undefined;
let bottomPosition = above
? document.body.getBoundingClientRect().height -
(window.pageYOffset || 0) -
bottom +
(overlap ? 0 : height + 5)
: undefined;
return {
position: 'absolute',
top: validate(topPosition),
bottom: validate(bottomPosition),
right: validate(rightPosition),
left: validate(leftPosition)
};
}
};
checkRef = ref => {
if (ref) {
if (ref.getBoundingClientRect().left < 0) {
ref.style.left = '3px';
}
}
};
render() {
const { onClose, isOpen, at, content, above } = this.props;
if (isOpen && !at) console.error('popover reference element not found', at);
return isOpen && at
? ReactDOM.createPortal(
<div
onClick={onClose}
css={{
position: 'absolute',
width: '100%',
height: '100%',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 9000
}}
>
<div
onClick={ev => ev.stopPropagation()}
css={{ zIndex: 10000 }}
className={!above ? 'pb-3' : ''}
style={this.getContentPosition()}
ref={this.checkRef}
>
{typeof content === 'function' ? content() : content}
</div>
</div>,
this.el
)
: null;
}
}
export default Popover;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment