import * as React from "react" import { View, ScrollView } from "@tarojs/components" import { Check, ChevronDown, ChevronUp } from "lucide-react-taro" import { cn } from "@/lib/utils" import { getRectById, getViewport } from "@/lib/measure" import { Portal } from "@/components/ui/portal" type SelectContextValue = { value?: string onValueChange?: (value: string) => void open?: boolean onOpenChange?: (open: boolean) => void triggerId: string selectedLabel?: string setSelectedLabel?: (label?: string) => void } const SelectContext = React.createContext(null) interface SelectProps { value?: string defaultValue?: string onValueChange?: (value: string) => void open?: boolean defaultOpen?: boolean onOpenChange?: (open: boolean) => void children?: React.ReactNode } const Select: React.FC = ({ value: valueProp, defaultValue, onValueChange, open: openProp, defaultOpen, onOpenChange, children, }) => { const baseIdRef = React.useRef( `select-${Math.random().toString(36).slice(2, 10)}` ) const [openState, setOpenState] = React.useState(defaultOpen || false) const open = openProp !== undefined ? openProp : openState const [valueState, setValueState] = React.useState(defaultValue || "") const value = valueProp !== undefined ? valueProp : valueState const [selectedLabel, setSelectedLabel] = React.useState( undefined ) const handleOpenChange = (newOpen: boolean) => { if (openProp === undefined) { setOpenState(newOpen) } onOpenChange?.(newOpen) } const handleValueChange = (newValue: string) => { if (valueProp === undefined) { setValueState(newValue) } onValueChange?.(newValue) handleOpenChange(false) } return ( {children} ) } const SelectGroup = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) SelectGroup.displayName = "SelectGroup" const SelectValue = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { placeholder?: string } >(({ className, placeholder, children, ...props }, ref) => { const context = React.useContext(SelectContext) const hasValue = !!context?.value const displayValue = children ? children : context?.selectedLabel || context?.value || placeholder return ( {displayValue} ) }) SelectValue.displayName = "SelectValue" const SelectTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { size?: "sm" | "default" disabled?: boolean } >(({ className, size = "default", disabled, children, onClick, ...props }, ref) => { const context = React.useContext(SelectContext) return ( { if (disabled) return onClick?.(e) e.stopPropagation() context?.onOpenChange?.(!context.open) }} > {children} ) }) SelectTrigger.displayName = "SelectTrigger" const SelectScrollUpButton = ({ className, ...props }: React.ComponentPropsWithoutRef) => ( ) SelectScrollUpButton.displayName = "SelectScrollUpButton" const SelectScrollDownButton = ({ className, ...props }: React.ComponentPropsWithoutRef) => ( ) SelectScrollDownButton.displayName = "SelectScrollDownButton" const SelectContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & Pick< { align?: "start" | "center" | "end" alignOffset?: number side?: "top" | "bottom" | "left" | "right" sideOffset?: number alignItemWithTrigger?: boolean }, "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger" > >( ( { className, children, side = "bottom", sideOffset = 4, align = "center", alignOffset = 0, alignItemWithTrigger = true, onClick, ...props }, ref ) => { const context = React.useContext(SelectContext) const contentId = React.useRef( `select-content-${Math.random().toString(36).slice(2, 10)}` ) const [position, setPosition] = React.useState<{ left: number top: number } | null>(null) const [anchorWidth, setAnchorWidth] = React.useState(null) React.useEffect(() => { if (!context?.open) { setPosition(null) setAnchorWidth(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 const { width: vw, height: vh } = getViewport() const margin = 8 const contentMainSize = alignItemWithTrigger && (side === "bottom" || side === "top") ? triggerRect.width : side === "left" || side === "right" ? contentRect.height : contentRect.width const crossStart = side === "left" || side === "right" ? triggerRect.top : triggerRect.left const crossSize = side === "left" || side === "right" ? triggerRect.height : triggerRect.width const contentCrossSize = (() => { if (side === "left" || side === "right") return contentRect.height return alignItemWithTrigger ? triggerRect.width : 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 (side === "bottom" || side === "top") { left = cross + alignOffset top = side === "bottom" ? triggerRect.top + triggerRect.height + sideOffset : triggerRect.top - contentRect.height - sideOffset } else { top = cross + alignOffset left = side === "right" ? triggerRect.left + triggerRect.width + sideOffset : triggerRect.left - contentRect.width - sideOffset } const clampWidth = side === "bottom" || side === "top" ? contentMainSize : contentRect.width const clampHeight = side === "left" || side === "right" ? contentMainSize : contentRect.height left = Math.min(Math.max(left, margin), vw - clampWidth - margin) top = Math.min(Math.max(top, margin), vh - clampHeight - margin) setAnchorWidth(triggerRect.width) setPosition({ 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, alignOffset, alignItemWithTrigger, context?.open, context?.triggerId, side, sideOffset, ]) if (!context?.open) return null const contentStyle: React.CSSProperties = position ? { left: position.left, top: position.top, width: alignItemWithTrigger && anchorWidth ? anchorWidth : undefined, } : { left: 0, top: 0, opacity: 0, pointerEvents: "none", } return ( context.onOpenChange?.(false)} /> { onClick?.(e) e.stopPropagation() }} {...props} > {children} ) }) SelectContent.displayName = "SelectContent" const SelectLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) SelectLabel.displayName = "SelectLabel" const SelectItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { value: string; disabled?: boolean } >(({ className, children, value, disabled, onClick, ...props }, ref) => { const context = React.useContext(SelectContext) const isSelected = context?.value === value const labelText = React.useMemo(() => { if (typeof children === "string") return children if (Array.isArray(children) && children.every((c) => typeof c === "string")) { return children.join("") } return undefined }, [children]) React.useEffect(() => { if (isSelected && labelText) { context?.setSelectedLabel?.(labelText) } }, [context, isSelected, labelText]) return ( { onClick?.(e) if (disabled) return e.stopPropagation() context?.setSelectedLabel?.(labelText) context?.onValueChange?.(value) }} {...props} > {children} {isSelected ? ( ) : null} ) }) SelectItem.displayName = "SelectItem" const SelectSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) SelectSeparator.displayName = "SelectSeparator" export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton, }