'use client' import * as SelectPrimitive from '@radix-ui/react-select' import React from 'react' import { SvgCheck, SvgTaillessLineArrowDown1, type SvgIcon, } from '@chainlink/blocks-icons' import { cn } from '../../utils/cn' import { DROPDOWN_ELEMENT_HEIGHT, DROPDOWN_LIST_MAX_HEIGHT, type DropdownSize, } from '../dropdownHeights' import { inputVariants } from '../Input' import { ClearIcon } from '../Input/icons' import { getDropdownItemBaseClassName, getDropdownItemIndicatorOffsetClassName, getInputControlContentTextClassName, getInputControlTextClassName, getInputControlValueSlotClassName, } from '../inputControlStyles' import { useScrollArrows, ScrollArrowUp, ScrollArrowDown, } from '../ScrollArrows' export type SelectSize = DropdownSize export type SelectIcon = React.ReactElement | undefined // Size context for Select components const SelectSizeContext = React.createContext('default') // Clear button: value + setter (controlled uses props, uncontrolled uses internal state). const SelectClearContext = React.createContext<{ value: string onValueChange: (value: string) => void clearable: boolean }>({ value: '', onValueChange: () => {}, clearable: true }) type SelectProps = React.ComponentPropsWithoutRef< typeof SelectPrimitive.Root > & { /** Visual density applied to the trigger and items. Defaults to `'default'`. */ size?: SelectSize /** When `false`, the hover-to-clear button is not shown. Defaults to `true`. */ clearable?: boolean } /** * Root component. Manages open state and the selected value. Supports both * controlled (`value` + `onValueChange`) and uncontrolled (`defaultValue`) modes. * Provides `size` and `clearable` context to all child components. */ const Select = ({ size = 'default', clearable = true, children, value: valueProp, onValueChange: onValueChangeProp, defaultValue, ...props }: SelectProps) => { const [internalValue, setInternalValue] = React.useState(defaultValue ?? '') const isControlled = valueProp !== undefined const value = isControlled ? (valueProp ?? '') : internalValue const onValueChange = React.useCallback( (v: string) => { if (!isControlled) setInternalValue(v) onValueChangeProp?.(v) }, [isControlled, onValueChangeProp], ) return ( {children} ) } Select.displayName = 'Select' // Hook to use size context const useSelectSize = () => { const context = React.useContext(SelectSizeContext) if (context === undefined) { throw new Error('useSelectSize must be used within a Select component') } return context } type SelectGroupProps = React.ComponentPropsWithoutRef< typeof SelectPrimitive.Group > & { ref?: React.Ref> } /** * Groups related items inside `SelectContent`. Pair with `SelectLabel` to add * a visible heading above the group. */ const SelectGroup = ({ ref, ...props }: SelectGroupProps) => ( ) SelectGroup.displayName = SelectPrimitive.Group.displayName type SelectValueProps = React.ComponentPropsWithoutRef< typeof SelectPrimitive.Value > & { ref?: React.Ref> } /** * Renders the selected value inside `SelectTrigger`. Shows `placeholder` when * nothing is selected. */ const SelectValue = ({ ref, className, ...props }: SelectValueProps & { className?: string }) => ( ) SelectValue.displayName = SelectPrimitive.Value.displayName type SelectIconComponentProps = React.ComponentPropsWithoutRef< typeof SelectPrimitive.Icon > & Pick & { ref?: React.Ref> } /** * The dropdown caret icon. Included automatically in the default `SelectTrigger`. * Add it manually when using `asChild` on `SelectTrigger` to retain the arrow * inside a custom element. */ const SelectIconInner = ({ className, color = 'currentColor', ref, ...props }: SelectIconComponentProps): React.ReactElement => { const size = useSelectSize() return ( ) } SelectIconInner.displayName = 'SelectIcon' // Create a properly-typed export that maintains the value 'SelectIcon' const SelectIconElement = SelectIconInner type SelectTriggerProps = React.ComponentPropsWithoutRef< typeof SelectPrimitive.Trigger > & { /** Replace the trigger with a custom element (e.g. `Button`). The child must forward its ref. */ asChild?: boolean width?: 'responsive' | 'hug' | 'full' ref?: React.Ref> } /** * The button that opens the dropdown. Renders the Blocks input style by default. * Pass `asChild` to replace it with a custom element such as `Button` or a plain * `button` — the child must forward its ref. */ const SelectTrigger = ({ className, children, asChild = false, width, ref, ...props }: SelectTriggerProps) => { const size = useSelectSize() const { value, onValueChange, clearable } = React.useContext(SelectClearContext) // Icon rotation styles for asChild case where custom icons might be provided const iconRotationStyles = 'group [&_[data-slot=select-icon]]:transition-transform [&_[data-slot=select-icon]]:duration-100 [&_[data-slot=select-icon]]:group-data-[state=open]:rotate-180' if (asChild) { return ( {children} ) } return ( {children}
{clearable && ( e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} > { e.stopPropagation() onValueChange?.('') }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() onValueChange?.('') } }} > )}
) } SelectTrigger.displayName = SelectPrimitive.Trigger.displayName type SelectContentProps = React.ComponentPropsWithoutRef< typeof SelectPrimitive.Content > & { container?: HTMLElement | null ref?: React.Ref> } /** * The dropdown panel that contains `SelectItem` elements. Renders inside a * portal and aligns to the trigger width by default. */ const SelectContent = ({ className, children, position = 'popper', container, ref, ...props }: SelectContentProps) => { const size = useSelectSize() const { scrollRef, containerRef, showScrollUp, showScrollDown, handleScroll, startAutoScroll, stopAutoScroll, scrollToStart, scrollToEnd, } = useScrollArrows() return (
} className="relative overflow-hidden" > } onScroll={handleScroll} tabIndex={-1} className={cn( 'overflow-y-auto overscroll-contain outline-none [scrollbar-width:none] [&::-webkit-scrollbar]:hidden', showScrollUp && 'pt-6', showScrollDown && 'pb-6', )} style={{ maxHeight: DROPDOWN_LIST_MAX_HEIGHT[size] }} > {children} {showScrollUp && ( startAutoScroll('up')} onHoverEnd={stopAutoScroll} onJump={scrollToStart} /> )} {showScrollDown && ( startAutoScroll('down')} onHoverEnd={stopAutoScroll} onJump={scrollToEnd} /> )}
) } SelectContent.displayName = SelectPrimitive.Content.displayName type SelectLabelProps = React.ComponentPropsWithoutRef< typeof SelectPrimitive.Label > & { ref?: React.Ref> } /** * A non-interactive label for a `SelectGroup`. Renders as a small bold heading * above the group's items. */ const SelectLabel = ({ className, ref, ...props }: SelectLabelProps) => ( ) SelectLabel.displayName = SelectPrimitive.Label.displayName type SelectItemProps = React.ComponentPropsWithoutRef< typeof SelectPrimitive.Item > & { /** Optional icon rendered to the left of the label. */ Icon?: React.ReactElement | undefined ref?: React.Ref> } /** * An individual option inside `SelectContent`. The selected item is highlighted * with a checkmark. Pass an `Icon` to show an icon to the left of the label. */ const SelectItem = ({ className, children, Icon, ref, ...props }: SelectItemProps) => { const size = useSelectSize() return ( {Icon && Icon} {children} ) } SelectItem.displayName = SelectPrimitive.Item.displayName type SelectSeparatorProps = React.ComponentPropsWithoutRef< typeof SelectPrimitive.Separator > & { ref?: React.Ref> } /** * A horizontal rule for visually separating groups of items inside `SelectContent`. */ const SelectSeparator = ({ className, ref, ...props }: SelectSeparatorProps) => ( ) SelectSeparator.displayName = SelectPrimitive.Separator.displayName export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectIconElement as SelectIcon, }