@wix/design-system
Version:
@wix/design-system
323 lines • 14.4 kB
JavaScript
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, 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,
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;
this.appendToNode = getAppendToElement(appendTo, this.targetRef);
if (this.appendToNode) {
this.portalNode?.remove();
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) {
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