popover.tsx raw
1 import * as React from 'react'
2 import * as PopoverPrimitive from '@radix-ui/react-popover'
3
4 import { cn } from '@/lib/utils'
5 import { createPortal } from 'react-dom'
6
7 const Popover = ({
8 open: controlledOpen,
9 onOpenChange: controlledOnOpenChange,
10 ...props
11 }: React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Root>) => {
12 const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false)
13 const isControlled = controlledOpen !== undefined
14 const open = isControlled ? controlledOpen : uncontrolledOpen
15 const backdropRef = React.useRef<HTMLDivElement>(null)
16
17 const handleOpenChange = React.useCallback(
18 (newOpen: boolean) => {
19 if (!isControlled) {
20 setUncontrolledOpen(newOpen)
21 }
22 controlledOnOpenChange?.(newOpen)
23 },
24 [isControlled, controlledOnOpenChange]
25 )
26
27 return (
28 <>
29 {open &&
30 createPortal(
31 <div
32 ref={backdropRef}
33 className="fixed inset-0 z-40 pointer-events-auto"
34 onClick={(e) => {
35 e.stopPropagation()
36 handleOpenChange(false)
37 }}
38 />,
39 document.body
40 )}
41 <PopoverPrimitive.Root {...props} open={open} onOpenChange={handleOpenChange} modal={false} />
42 </>
43 )
44 }
45 Popover.displayName = 'Popover'
46
47 const PopoverTrigger = PopoverPrimitive.Trigger
48
49 const PopoverAnchor = PopoverPrimitive.Anchor
50
51 const PopoverContent = React.forwardRef<
52 React.ElementRef<typeof PopoverPrimitive.Content>,
53 React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
54 >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
55 <PopoverPrimitive.Portal>
56 <PopoverPrimitive.Content
57 ref={ref}
58 align={align}
59 sideOffset={sideOffset}
60 collisionPadding={10}
61 className={cn(
62 'z-50 w-72 rounded-xl border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
63 className
64 )}
65 onOpenAutoFocus={(e) => e.preventDefault()}
66 {...props}
67 />
68 </PopoverPrimitive.Portal>
69 ))
70 PopoverContent.displayName = PopoverPrimitive.Content.displayName
71
72 export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
73