import { React, PropTypes, classnames } from '@gravityforms/libraries';
const { forwardRef, useCallback, useEffect, useState } = React;
const TOP = 'top';
const BOTTOM = 'bottom';
const LEFT = 'left';
const RIGHT = 'right';
/**
* @function getPlacement
* @description Calculate the left and top position based on alignment and placement.
*
* @since 6.0.1
*
* @param {string} align The alignment of the popover.
* @param {string} placement The placement of the popover.
* @param {object|null} popoverRef Ref to the popover element.
* @param {object|null} triggerRef Ref to the trigger element.
* @param {object|null} containerRef Ref to the container element.
*
* @return {object} The calculated left and top position for the popover.
*/
const getPlacement = ( align, placement, popoverRef, triggerRef, containerRef ) => {
let left = 0;
let top = 0;
const triggerRect = triggerRef?.current?.getBoundingClientRect?.();
const popoverRect = popoverRef?.current?.getBoundingClientRect?.();
let containerRect = containerRef?.current?.getBoundingClientRect?.();
if ( ! containerRect ) {
containerRect = {
top: 0,
left: 0,
};
}
if ( triggerRect && popoverRect && containerRect ) {
switch ( placement ) {
case TOP:
top = triggerRect.top - containerRect.top - popoverRect.height;
if ( align === LEFT ) {
left = triggerRect.left - containerRect.left;
} else if ( align === RIGHT ) {
left = triggerRect.right - containerRect.left - popoverRect.width;
}
break;
case BOTTOM:
top = triggerRect.bottom - containerRect.top;
if ( align === LEFT ) {
left = triggerRect.left - containerRect.left;
} else if ( align === RIGHT ) {
left = triggerRect.right - containerRect.left - popoverRect.width;
}
break;
case LEFT:
left = triggerRect.left - containerRect.left - popoverRect.width;
if ( align === TOP ) {
top = triggerRect.top - containerRect.top;
} else if ( align === BOTTOM ) {
top = triggerRect.bottom - containerRect.top - popoverRect.height;
}
break;
case RIGHT:
left = triggerRect.right - containerRect.left;
if ( align === TOP ) {
top = triggerRect.top - containerRect.top;
} else if ( align === BOTTOM ) {
top = triggerRect.bottom - containerRect.top - popoverRect.height;
}
break;
default:
break;
}
}
return { left, top };
};
/**
* @module Popover
* @description A popover component. Should be used with the `usePopup` hook to manage visibility state.
*
* @since 6.0.1
*
* @param {object} props Component props.
* @param {string} props.align The alignment of the popover, used with placement, one of `left`, `right`, 'top', or 'bottom'.
* if placement is `top` or `bottom`, align controls horizontal alignment.
* if placement is `left` or `right`, align controls vertical alignment.
* @param {boolean} props.autoPlacement Whether to autoplace the popover based on the trigger element.
* @param {JSX.Element} props.children The popover content.
* @param {object|null} props.containerRef Ref to the container element.
* @param {object} props.customAttributes Custom attributes for the component.
* @param {string|Array|object} props.customClasses Custom classes for the component.
* @param {boolean} props.isHide Whether the popover is hidden.
* @param {boolean} props.isOpen Whether the popover is open.
* @param {boolean} props.isReveal Whether the popover is revealed.
* @param {string} props.placement The placement of the popover, one of `top`, `bottom`, `left`, or `right`.
* @param {object} props.popoverAttributes Custom attributes for the popover element.
* @param {string|Array|object} props.popoverClasses Custom classes for the popover element.
* @param {object|null} props.popoverRef Ref to the popover element.
* @param {object|null} props.triggerRef Ref to the trigger element.
* @param {number} props.width The width of the popover in pixels.
* @param {object|null} ref Ref to the component.
*
* @return {JSX.Element} The popover component.
*/
const Popover = forwardRef( ( {
align = LEFT,
autoPlacement = false,
children = null,
containerRef = null,
customAttributes = {},
customClasses = [],
isHide = false,
isOpen = false,
isReveal = false,
placement = BOTTOM,
popoverAttributes = {},
popoverClasses = [],
popoverRef = null,
triggerRef = null,
width = 0,
}, ref ) => {
const {
left: initialLeft,
top: initialTop,
} = getPlacement( align, placement, popoverRef, triggerRef, containerRef );
const [ left, setLeft ] = useState( initialLeft );
const [ top, setTop ] = useState( initialTop );
const updatePlacement = useCallback( () => {
const { left: newLeft, top: newTop } = getPlacement( align, placement, popoverRef, triggerRef, containerRef );
setLeft( newLeft );
setTop( newTop );
}, [ align, placement, popoverRef, triggerRef, containerRef ] );
// Update placement when revealed.
useEffect( () => {
if ( ! isReveal ) {
return;
}
updatePlacement();
}, [ isReveal, updatePlacement ] );
const componentProps = {
...customAttributes,
className: classnames( {
'gform-popover': true,
[ `gform-popover--align-${ align }` ]: true,
[ `gform-popover--placement-${ placement }` ]: true,
'gform-popover--open': isOpen,
'gform-popover--reveal': isReveal,
'gform-popover--hide': isHide,
}, customClasses ),
};
if ( autoPlacement ) {
componentProps.style = {
insetInlineStart: `${ left }px`,
insetBlockStart: `${ top }px`,
};
}
const popoverProps = {
...popoverAttributes,
className: classnames( {
'gform-popover__popover': true,
}, popoverClasses ),
role: 'listbox',
tabIndex: '-1',
style: {
inlineSize: width ? `${ width }px` : undefined,
},
};
return (
<div { ...componentProps } ref={ ref }>
<div { ...popoverProps } ref={ popoverRef }>
{ children }
</div>
</div>
);
} );
Popover.propTypes = {
align: PropTypes.oneOf( [ LEFT, RIGHT, TOP, BOTTOM ] ),
autoPlacement: PropTypes.bool,
children: PropTypes.node,
customAttributes: PropTypes.object,
customClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
isHide: PropTypes.bool,
isOpen: PropTypes.bool,
isReveal: PropTypes.bool,
placement: PropTypes.oneOf( [ TOP, BOTTOM, LEFT, RIGHT ] ),
popoverAttributes: PropTypes.object,
popoverClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
popoverRef: PropTypes.object,
triggerRef: PropTypes.object,
width: PropTypes.number,
};
Popover.displayName = 'Popover';
export default Popover;