164 lines
5.3 KiB
TypeScript
164 lines
5.3 KiB
TypeScript
import { useDocumentRect } from '@/Hooks/useDocumentRect'
|
|
import { useAutoElementRect } from '@/Hooks/useElementRect'
|
|
import { classNames } from '@standardnotes/utils'
|
|
import { CSSProperties, useCallback, useLayoutEffect, useRef, useState } from 'react'
|
|
import Portal from '../Portal/Portal'
|
|
import { PopoverCSSProperties, getPositionedPopoverStyles } from './GetPositionedPopoverStyles'
|
|
import { PopoverContentProps } from './Types'
|
|
import { usePopoverCloseOnClickOutside } from './Utils/usePopoverCloseOnClickOutside'
|
|
import { useDisableBodyScrollOnMobile } from '@/Hooks/useDisableBodyScrollOnMobile'
|
|
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
|
import { getAdjustedStylesForNonPortalPopover } from './Utils/getAdjustedStylesForNonPortal'
|
|
import { DialogWithClose } from '@/Utils/CloseOpenModalsAndPopovers'
|
|
import { mergeRefs } from '@/Hooks/mergeRefs'
|
|
|
|
const PositionedPopoverContent = ({
|
|
align = 'end',
|
|
anchorElement,
|
|
anchorPoint,
|
|
children,
|
|
childPopovers,
|
|
className,
|
|
id,
|
|
overrideZIndex,
|
|
side = 'bottom',
|
|
togglePopover,
|
|
disableClickOutside,
|
|
disableMobileFullscreenTakeover,
|
|
disableFlip,
|
|
maxHeight,
|
|
portal = true,
|
|
offset,
|
|
hideOnClickInModal = false,
|
|
setAnimationElement,
|
|
containerClassName,
|
|
}: PopoverContentProps) => {
|
|
const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
|
|
const popoverRect = useAutoElementRect(popoverElement)
|
|
const resolvedAnchorElement = anchorElement && 'current' in anchorElement ? anchorElement.current : anchorElement
|
|
const anchorElementRect = useAutoElementRect(resolvedAnchorElement, {
|
|
updateOnWindowResize: true,
|
|
})
|
|
const anchorPointRect = DOMRect.fromRect({
|
|
x: anchorPoint?.x,
|
|
y: anchorPoint?.y,
|
|
})
|
|
const anchorRect = anchorPoint ? anchorPointRect : anchorElementRect
|
|
const documentRect = useDocumentRect()
|
|
const isDesktopScreen = useMediaQuery(MediaQueryBreakpoints.md)
|
|
|
|
const styles = getPositionedPopoverStyles({
|
|
align,
|
|
anchorRect,
|
|
documentRect,
|
|
popoverRect: popoverRect ?? popoverElement?.getBoundingClientRect(),
|
|
side,
|
|
disableMobileFullscreenTakeover,
|
|
disableFlip,
|
|
maxHeightFunction: maxHeight,
|
|
offset,
|
|
})
|
|
|
|
if (!styles) {
|
|
document.body.style.overflow = 'hidden'
|
|
}
|
|
|
|
useLayoutEffect(() => {
|
|
return () => {
|
|
document.body.style.overflow = ''
|
|
}
|
|
}, [])
|
|
|
|
let adjustedStyles: PopoverCSSProperties | undefined = undefined
|
|
|
|
if (!portal && popoverElement && styles) {
|
|
adjustedStyles = getAdjustedStylesForNonPortalPopover(popoverElement, styles)
|
|
}
|
|
|
|
usePopoverCloseOnClickOutside({
|
|
popoverElement,
|
|
anchorElement: resolvedAnchorElement,
|
|
togglePopover,
|
|
childPopovers,
|
|
hideOnClickInModal,
|
|
disabled: disableClickOutside,
|
|
})
|
|
|
|
useDisableBodyScrollOnMobile()
|
|
|
|
const canCorrectInitialScroll = useRef(true)
|
|
const correctInitialScrollForOverflowedContent = useCallback((element: HTMLElement | null) => {
|
|
if (element && element.scrollTop > 0 && canCorrectInitialScroll.current) {
|
|
element.scrollTop = 0
|
|
canCorrectInitialScroll.current = false
|
|
}
|
|
}, [])
|
|
|
|
const addCloseMethod = useCallback(
|
|
(element: HTMLDivElement | null) => {
|
|
if (element && togglePopover) {
|
|
;(element as DialogWithClose).close = togglePopover
|
|
}
|
|
},
|
|
[togglePopover],
|
|
)
|
|
|
|
return (
|
|
<Portal disabled={!portal}>
|
|
<div
|
|
className={classNames(
|
|
'absolute left-0 top-0 flex w-full min-w-80 cursor-auto flex-col md:h-auto md:max-w-xs',
|
|
!disableMobileFullscreenTakeover && 'h-full',
|
|
overrideZIndex ? overrideZIndex : 'z-dropdown-menu',
|
|
isDesktopScreen || disableMobileFullscreenTakeover ? 'invisible' : '',
|
|
containerClassName,
|
|
)}
|
|
style={
|
|
{
|
|
...styles,
|
|
...adjustedStyles,
|
|
} as CSSProperties
|
|
}
|
|
ref={mergeRefs([setPopoverElement, addCloseMethod])}
|
|
id={'popover/' + id}
|
|
data-popover={id}
|
|
onKeyDown={(event) => {
|
|
if (event.key === KeyboardKey.Escape) {
|
|
event.stopPropagation()
|
|
togglePopover?.()
|
|
if (resolvedAnchorElement) {
|
|
resolvedAnchorElement.focus()
|
|
}
|
|
}
|
|
}}
|
|
onBlur={() => {
|
|
setTimeout(() => {
|
|
if (document.activeElement && document.activeElement.tagName === 'IFRAME') {
|
|
togglePopover?.()
|
|
}
|
|
})
|
|
}}
|
|
>
|
|
<div
|
|
className={classNames(
|
|
'overflow-y-auto rounded border border-[--popover-border-color] bg-[--popover-background-color] shadow-main [backdrop-filter:var(--popover-backdrop-filter)]',
|
|
!isDesktopScreen && !disableMobileFullscreenTakeover ? 'pb-safe-bottom pt-safe-top' : '',
|
|
'transition-[transform,opacity] duration-75 [transform-origin:var(--transform-origin)] motion-reduce:transition-opacity',
|
|
styles ? 'scale-100 opacity-100' : 'scale-95 opacity-0',
|
|
className,
|
|
)}
|
|
ref={mergeRefs([correctInitialScrollForOverflowedContent, setAnimationElement])}
|
|
onScroll={() => {
|
|
canCorrectInitialScroll.current = false
|
|
}}
|
|
>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</Portal>
|
|
)
|
|
}
|
|
|
|
export default PositionedPopoverContent
|