import * as React from "react"
import { ScrollView, View } from "@tarojs/components"
import { Check, ChevronRight } from "lucide-react-taro"
import { cn } from "@/lib/utils"
import { isH5 } from "@/lib/platform"
import { computePosition, getRectById } from "@/lib/measure"
import { Portal } from "@/components/ui/portal"
const DropdownMenuContext = React.createContext<{
open?: boolean
onOpenChange?: (open: boolean) => void
triggerId: string
} | null>(null)
interface DropdownMenuProps {
children: React.ReactNode
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
}
const DropdownMenu = ({ open: openProp, defaultOpen, onOpenChange, children }: DropdownMenuProps) => {
const baseIdRef = React.useRef(`dropdown-menu-${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)
}
return (
{children}
)
}
const DropdownMenuTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, onClick, ...props }, ref) => {
const context = React.useContext(DropdownMenuContext)
return (
{
e.stopPropagation()
context?.onOpenChange?.(!context.open)
onClick?.(e)
}}
>
{children}
)
})
DropdownMenuTrigger.displayName = "DropdownMenuTrigger"
const DropdownMenuGroup = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DropdownMenuGroup.displayName = "DropdownMenuGroup"
const DropdownMenuPortal = ({ children }: { children: React.ReactNode }) => {
return <>{children}>
}
const DropdownMenuRadioGroupContext = React.createContext<{
value?: string
onValueChange?: (value: string) => void
} | null>(null)
interface DropdownMenuRadioGroupProps extends React.ComponentPropsWithoutRef {
value?: string
defaultValue?: string
onValueChange?: (value: string) => void
}
const DropdownMenuRadioGroup = React.forwardRef<
React.ElementRef,
DropdownMenuRadioGroupProps
>(({ value: valueProp, defaultValue, onValueChange, ...props }, ref) => {
const [valueState, setValueState] = React.useState(defaultValue)
const value = valueProp !== undefined ? valueProp : valueState
const handleValueChange = (next: string) => {
if (valueProp === undefined) {
setValueState(next)
}
onValueChange?.(next)
}
return (
)
})
DropdownMenuRadioGroup.displayName = "DropdownMenuRadioGroup"
const DropdownMenuSubContext = React.createContext<{
open?: boolean
onOpenChange?: (open: boolean) => void
triggerId: string
} | null>(null)
interface DropdownMenuSubProps {
children: React.ReactNode
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
}
const DropdownMenuSub = ({ open: openProp, defaultOpen, onOpenChange, children }: DropdownMenuSubProps) => {
const parent = React.useContext(DropdownMenuContext)
const baseIdRef = React.useRef(`dropdown-menu-sub-${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 (parent?.open === false && open) {
handleOpenChange(false)
}
}, [open, parent?.open])
return (
{children}
)
}
interface DropdownMenuContentProps extends React.ComponentPropsWithoutRef {
align?: "start" | "center" | "end"
side?: "top" | "bottom" | "left" | "right"
sideOffset?: number
}
const DropdownMenuContent = React.forwardRef<
React.ElementRef,
DropdownMenuContentProps
>(({ className, align = "start", side = "bottom", sideOffset = 4, children, ...props }, ref) => {
const context = React.useContext(DropdownMenuContext)
const contentId = React.useRef(`dropdown-menu-content-${Math.random().toString(36).slice(2, 10)}`)
const [position, setPosition] = React.useState<{ left: number; top: number } | null>(null)
React.useEffect(() => {
if (!context?.open) {
setPosition(null)
return
}
let cancelled = false
const compute = async () => {
if (!context?.triggerId) return
const [triggerRect, contentRect] = await Promise.all([
getRectById(context.triggerId),
getRectById(contentId.current),
])
if (cancelled) return
if (!triggerRect?.width || !contentRect?.width) return
setPosition(
computePosition({
triggerRect,
contentRect,
align,
side,
sideOffset,
})
)
}
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, side, sideOffset])
React.useEffect(() => {
if (!context?.open) return
if (!isH5() || typeof document === "undefined") return
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
context?.onOpenChange?.(false)
}
}
document.addEventListener("keydown", onKeyDown)
return () => document.removeEventListener("keydown", onKeyDown)
}, [context?.open])
if (!context?.open) return null
const baseClassName =
"fixed z-50 min-w-32 overflow-hidden rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground ring-opacity-10 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"
const contentStyle = position
? ({ left: position.left, top: position.top } as React.CSSProperties)
: ({
left: 0,
top: 0,
opacity: 0,
pointerEvents: "none",
} as React.CSSProperties)
return (
context.onOpenChange?.(false)} />
e.stopPropagation()}
>
{children}
)
})
DropdownMenuContent.displayName = "DropdownMenuContent"
const DropdownMenuItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean
variant?: "default" | "destructive"
disabled?: boolean
closeOnSelect?: boolean
}
>(({ className, inset, variant = "default", disabled, closeOnSelect = true, onClick, ...props }, ref) => {
const context = React.useContext(DropdownMenuContext)
return (
{
if (disabled) return
onClick?.(e)
if (closeOnSelect) context?.onOpenChange?.(false)
}}
/>
)
})
DropdownMenuItem.displayName = "DropdownMenuItem"
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
checked?: boolean
inset?: boolean
disabled?: boolean
closeOnSelect?: boolean
}
>(({ className, children, checked, inset, disabled, closeOnSelect = false, onClick, ...props }, ref) => {
const context = React.useContext(DropdownMenuContext)
return (
{
if (disabled) return
onClick?.(e)
if (closeOnSelect) context?.onOpenChange?.(false)
}}
>
{checked && }
{children}
)
})
DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem"
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
value: string
checked?: boolean
inset?: boolean
disabled?: boolean
closeOnSelect?: boolean
}
>(({ className, children, value, checked: checkedProp, inset, disabled, closeOnSelect = false, onClick, ...props }, ref) => {
const context = React.useContext(DropdownMenuContext)
const group = React.useContext(DropdownMenuRadioGroupContext)
const checked = checkedProp !== undefined ? checkedProp : group?.value === value
return (
{
if (disabled) return
group?.onValueChange?.(value)
onClick?.(e)
if (closeOnSelect) context?.onOpenChange?.(false)
}}
>
{checked && }
{children}
)
})
DropdownMenuRadioItem.displayName = "DropdownMenuRadioItem"
const DropdownMenuLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
))
DropdownMenuLabel.displayName = "DropdownMenuLabel"
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DropdownMenuSeparator.displayName = "DropdownMenuSeparator"
const DropdownMenuShortcut = ({ className, ...props }: React.ComponentPropsWithoutRef) => {
return (
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean
disabled?: boolean
}
>(({ className, inset, disabled, children, onClick, ...props }, ref) => {
const subContext = React.useContext(DropdownMenuSubContext)
return (
{
e.stopPropagation()
if (disabled) return
subContext?.onOpenChange?.(!subContext.open)
onClick?.(e)
}}
>
{children}
)
})
DropdownMenuSubTrigger.displayName = "DropdownMenuSubTrigger"
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef,
DropdownMenuContentProps
>(({ className, align = "start", side = "right", sideOffset = 4, children, ...props }, ref) => {
const parent = React.useContext(DropdownMenuContext)
const subContext = React.useContext(DropdownMenuSubContext)
const contentId = React.useRef(`dropdown-menu-sub-content-${Math.random().toString(36).slice(2, 10)}`)
const [position, setPosition] = React.useState<{ left: number; top: number } | null>(null)
React.useEffect(() => {
if (!parent?.open || !subContext?.open) {
setPosition(null)
return
}
let cancelled = false
const compute = async () => {
if (!subContext?.triggerId) return
const [triggerRect, contentRect] = await Promise.all([
getRectById(subContext.triggerId),
getRectById(contentId.current),
])
if (cancelled) return
if (!triggerRect?.width || !contentRect?.width) return
setPosition(
computePosition({
triggerRect,
contentRect,
align,
side,
sideOffset,
})
)
}
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, parent?.open, side, sideOffset, subContext?.open, subContext?.triggerId])
if (!parent?.open || !subContext?.open) return null
const baseClassName =
"fixed z-50 min-w-[96px] overflow-hidden rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground ring-opacity-10 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"
const contentStyle = position
? ({ left: position.left, top: position.top } as React.CSSProperties)
: ({
left: 0,
top: 0,
opacity: 0,
pointerEvents: "none",
} as React.CSSProperties)
return (
e.stopPropagation()}
>
{children}
)
})
DropdownMenuSubContent.displayName = "DropdownMenuSubContent"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}