UNPKG

semantic-ui-react

Version:
396 lines (324 loc) 13.2 kB
import _extends from 'babel-runtime/helpers/extends'; import _classCallCheck from 'babel-runtime/helpers/classCallCheck'; import _createClass from 'babel-runtime/helpers/createClass'; import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn'; import _inherits from 'babel-runtime/helpers/inherits'; import _pick from 'lodash/pick'; import _reduce from 'lodash/reduce'; import _assign from 'lodash/assign'; import _invoke from 'lodash/invoke'; import _isArray from 'lodash/isArray'; import _mapValues from 'lodash/mapValues'; import _isNumber from 'lodash/isNumber'; import _includes from 'lodash/includes'; import _without from 'lodash/without'; import cx from 'classnames'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { eventStack, childrenUtils, customPropTypes, getElementType, getUnhandledProps, isBrowser, META, SUI, useKeyOnly, useKeyOrValueAndKey } from '../../lib'; import Portal from '../../addons/Portal'; import PopupContent from './PopupContent'; import PopupHeader from './PopupHeader'; export var POSITIONS = ['top left', 'top right', 'bottom right', 'bottom left', 'right center', 'left center', 'top center', 'bottom center']; /** * A Popup displays additional information on top of a page. */ var Popup = function (_Component) { _inherits(Popup, _Component); function Popup() { var _ref; var _temp, _this, _ret; _classCallCheck(this, Popup); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = Popup.__proto__ || Object.getPrototypeOf(Popup)).call.apply(_ref, [this].concat(args))), _this), _this.state = {}, _this.hideOnScroll = function (e) { _this.setState({ closed: true }); eventStack.unsub('scroll', _this.hideOnScroll, { target: window }); setTimeout(function () { return _this.setState({ closed: false }); }, 50); _this.handleClose(e); }, _this.handleClose = function (e) { _invoke(_this.props, 'onClose', e, _this.props); }, _this.handleOpen = function (e) { _this.coords = e.currentTarget.getBoundingClientRect(); var onOpen = _this.props.onOpen; if (onOpen) onOpen(e, _this.props); }, _this.handlePortalMount = function (e) { var hideOnScroll = _this.props.hideOnScroll; if (hideOnScroll) eventStack.sub('scroll', _this.hideOnScroll, { target: window }); _invoke(_this.props, 'onMount', e, _this.props); }, _this.handlePortalUnmount = function (e) { var hideOnScroll = _this.props.hideOnScroll; if (hideOnScroll) eventStack.unsub('scroll', _this.hideOnScroll, { target: window }); _invoke(_this.props, 'onUnmount', e, _this.props); }, _this.handlePopupRef = function (popupRef) { _this.popupCoords = popupRef ? popupRef.getBoundingClientRect() : null; _this.setPopupStyle(); }, _temp), _possibleConstructorReturn(_this, _ret); } _createClass(Popup, [{ key: 'computePopupStyle', value: function computePopupStyle(positions) { var style = { position: 'absolute' // Do not access window/document when server side rendering };if (!isBrowser()) return style; var offset = this.props.offset; var _window = window, pageYOffset = _window.pageYOffset, pageXOffset = _window.pageXOffset; var _document$documentEle = document.documentElement, clientWidth = _document$documentEle.clientWidth, clientHeight = _document$documentEle.clientHeight; if (_includes(positions, 'right')) { style.right = Math.round(clientWidth - (this.coords.right + pageXOffset)); style.left = 'auto'; } else if (_includes(positions, 'left')) { style.left = Math.round(this.coords.left + pageXOffset); style.right = 'auto'; } else { // if not left nor right, we are horizontally centering the element var xOffset = (this.coords.width - this.popupCoords.width) / 2; style.left = Math.round(this.coords.left + xOffset + pageXOffset); style.right = 'auto'; } if (_includes(positions, 'top')) { style.bottom = Math.round(clientHeight - (this.coords.top + pageYOffset)); style.top = 'auto'; } else if (_includes(positions, 'bottom')) { style.top = Math.round(this.coords.bottom + pageYOffset); style.bottom = 'auto'; } else { // if not top nor bottom, we are vertically centering the element var yOffset = (this.coords.height + this.popupCoords.height) / 2; style.top = Math.round(this.coords.bottom + pageYOffset - yOffset); style.bottom = 'auto'; var _xOffset = this.popupCoords.width + 8; if (_includes(positions, 'right')) { style.right -= _xOffset; } else { style.left -= _xOffset; } } if (offset) { if (_isNumber(style.right)) { style.right -= offset; } else { style.left -= offset; } } return style; } // check if the style would display // the popup outside of the view port }, { key: 'isStyleInViewport', value: function isStyleInViewport(style) { var _window2 = window, pageYOffset = _window2.pageYOffset, pageXOffset = _window2.pageXOffset; var _document$documentEle2 = document.documentElement, clientWidth = _document$documentEle2.clientWidth, clientHeight = _document$documentEle2.clientHeight; var element = { top: style.top, left: style.left, width: this.popupCoords.width, height: this.popupCoords.height }; if (_isNumber(style.right)) { element.left = clientWidth - style.right - element.width; } if (_isNumber(style.bottom)) { element.top = clientHeight - style.bottom - element.height; } // hidden on top if (element.top < pageYOffset) return false; // hidden on the bottom if (element.top + element.height > pageYOffset + clientHeight) return false; // hidden the left if (element.left < pageXOffset) return false; // hidden on the right if (element.left + element.width > pageXOffset + clientWidth) return false; return true; } }, { key: 'setPopupStyle', value: function setPopupStyle() { if (!this.coords || !this.popupCoords) return; var position = this.props.position; var style = this.computePopupStyle(position); // Lets detect if the popup is out of the viewport and adjust // the position accordingly var positions = _without(POSITIONS, position).concat([position]); for (var i = 0; !this.isStyleInViewport(style) && i < positions.length; i += 1) { style = this.computePopupStyle(positions[i]); position = positions[i]; } // Append 'px' to every numerical values in the style style = _mapValues(style, function (value) { return _isNumber(value) ? value + 'px' : value; }); this.setState({ style: style, position: position }); } }, { key: 'getPortalProps', value: function getPortalProps() { var portalProps = {}; var _props = this.props, on = _props.on, hoverable = _props.hoverable; var normalizedOn = _isArray(on) ? on : [on]; if (hoverable) { portalProps.closeOnPortalMouseLeave = true; portalProps.mouseLeaveDelay = 300; } if (_includes(normalizedOn, 'click')) { portalProps.openOnTriggerClick = true; portalProps.closeOnTriggerClick = true; portalProps.closeOnDocumentClick = true; } if (_includes(normalizedOn, 'focus')) { portalProps.openOnTriggerFocus = true; portalProps.closeOnTriggerBlur = true; } if (_includes(normalizedOn, 'hover')) { portalProps.openOnTriggerMouseEnter = true; portalProps.closeOnTriggerMouseLeave = true; // Taken from SUI: https://git.io/vPmCm portalProps.mouseLeaveDelay = 70; portalProps.mouseEnterDelay = 50; } return portalProps; } }, { key: 'render', value: function render() { var _props2 = this.props, basic = _props2.basic, children = _props2.children, className = _props2.className, content = _props2.content, flowing = _props2.flowing, header = _props2.header, inverted = _props2.inverted, size = _props2.size, trigger = _props2.trigger, wide = _props2.wide; var _state = this.state, position = _state.position, closed = _state.closed; var style = _assign({}, this.state.style, this.props.style); var classes = cx('ui', position, size, useKeyOrValueAndKey(wide, 'wide'), useKeyOnly(basic, 'basic'), useKeyOnly(flowing, 'flowing'), useKeyOnly(inverted, 'inverted'), 'popup transition visible', className); if (closed) return trigger; var unhandled = getUnhandledProps(Popup, this.props); var portalPropNames = Portal.handledProps; var rest = _reduce(unhandled, function (acc, val, key) { if (!_includes(portalPropNames, key)) acc[key] = val; return acc; }, {}); var portalProps = _pick(unhandled, portalPropNames); var ElementType = getElementType(Popup, this.props); var popupJSX = React.createElement( ElementType, _extends({}, rest, { className: classes, style: style, ref: this.handlePopupRef }), children, childrenUtils.isNil(children) && PopupHeader.create(header), childrenUtils.isNil(children) && PopupContent.create(content) ); var mergedPortalProps = _extends({}, this.getPortalProps(), portalProps); return React.createElement( Portal, _extends({}, mergedPortalProps, { trigger: trigger, onClose: this.handleClose, onMount: this.handlePortalMount, onOpen: this.handleOpen, onUnmount: this.handlePortalUnmount }), popupJSX ); } }]); return Popup; }(Component); Popup.defaultProps = { position: 'top left', on: 'hover' }; Popup._meta = { name: 'Popup', type: META.TYPES.MODULE }; Popup.Content = PopupContent; Popup.Header = PopupHeader; Popup.handledProps = ['as', 'basic', 'children', 'className', 'content', 'flowing', 'header', 'hideOnScroll', 'hoverable', 'inverted', 'offset', 'on', 'onClose', 'onMount', 'onOpen', 'onUnmount', 'position', 'size', 'style', 'trigger', 'wide']; export default Popup; Popup.propTypes = process.env.NODE_ENV !== "production" ? { /** An element type to render as (string or function). */ as: customPropTypes.as, /** Display the popup without the pointing arrow. */ basic: PropTypes.bool, /** Primary content. */ children: PropTypes.node, /** Additional classes. */ className: PropTypes.string, /** Simple text content for the popover. */ content: customPropTypes.itemShorthand, /** A flowing Popup has no maximum width and continues to flow to fit its content. */ flowing: PropTypes.bool, /** Takes up the entire width of its offset container. */ // TODO: implement the Popup fluid layout // fluid: PropTypes.bool, /** Header displayed above the content in bold. */ header: customPropTypes.itemShorthand, /** Hide the Popup when scrolling the window. */ hideOnScroll: PropTypes.bool, /** Whether the popup should not close on hover. */ hoverable: PropTypes.bool, /** Invert the colors of the Popup. */ inverted: PropTypes.bool, /** Horizontal offset in pixels to be applied to the Popup. */ offset: PropTypes.number, /** Events triggering the popup. */ on: PropTypes.oneOfType([PropTypes.oneOf(['hover', 'click', 'focus']), PropTypes.arrayOf(PropTypes.oneOf(['hover', 'click', 'focus']))]), /** * Called when a close event happens. * * @param {SyntheticEvent} event - React's original SyntheticEvent. * @param {object} data - All props. */ onClose: PropTypes.func, /** * Called when the portal is mounted on the DOM. * * @param {null} * @param {object} data - All props. */ onMount: PropTypes.func, /** * Called when an open event happens. * * @param {SyntheticEvent} event - React's original SyntheticEvent. * @param {object} data - All props. */ onOpen: PropTypes.func, /** * Called when the portal is unmounted from the DOM. * * @param {null} * @param {object} data - All props. */ onUnmount: PropTypes.func, /** Position for the popover. */ position: PropTypes.oneOf(POSITIONS), /** Popup size. */ size: PropTypes.oneOf(_without(SUI.SIZES, 'medium', 'big', 'massive')), /** Custom Popup style. */ style: PropTypes.object, /** Element to be rendered in-place where the popup is defined. */ trigger: PropTypes.node, /** Popup width. */ wide: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['very'])]) } : {};