UNPKG

wix-style-react

Version:
336 lines • 14.7 kB
import React from 'react'; import Portal from '../../utils/ReactPortal/ReactPortal'; import { Manager, Reference, Popper } from 'react-popper'; import { CSSTransition } from 'react-transition-group'; import { ClickOutside } from './utils/ClickOutside'; import { createModifiers } from './utils/modifiers'; import { filterDataProps } from './utils/filter-data-props'; import uniqueId from 'lodash/uniqueId'; import { buildChildrenObject, createComponentThatRendersItsChildren, shouldAnimatePopover, attachClasses, detachClasses, getArrowShift, } from './utils/utils'; import { popoverTestUtils } from './utils/helpers'; import { getAppendToElement } from './utils/getAppendToElement'; import { WixStyleReactContext } from '../../WixStyleReactProvider/context'; import { st, classes } from '../Popover.st.css'; // This is here and not in the test setup because we don't want consumers to need to run it as well let testId = '0'; const isTestEnv = process.env.NODE_ENV === 'test'; if (isTestEnv && typeof document !== 'undefined' && !document.createRange) { popoverTestUtils.createRange(); } /** * Popover */ export class PopoverCore extends React.Component { constructor(props) { super(props); this.portalNode = null; this.portalClasses = ''; this.appendToNode = null; this.clickOutsideRef = null; // Timer instances for the show/hide delays this._hideTimeout = null; this._showTimeout = null; this._handleClickOutside = (event) => { const { onClickOutside: onClickOutsideCallback, shown, disableClickOutsideWhenClosed, } = this.props; if (onClickOutsideCallback && !(disableClickOutsideWhenClosed && !shown)) { onClickOutsideCallback(event); } }; this._onKeyDown = (e) => { const { onEscPress } = this.props; if (onEscPress && e.key === 'Escape') { onEscPress(e); } }; /** * Checks to see if the focused element is outside the Popover content */ this._onDocumentKeyUp = (e) => { const { onTabOut } = this.props; if (typeof document !== 'undefined' && this.popoverContentRef.current && !this.popoverContentRef.current.contains(document.activeElement)) { onTabOut && onTabOut(e); } }; this.state = { isMounted: false, shown: props.shown || false, }; if (isTestEnv) { testId = popoverTestUtils.generateId(); } this.clickOutsideRef = React.createRef(); this.popoverContentRef = React.createRef(); this.clickOutsideClass = uniqueId('clickOutside'); this.contentHook = `popover-content-${props.dataHook || ''}-${testId}`; } focus() { if (this.popoverContentRef.current) { this.popoverContentRef.current.focus(); } } getPopperContentStructure(childrenObject) { const { shown } = this.state; const { moveBy, appendTo, placement, showArrow, moveArrowTo, flip, fixed, customArrow, role, id, zIndex, minWidth, maxWidth, width, dynamicWidth, onEscPress, tabIndex, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedBy, timeout, theme, } = this.props; const shouldAnimate = shouldAnimatePopover({ timeout }); const modifiers = createModifiers({ minWidth, width, dynamicWidth, moveBy, appendTo, shouldAnimate, flip, placement, fixed, isTestEnv, }); const mergeRefs = (...refs) => { const filteredRefs = refs.filter(Boolean); if (!filteredRefs.length) { return null; } if (filteredRefs.length === 0) { return filteredRefs[0]; } return (inst) => { for (const ref of filteredRefs) { if (typeof ref === 'function') { ref(inst); } else if (ref) { ref.current = inst; } } }; }; const popperWithArrow = (React.createElement(Popper, { modifiers: modifiers, placement: placement }, ({ ref, style: popperStyles, placement: popperPlacement, arrowProps, scheduleUpdate, }) => { this.popperScheduleUpdate = scheduleUpdate; return (React.createElement("div", { "data-hook": "popover-content", className: st(this.clickOutsideClass, this.context.newBrandingClass), "data-content-element": this.contentHook, ref: ref, style: { ...popperStyles, zIndex } }, showArrow && this.renderArrow(arrowProps, moveArrowTo, popperPlacement || placement, customArrow), React.createElement("div", { id: id, role: role, tabIndex: tabIndex, ref: this.popoverContentRef, style: { maxWidth }, className: st(classes.content, { skin: theme, placement: popperPlacement || placement, hasArrow: true, }), onKeyDown: shown && onEscPress ? this._onKeyDown : undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedBy }, childrenObject.Content))); })); const popper = (React.createElement(Popper, { modifiers: modifiers, placement: placement }, ({ ref, style: popperStyles, placement: popperPlacement, scheduleUpdate, }) => { this.popperScheduleUpdate = scheduleUpdate; return (React.createElement("div", { id: id, ref: mergeRefs(ref, this.popoverContentRef), role: role, tabIndex: tabIndex, className: st(classes.content, { skin: theme, placement: popperPlacement || placement, }, this.clickOutsideClass, this.context.newBrandingClass), "data-hook": "popover-content", style: { ...popperStyles, zIndex, maxWidth }, "data-content-element": this.contentHook, onKeyDown: shown && onEscPress ? this._onKeyDown : undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedBy }, childrenObject.Content)); })); return this.wrapWithAnimations(showArrow ? popperWithArrow : popper); } applyStylesToPortaledNode() { const { shown } = this.state; const shouldAnimate = shouldAnimatePopover(this.props); if (shouldAnimate || shown) { attachClasses(this.portalNode, this.portalClasses); } else { detachClasses(this.portalNode, this.portalClasses); } } wrapWithAnimations(popper) { const { timeout } = this.props; const { shown } = this.state; const shouldAnimate = shouldAnimatePopover(this.props); return shouldAnimate ? (React.createElement(CSSTransition, { in: shown, timeout: timeout, unmountOnExit: true, classNames: { enter: classes.animationEnter, enterActive: classes.animationEnterActive, exit: classes.animationExit, exitActive: classes.animationExitActive, }, addEndListener: () => { }, onExited: () => detachClasses(this.portalNode, this.portalClasses) }, popper)) : (popper); } renderPopperContent(childrenObject) { const popper = this.getPopperContentStructure(childrenObject); return this.portalNode ? (React.createElement(Portal, { node: this.portalNode }, popper)) : (popper); } renderArrow(arrowProps, moveArrowTo, placement, customArrow) { const { theme } = this.props; const commonProps = { ref: arrowProps.ref, key: 'popover-arrow', 'data-hook': 'popover-arrow', style: { ...arrowProps.style, ...getArrowShift(moveArrowTo, placement), }, }; if (customArrow) { return customArrow(placement, commonProps); } return (React.createElement("div", { ...commonProps, className: st(classes.arrow, { skin: theme, placement }) })); } componentDidMount() { const { shown, onTabOut } = this.props; this.initAppendToNode(); if (onTabOut && shown) { this._setBlurByKeyboardListener(); } this.setState({ isMounted: true }); } _setBlurByKeyboardListener() { if (typeof document !== 'undefined') { document.addEventListener('keyup', this._onDocumentKeyUp, true); } } _removeBlurListener() { if (typeof document !== 'undefined') { document.removeEventListener('keyup', this._onDocumentKeyUp, true); } } initAppendToNode() { const { appendTo } = this.props; this.appendToNode = getAppendToElement(appendTo, this.targetRef); if (this.appendToNode) { this.portalNode = document.createElement('div'); this.portalNode.setAttribute('data-hook', 'popover-portal'); /** * reset overlay wrapping layer * so that styles from copied classnames * won't break the overlay: * - content is position relative to body * - overlay layer is hidden */ Object.assign(this.portalNode.style, { position: 'static', display: 'block', top: 0, left: 0, width: 0, height: 0, }); this.appendToNode.appendChild(this.portalNode); } } hidePopover() { const { isMounted } = this.state; const { hideDelay, onTabOut, onHide } = this.props; if (!isMounted || this._hideTimeout) { return; } if (this._showTimeout) { clearTimeout(this._showTimeout); this._showTimeout = null; } if (onTabOut) { this._removeBlurListener(); } if (hideDelay) { this._hideTimeout = setTimeout(() => { this.setState({ shown: false }); onHide?.(); }, hideDelay); } else { this.setState({ shown: false }); onHide?.(); } } showPopover() { const { isMounted } = this.state; const { showDelay, onTabOut, onShow } = this.props; if (!isMounted || this._showTimeout) { return; } if (this._hideTimeout) { clearTimeout(this._hideTimeout); this._hideTimeout = null; } if (onTabOut) { this._setBlurByKeyboardListener(); } if (showDelay) { this._showTimeout = setTimeout(() => { this.setState({ shown: true }); onShow?.(); }, showDelay); } else { this.setState({ shown: true }); onShow?.(); } } componentWillUnmount() { if (this.portalNode && this.appendToNode && this.appendToNode.children.length) { // FIXME: What if component is updated with a different appendTo? It is a far-fetched use-case, // but we would need to remove the portaled node, and created another one. this.appendToNode.removeChild(this.portalNode); } this.portalNode = null; if (this._hideTimeout) { clearTimeout(this._hideTimeout); this._hideTimeout = null; } if (this._showTimeout) { clearTimeout(this._showTimeout); this._showTimeout = null; } } updatePosition() { if (this.popperScheduleUpdate) { this.popperScheduleUpdate(); } } componentDidUpdate(prevProps) { const { theme: skin, className, shown } = this.props; if (this.portalNode) { // Re-calculate the portal's styles this.portalClasses = st(classes.root, { skin }, className); // Apply the styles to the portal this.applyStylesToPortaledNode(); } // Update popover visibility if (prevProps.shown !== shown) { if (shown) { this.showPopover(); } else { this.hidePopover(); } } else { // Update popper's position this.updatePosition(); } } render() { const { onMouseEnter, onMouseLeave, onKeyDown, onClick, children, className, style, fluid, theme: skin, dataHook, zIndex, excludeClass, } = this.props; const { isMounted, shown } = this.state; const childrenObject = buildChildrenObject(children, { Element: null, Content: null, }); const shouldAnimate = shouldAnimatePopover(this.props); const shouldRenderPopper = isMounted && (shouldAnimate || shown); return ( // @ts-ignore React.createElement(Manager, null, React.createElement(ClickOutside, { rootRef: this.clickOutsideRef, onClickOutside: shown ? this._handleClickOutside : undefined, excludeClass: excludeClass ? [this.clickOutsideClass, excludeClass] : this.clickOutsideClass }, React.createElement("div", { ref: this.clickOutsideRef, style: style, "data-hook": dataHook, "data-content-hook": this.contentHook, className: st(classes.root, { fluid, skin }, className), onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, "data-zindex": zIndex, ...filterDataProps(this.props) }, React.createElement(Reference, { innerRef: r => (this.targetRef = r) }, ({ ref }) => (React.createElement("div", { ref: ref, className: classes.element, "data-hook": "popover-element", onClick: onClick, onKeyDown: onKeyDown }, childrenObject.Element))), shouldRenderPopper && this.renderPopperContent(childrenObject))))); } } PopoverCore.displayName = 'Popover'; PopoverCore.defaultProps = { flip: true, fixed: false, zIndex: 1000, shown: false, placement: 'bottom', excludeClass: '', }; PopoverCore.contextType = WixStyleReactContext; PopoverCore.Element = createComponentThatRendersItsChildren('Popover.Element'); PopoverCore.Content = createComponentThatRendersItsChildren('Popover.Content'); //# sourceMappingURL=PopoverCore.js.map