import {PropsWithChildren, useRef, useLayoutEffect, KeyboardEvent, CSSProperties} from 'react'; import cls from 'classnames'; import {clamp, renderForeground} from '@core0/utils'; import {Transition, TransitionType} from '@core0/transition'; import {Backdrop} from '@core0/backdrop'; import cn from './popover.module.styl'; export type HorizontalAlign = 'popover-left-to-anchor-left' | 'popover-left-to-anchor-right' | 'popover-center-to-anchor-center' | 'popover-right-to-anchor-left' | 'popover-right-to-anchor-right'; export type VerticalAlign = 'popover-top-to-anchor-top' | 'popover-top-to-anchor-bottom' | 'popover-center-to-anchor-center' | 'popover-bottom-to-anchor-top' | 'popover-bottom-to-anchor-bottom'; export interface PopoverProps extends Pick< CSSProperties, | 'paddingTop' | 'paddingRight' | 'paddingBottom' | 'paddingLeft' | 'width' | 'minWidth' | 'maxWidth' | 'height' | 'minHeight' | 'maxHeight' > { onClose?(output?: Output): void; anchorRect: DOMRect; nestedLayerIdx?: number; nestedGroupId?: string; noBackdrop?: boolean; transitionType?: TransitionType; verticalAlign?: VerticalAlign; horizontalAlign?: HorizontalAlign; backdropBackground?: string; backdropOpacity?: string; canBackdropClosePopover?: boolean; } export function Popover(props: PropsWithChildren>): JSX.Element { const { onClose = () => {}, anchorRect, nestedGroupId, nestedLayerIdx, transitionType = 'fade-in', verticalAlign = 'popover-center-to-anchor-center', horizontalAlign = 'popover-center-to-anchor-center', noBackdrop = false, canBackdropClosePopover = true, backdropBackground, backdropOpacity, paddingTop, paddingRight, paddingBottom, paddingLeft, width, minWidth, maxWidth, height, minHeight, maxHeight, children } = props; const hostElmRef = useRef(null); const backdropElmRef = useRef(null); const contentContainerElmRef = useRef(null); const transformOrigin = verticalAlign === 'popover-center-to-anchor-center' ? 'center' : verticalAlign === 'popover-top-to-anchor-bottom' ? 'top' : verticalAlign === 'popover-top-to-anchor-top' ? 'top' : verticalAlign === 'popover-bottom-to-anchor-bottom' ? 'bottom' : verticalAlign === 'popover-bottom-to-anchor-top' ? 'bottom' : undefined; /* **************************************** * Effect Hooks **************************************** */ useLayoutEffect(() => { if (!anchorRect) return; const contentContainerElm = contentContainerElmRef.current; if (!contentContainerElm) return; const {top: anchorTop, left: anchorLeft, width: anchorWidth, height: anchorHeight} = anchorRect; const {offsetWidth: contentContainerElmWidth, offsetHeight: contentContainerElmHeight} = contentContainerElm; requestAnimationFrame(() => { const viewportWidth = window.innerWidth ?? document.documentElement.offsetWidth; const viewportHeight = window.innerHeight ?? document.documentElement.offsetHeight; const top = verticalAlign === 'popover-center-to-anchor-center' ? anchorTop + (anchorHeight - contentContainerElmHeight) / 2 : verticalAlign === 'popover-bottom-to-anchor-top' ? anchorTop - contentContainerElmHeight : verticalAlign === 'popover-bottom-to-anchor-bottom' ? anchorTop + anchorHeight - contentContainerElmHeight : verticalAlign === 'popover-top-to-anchor-bottom' ? anchorTop + anchorHeight : verticalAlign === 'popover-top-to-anchor-top' ? anchorTop : 0; const left = horizontalAlign === 'popover-center-to-anchor-center' ? anchorLeft + (anchorWidth - contentContainerElmWidth) / 2 : horizontalAlign === 'popover-left-to-anchor-left' ? anchorLeft : horizontalAlign === 'popover-left-to-anchor-right' ? anchorLeft + anchorWidth : horizontalAlign === 'popover-right-to-anchor-left' ? anchorLeft - contentContainerElmWidth : horizontalAlign === 'popover-right-to-anchor-right' ? anchorLeft + anchorWidth - contentContainerElmWidth : 0; const margin = 10; const clampedTop = clamp(margin, top, viewportHeight - contentContainerElmHeight - margin); const clampedLeft = clamp(margin, left, viewportWidth - contentContainerElmWidth - margin); contentContainerElm.style.top = `${clampedTop}px`; contentContainerElm.style.left = `${clampedLeft}px`; contentContainerElm.style.height = contentContainerElmHeight > viewportHeight - margin * 2 ? `${viewportHeight - margin * 2}px` : 'auto'; contentContainerElm.style.width = contentContainerElmWidth > viewportWidth - margin * 2 ? `${viewportWidth - margin * 2}px` : 'auto'; }); }, []); useLayoutEffect(() => { if (noBackdrop) return; hostElmRef?.current?.focus(); }, []); useLayoutEffect(() => { if (noBackdrop) return; const requestId = requestAnimationFrame(() => { if (backdropElmRef?.current?.style?.opacity && backdropOpacity) { backdropElmRef.current.style.opacity = backdropOpacity; } }); return () => cancelAnimationFrame(requestId); }, []); /* **************************************** * Event Handlers **************************************** */ function backdropClickHandler() { if (canBackdropClosePopover) onClose(); } function hostElmKeyDownHandler(evt: KeyboardEvent) { if (evt.key.toLowerCase() === 'escape') onClose(); } return renderForeground(
{!noBackdrop && ( )}
{children}
); }