@@ -141,51 +141,66 @@ public struct SearchableMenu: View {
141141 public var body : some View {
142142 VStack ( spacing: 2 ) {
143143 HStack ( spacing: 0 ) {
144- ZStack ( alignment: . leading) {
145- if searchText. isEmpty {
146- Text ( placeholder)
147- . foregroundColor ( placeholderColor. opacity ( 0.7 ) )
148- . font ( font)
149- . padding ( . horizontal, 10 )
150- . padding ( . vertical, 5 )
151- }
152- TextField ( " " , text: $searchText, onEditingChanged: { isEditing in
153- isDropdownVisible = isEditing
154- if isEditing {
155- isOptionSelected = false
156- onTap ( )
157- isSearchFieldFocused = true
144+ HStack {
145+ ZStack ( alignment: . leading) {
146+ if searchText. isEmpty {
147+ Text ( placeholder)
148+ . foregroundColor ( placeholderColor. opacity ( 0.7 ) )
149+ . font ( font)
150+ . padding ( . horizontal, 10 )
151+ . padding ( . vertical, 5 )
158152 }
159- } )
160- . font ( font)
161- . tint ( accentColor)
162- . foregroundColor ( textColor)
163- . padding ( . horizontal, 10 )
164- . padding ( . vertical, 5 )
165- . textInputAutocapitalization ( . never)
166- . disableAutocorrection ( true )
167- . focused ( $isSearchFieldFocused)
168- }
169-
170- Button ( action: {
171- isDropdownVisible. toggle ( )
172- isSearchFieldFocused = isDropdownVisible
173- } ) {
174- dropdownIcon
175- . resizable ( )
176- . aspectRatio ( contentMode: . fit)
177- . frame ( width: 18 , height: 18 )
178- . foregroundColor ( accentColor)
179- . rotationEffect ( . degrees( isDropdownVisible ? 180 : 0 ) )
153+ TextField ( " " , text: $searchText, onEditingChanged: { isEditing in isDropdownVisible = isEditing
154+ if isEditing {
155+ isOptionSelected = false
156+ onTap ( )
157+ isSearchFieldFocused = true
158+ }
159+ } )
160+ . font ( font)
161+ . tint ( accentColor)
162+ . foregroundColor ( textColor)
163+ . padding ( . horizontal, 10 )
164+ . padding ( . vertical, 5 )
165+ . textInputAutocapitalization ( . never)
166+ . disableAutocorrection ( true )
167+ . focused ( $isSearchFieldFocused)
168+ . onTapGesture {
169+ if !searchText. isEmpty {
170+ searchText = " "
171+ isOptionSelected = false
172+ isDropdownVisible = true
173+ onTap ( )
174+ isSearchFieldFocused = true
175+ }
176+ }
177+ . onSubmit {
178+ if let topOption = filteredOptions. first {
179+ selectOption ( topOption)
180+ } else if addNew && !searchText. isEmpty {
181+ selectOption ( searchText)
182+ }
183+ }
184+ }
185+ Button ( action: {
186+ isDropdownVisible. toggle ( )
187+ isSearchFieldFocused = isDropdownVisible
188+ } ) {
189+ dropdownIcon
190+ . resizable ( )
191+ . aspectRatio ( contentMode: . fit)
192+ . frame ( width: 18 , height: 18 )
193+ . foregroundColor ( borderColor)
194+ . rotationEffect ( . degrees( isDropdownVisible ? 180 : 0 ) )
195+ }
196+ . frame ( width: 35 )
197+ . padding ( . trailing, 8 )
180198 }
181- . frame ( width: 35 )
182- . padding ( . trailing, 8 )
183199 }
184200 . frame ( maxWidth: . infinity, alignment: . leading)
185201 . frame ( height: height)
186- . background (
187- RoundedRectangle ( cornerRadius: cornerRadius)
188- . strokeBorder ( computedBorderColor, lineWidth: 1 )
202+ . background ( RoundedRectangle ( cornerRadius: cornerRadius)
203+ . strokeBorder ( computedBorderColor, lineWidth: 1 )
189204 )
190205
191206 if isDropdownVisible {
@@ -204,9 +219,22 @@ public struct SearchableMenu: View {
204219 }
205220 . padding ( . vertical, 4 )
206221 . background ( index == 0 && !searchText. isEmpty ? accentColor : Color . clear)
222+ . cornerRadius ( 8 )
207223 }
208224 }
209- if addNew && searchText != " " && !options. contains ( where: { $0. lowercased ( ) == searchText. lowercased ( ) } ) {
225+ let trimmedSearch = searchText. trimmingCharacters ( in: . whitespacesAndNewlines)
226+ let allowedCharacters = CharacterSet . alphanumerics
227+ let normalizedSearch = trimmedSearch. lowercased ( )
228+ . components ( separatedBy: allowedCharacters. inverted)
229+ . joined ( )
230+ let normalizedOptions = options. map {
231+ $0. lowercased ( )
232+ . components ( separatedBy: allowedCharacters. inverted)
233+ . joined ( )
234+ }
235+ let hasExactMatch = normalizedOptions. contains { $0 == normalizedSearch }
236+ let hasPrefixMatch = normalizedOptions. contains { normalizedSearch != " " && $0. hasPrefix ( normalizedSearch) }
237+ if addNew && !trimmedSearch. isEmpty && !hasExactMatch && !hasPrefixMatch {
210238 Button ( action: {
211239 isOptionSelected = true
212240 isDropdownVisible = false
@@ -220,6 +248,7 @@ public struct SearchableMenu: View {
220248 }
221249 . padding ( . vertical, 4 )
222250 . background ( accentColor)
251+ . cornerRadius ( 8 )
223252 }
224253 } else if !addNew && searchText != " " && filteredOptions. isEmpty {
225254 Text ( noMatchText)
0 commit comments