'use client' import * as PopoverPrimitive from '@radix-ui/react-popover' import * as React from 'react' import { SvgTaillessLineArrowDown1, SvgTaillessLineArrowUp1, } from '@chainlink/blocks-icons' import { cn, getIconUrl } from '../../utils' import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from '../Command' import { inputVariants } from '../Input' import { ClearIcon } from '../Input/icons' import { Popover, PopoverTrigger } from '../Popover' import { typographyVariants } from '../Typography' /** * Custom PopoverContent for LegacyCombobox that doesn't use a Portal. * This allows the content to stay within the DOM hierarchy, * which is required for proper scrolling inside modal dialogs (SidePanel, Sheet, etc.) */ const LegacyComboboxPopoverContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, sideOffset = 4, ...props }, ref) => ( )) LegacyComboboxPopoverContent.displayName = 'LegacyComboboxPopoverContent' export interface LegacyComboboxProps { size?: 'default' | 'sm' disabled?: boolean clearable?: boolean placeholder?: string value?: string onValueChange?: (value: string) => void defaultOpen?: boolean children: React.ReactNode } interface LegacyComboboxContextValue { open: boolean setOpen: (open: boolean) => void value: string setValue: (value: string) => void size: 'default' | 'sm' disabled: boolean clearable: boolean placeholder: string compact: boolean setCompact: (compact: boolean) => void selectedContent: React.ReactNode | null setSelectedContent: (content: React.ReactNode | null) => void } const LegacyComboboxContext = React.createContext< LegacyComboboxContextValue | undefined >(undefined) const useLegacyComboboxContext = () => { const context = React.useContext(LegacyComboboxContext) if (!context) { throw new Error( 'LegacyCombobox components must be used within a LegacyCombobox', ) } return context } export function LegacyCombobox({ size = 'default', disabled = false, clearable = true, placeholder = 'Select...', value: controlledValue, onValueChange, defaultOpen = false, children, }: LegacyComboboxProps) { const [open, setOpen] = React.useState(defaultOpen) const [internalValue, setInternalValue] = React.useState('') const [compact, setCompact] = React.useState(false) const [selectedContent, setSelectedContent] = React.useState(null) const value = controlledValue !== undefined ? controlledValue : internalValue const setValue = (newValue: string) => { if (controlledValue === undefined) { setInternalValue(newValue) } onValueChange?.(newValue) } return ( {children} ) } type LegacyComboboxIconProps = React.HTMLAttributes & { ref?: React.Ref } const LegacyComboboxIcon = ({ className, ref, ...props }: LegacyComboboxIconProps) => { const { size } = useLegacyComboboxContext() return (
) } LegacyComboboxIcon.displayName = 'LegacyComboboxIcon' const LegacyComboboxClearIcon = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => { const { size, value, setValue, setSelectedContent, disabled, clearable } = useLegacyComboboxContext() if (!value || disabled || !clearable) return null const handleClear = (event: React.MouseEvent | React.KeyboardEvent) => { event.stopPropagation() event.preventDefault() setValue('') setSelectedContent(null) } return (
{ if (event.key === 'Enter' || event.key === ' ') { handleClear(event) } }} className={cn( 'pointer-events-none cursor-pointer opacity-0 transition-opacity', 'group-hover:pointer-events-auto group-hover:opacity-100', 'focus:pointer-events-auto focus:opacity-100', 'text-input-muted-foreground hover:text-muted-foreground focus-visible:text-muted-foreground', 'focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring', 'data-[size=default]:h-4 data-[size=sm]:h-3 data-[size=default]:w-4 data-[size=sm]:w-3', '[&_svg]:h-full [&_svg]:w-full', className, )} {...props} >
) }) LegacyComboboxClearIcon.displayName = 'LegacyComboboxClearIcon' interface LegacyComboboxTriggerProps { className?: string children: React.ReactNode asChild?: boolean hideIcon?: boolean } export function LegacyComboboxTrigger({ className, children, asChild = false, hideIcon = false, }: LegacyComboboxTriggerProps) { const { open, disabled, size, compact } = useLegacyComboboxContext() const iconRotationStyles = '[&_[data-slot=combobox-icon]]:transition-transform [&_[data-slot=combobox-icon]]:duration-100 [&_[data-slot=combobox-icon]]:data-[state=open]:rotate-180' if (asChild) { return ( {children} ) } return ( {children}
{!hideIcon && }
) } interface LegacyComboboxValueProps { placeholder?: string hideText?: boolean children?: React.ReactNode } export function LegacyComboboxValue({ placeholder: customPlaceholder, hideText, children, }: LegacyComboboxValueProps) { const { value, placeholder, selectedContent, setCompact, size } = useLegacyComboboxContext() React.useEffect(() => { setCompact(!!hideText) }, [hideText, setCompact]) const showPlaceholder = !value const content = children || (showPlaceholder ? customPlaceholder || placeholder : selectedContent) return ( {content} ) } interface LegacyComboboxContentProps { children: React.ReactNode className?: string } export function LegacyComboboxContent({ children, className, }: LegacyComboboxContentProps) { const { size, open, compact } = useLegacyComboboxContext() const listRef = React.useRef(null) const scrollIntervalRef = React.useRef | null>( null, ) const [showScrollDown, setShowScrollDown] = React.useState(false) const [showScrollUp, setShowScrollUp] = React.useState(false) const handleScroll = React.useCallback(() => { const el = listRef.current if (!el) return const { scrollTop, clientHeight, scrollHeight } = el setShowScrollDown(scrollTop + clientHeight < scrollHeight - 1) setShowScrollUp(scrollTop > 1) }, []) const stopAutoScroll = React.useCallback(() => { if (scrollIntervalRef.current) { clearInterval(scrollIntervalRef.current) scrollIntervalRef.current = null } }, []) const startAutoScroll = React.useCallback( (direction: 'up' | 'down') => { stopAutoScroll() scrollIntervalRef.current = setInterval(() => { const el = listRef.current if (!el) return const atBoundary = direction === 'down' ? el.scrollTop + el.clientHeight >= el.scrollHeight - 1 : el.scrollTop <= 1 if (atBoundary) { stopAutoScroll() handleScroll() return } el.scrollTop += direction === 'down' ? 4 : -4 handleScroll() }, 16) }, [handleScroll, stopAutoScroll], ) const scrollToTop = React.useCallback(() => { requestAnimationFrame(() => { if (listRef.current) { listRef.current.scrollTop = 0 } handleScroll() }) }, [handleScroll]) React.useEffect(() => { if (!listRef.current) return const ro = new ResizeObserver(() => handleScroll()) ro.observe(listRef.current) handleScroll() return () => { ro.disconnect() } }, [handleScroll]) React.useEffect(() => { if (open) { requestAnimationFrame(() => handleScroll()) } else { stopAutoScroll() } }, [open, handleScroll, stopAutoScroll]) React.useEffect(() => { return () => stopAutoScroll() }, [stopAutoScroll]) return (
No results found. {children} {showScrollUp && (
startAutoScroll('up')} onMouseLeave={stopAutoScroll} onClick={() => { stopAutoScroll() listRef.current?.scrollTo({ top: 0, behavior: 'smooth', }) }} className="pointer-events-auto absolute inset-x-0 top-0 z-10 flex h-6 cursor-pointer items-center justify-center bg-background" >
)} {showScrollDown && (
startAutoScroll('down')} onMouseLeave={stopAutoScroll} onClick={() => { stopAutoScroll() listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: 'smooth', }) }} className="pointer-events-auto absolute inset-x-0 bottom-0 z-10 flex h-6 cursor-pointer items-center justify-center bg-background" >
)}
) } interface OptionRowProps { value: string children: React.ReactNode disabled?: boolean defaultValue?: boolean className?: string } export function OptionRow({ value: optionValue, children, disabled, defaultValue, className, }: OptionRowProps) { const { value, setValue, setOpen, size, setSelectedContent, clearable } = useLegacyComboboxContext() const isSelected = value === optionValue React.useLayoutEffect(() => { if (defaultValue && !value) { setValue(optionValue) setSelectedContent(children) } }, [defaultValue, value, optionValue, children, setValue, setSelectedContent]) // Ensure selected content is in sync when value is controlled or changes externally React.useLayoutEffect(() => { if (isSelected) { setSelectedContent(children) } }, [isSelected, children, setSelectedContent]) return ( { if (!clearable && currentValue === value) { setOpen(false) return } const newValue = currentValue === value ? '' : currentValue setValue(newValue) setSelectedContent(newValue ? children : null) setOpen(false) }} className={cn( typographyVariants({ variant: size === 'default' ? 'body-s' : 'body-xs', }), 'relative flex w-full cursor-default select-none items-center gap-2 px-4 py-3 outline-none', 'text-muted-foreground', 'data-[selected=true]:bg-accent', 'data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50', '[&_svg]:text-muted-foreground', isSelected && '!text-brand', isSelected && '[&_span]:text-brand', isSelected && '[&_svg]:!text-brand', className, )} > {isSelected && ( )} {children} ) } export { LegacyComboboxIcon }