@butternutbox/pawprint-native 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,5 @@
1
1
  import React, { useState } from "react"
2
2
  import { View, StyleSheet } from "react-native"
3
- import { PortalHost } from "@rn-primitives/portal"
4
3
  import { SelectField, useSelectField } from "."
5
4
  import { Typography } from "../../atoms/Typography"
6
5
  import { Icon } from "../../atoms/Icon"
@@ -8,10 +7,7 @@ import { Search, Settings, Lock, Info } from "@butternutbox/pawprint-icons/core"
8
7
  import type { Option } from "@rn-primitives/select"
9
8
 
10
9
  const StoryWrapper = ({ children }: { children: React.ReactNode }) => (
11
- <>
12
- {children}
13
- <PortalHost />
14
- </>
10
+ <>{children}</>
15
11
  )
16
12
 
17
13
  export default {
@@ -304,6 +300,115 @@ export const Controlled = () => {
304
300
  )
305
301
  }
306
302
 
303
+ export const Searchable = () => (
304
+ <StoryWrapper>
305
+ <View style={styles.column}>
306
+ <View style={styles.section}>
307
+ <Typography size="sm" weight="semiBold" color="tertiary">
308
+ Searchable Select
309
+ </Typography>
310
+ <SelectField
311
+ label="Country"
312
+ placeholder="Select a country"
313
+ description="Type to filter countries"
314
+ searchable
315
+ searchPlaceholder="Search countries..."
316
+ >
317
+ <SelectField.Item value="au">Australia</SelectField.Item>
318
+ <SelectField.Item value="br">Brazil</SelectField.Item>
319
+ <SelectField.Item value="ca">Canada</SelectField.Item>
320
+ <SelectField.Item value="dk">Denmark</SelectField.Item>
321
+ <SelectField.Item value="eg">Egypt</SelectField.Item>
322
+ <SelectField.Item value="fr">France</SelectField.Item>
323
+ <SelectField.Item value="de">Germany</SelectField.Item>
324
+ <SelectField.Item value="in">India</SelectField.Item>
325
+ <SelectField.Item value="jp">Japan</SelectField.Item>
326
+ <SelectField.Item value="mx">Mexico</SelectField.Item>
327
+ <SelectField.Item value="nl">Netherlands</SelectField.Item>
328
+ <SelectField.Item value="nz">New Zealand</SelectField.Item>
329
+ <SelectField.Item value="no">Norway</SelectField.Item>
330
+ <SelectField.Item value="pl">Poland</SelectField.Item>
331
+ <SelectField.Item value="pt">Portugal</SelectField.Item>
332
+ <SelectField.Item value="es">Spain</SelectField.Item>
333
+ <SelectField.Item value="se">Sweden</SelectField.Item>
334
+ <SelectField.Item value="ch">Switzerland</SelectField.Item>
335
+ <SelectField.Item value="uk">United Kingdom</SelectField.Item>
336
+ <SelectField.Item value="us">United States</SelectField.Item>
337
+ </SelectField>
338
+ </View>
339
+ </View>
340
+ </StoryWrapper>
341
+ )
342
+
343
+ export const SearchableControlled = () => {
344
+ const [value, setValue] = useState<Option | string | null>(null)
345
+
346
+ const displayValue = (() => {
347
+ if (!value) return "(none)"
348
+ if (typeof value === "object" && "value" in value) return value.value
349
+ return String(value)
350
+ })()
351
+
352
+ return (
353
+ <StoryWrapper>
354
+ <View style={styles.column}>
355
+ <View style={styles.section}>
356
+ <Typography size="sm" weight="semiBold" color="tertiary">
357
+ Controlled value: {displayValue}
358
+ </Typography>
359
+ <SelectField
360
+ label="Country"
361
+ placeholder="Select a country"
362
+ description="Select a country — the clear button resets the controlled value"
363
+ searchable
364
+ searchPlaceholder="Search countries..."
365
+ value={value}
366
+ onValueChange={(newValue) => setValue(newValue)}
367
+ >
368
+ <SelectField.Item value="au">Australia</SelectField.Item>
369
+ <SelectField.Item value="ca">Canada</SelectField.Item>
370
+ <SelectField.Item value="fr">France</SelectField.Item>
371
+ <SelectField.Item value="de">Germany</SelectField.Item>
372
+ <SelectField.Item value="jp">Japan</SelectField.Item>
373
+ <SelectField.Item value="uk">United Kingdom</SelectField.Item>
374
+ <SelectField.Item value="us">United States</SelectField.Item>
375
+ </SelectField>
376
+ </View>
377
+ </View>
378
+ </StoryWrapper>
379
+ )
380
+ }
381
+
382
+ export const SearchableDefaultPlaceholder = () => (
383
+ <StoryWrapper>
384
+ <View style={styles.column}>
385
+ <View style={styles.section}>
386
+ <Typography size="sm" weight="semiBold" color="tertiary">
387
+ Default Search Placeholder
388
+ </Typography>
389
+ <SelectField label="Pet breed" placeholder="Select a breed" searchable>
390
+ <SelectField.Item value="labrador">
391
+ Labrador Retriever
392
+ </SelectField.Item>
393
+ <SelectField.Item value="german-shepherd">
394
+ German Shepherd
395
+ </SelectField.Item>
396
+ <SelectField.Item value="golden-retriever">
397
+ Golden Retriever
398
+ </SelectField.Item>
399
+ <SelectField.Item value="bulldog">Bulldog</SelectField.Item>
400
+ <SelectField.Item value="poodle">Poodle</SelectField.Item>
401
+ <SelectField.Item value="beagle">Beagle</SelectField.Item>
402
+ <SelectField.Item value="rottweiler">Rottweiler</SelectField.Item>
403
+ <SelectField.Item value="yorkshire-terrier">
404
+ Yorkshire Terrier
405
+ </SelectField.Item>
406
+ </SelectField>
407
+ </View>
408
+ </View>
409
+ </StoryWrapper>
410
+ )
411
+
307
412
  const styles = StyleSheet.create({
308
413
  container: {
309
414
  width: 320
@@ -1,6 +1,6 @@
1
1
  import React from "react"
2
2
  import { View } from "react-native"
3
- import { screen } from "@testing-library/react"
3
+ import { screen, fireEvent, act } from "@testing-library/react"
4
4
  import { describe, it, expect, vi } from "vitest"
5
5
  import { renderWithTheme } from "../../../test-utils"
6
6
  import { SelectField } from "./SelectField"
@@ -194,7 +194,7 @@ describe("SelectField", () => {
194
194
  })
195
195
 
196
196
  describe("uncontrolled mode with defaultValue", () => {
197
- it("uses defaultValue as initial value", () => {
197
+ it("renders with defaultValue without crashing and shows items when opened", () => {
198
198
  renderWithTheme(
199
199
  <SelectField
200
200
  label="Country"
@@ -206,11 +206,180 @@ describe("SelectField", () => {
206
206
  </SelectField>
207
207
  )
208
208
 
209
+ fireEvent.click(screen.getAllByRole("button")[0])
209
210
  expect(screen.getByText("United Kingdom")).toBeInTheDocument()
210
211
  })
211
212
  })
212
213
 
214
+ describe("when searchable", () => {
215
+ const openDropdown = () => {
216
+ fireEvent.click(screen.getAllByRole("button")[0])
217
+ }
218
+
219
+ const openDropdownWithContent = () => {
220
+ vi.useFakeTimers()
221
+ fireEvent.click(screen.getAllByRole("button")[0])
222
+ act(() => vi.advanceTimersByTime(500))
223
+ vi.useRealTimers()
224
+ }
225
+
226
+ it("shows search input in the trigger when dropdown is opened", () => {
227
+ renderWithTheme(
228
+ <SelectField label="Country" placeholder="Select a country" searchable>
229
+ <SelectField.Item value="uk">United Kingdom</SelectField.Item>
230
+ <SelectField.Item value="us">United States</SelectField.Item>
231
+ </SelectField>
232
+ )
233
+
234
+ expect(screen.queryByPlaceholderText("Search...")).not.toBeInTheDocument()
235
+ openDropdown()
236
+ expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument()
237
+ })
238
+
239
+ it("does not show search input when not searchable", () => {
240
+ renderWithTheme(
241
+ <SelectField label="Country" placeholder="Select a country">
242
+ <SelectField.Item value="uk">United Kingdom</SelectField.Item>
243
+ <SelectField.Item value="us">United States</SelectField.Item>
244
+ </SelectField>
245
+ )
246
+
247
+ openDropdown()
248
+ expect(screen.queryByPlaceholderText("Search...")).not.toBeInTheDocument()
249
+ })
250
+
251
+ it("renders with a custom searchPlaceholder", () => {
252
+ renderWithTheme(
253
+ <SelectField
254
+ label="Country"
255
+ placeholder="Select a country"
256
+ searchable
257
+ searchPlaceholder="Find a country..."
258
+ >
259
+ <SelectField.Item value="uk">United Kingdom</SelectField.Item>
260
+ </SelectField>
261
+ )
262
+
263
+ openDropdown()
264
+ expect(
265
+ screen.getByPlaceholderText("Find a country...")
266
+ ).toBeInTheDocument()
267
+ })
268
+
269
+ it("filters items based on the search query", () => {
270
+ renderWithTheme(
271
+ <SelectField label="Country" placeholder="Select a country" searchable>
272
+ <SelectField.Item value="uk">United Kingdom</SelectField.Item>
273
+ <SelectField.Item value="us">United States</SelectField.Item>
274
+ <SelectField.Item value="ca">Canada</SelectField.Item>
275
+ </SelectField>
276
+ )
277
+
278
+ openDropdownWithContent()
279
+ fireEvent.change(screen.getByPlaceholderText("Search..."), {
280
+ target: { value: "united" }
281
+ })
282
+
283
+ expect(screen.getByText("United Kingdom")).toBeInTheDocument()
284
+ expect(screen.getByText("United States")).toBeInTheDocument()
285
+ expect(screen.queryByText("Canada")).not.toBeInTheDocument()
286
+ })
287
+
288
+ it("is case-insensitive when filtering", () => {
289
+ renderWithTheme(
290
+ <SelectField label="Country" placeholder="Select a country" searchable>
291
+ <SelectField.Item value="uk">United Kingdom</SelectField.Item>
292
+ <SelectField.Item value="ca">Canada</SelectField.Item>
293
+ </SelectField>
294
+ )
295
+
296
+ openDropdownWithContent()
297
+ fireEvent.change(screen.getByPlaceholderText("Search..."), {
298
+ target: { value: "CANADA" }
299
+ })
300
+
301
+ expect(screen.getByText("Canada")).toBeInTheDocument()
302
+ expect(screen.queryByText("United Kingdom")).not.toBeInTheDocument()
303
+ })
304
+
305
+ it("shows default 'No results found' when search matches no items", () => {
306
+ renderWithTheme(
307
+ <SelectField label="Country" placeholder="Select a country" searchable>
308
+ <SelectField.Item value="uk">United Kingdom</SelectField.Item>
309
+ <SelectField.Item value="us">United States</SelectField.Item>
310
+ </SelectField>
311
+ )
312
+
313
+ openDropdownWithContent()
314
+ fireEvent.change(screen.getByPlaceholderText("Search..."), {
315
+ target: { value: "xyz" }
316
+ })
317
+
318
+ expect(screen.getByText("No results found")).toBeInTheDocument()
319
+ })
320
+
321
+ it("shows custom noResultsText when provided", () => {
322
+ renderWithTheme(
323
+ <SelectField
324
+ label="Country"
325
+ placeholder="Select a country"
326
+ searchable
327
+ noResultsText="No countries match your search"
328
+ >
329
+ <SelectField.Item value="uk">United Kingdom</SelectField.Item>
330
+ </SelectField>
331
+ )
332
+
333
+ openDropdownWithContent()
334
+ fireEvent.change(screen.getByPlaceholderText("Search..."), {
335
+ target: { value: "xyz" }
336
+ })
337
+
338
+ expect(
339
+ screen.getByText("No countries match your search")
340
+ ).toBeInTheDocument()
341
+ })
342
+
343
+ it("shows all items when search query is cleared", () => {
344
+ renderWithTheme(
345
+ <SelectField label="Country" placeholder="Select a country" searchable>
346
+ <SelectField.Item value="uk">United Kingdom</SelectField.Item>
347
+ <SelectField.Item value="ca">Canada</SelectField.Item>
348
+ </SelectField>
349
+ )
350
+
351
+ openDropdownWithContent()
352
+ const input = screen.getByPlaceholderText("Search...")
353
+ fireEvent.change(input, { target: { value: "canada" } })
354
+ expect(screen.queryByText("United Kingdom")).not.toBeInTheDocument()
355
+
356
+ fireEvent.change(input, { target: { value: "" } })
357
+ expect(screen.getByText("United Kingdom")).toBeInTheDocument()
358
+ expect(screen.getByText("Canada")).toBeInTheDocument()
359
+ })
360
+
361
+ it("closes the dropdown and clears the search when closed", () => {
362
+ renderWithTheme(
363
+ <SelectField label="Country" placeholder="Select a country" searchable>
364
+ <SelectField.Item value="uk">United Kingdom</SelectField.Item>
365
+ <SelectField.Item value="ca">Canada</SelectField.Item>
366
+ </SelectField>
367
+ )
368
+
369
+ openDropdownWithContent()
370
+ fireEvent.change(screen.getByPlaceholderText("Search..."), {
371
+ target: { value: "canada" }
372
+ })
373
+ expect(screen.queryByText("United Kingdom")).not.toBeInTheDocument()
374
+
375
+ fireEvent.click(screen.getByTestId("select-overlay"))
376
+ expect(screen.queryByPlaceholderText("Search...")).not.toBeInTheDocument()
377
+ })
378
+ })
379
+
213
380
  describe("with items", () => {
381
+ const openDropdown = () => fireEvent.click(screen.getAllByRole("button")[0])
382
+
214
383
  it("renders items with leadingIcon", () => {
215
384
  renderWithTheme(
216
385
  <SelectField label="Country" placeholder="Select a country">
@@ -223,6 +392,7 @@ describe("SelectField", () => {
223
392
  </SelectField>
224
393
  )
225
394
 
395
+ openDropdown()
226
396
  expect(screen.getByTestId("icon")).toBeInTheDocument()
227
397
  })
228
398
 
@@ -235,6 +405,7 @@ describe("SelectField", () => {
235
405
  </SelectField>
236
406
  )
237
407
 
408
+ openDropdown()
238
409
  expect(screen.getByText("Europe")).toBeInTheDocument()
239
410
  })
240
411
 
@@ -247,6 +418,7 @@ describe("SelectField", () => {
247
418
  </SelectField>
248
419
  )
249
420
 
421
+ openDropdown()
250
422
  const item = screen.getByText("United Kingdom")
251
423
  expect(item).toBeInTheDocument()
252
424
  })
@@ -1,12 +1,16 @@
1
1
  import React from "react"
2
- import { View, ViewProps } from "react-native"
2
+ import { View, ViewProps, TextInput, Keyboard, Pressable } from "react-native"
3
3
  import styled from "@emotion/native"
4
4
  import * as SelectPrimitive from "@rn-primitives/select"
5
5
  import type { Option } from "@rn-primitives/select"
6
6
  import type { InputState } from "../../atoms/Input/InputField"
7
+ import { InputText } from "../../atoms/Input/InputField"
7
8
  import { InputError } from "../../atoms/Input/InputError"
8
9
  import { InputLabel } from "../../atoms/Input/InputLabel"
9
10
  import { InputDescription } from "../../atoms/Input/InputDescription"
11
+ import { Typography } from "../../atoms/Typography"
12
+ import { Icon } from "../../atoms/Icon"
13
+ import { Search, Cancel } from "@butternutbox/pawprint-icons/core"
10
14
  import { SelectFieldTrigger } from "./SelectFieldTrigger"
11
15
  import { SelectFieldValue } from "./SelectFieldValue"
12
16
  import { SelectFieldContent } from "./SelectFieldContent"
@@ -14,6 +18,18 @@ import { SelectFieldItem } from "./SelectFieldItem"
14
18
 
15
19
  const parseTokenValue = (value: string): number => parseFloat(value)
16
20
 
21
+ const StyledNoResults = styled(View)(({ theme }) => {
22
+ const { dropdown } = theme.tokens.components.dropdownList
23
+
24
+ return {
25
+ paddingVertical: parseTokenValue(dropdown.listItem.spacing.verticalPadding),
26
+ paddingHorizontal: parseTokenValue(
27
+ dropdown.listItem.spacing.horiztonalPadding
28
+ ),
29
+ alignItems: "center"
30
+ }
31
+ })
32
+
17
33
  const StyledRoot = styled(View)(({ theme }) => {
18
34
  const { spacing } = theme.tokens.components.inputs
19
35
 
@@ -35,6 +51,9 @@ type SelectFieldOwnProps<Value = Option | string> = {
35
51
  onValueChange?: (value: Value | null) => void
36
52
  children?: React.ReactNode
37
53
  disabled?: boolean
54
+ searchable?: boolean
55
+ searchPlaceholder?: string
56
+ noResultsText?: string
38
57
  }
39
58
 
40
59
  export type SelectFieldProps<Value = Option> = SelectFieldOwnProps<Value> &
@@ -79,6 +98,21 @@ export type SelectFieldProps<Value = Option> = SelectFieldOwnProps<Value> &
79
98
  * </SelectField>
80
99
  * ```
81
100
  *
101
+ * **Searchable:**
102
+ * @example
103
+ * ```tsx
104
+ * <SelectField
105
+ * label="Country"
106
+ * placeholder="Select a country"
107
+ * searchable
108
+ * searchPlaceholder="Search countries..."
109
+ * noResultsText="No countries found"
110
+ * >
111
+ * <SelectField.Item value="uk">United Kingdom</SelectField.Item>
112
+ * <SelectField.Item value="us">United States</SelectField.Item>
113
+ * </SelectField>
114
+ * ```
115
+ *
82
116
  * **Compound Component API:**
83
117
  * @example
84
118
  * ```tsx
@@ -107,6 +141,9 @@ export type SelectFieldProps<Value = Option> = SelectFieldOwnProps<Value> &
107
141
  * @param {Value | null} [defaultValue] - Default value for uncontrolled mode
108
142
  * @param {function} [onValueChange] - Value change handler
109
143
  * @param {boolean} [disabled] - Disables the select
144
+ * @param {boolean} [searchable=false] - Enables inline search filtering of items
145
+ * @param {string} [searchPlaceholder="Search..."] - Placeholder text for the search input
146
+ * @param {string} [noResultsText="No results found"] - Text shown when search returns no matches
110
147
  */
111
148
  const SelectFieldRoot = React.forwardRef<View, SelectFieldProps>(
112
149
  (
@@ -123,14 +160,57 @@ const SelectFieldRoot = React.forwardRef<View, SelectFieldProps>(
123
160
  defaultValue,
124
161
  onValueChange,
125
162
  disabled,
163
+ searchable = false,
164
+ searchPlaceholder,
165
+ noResultsText = "No results found",
126
166
  ...rest
127
167
  },
128
168
  ref
129
169
  ) => {
130
170
  const [isOpen, setIsOpen] = React.useState(false)
171
+ const [showContent, setShowContent] = React.useState(false)
131
172
  const [internalValue, setInternalValue] = React.useState(
132
173
  defaultValue ?? null
133
174
  )
175
+ const [searchQuery, setSearchQuery] = React.useState("")
176
+ const [clearKey, setClearKey] = React.useState(0)
177
+ const searchInputRef = React.useRef<TextInput>(null)
178
+
179
+ React.useEffect(() => {
180
+ if (!isOpen) {
181
+ setShowContent(false)
182
+ return
183
+ }
184
+ if (!searchable) {
185
+ setShowContent(true)
186
+ return
187
+ }
188
+
189
+ const openTimer = setTimeout(() => {
190
+ setShowContent(true)
191
+ searchInputRef.current?.focus()
192
+ }, 0)
193
+
194
+ // The Portal cascade (SelectPrimitive.Content onLayout → setContentLayout
195
+ // → Root context re-render via Zustand) blurs the input after content
196
+ // mounts. Re-focus once the keyboard is fully shown
197
+ const keyboardSub = Keyboard.addListener("keyboardDidShow", () => {
198
+ setTimeout(() => searchInputRef.current?.focus(), 0)
199
+ })
200
+
201
+ // Hardware keyboard: keyboardDidShow never fires, so re-focus after a
202
+ // short delay to recover from the Portal cascade blur.
203
+ const hardwareKeyboardTimer = setTimeout(
204
+ () => searchInputRef.current?.focus(),
205
+ 100
206
+ )
207
+
208
+ return () => {
209
+ clearTimeout(openTimer)
210
+ clearTimeout(hardwareKeyboardTimer)
211
+ keyboardSub.remove()
212
+ }
213
+ }, [isOpen, searchable])
134
214
 
135
215
  const isCompound = React.Children.toArray(children).some(
136
216
  (child) =>
@@ -151,15 +231,68 @@ const SelectFieldRoot = React.forwardRef<View, SelectFieldProps>(
151
231
  onValueChange?.(valueToSet)
152
232
  }
153
233
 
234
+ const handleOpenChange = (open: boolean) => {
235
+ setIsOpen(open)
236
+ if (!open) setSearchQuery("")
237
+ }
238
+
154
239
  const currentValue = value !== undefined ? value : internalValue
155
240
 
241
+ const allItems = React.Children.toArray(children)
242
+
243
+ const filteredItems = searchable
244
+ ? allItems.filter((child) => {
245
+ if (!searchQuery) return true
246
+ if (React.isValidElement(child) && child.type === SelectFieldItem) {
247
+ const label = String(
248
+ (child.props as { children?: unknown }).children ?? ""
249
+ )
250
+ return label.toLowerCase().includes(searchQuery.toLowerCase())
251
+ }
252
+ return true
253
+ })
254
+ : allItems
255
+
256
+ const hasNoResults =
257
+ searchable && searchQuery.length > 0 && filteredItems.length === 0
258
+
259
+ const searchActionIcon = (() => {
260
+ if (!searchable) return undefined
261
+
262
+ const showClear =
263
+ (isOpen && searchQuery.length > 0) || (!isOpen && Boolean(currentValue))
264
+ if (!showClear) return <Icon icon={Search} size="md" />
265
+
266
+ return (
267
+ <Pressable
268
+ onPress={
269
+ isOpen
270
+ ? () => {
271
+ setSearchQuery("")
272
+ searchInputRef.current?.focus()
273
+ }
274
+ : () => {
275
+ setInternalValue(null)
276
+ onValueChange?.(null)
277
+ setClearKey((k) => k + 1)
278
+ }
279
+ }
280
+ accessibilityLabel={isOpen ? "Clear search" : "Clear selection"}
281
+ accessibilityRole="button"
282
+ hitSlop={8}
283
+ >
284
+ <Icon icon={Cancel} size="md" />
285
+ </Pressable>
286
+ )
287
+ })()
288
+
156
289
  return (
157
290
  <SelectPrimitive.Root
158
- value={value ?? undefined}
159
- defaultValue={defaultValue ?? undefined}
291
+ key={clearKey}
292
+ value={currentValue ?? undefined}
160
293
  onValueChange={handleValueChange}
161
294
  disabled={disabled}
162
- onOpenChange={setIsOpen}
295
+ onOpenChange={handleOpenChange}
163
296
  >
164
297
  <StyledRoot ref={ref} {...rest}>
165
298
  {label && (
@@ -171,39 +304,62 @@ const SelectFieldRoot = React.forwardRef<View, SelectFieldProps>(
171
304
  state={state}
172
305
  leadingIcon={leadingIcon}
173
306
  isOpen={isOpen}
307
+ isSearching={searchable && isOpen}
308
+ actionIcon={searchActionIcon}
174
309
  >
175
- <SelectFieldValue placeholder={placeholder} />
310
+ {searchable && isOpen ? (
311
+ <InputText
312
+ ref={searchInputRef}
313
+ value={searchQuery}
314
+ onChangeText={setSearchQuery}
315
+ placeholder={searchPlaceholder ?? "Search..."}
316
+ returnKeyType="search"
317
+ clearButtonMode="never"
318
+ accessibilityLabel="Search options"
319
+ />
320
+ ) : (
321
+ <SelectFieldValue placeholder={placeholder} />
322
+ )}
176
323
  </SelectFieldTrigger>
177
324
  {description && (
178
325
  <InputDescription state={state}>{description}</InputDescription>
179
326
  )}
180
327
  {error && state === "error" && <InputError>{error}</InputError>}
181
328
  </StyledRoot>
182
- <SelectFieldContent>
183
- {React.Children.map(children, (child) => {
184
- if (React.isValidElement(child) && child.type === SelectFieldItem) {
185
- // Extract value from object if needed
186
- const selectedValue =
187
- currentValue &&
188
- typeof currentValue === "object" &&
189
- "value" in currentValue
190
- ? (currentValue as { value: string }).value
191
- : currentValue
192
-
193
- const childProps = child.props as { value: string }
194
- return React.cloneElement(
195
- child as React.ReactElement<{
196
- value: string
197
- isSelected?: boolean
198
- }>,
199
- {
200
- isSelected: childProps.value === selectedValue
201
- }
202
- )
203
- }
204
- return child
205
- })}
206
- </SelectFieldContent>
329
+ {showContent && (
330
+ <SelectFieldContent>
331
+ {filteredItems.map((child) => {
332
+ if (
333
+ React.isValidElement(child) &&
334
+ child.type === SelectFieldItem
335
+ ) {
336
+ const selectedValue =
337
+ currentValue &&
338
+ typeof currentValue === "object" &&
339
+ "value" in currentValue
340
+ ? (currentValue as { value: string }).value
341
+ : currentValue
342
+
343
+ const childProps = child.props as { value: string }
344
+ return React.cloneElement(
345
+ child as React.ReactElement<{
346
+ value: string
347
+ isSelected?: boolean
348
+ }>,
349
+ {
350
+ isSelected: childProps.value === selectedValue
351
+ }
352
+ )
353
+ }
354
+ return child
355
+ })}
356
+ {hasNoResults && (
357
+ <StyledNoResults>
358
+ <Typography size="sm">{noResultsText}</Typography>
359
+ </StyledNoResults>
360
+ )}
361
+ </SelectFieldContent>
362
+ )}
207
363
  </SelectPrimitive.Root>
208
364
  )
209
365
  }