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 { WixDesignSystemContext } from '../../WixDesignSystemProvider/context';
import { st, classes } from '../Popover.st.css.js';
import { mergeRefs } from '../../utils/mergeRefs';
import { ZIndex } from '../../common/ZIndex';
// 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 = true, } = 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 = (event) => {
            const { onTabOut } = this.props;
            if (typeof document !== 'undefined' &&
                this.popoverContentRef.current &&
                !this.popoverContentRef.current.contains(document.activeElement)) {
                onTabOut && onTabOut(event);
            }
        };
        this.state = {
            isMounted: false,
            shown: props.shown || false,
        };
        if (isTestEnv) {
            testId = popoverTestUtils.generateId();
        }
        this.clickOutsideRef = React.createRef();
        this.transitionRef = 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, flipModifier, fixed, customArrow, role, id, zIndex, minWidth, maxWidth, width, dynamicWidth, onEscPress, tabIndex, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedBy, timeout, skin, } = this.props;
        const shouldAnimate = shouldAnimatePopover({ timeout });
        const modifiers = createModifiers({
            minWidth,
            width,
            dynamicWidth,
            moveBy,
            appendTo,
            shouldAnimate,
            flip,
            flipModifier,
            placement,
            fixed,
            isTestEnv,
        });
        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: mergeRefs(ref, this.transitionRef), 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,
                        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, this.transitionRef), role: role, tabIndex: tabIndex, className: st(classes.content, {
                    skin,
                    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, { nodeRef: this.transitionRef, 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 { skin } = 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, placement }) }));
    }
    componentDidMount() {
        const { shown, onTabOut } = this.props;
        this.appendPortalToNode();
        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);
        }
    }
    appendPortalToNode() {
        const { appendTo } = this.props;
        const { contextClassName } = this.context;
        this.appendToNode = getAppendToElement(appendTo, this.targetRef);
        if (this.appendToNode) {
            this.portalNode?.remove();
            this.portalNode = document.createElement('div');
            this.portalNode.setAttribute('data-hook', 'popover-portal');
            if (contextClassName) {
                this.portalNode.classList.add(contextClassName);
            }
            /**
             * 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) {
            this.appendToNode.removeChild(this.portalNode);
            this.portalNode.remove();
        }
        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 { skin, className, shown } = this.props;
        // allows reactive appendTo
        if (prevProps.appendTo !== this.props.appendTo) {
            this.appendPortalToNode();
        }
        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, skin, dataHook, zIndex, excludeClass, interactive, } = 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 (React.createElement(Manager, null,
            React.createElement(ClickOutside, { rootRef: this.clickOutsideRef, onClickOutside: this._handleClickOutside, 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), "data-zindex": zIndex, onMouseEnter: interactive ? onMouseEnter : undefined, onMouseLeave: interactive ? onMouseLeave : undefined, ...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, onMouseEnter: interactive ? undefined : onMouseEnter, onMouseLeave: interactive ? undefined : onMouseLeave }, childrenObject.Element))),
                    shouldRenderPopper && this.renderPopperContent(childrenObject)))));
    }
}
PopoverCore.displayName = 'Popover';
PopoverCore.defaultProps = {
    flip: true,
    fixed: false,
    zIndex: ZIndex.popover,
    shown: false,
    placement: 'bottom',
    excludeClass: '',
    interactive: true,
};
PopoverCore.contextType = WixDesignSystemContext;
PopoverCore.Element = createComponentThatRendersItsChildren('Popover.Element');
PopoverCore.Content = createComponentThatRendersItsChildren('Popover.Content');
//# sourceMappingURL=PopoverCore.js.map