{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "popover", "type": "registry:ui", "title": "Popover", "description": "A popover component that can be used to display additional information.", "files": [ { "path": "registry/tra-kit/components/popover.tsx", "content": "import React, { createContext, useContext, useRef, useEffect, useState, useMemo } from 'react';\r\nimport { createPortal } from 'react-dom';\r\nimport { cn, preventScrollShift } from '@/lib/utils';\r\n\r\ntype PopoverContextType = {\r\n open: boolean;\r\n setOpen: (open: boolean) => void;\r\n triggerRef: React.RefObject;\r\n contentRef: React.RefObject;\r\n styles: React.CSSProperties;\r\n disabled: boolean;\r\n};\r\n\r\nconst PopoverContext = createContext(undefined);\r\n\r\nconst usePopover = () => {\r\n const context = useContext(PopoverContext);\r\n if (!context) throw new Error('usePopover must be used within Popover');\r\n return context;\r\n};\r\n\r\nexport interface PopoverProps {\r\n children: React.ReactNode;\r\n open?: boolean;\r\n onOpenChange?: (open: boolean) => void;\r\n disabled?: boolean;\r\n dropdownAlign?: 'left' | 'right';\r\n forceTriggerWidth?: boolean;\r\n maxHeight?: number | string;\r\n}\r\n\r\nconst Popover: React.FC = ({\r\n children,\r\n open: controlledOpen,\r\n onOpenChange,\r\n disabled = false,\r\n dropdownAlign = 'left',\r\n forceTriggerWidth = false,\r\n maxHeight = 320,\r\n}) => {\r\n const [open, setOpen] = useState(controlledOpen ?? false);\r\n const triggerRef = useRef(null as unknown as HTMLDivElement);\r\n const contentRef = useRef(null as unknown as HTMLDivElement);\r\n const [styles, setStyles] = useState({});\r\n\r\n useEffect(() => {\r\n if (open) {\r\n preventScrollShift.lock();\r\n } else {\r\n preventScrollShift.unlock();\r\n }\r\n return () => {\r\n preventScrollShift.unlock();\r\n };\r\n }, [open]);\r\n\r\n useEffect(() => {\r\n if (controlledOpen !== undefined) {\r\n setOpen(controlledOpen);\r\n }\r\n }, [controlledOpen]);\r\n\r\n const handleOpenChange = (newOpen: boolean) => {\r\n if (disabled) return;\r\n if (onOpenChange) {\r\n onOpenChange(newOpen);\r\n }\r\n if (controlledOpen === undefined) {\r\n setOpen(newOpen);\r\n }\r\n };\r\n\r\n const updatePosition = () => {\r\n if (open && triggerRef.current) {\r\n const rect = triggerRef.current.getBoundingClientRect();\r\n const spaceBelow = window.innerHeight - rect.bottom;\r\n // const spaceAbove = rect.top;\r\n\r\n const maxHeightPx =\r\n typeof maxHeight === 'number' ? maxHeight : parseInt(maxHeight as string, 10) || 320;\r\n setStyles({\r\n position: 'fixed',\r\n top: spaceBelow > maxHeightPx ? `${rect.bottom}px` : 'auto',\r\n bottom: spaceBelow <= maxHeightPx ? `${window.innerHeight - rect.top}px` : 'auto',\r\n left: dropdownAlign === 'left' ? `${rect.left}px` : '',\r\n right: dropdownAlign === 'right' ? `${window.innerWidth - rect.right}px` : '',\r\n width: forceTriggerWidth ? `${rect.width}px` : `fit-content`,\r\n maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight,\r\n zIndex: 50,\r\n });\r\n }\r\n };\r\n\r\n useEffect(() => {\r\n updatePosition();\r\n window.addEventListener('scroll', updatePosition, true);\r\n window.addEventListener('resize', updatePosition);\r\n\r\n return () => {\r\n window.removeEventListener('scroll', updatePosition, true);\r\n window.removeEventListener('resize', updatePosition);\r\n };\r\n }, [open]);\r\n\r\n useEffect(() => {\r\n const handleClickOutside = (event: MouseEvent) => {\r\n if (\r\n contentRef.current &&\r\n !contentRef.current.contains(event.target as Node) &&\r\n !triggerRef.current?.contains(event.target as Node)\r\n ) {\r\n handleOpenChange(false);\r\n }\r\n };\r\n\r\n if (open) {\r\n document.addEventListener('mousedown', handleClickOutside);\r\n }\r\n\r\n return () => {\r\n document.removeEventListener('mousedown', handleClickOutside);\r\n };\r\n }, [open]);\r\n\r\n const contextValue = useMemo(\r\n () => ({\r\n open,\r\n setOpen: handleOpenChange,\r\n triggerRef,\r\n contentRef,\r\n styles,\r\n disabled,\r\n }),\r\n [open, styles, triggerRef, contentRef, disabled],\r\n );\r\n\r\n return (\r\n \r\n
{children}
\r\n
\r\n );\r\n};\r\n\r\nPopover.displayName = 'Popover';\r\n\r\ninterface PopoverTriggerProps {\r\n children: React.ReactNode;\r\n className?: string;\r\n asChild?: boolean;\r\n}\r\n\r\nconst PopoverTrigger = React.forwardRef(\r\n ({ children, className, asChild = false }, forwardedRef) => {\r\n const { open, setOpen, triggerRef, disabled } = usePopover();\r\n\r\n // @ts-ignore\r\n const handleClick = (e?: React.MouseEvent) => {\r\n if (!disabled) {\r\n setOpen(!open);\r\n }\r\n };\r\n\r\n const setTriggerNode = (node: HTMLElement | null) => {\r\n (triggerRef as React.MutableRefObject).current = node;\r\n if (!forwardedRef) return;\r\n if (typeof forwardedRef === 'function') forwardedRef(node);\r\n else (forwardedRef as React.MutableRefObject).current = node;\r\n };\r\n\r\n if (asChild && React.isValidElement(children)) {\r\n return React.cloneElement(children, {\r\n ref: (node: HTMLElement) => {\r\n setTriggerNode(node);\r\n // Preserve the original ref if it exists\r\n const { ref } = children as any;\r\n if (typeof ref === 'function') {\r\n ref(node);\r\n } else if (ref) {\r\n ref.current = node;\r\n }\r\n },\r\n onClick: (e: React.MouseEvent) => {\r\n handleClick(e);\r\n // Call the original onClick if it exists\r\n const originalOnClick = (children as any)?.props?.onClick;\r\n if (typeof originalOnClick === 'function') {\r\n originalOnClick(e);\r\n }\r\n },\r\n 'data-state': open ? 'open' : 'closed',\r\n 'data-disabled': disabled,\r\n } as any);\r\n }\r\n\r\n return (\r\n \r\n {children}\r\n \r\n );\r\n },\r\n);\r\n\r\nPopoverTrigger.displayName = 'PopoverTrigger';\r\n\r\ninterface PopoverContentProps {\r\n children: React.ReactNode;\r\n className?: string;\r\n align?: 'center' | 'left' | 'right';\r\n side?: 'top' | 'bottom';\r\n style?: React.CSSProperties;\r\n}\r\n\r\nconst PopoverContent = React.forwardRef(\r\n ({ children, className, align, side, style: styleOverride }, forwardedRef) => {\r\n const { open, contentRef, styles, setOpen, triggerRef } = usePopover();\r\n\r\n // Merge the refs\r\n const mergedRef = (node: HTMLDivElement | null) => {\r\n if (forwardedRef) {\r\n if (typeof forwardedRef === 'function') {\r\n forwardedRef(node as any);\r\n } else {\r\n (forwardedRef as React.MutableRefObject).current = node;\r\n }\r\n }\r\n if (contentRef.current !== node) {\r\n (contentRef as React.MutableRefObject).current = node;\r\n }\r\n };\r\n\r\n if (!open) return null;\r\n\r\n // Compute final styles, allowing align/side overrides based on trigger rect\r\n const finalStyles: React.CSSProperties = { ...styles };\r\n try {\r\n const rect = triggerRef.current?.getBoundingClientRect();\r\n if (rect) {\r\n if (align === 'right') {\r\n finalStyles.right = `${window.innerWidth - rect.right}px`;\r\n delete finalStyles.left;\r\n } else if (align === 'left') {\r\n finalStyles.left = `${rect.left}px`;\r\n delete finalStyles.right;\r\n }\r\n\r\n if (side === 'top') {\r\n finalStyles.bottom = `${window.innerHeight - rect.top}px`;\r\n delete finalStyles.top;\r\n } else if (side === 'bottom') {\r\n finalStyles.top = `${rect.bottom}px`;\r\n delete finalStyles.bottom;\r\n }\r\n }\r\n } catch (e) {\r\n // ignore measurement errors in server or unusual envs\r\n }\r\n\r\n const mergedStyles = {\r\n position: 'fixed',\r\n zIndex: 50,\r\n ...finalStyles,\r\n ...(styleOverride || {}),\r\n };\r\n\r\n return createPortal(\r\n <>\r\n setOpen(false)}\r\n />\r\n \r\n {children}\r\n \r\n ,\r\n document.body,\r\n );\r\n },\r\n);\r\n\r\nPopoverContent.displayName = 'PopoverContent';\r\n\r\nexport { Popover, PopoverTrigger, PopoverContent };\r\n", "type": "registry:ui" } ] }