import React, { useEffect, useRef, useState, forwardRef, useMemo } from 'react'; import { Modal, View, TouchableWithoutFeedback, BackHandler, Dimensions } from 'react-native'; import { useSafeAreaInsets } from '@idealyst/theme'; import { PopoverProps } from './types'; import { popoverStyles } from './Popover.styles'; import { calculateSmartPosition } from '../utils/positionUtils.native'; import { BoundedModalContent } from '../internal/BoundedModalContent.native'; import { getNativeInteractiveAccessibilityProps } from '../utils/accessibility'; import type { IdealystElement } from '../utils/refTypes'; const Popover = forwardRef(({ open, onOpenChange, anchor, children, placement = 'bottom', offset = 8, closeOnClickOutside = true, showArrow = false, style, testID, id, // Accessibility props accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityHidden, }, _ref) => { // Generate native accessibility props const nativeA11yProps = useMemo(() => { return getNativeInteractiveAccessibilityProps({ accessibilityLabel, accessibilityHint, accessibilityRole: accessibilityRole ?? 'none', accessibilityHidden, }); }, [accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityHidden]); const [popoverPosition, setPopoverPosition] = useState({ top: 0, left: 0, width: 0 }); const [popoverSize, setPopoverSize] = useState({ width: 0, height: 0 }); const anchorMeasurements = useRef({ x: 0, y: 0, width: 0, height: 0 }); const insets = useSafeAreaInsets(); // Apply variants popoverStyles.useVariants({}); // Get dynamic styles - call as functions for theme reactivity const backdropStyle = (popoverStyles.backdrop as any)({}); const containerStyle = (popoverStyles.container as any)({}); const arrowStyle = (popoverStyles.arrow as any)({}); const contentStyle = (popoverStyles.content as any)({}); // Determine if anchor is a ref object const anchorRefToUse = React.useMemo(() => { if (!anchor || typeof anchor !== 'object') return null; // Check if it has 'current' property (ref object) if ('current' in anchor) { return anchor as React.RefObject; } return null; }, [anchor]); // Handle Android back button useEffect(() => { if (!open) return; const handleBackPress = () => { onOpenChange(false); return true; }; const backHandler = BackHandler.addEventListener('hardwareBackPress', handleBackPress); return () => backHandler.remove(); }, [open, onOpenChange]); // Measure anchor position when opening useEffect(() => { if (open && anchorRefToUse?.current) { anchorRefToUse.current?.measureInWindow((x: number, y: number, width: number, height: number) => { anchorMeasurements.current = { x, y, width, height }; calculatePopoverPosition(x, y, width, height); }); } }, [open, placement, anchorRefToUse]); // Recalculate position when popover size changes useEffect(() => { if (open && popoverSize.width > 0 && popoverSize.height > 0) { const { x, y, width, height } = anchorMeasurements.current; if (x > 0 || y > 0) { calculatePopoverPosition(x, y, width, height); } } }, [popoverSize, open]); const calculatePopoverPosition = (x: number, y: number, width: number, height: number) => { // Use measured size if available, otherwise use estimates const popoverWidth = popoverSize.width || 200; const desiredMaxHeight = 500; // Maximum height we want for popovers // For flip detection, use maxHeight so it properly detects when there's not enough space // But if we have a measured size that's SMALLER than maxHeight, use that for final positioning // to avoid unnecessary gaps (this happens when content naturally fits) const heightForPositioning = popoverSize.height > 0 && popoverSize.height < desiredMaxHeight ? popoverSize.height : desiredMaxHeight; const desiredSize = { width: popoverWidth, height: heightForPositioning }; // Use smart positioning with boundary detection and flipping const position = calculateSmartPosition( { x, y, width, height }, desiredSize, placement, offset, false, insets ); setPopoverPosition({ top: position.top, left: position.left, width }); }; const handleBackdropPress = () => { if (closeOnClickOutside) { onOpenChange(false); } }; const handlePopoverLayout = (event: any) => { const { width, height } = event.nativeEvent.layout; // Only update if size has changed significantly (to avoid infinite loops) if (Math.abs(width - popoverSize.width) > 1 || Math.abs(height - popoverSize.height) > 1) { setPopoverSize({ width, height }); } }; const { width: screenWidth } = Dimensions.get('window'); const maxPopoverWidth = screenWidth - 24; // 12px padding on each side if (!open || popoverPosition.top === 0) { return null; } return ( onOpenChange(false)} supportedOrientations={['portrait', 'landscape']} testID={testID} nativeID={id} > {showArrow && } {children} ); }); Popover.displayName = 'Popover'; export default Popover;