Created
August 31, 2018 21:55
-
-
Save willhoney7/e39ef6f2598964094f7b97c4d9396db0 to your computer and use it in GitHub Desktop.
Popover component can be used for Dropdowns/Tooltips/etc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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