import * as React from "react"
import { View } from "@tarojs/components"
import Taro from "@tarojs/taro"
import { cn } from "@/lib/utils"
import { Portal } from "@/components/ui/portal"
import { useKeyboardOffset } from "@/lib/hooks/use-keyboard-offset"
import { isH5 } from "@/lib/platform"
const PopoverContext = React.createContext<{
open?: boolean
onOpenChange?: (open: boolean) => void
triggerId: string
} | null>(null)
interface PopoverProps {
children: React.ReactNode
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
}
const Popover = ({ open: openProp, defaultOpen, onOpenChange, children }: PopoverProps) => {
const baseIdRef = React.useRef(
`popover-${Math.random().toString(36).slice(2, 10)}`
)
const [openState, setOpenState] = React.useState(defaultOpen || false)
const open = openProp !== undefined ? openProp : openState
const handleOpenChange = (newOpen: boolean) => {
if (openProp === undefined) {
setOpenState(newOpen)
}
onOpenChange?.(newOpen)
}
React.useEffect(() => {
if (!isH5()) return
if (!open) return
const onKeyDown = (e: KeyboardEvent) => {
if (e.key !== "Escape") return
e.stopPropagation()
handleOpenChange(false)
}
window.addEventListener("keydown", onKeyDown, true)
return () => {
window.removeEventListener("keydown", onKeyDown, true)
}
}, [open])
return (
{children}
)
}
const PopoverTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & { asChild?: boolean }
>(({ className, children, asChild, ...props }, ref) => {
const context = React.useContext(PopoverContext)
return (
{
e.stopPropagation()
context?.onOpenChange?.(!context.open)
}}
{...props}
>
{children}
)
})
PopoverTrigger.displayName = "PopoverTrigger"
interface PopoverContentProps extends React.ComponentPropsWithoutRef {
align?: "center" | "start" | "end"
side?: "top" | "bottom" | "left" | "right"
position?: "top" | "bottom" | "left" | "right"
sideOffset?: number
}
const PopoverContent = React.forwardRef<
React.ElementRef,
PopoverContentProps
>(({ className, align = "center", side, position: positionProp, sideOffset = 4, style, ...props }, ref) => {
const context = React.useContext(PopoverContext)
const offset = useKeyboardOffset()
const resolvedSide = positionProp ?? side ?? "bottom"
const contentId = React.useRef(
`popover-content-${Math.random().toString(36).slice(2, 10)}`
)
const [contentPosition, setContentPosition] = React.useState<
| {
left: number
top: number
}
| null
>(null)
React.useEffect(() => {
if (!context?.open) {
setContentPosition(null)
return
}
let cancelled = false
const getViewport = () => {
if (isH5() && typeof window !== "undefined") {
return { width: window.innerWidth, height: window.innerHeight }
}
try {
const info = Taro.getSystemInfoSync()
return { width: info.windowWidth, height: info.windowHeight }
} catch {
return { width: 375, height: 667 }
}
}
const getRectH5 = (id: string) => {
if (!isH5() || typeof document === "undefined") return null
const el = document.getElementById(id)
if (!el) return null
const r = el.getBoundingClientRect()
return { left: r.left, top: r.top, width: r.width, height: r.height }
}
const getRect = (id: string) => {
const h5Rect = getRectH5(id)
if (h5Rect) return Promise.resolve(h5Rect)
return new Promise((resolve) => {
const query = Taro.createSelectorQuery()
query
.select(`#${id}`)
.boundingClientRect((res) => {
const rect = Array.isArray(res) ? res[0] : res
resolve(rect || null)
})
.exec()
})
}
const compute = async () => {
const [triggerRect, contentRect] = await Promise.all([
getRect(context.triggerId),
getRect(contentId.current),
])
if (cancelled) return
if (!triggerRect?.width || !contentRect?.width) return
const viewport = getViewport()
const vw = viewport.width
const vh = Math.max(0, viewport.height - (isH5() ? 0 : offset || 0))
const margin = 8
const crossStart =
resolvedSide === "left" || resolvedSide === "right" ? triggerRect.top : triggerRect.left
const crossSize =
resolvedSide === "left" || resolvedSide === "right" ? triggerRect.height : triggerRect.width
const contentCrossSize =
resolvedSide === "left" || resolvedSide === "right" ? contentRect.height : contentRect.width
const cross = (() => {
if (align === "start") return crossStart
if (align === "end") return crossStart + crossSize - contentCrossSize
return crossStart + crossSize / 2 - contentCrossSize / 2
})()
let left = 0
let top = 0
if (resolvedSide === "bottom" || resolvedSide === "top") {
left = cross
top =
resolvedSide === "bottom"
? triggerRect.top + triggerRect.height + sideOffset
: triggerRect.top - contentRect.height - sideOffset
} else {
top = cross
left =
resolvedSide === "right"
? triggerRect.left + triggerRect.width + sideOffset
: triggerRect.left - contentRect.width - sideOffset
}
left = Math.min(Math.max(left, margin), vw - contentRect.width - margin)
top = Math.min(Math.max(top, margin), vh - contentRect.height - margin)
setContentPosition({ left, top })
}
const raf = (() => {
if (typeof requestAnimationFrame !== "undefined") {
return requestAnimationFrame(() => compute())
}
return setTimeout(() => compute(), 0) as unknown as number
})()
return () => {
cancelled = true
if (typeof cancelAnimationFrame !== "undefined") {
cancelAnimationFrame(raf)
} else {
clearTimeout(raf)
}
}
}, [align, context?.open, context?.triggerId, offset, resolvedSide, sideOffset])
if (!context?.open) return null
const baseClassName =
"fixed z-50 w-72 rounded-md 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"
const contentStyle = contentPosition
? ({
...(style as object),
left: contentPosition.left,
top: contentPosition.top,
} as React.CSSProperties)
: ({
...(style as object),
left: 0,
top: 0,
opacity: 0,
pointerEvents: "none",
} as React.CSSProperties)
return (
context.onOpenChange?.(false)}
/>
)
})
PopoverContent.displayName = "PopoverContent"
const PopoverHeader = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
PopoverHeader.displayName = "PopoverHeader"
const PopoverTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
PopoverTitle.displayName = "PopoverTitle"
const PopoverDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
PopoverDescription.displayName = "PopoverDescription"
export {
Popover,
PopoverTrigger,
PopoverContent,
PopoverHeader,
PopoverTitle,
PopoverDescription,
}