import React, { useRef, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { EmojiPicker } from './EmojiPicker'; import { GifPicker } from './GifPicker'; export type InputPopoverTab = 'emoji' | 'gif'; interface InputPopoverProps { activeTab: InputPopoverTab; onClose: () => void; onEmojiSelect: (emoji: { native: string }) => void; onGifSelect: (url: string) => void; anchorRef: React.RefObject; gifEnabled: boolean; onTabChange: (tab: InputPopoverTab) => void; } export function InputPopover({ activeTab, onClose, onEmojiSelect, onGifSelect, anchorRef, gifEnabled, onTabChange, }: InputPopoverProps) { const floatingRef = useRef(null); // Position above the anchor const updatePosition = useCallback(() => { const anchor = anchorRef.current; const floating = floatingRef.current; if (!anchor || !floating) return; const anchorRect = anchor.getBoundingClientRect(); const floatingRect = floating.getBoundingClientRect(); const vw = window.innerWidth; let left = anchorRect.right - floatingRect.width; let top = anchorRect.top - floatingRect.height - 8; // Flip below if no room above if (top < 8) { top = anchorRect.bottom + 8; } // Clamp horizontal left = Math.max(8, Math.min(left, vw - floatingRect.width - 8)); floating.style.top = `${top}px`; floating.style.left = `${left}px`; }, [anchorRef]); useEffect(() => { updatePosition(); window.addEventListener('resize', updatePosition); window.addEventListener('scroll', updatePosition, true); return () => { window.removeEventListener('resize', updatePosition); window.removeEventListener('scroll', updatePosition, true); }; }, [updatePosition, activeTab]); // Re-position after the picker renders (it may change height) useEffect(() => { const frame = requestAnimationFrame(updatePosition); return () => cancelAnimationFrame(frame); }, [activeTab, updatePosition]); // Click outside to close useEffect(() => { const handler = (e: MouseEvent) => { const floating = floatingRef.current; const anchor = anchorRef.current; if (!floating) return; if (floating.contains(e.target as Node)) return; if (anchor && anchor.contains(e.target as Node)) return; onClose(); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [onClose, anchorRef]); // Escape to close useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.stopPropagation(); onClose(); } }; document.addEventListener('keydown', handler); return () => document.removeEventListener('keydown', handler); }, [onClose]); const availableTabs: { key: InputPopoverTab; label: string }[] = [ { key: 'emoji', label: 'Emoji' }, ]; if (gifEnabled) { availableTabs.splice(0, 0, { key: 'gif', label: 'GIF' }); } const showTabs = availableTabs.length > 1; return createPortal(
{/* Tab bar */} {showTabs && (
{availableTabs.map((t) => ( ))}
)} {/* Content */}
{activeTab === 'emoji' && ( )} {activeTab === 'gif' && gifEnabled && ( )}
, document.body, ); }