import { useEffect, type CSSProperties } from 'react'; import type { PopoverAnchorNative, PopoverPlacement } from './types'; export function usePopoverPositioning( popoverRef: React.RefObject, anchor: PopoverAnchorNative | undefined, anchorName: string, placementStyles: ExtendedCSSProperties, offsetX = 0, offsetY = 0, matchWidth = false, ) { useEffect(() => { const popover = popoverRef.current; const anchorElement = anchor?.current; if (!popover) return; // Reset all popover styles Object.keys(popover.style).forEach((key: string) => { popover.style.removeProperty(key); }); if (!anchorElement) { return; } // Reset all popover styles Object.keys(popover.style).forEach((key: string) => { popover.style.removeProperty(key); }); if (anchorName) { anchorElement.style.setProperty('anchor-name', anchorName); popover.style.setProperty('position-anchor', anchorName); } // Map camelCase properties to kebab-case CSS properties const propertyMap: Record = { positionArea: 'position-area', alignSelf: 'align-self', justifySelf: 'justify-self', positionTryFallbacks: 'position-try-fallbacks', }; // Apply all placement styles Object.entries(placementStyles).forEach(([key, value]) => { if (value !== undefined && value !== null) { const cssProperty = propertyMap[key] || key; const cssValue = typeof value === 'string' ? value : String(value); popover.style.setProperty(cssProperty, cssValue); } }); if (matchWidth) { popover.style.width = 'anchor-size(width)'; } if (offsetX !== 0 || offsetY !== 0) { popover.style.marginLeft = offsetX + 'px'; popover.style.marginTop = offsetY + 'px'; } }, [popoverRef, anchor, anchorName, placementStyles, offsetX, offsetY, matchWidth]); } type ExtendedCSSProperties = CSSProperties & { positionArea?: string; positionTryFallbacks?: string; }; // Only needed to simplify props, user can also use advanced placement styles for more control export function getPlacementStyles(placement: PopoverPlacement): ExtendedCSSProperties { const baseStyles: ExtendedCSSProperties = { margin: 0 }; switch (placement) { case 'bottom': return { ...baseStyles, positionArea: 'bottom', justifySelf: 'anchor-center', }; case 'bottom-start': return { ...baseStyles, positionArea: 'bottom span-right', justifySelf: 'start', }; case 'bottom-end': return { ...baseStyles, positionArea: 'bottom span-left', justifySelf: 'end', }; case 'top': return { ...baseStyles, positionArea: 'top', justifySelf: 'anchor-center', }; case 'top-start': return { ...baseStyles, positionArea: 'top span-right', justifySelf: 'start', }; case 'top-end': return { ...baseStyles, positionArea: 'top span-left', justifySelf: 'end', }; case 'right': return { ...baseStyles, positionArea: 'none', left: 'anchor(right)', top: 'anchor(center)', transform: 'translateY(-50%)', positionTryFallbacks: 'none', }; case 'right-start': return { ...baseStyles, positionArea: 'span-bottom right', alignSelf: 'start', }; case 'right-end': return { ...baseStyles, positionArea: 'span-top right', alignSelf: 'end', }; case 'left': return { ...baseStyles, positionArea: 'none', right: 'anchor(left)', top: 'anchor(center)', transform: 'translateY(-50%)', positionTryFallbacks: 'none', }; case 'left-start': return { ...baseStyles, positionArea: 'span-bottom left', alignSelf: 'start', }; case 'left-end': return { ...baseStyles, positionArea: 'span-top left', alignSelf: 'end', }; default: return baseStyles; } }