UNPKG

@tikpage/reactjs-popup

Version:

React Popup Component - Modals,Tooltips and Menus —  All in one

396 lines (365 loc) 11.6 kB
import React from 'react'; import calculatePosition from './Utils'; import styles from './index.css.js'; import BodyEnd from './BodyEnd'; const POSITION_TYPES = [ 'top left', 'top center', 'top right', 'right top', 'right center', 'right bottom', 'bottom left', 'bottom center', 'bottom right', 'left top', 'left center', 'left bottom', 'center center', ]; export default class Popup extends React.PureComponent { static defaultProps = { trigger: null, onOpen: () => { }, onClose: () => { }, defaultOpen: false, open: false, disabled: false, closeOnDocumentClick: true, repositionOnResize: true, closeOnEscape: true, on: ['click'], contentStyle: {}, arrowStyle: {}, overlayStyle: {}, className: '', position: 'bottom center', modal: false, lockScroll: false, arrow: true, offsetX: 0, offsetY: 0, mouseEnterDelay: 100, mouseLeaveDelay: 100, keepTooltipInside: false, }; constructor(props) { super(props); this.setTriggerRef = r => (this.TriggerEl = r); this.setContentRef = r => (this.ContentEl = r); this.setArrowRef = r => (this.ArrowEl = r); this.setHelperRef = r => (this.HelperEl = r); this.timeOut = 0; const { open, modal, defaultOpen, trigger } = props; this.state = { isOpen: open || defaultOpen, modal: modal ? true : !trigger, // we create this modal state because the popup can't be a tooltip if the trigger prop doesn't exist }; } componentDidMount() { const { closeOnEscape, defaultOpen, repositionOnResize } = this.props; if (defaultOpen) { this.setPosition(); this.lockScroll(); } if (closeOnEscape) { /* eslint-disable-next-line no-undef */ window.addEventListener('keyup', this.onEscape); } if (repositionOnResize) { /* eslint-disable-next-line no-undef */ window.addEventListener('resize', this.repositionOnResize); } } componentDidUpdate(prevProps) { const { open, disabled } = this.props; const { isOpen } = this.state; if (prevProps.open !== open) { if (open) this.openPopup(); else this.closePopup(undefined, true); } if (prevProps.disabled !== disabled && disabled && isOpen) { this.closePopup(); } } componentWillUnmount() { // kill any function to execute if the component is unmounted clearTimeout(this.timeOut); const { closeOnEscape, repositionOnResize } = this.props; // remove events listeners if (closeOnEscape) { /* eslint-disable-next-line no-undef */ window.removeEventListener('keyup', this.onEscape); } if (repositionOnResize) { /* eslint-disable-next-line no-undef */ window.removeEventListener('resize', this.repositionOnResize); } this.resetScroll(); } repositionOnResize = () => { this.setPosition(); }; onEscape = e => { if (e.key === 'Escape') this.closePopup(); }; lockScroll = () => { const { lockScroll } = this.props; const { modal } = this.state; if (modal && lockScroll) /* eslint-disable-next-line no-undef */ document.getElementsByTagName('body')[0].style.overflow = 'hidden'; }; resetScroll = () => { const { lockScroll } = this.props; const { modal } = this.state; if (modal && lockScroll) /* eslint-disable-next-line no-undef */ document.getElementsByTagName('body')[0].style.overflow = 'auto'; }; togglePopup = e => { // https://reactjs.org/docs/events.html#event-pooling e.persist(); if (this.state.isOpen) this.closePopup(e); else this.openPopup(e); }; openPopup = e => { const { disabled, onOpen } = this.props; const { isOpen } = this.state; if (isOpen || disabled) return; onOpen(e); this.setState({ isOpen: true }, () => { this.setPosition(); this.lockScroll(); }); }; closePopup = e => { const { onClose } = this.props; const { isOpen } = this.state; if (!isOpen) return; onClose(e); this.setState({ isOpen: false }, () => { this.resetScroll(); }); }; onMouseEnter = () => { clearTimeout(this.timeOut); const { mouseEnterDelay } = this.props; this.timeOut = setTimeout(() => this.openPopup(), mouseEnterDelay); }; onMouseLeave = () => { clearTimeout(this.timeOut); const { mouseLeaveDelay } = this.props; this.timeOut = setTimeout(() => this.closePopup(), mouseLeaveDelay); }; getTooltipBoundary = () => { const { keepTooltipInside } = this.props; let boundingBox = { top: 0, left: 0, /* eslint-disable-next-line no-undef */ width: window.innerWidth, /* eslint-disable-next-line no-undef */ height: window.innerHeight, }; if (typeof keepTooltipInside === 'string') { /* eslint-disable-next-line no-undef */ const selector = document.querySelector(keepTooltipInside); if (process.env.NODE_ENV !== 'production') { if (selector === null) throw new Error( `${keepTooltipInside} selector does not exist : keepTooltipInside must be a valid html selector 'class' or 'Id' or a boolean value`, ); } boundingBox = selector.getBoundingClientRect(); } return boundingBox; }; setPosition = () => { const { modal, isOpen } = this.state; if (modal || !isOpen) return; const { arrow, position, offsetX, offsetY, keepTooltipInside, arrowStyle, className, } = this.props; const helper = this.HelperEl.getBoundingClientRect(); const trigger = this.TriggerEl.getBoundingClientRect(); const content = this.ContentEl.getBoundingClientRect(); const boundingBox = this.getTooltipBoundary(); let positions = Array.isArray(position) ? position : [position]; // keepTooltipInside would be activated if the keepTooltipInside exist or the position is Array if (keepTooltipInside || Array.isArray(position)) positions = [...positions, ...POSITION_TYPES]; const cords = calculatePosition( trigger, content, positions, arrow, { offsetX, offsetY, }, boundingBox, ); this.ContentEl.style.top = `${cords.top - helper.top}px`; this.ContentEl.style.left = `${cords.left - helper.left}px`; if (arrow) { this.ArrowEl.style.transform = cords.transform; this.ArrowEl.style['-ms-transform'] = cords.transform; this.ArrowEl.style['-webkit-transform'] = cords.transform; this.ArrowEl.style.top = arrowStyle.top || cords.arrowTop; this.ArrowEl.style.left = arrowStyle.left || cords.arrowLeft; this.ArrowEl.classList.add(`popup-arrow`); if (className !== '') { this.ArrowEl.classList.add(`${className}-arrow`); } } if ( /* eslint-disable-next-line no-undef */ window .getComputedStyle(this.TriggerEl, null) .getPropertyValue('position') === 'static' || /* eslint-disable-next-line no-undef */ window .getComputedStyle(this.TriggerEl, null) .getPropertyValue('position') === '' ) this.TriggerEl.style.position = 'relative'; }; addWarperAction = () => { const { contentStyle, className, on } = this.props; const { modal } = this.state; const popupContentStyle = modal ? styles.popupContent.modal : styles.popupContent.tooltip; const childrenElementProps = { className: `popup-content ${ className !== '' ? `${className}-content` : '' }`, style: Object.assign({}, popupContentStyle, contentStyle), ref: this.setContentRef, onClick: e => { e.stopPropagation(); }, }; if (!modal && on.indexOf('hover') >= 0) { childrenElementProps.onMouseEnter = this.onMouseEnter; childrenElementProps.onMouseLeave = this.onMouseLeave; } return childrenElementProps; }; renderTrigger = () => { const triggerProps = { key: 'T', ref: this.setTriggerRef }; const { on, trigger } = this.props; const { isOpen } = this.state; const onAsArray = Array.isArray(on) ? on : [on]; for (let i = 0, len = onAsArray.length; i < len; i++) { switch (onAsArray[i]) { case 'click': triggerProps.onClick = this.togglePopup; break; case 'hover': triggerProps.onMouseEnter = this.onMouseEnter; triggerProps.onMouseLeave = this.onMouseLeave; break; case 'focus': triggerProps.onFocus = this.onMouseEnter; break; default: } } if (typeof trigger === 'function') return !!trigger && React.cloneElement(trigger(isOpen), triggerProps); return !!trigger && React.cloneElement(trigger, triggerProps); }; renderContent = () => { const { arrow, arrowStyle, children } = this.props; const { modal, isOpen } = this.state; return ( <div {...this.addWarperAction()} key="C"> {arrow && !modal && ( <div ref={this.setArrowRef} style={Object.assign({}, styles.popupArrow, arrowStyle)} /> )} {typeof children === 'function' ? children(this.closePopup, isOpen) : children} </div> ); }; render() { const { overlayStyle, closeOnDocumentClick, className, on, trigger, } = this.props; const { modal, isOpen } = this.state; const overlay = isOpen && !(on.indexOf('hover') >= 0); const ovStyle = modal ? styles.overlay.modal : styles.overlay.tooltip; return [ this.renderTrigger(), overlay && ( <BodyEnd key="O" className={`popup-overlay tikfeed-popup-overlay ${ className !== '' ? `${className}-overlay` : '' }`} style={Object.assign({}, ovStyle, overlayStyle)} onClick={closeOnDocumentClick ? this.closePopup : undefined}> {modal && this.renderContent()} </BodyEnd> ), isOpen && !modal && this.renderContent(), ]; } } if (process.env.NODE_ENV !== 'production') { const PropTypes = require('prop-types'); const TRIGGER_TYPES = ['hover', 'click', 'focus']; Popup.propTypes = { arrowStyle: PropTypes.object, contentStyle: PropTypes.object, overlayStyle: PropTypes.object, className: PropTypes.string, modal: PropTypes.bool, arrow: PropTypes.bool, closeOnDocumentClick: PropTypes.bool, repositionOnResize: PropTypes.bool, disabled: PropTypes.bool, closeOnEscape: PropTypes.bool, lockScroll: PropTypes.bool, offsetX: PropTypes.number, offsetY: PropTypes.number, mouseEnterDelay: PropTypes.number, mouseLeaveDelay: PropTypes.number, onOpen: PropTypes.func, onClose: PropTypes.func, open: PropTypes.bool, defaultOpen: PropTypes.bool, trigger: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), // for uncontrolled component we don't need the trigger Element on: PropTypes.oneOfType([ PropTypes.oneOf(TRIGGER_TYPES), PropTypes.arrayOf(PropTypes.oneOf(TRIGGER_TYPES)), ]), children: PropTypes.oneOfType([ PropTypes.func, PropTypes.element, PropTypes.string, ]).isRequired, position: PropTypes.oneOfType([ PropTypes.oneOf(POSITION_TYPES), PropTypes.arrayOf(PropTypes.oneOf(POSITION_TYPES)), ]), keepTooltipInside: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), }; }