1+ import { cn } from "@/lib/utils" ;
2+ import { useCallback , useMemo , useRef , useState } from "react" ;
3+ import { MultiSelect } from "./multi-select" ;
4+
5+ export interface SignatureOption {
6+ label : string ;
7+ value : string ;
8+ abi ?: string ;
9+ }
10+
11+ interface SignatureSelectorProps {
12+ options : SignatureOption [ ] ;
13+ value : string ;
14+ onChange : ( val : string ) => void ;
15+ setAbi ?: ( abi : string ) => void ;
16+ placeholder ?: string ;
17+ disabled ?: boolean ;
18+ secondaryTextFormatter ?: ( sig : SignatureOption ) => string ;
19+ className ?: string ;
20+ }
21+
22+ export function SignatureSelector ( {
23+ options,
24+ value,
25+ onChange,
26+ setAbi,
27+ placeholder = "Select or enter a signature" ,
28+ disabled,
29+ secondaryTextFormatter,
30+ className,
31+ } : SignatureSelectorProps ) {
32+ const [ searchValue , setSearchValue ] = useState ( "" ) ;
33+ const inputRef = useRef < HTMLInputElement > ( null ) ;
34+
35+ // Memoize options with formatted secondary text if provided
36+ const formattedOptions = useMemo ( ( ) => {
37+ return options . map ( ( opt ) => ( {
38+ ...opt ,
39+ label : secondaryTextFormatter
40+ ? `${ opt . label } • ${ secondaryTextFormatter ( opt ) } `
41+ : opt . label ,
42+ } ) ) ;
43+ } , [ options , secondaryTextFormatter ] ) ;
44+
45+ // Check if the current value is a custom value (not in options)
46+ const isCustomValue = value && ! options . some ( ( opt ) => opt . value === value ) ;
47+
48+ // Add the custom value as an option if needed
49+ const allOptions = useMemo ( ( ) => {
50+ if ( isCustomValue && value ) {
51+ return [ ...formattedOptions , { label : value , value } ] ;
52+ }
53+ return formattedOptions ;
54+ } , [ formattedOptions , isCustomValue , value ] ) ;
55+
56+ // Single-select MultiSelect wrapper
57+ const handleSelectedValuesChange = useCallback (
58+ ( selected : string [ ] ) => {
59+ // Always use the last selected value for single-select behavior
60+ const selectedValue =
61+ selected . length > 0 ? ( selected [ selected . length - 1 ] ?? "" ) : "" ;
62+ onChange ( selectedValue ) ;
63+ const found = options . find ( ( opt ) => opt . value === selectedValue ) ;
64+ if ( setAbi ) {
65+ setAbi ( found ?. abi || "" ) ;
66+ }
67+ setSearchValue ( "" ) ;
68+ } ,
69+ [ onChange , setAbi , options ] ,
70+ ) ;
71+
72+ // Handle custom value entry
73+ const handleInputKeyDown = ( event : React . KeyboardEvent < HTMLInputElement > ) => {
74+ if ( event . key === "Enter" && searchValue . trim ( ) ) {
75+ if ( ! options . some ( ( opt ) => opt . value === searchValue . trim ( ) ) ) {
76+ onChange ( searchValue . trim ( ) ) ;
77+ if ( setAbi ) setAbi ( "" ) ;
78+ setSearchValue ( "" ) ;
79+ // Optionally blur input
80+ inputRef . current ?. blur ( ) ;
81+ }
82+ }
83+ } ;
84+
85+ // Custom render for MultiSelect's search input
86+ const customSearchInput = (
87+ < input
88+ ref = { inputRef }
89+ type = "text"
90+ className = { cn (
91+ "w-full border-0 border-border border-b bg-transparent py-4 pr-2 pl-10 text-sm focus-visible:ring-0 focus-visible:ring-offset-0" ,
92+ disabled && "cursor-not-allowed opacity-50" ,
93+ ) }
94+ placeholder = { placeholder }
95+ value = { searchValue }
96+ onChange = { ( e ) => setSearchValue ( e . target . value ) }
97+ onKeyDown = { handleInputKeyDown }
98+ disabled = { disabled }
99+ autoComplete = "off"
100+ />
101+ ) ;
102+
103+ return (
104+ < div className = { className } >
105+ < MultiSelect
106+ options = { allOptions }
107+ selectedValues = { value ? [ value ] : [ ] }
108+ onSelectedValuesChange = { handleSelectedValuesChange }
109+ placeholder = { placeholder }
110+ maxCount = { 1 }
111+ disabled = { disabled }
112+ searchPlaceholder = { placeholder }
113+ customTrigger = { null }
114+ renderOption = { ( option ) => < span > { option . label } </ span > }
115+ overrideSearchFn = { ( option , searchTerm ) =>
116+ option . label . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
117+ option . value . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) )
118+ }
119+ customSearchInput = { customSearchInput }
120+ />
121+ { isCustomValue && (
122+ < div className = "mt-2 rounded border border-warning-200 bg-warning-50 px-2 py-1 text-warning-700 text-xs" >
123+ You entered a custom signature. Please provide the ABI below.
124+ </ div >
125+ ) }
126+ </ div >
127+ ) ;
128+ }
0 commit comments