@lotics/ui 1.27.0 → 2.0.0

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.
package/src/combobox.tsx CHANGED
@@ -1,164 +1,420 @@
1
- import { Pressable, StyleProp, ViewStyle, StyleSheet } from "react-native";
2
- import { useState, useCallback } from "react";
1
+ import {
2
+ View,
3
+ ScrollView,
4
+ StyleSheet,
5
+ Pressable,
6
+ type StyleProp,
7
+ type ViewStyle,
8
+ type TextInput as RNTextInput,
9
+ } from "react-native";
10
+ import { useCallback, useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
3
11
  import { colors } from "./colors";
4
12
  import { Text } from "./text";
5
13
  import { Icon } from "./icon";
6
- import { Popover, PopoverTrigger, PopoverContent } from "./popover";
7
- import { PickerMenu } from "./picker_menu";
8
- import { PickerOption } from "./picker";
14
+ import { TextInputField } from "./text_input_field";
15
+ import { MenuButton } from "./menu_button";
16
+ import { ActivityIndicator } from "./activity_indicator";
17
+ import { Popover, PopoverContent } from "./popover";
18
+ import type { PickerOption } from "./picker";
9
19
  import { useDebouncedCallback } from "./use_debounced_callback";
20
+ import { useListKeyboardNav } from "./use_list_keyboard_nav";
10
21
 
11
- export interface ComboboxProps<T extends string = string> {
12
- /** The selected option. It carries its own label, so the trigger still shows
13
- * the selection even when that option is no longer in the current (async)
14
- * result set. */
15
- value: PickerOption<T> | null;
16
- onValueChange: (option: PickerOption<T>) => void;
17
- /** Result options to show in the list. In server-search mode the consumer
18
- * refreshes these in response to `onSearchChange`. */
19
- options: PickerOption<T>[];
20
- /** Called (debounced) when the search text changes. Provide it to drive
21
- * server-side search — the consumer re-queries and updates `options`. Omit to
22
- * filter the given `options` locally (static list). */
22
+ export interface ComboboxProps<T extends string = string, D = unknown> {
23
+ /** Result options for the current query. With `onSearchChange` the consumer
24
+ * refreshes these server-side; without it, they're filtered locally by label. */
25
+ options: PickerOption<T, D>[];
26
+ /** Fires when the user picks an option (or a custom value, if `allowCustom`). */
27
+ onValueChange: (option: PickerOption<T, D>) => void;
28
+ /** Server-side search: debounced, fires as the user types. Omit for local
29
+ * label filtering of `options`. */
23
30
  onSearchChange?: (query: string) => void;
24
- /** Show a loading indicator while async results are in flight. */
31
+ /** Multi-select: selected options render as dismissible chips inside the input;
32
+ * the input stays a search field. Pair with `value` (the selected array) and
33
+ * `onRemove`. */
34
+ multi?: boolean;
35
+ /** Selected option(s). Single → the option to reflect in the input (or null).
36
+ * Multi → the array of selected options rendered as chips. */
37
+ value?: PickerOption<T, D> | PickerOption<T, D>[] | null;
38
+ /** Multi: remove a selected chip. */
39
+ onRemove?: (option: PickerOption<T, D>) => void;
40
+ /** Single only: reflect the picked label in the input (classic autocomplete).
41
+ * Default true. Set false to keep the input a pure search and render the
42
+ * selection yourself (e.g. in a list below). */
43
+ reflectSelection?: boolean;
44
+ /** Custom rendering of an option row in the dropdown. The input itself is a
45
+ * plain text field (it can only show the label string) — to display a
46
+ * selection with custom rendering, use `reflectSelection={false}` and render
47
+ * the picked option(s) yourself below. */
48
+ renderOptionContent?: (option: PickerOption<T, D>) => ReactNode;
49
+ /** Row subtitle pulled from the option (single-line, under the title). */
50
+ getOptionDescription?: (option: PickerOption<T, D>) => string | undefined;
51
+ /** Accept free text: when the query matches no option, offer it as a custom
52
+ * value — `onValueChange` receives `{ value: query, label: query }`. */
53
+ allowCustom?: boolean;
54
+ /** Label for the free-entry row (default: `Add "<query>"`). Return null to
55
+ * suppress it for a given query. */
56
+ customOptionLabel?: (query: string) => string | null;
57
+ /** Shown — under a heading — when the input is focused but empty. */
58
+ recentOptions?: PickerOption<T, D>[];
25
59
  loading?: boolean;
26
- /** Debounce applied to `onSearchChange`, in ms. Default 200. */
27
60
  searchDebounceMs?: number;
28
61
  placeholder?: string;
29
- searchPlaceholder?: string;
30
- /** Shown when there are no results and not loading. Default: "No results". */
62
+ recentsLabel?: string;
31
63
  emptyText?: string;
32
- /** Accept free entry: when the typed query matches no option, offer it as a
33
- * custom value. `onValueChange` then receives `{ value: query, label: query }`.
34
- * The consumer decides what an unknown value means (e.g. a manually-typed id). */
35
- allowCustom?: boolean;
36
- /** Label for the free-entry row (default: the raw query). Return `null` to
37
- * suppress it for a given query (e.g. still incomplete/invalid). */
38
- customOptionLabel?: (query: string) => string | null;
64
+ accessibilityLabel?: string;
39
65
  disabled?: boolean;
40
66
  autoFocus?: boolean;
41
67
  testID?: string;
42
68
  style?: StyleProp<ViewStyle>;
43
69
  }
44
70
 
71
+ const OPTION_HEIGHT = 52;
72
+ const CUSTOM_VALUE = "__combobox_custom__";
73
+
45
74
  /**
46
- * Async-search select a keyboard-accessible combobox. The trigger opens a
47
- * Popover with a search box and a result list (arrows to move, Enter to pick,
48
- * Esc to close; the trigger is tab-focusable and opens on Enter/Space).
75
+ * The ARIA combobox: an editable input whose typing drives a (debounced) search,
76
+ * with results in a Popover listbox below. The input owns the keyboard (↑/↓ move
77
+ * the active row, Enter selects, Esc closes) not a button trigger.
78
+ *
79
+ * Selection display is flexible:
80
+ * - single (default): the picked label reflects into the input (`reflectSelection`),
81
+ * or set `reflectSelection={false}` to keep the input a pure search and render
82
+ * the selection below yourself;
83
+ * - `multi`: selected options render as dismissible chips inside the input and
84
+ * the input stays a search field.
49
85
  *
50
- * Pass `onSearchChange` to drive search **server-side** (the consumer re-queries
51
- * and refreshes `options`) — the right pattern for large/growing datasets where
52
- * prefetching the whole table to the client is wrong. Omit it to filter the
53
- * given `options` locally, so the same component also serves static lists.
86
+ * For a "select from a known list" control (no search box, typeahead), use `Picker`.
54
87
  */
55
- export function Combobox<T extends string>(props: ComboboxProps<T>) {
88
+ export function Combobox<T extends string = string, D = unknown>(props: ComboboxProps<T, D>) {
56
89
  const {
57
- value,
58
- onValueChange,
59
90
  options,
91
+ onValueChange,
60
92
  onSearchChange,
61
- loading,
93
+ multi = false,
94
+ value,
95
+ onRemove,
96
+ reflectSelection = true,
97
+ renderOptionContent,
98
+ getOptionDescription,
99
+ allowCustom = false,
100
+ customOptionLabel,
101
+ recentOptions,
102
+ loading = false,
62
103
  searchDebounceMs = 200,
63
104
  placeholder,
64
- searchPlaceholder,
65
- emptyText,
66
- allowCustom,
67
- customOptionLabel,
105
+ recentsLabel = "Recent",
106
+ emptyText = "No results",
107
+ accessibilityLabel = "Results",
68
108
  disabled = false,
69
109
  autoFocus = false,
70
110
  testID,
71
111
  style,
72
112
  } = props;
73
113
 
114
+ const chips = multi && Array.isArray(value) ? value : [];
115
+ const single = !multi && value && !Array.isArray(value) ? value : null;
116
+
117
+ const [query, setQuery] = useState(
118
+ !multi && reflectSelection && single ? (single.label ?? single.value) : "",
119
+ );
74
120
  const [open, setOpen] = useState(autoFocus);
75
- const debouncedSearch = useDebouncedCallback(onSearchChange, searchDebounceMs);
121
+ const triggerRef = useRef<View>(null);
122
+ const inputRef = useRef<RNTextInput>(null);
123
+ const scrollRef = useRef<ScrollView>(null);
124
+ const baseId = useId();
125
+ const listboxId = `${baseId}-listbox`;
126
+ const optionId = (i: number) => `${baseId}-option-${i}`;
76
127
 
77
- const handleToggle = useCallback(() => {
78
- if (!disabled) setOpen((o) => !o);
79
- }, [disabled]);
128
+ const searching = query.trim().length > 0;
129
+ const isServer = onSearchChange !== undefined;
80
130
 
81
- const handleSelect = useCallback(
82
- (v: T) => {
83
- const opt = options.find((o) => o.value === v);
84
- if (opt) onValueChange(opt);
85
- else if (allowCustom && v) onValueChange({ value: v, label: v });
131
+ // Local filtering when not server-driven. Reflected single value shouldn't
132
+ // filter the list to itself, so only filter once the query diverges.
133
+ const filtered = useMemo(() => {
134
+ if (isServer || !searching) return options;
135
+ if (!multi && reflectSelection && single && query === (single.label ?? single.value)) {
136
+ return options;
137
+ }
138
+ const q = query.toLowerCase();
139
+ return options.filter((o) => (o.label ?? o.value).toLowerCase().includes(q));
140
+ }, [isServer, searching, options, multi, reflectSelection, single, query]);
141
+
142
+ // Free-entry row, appended when the query matches no option label exactly.
143
+ const customRow = useMemo((): PickerOption<T, D> | null => {
144
+ if (!allowCustom || !searching) return null;
145
+ const q = query.trim();
146
+ const exact = options.some((o) => (o.label ?? o.value).toLowerCase() === q.toLowerCase());
147
+ if (exact) return null;
148
+ const label = customOptionLabel ? customOptionLabel(q) : `Add "${q}"`;
149
+ if (label === null) return null;
150
+ return { value: q as T, label, testID: CUSTOM_VALUE };
151
+ }, [allowCustom, searching, query, options, customOptionLabel]);
152
+
153
+ const list = useMemo(() => {
154
+ const base = searching ? filtered : (recentOptions ?? []);
155
+ return customRow ? [...base, customRow] : base;
156
+ }, [searching, filtered, recentOptions, customRow]);
157
+
158
+ const debouncedSearch = useDebouncedCallback(onSearchChange ?? (() => {}), searchDebounceMs);
159
+
160
+ const scrollToIndex = useCallback((index: number) => {
161
+ scrollRef.current?.scrollTo({ y: Math.max(0, index * OPTION_HEIGHT - 80), animated: false });
162
+ }, []);
163
+
164
+ const commit = useCallback(
165
+ (opt: PickerOption<T, D>) => {
166
+ onValueChange(opt);
167
+ if (isServer) {
168
+ debouncedSearch.cancel();
169
+ onSearchChange?.("");
170
+ }
171
+ if (multi) {
172
+ // Input stays a search field; the chip is rendered from `value`.
173
+ setQuery("");
174
+ } else if (reflectSelection) {
175
+ setQuery(opt.label ?? opt.value);
176
+ } else {
177
+ setQuery("");
178
+ }
86
179
  setOpen(false);
180
+ inputRef.current?.focus();
87
181
  },
88
- [options, onValueChange, allowCustom],
182
+ [onValueChange, isServer, debouncedSearch, onSearchChange, multi, reflectSelection],
89
183
  );
90
184
 
185
+ const handleSelect = useCallback(
186
+ (index: number) => {
187
+ const opt = list[index];
188
+ if (!opt || opt.disabled) return;
189
+ // The custom row displays `Add "x"` but emits the raw query as both value
190
+ // and label (so it reflects/round-trips as the typed text, not "Add …").
191
+ const emitted =
192
+ opt.testID === CUSTOM_VALUE
193
+ ? ({ value: opt.value, label: opt.value } as PickerOption<T, D>)
194
+ : opt;
195
+ commit(emitted);
196
+ },
197
+ [list, commit],
198
+ );
199
+
200
+ const { activeIndex, setActiveIndex, handleKey } = useListKeyboardNav({
201
+ count: list.length,
202
+ isDisabled: (i) => list[i]?.disabled ?? false,
203
+ onSelect: handleSelect,
204
+ onClose: () => setOpen(false),
205
+ onActiveChange: scrollToIndex,
206
+ });
207
+
208
+ const firstValue = list[0]?.value;
209
+ useEffect(() => {
210
+ setActiveIndex(0);
211
+ }, [searching, list.length, firstValue, setActiveIndex]);
212
+
213
+ const handleChangeText = useCallback(
214
+ (text: string) => {
215
+ setQuery(text);
216
+ if (!disabled) setOpen(true);
217
+ if (isServer) debouncedSearch(text);
218
+ },
219
+ [disabled, isServer, debouncedSearch],
220
+ );
221
+
222
+ const handleKeyPress = useCallback(
223
+ (e: { nativeEvent: { key: string }; preventDefault: () => void }) => {
224
+ const key = e.nativeEvent.key;
225
+ // Backspace on an empty query removes the last chip (token-input idiom).
226
+ if (multi && key === "Backspace" && query.length === 0 && chips.length > 0) {
227
+ onRemove?.(chips[chips.length - 1]);
228
+ return;
229
+ }
230
+ if (!open && (key === "ArrowDown" || key === "ArrowUp")) setOpen(true);
231
+ if (handleKey(key)) e.preventDefault();
232
+ },
233
+ [multi, query, chips, open, handleKey, onRemove],
234
+ );
235
+
236
+ const showList = open && !disabled && (searching || list.length > 0);
237
+ const activeOptionId =
238
+ activeIndex >= 0 && activeIndex < list.length ? optionId(activeIndex) : undefined;
239
+
91
240
  return (
92
- <Popover
93
- open={open && !disabled}
94
- onOpenChange={setOpen}
95
- side="bottom"
96
- align="start"
97
- inheritTriggerWidth={true}
98
- >
99
- <PopoverTrigger>
100
- <Pressable
241
+ <View style={style}>
242
+ <View ref={triggerRef} style={multi ? styles.multiBox : undefined}>
243
+ {multi
244
+ ? chips.map((chip) => (
245
+ <View key={chip.value} style={styles.chip}>
246
+ <Text size="sm" numberOfLines={1}>
247
+ {chip.label ?? chip.value}
248
+ </Text>
249
+ <Pressable
250
+ accessibilityRole="button"
251
+ accessibilityLabel={`Remove ${chip.label ?? chip.value}`}
252
+ onPress={() => onRemove?.(chip)}
253
+ style={styles.chipRemove}
254
+ >
255
+ <Icon name="x" size={12} color={colors.zinc["500"]} />
256
+ </Pressable>
257
+ </View>
258
+ ))
259
+ : null}
260
+ <TextInputField
261
+ ref={inputRef}
101
262
  testID={testID}
102
- // A bare Pressable renders as an unfocusable <div> on web; `button`
103
- // puts it in the tab order and maps Enter/Space to onPress, while
104
- // `expanded` announces open/closed.
105
- accessibilityRole="button"
106
- accessibilityState={{ expanded: open, disabled }}
107
- style={[styles.trigger, open && styles.opened, disabled && styles.disabled, style]}
108
- onPress={!disabled ? handleToggle : undefined}
109
- disabled={disabled}
110
- >
111
- {value ? (
112
- <Text userSelect="none" numberOfLines={1}>
113
- {value.label ?? value.value}
114
- </Text>
115
- ) : (
116
- <Text color="zinc-500" userSelect="none" numberOfLines={1}>
117
- {placeholder}
118
- </Text>
119
- )}
120
- <Icon name="chevron-down" size={18} color={colors.zinc["500"]} />
121
- </Pressable>
122
- </PopoverTrigger>
123
- <PopoverContent>
124
- <PickerMenu
125
- options={options}
126
- value={value?.value ?? null}
127
- onValueChange={handleSelect}
128
- onRequestClose={() => setOpen(false)}
129
- enableSearch
130
- allowCustom={allowCustom}
131
- customOptionLabel={customOptionLabel}
132
- onSearchChange={debouncedSearch}
133
- // Local filtering is wrong when the consumer drives search server-side.
134
- serverFiltered={onSearchChange !== undefined}
135
- loading={loading}
136
- emptyText={emptyText}
137
- searchPlaceholder={searchPlaceholder}
263
+ icon={multi ? undefined : "search"}
264
+ value={query}
265
+ onChangeText={handleChangeText}
266
+ onFocus={() => {
267
+ if (!disabled) setOpen(true);
268
+ }}
269
+ onKeyPress={handleKeyPress}
270
+ placeholder={chips.length > 0 ? undefined : placeholder}
271
+ placeholderTextColor={colors.zinc["400"]}
272
+ editable={!disabled}
273
+ autoFocus={autoFocus}
274
+ autoCapitalize="none"
275
+ autoCorrect={false}
276
+ style={multi ? styles.multiInput : undefined}
277
+ role="combobox"
278
+ aria-expanded={showList}
279
+ aria-controls={listboxId}
280
+ aria-activedescendant={activeOptionId}
281
+ aria-autocomplete="list"
138
282
  />
139
- </PopoverContent>
140
- </Popover>
283
+ </View>
284
+ <Popover
285
+ open={showList}
286
+ onOpenChange={setOpen}
287
+ triggerRef={triggerRef}
288
+ side="bottom"
289
+ align="start"
290
+ offset={4}
291
+ inheritTriggerWidth
292
+ >
293
+ <PopoverContent manageFocus={false} disableBodyScroll testID={testID ? `${testID}-popover` : undefined}>
294
+ <View style={styles.menu}>
295
+ {!searching && (recentOptions?.length ?? 0) > 0 ? (
296
+ <View style={styles.sectionHeader}>
297
+ <Text size="xs" weight="medium" color="zinc-500">
298
+ {recentsLabel}
299
+ </Text>
300
+ </View>
301
+ ) : null}
302
+ <ScrollView
303
+ ref={scrollRef}
304
+ style={styles.scroll}
305
+ nativeID={listboxId}
306
+ accessibilityLabel={searching ? accessibilityLabel : recentsLabel}
307
+ keyboardShouldPersistTaps="handled"
308
+ role={"listbox" as "list"}
309
+ >
310
+ {loading ? (
311
+ <View style={styles.statusRow}>
312
+ <ActivityIndicator />
313
+ </View>
314
+ ) : list.length === 0 ? (
315
+ searching ? (
316
+ <View style={styles.statusRow}>
317
+ <Text size="sm" color="zinc-500">
318
+ {emptyText}
319
+ </Text>
320
+ </View>
321
+ ) : null
322
+ ) : (
323
+ list.map((opt, i) => {
324
+ const isCustom = opt.testID === CUSTOM_VALUE;
325
+ const desc = !isCustom ? getOptionDescription?.(opt) : undefined;
326
+ const label = opt.label ?? opt.value;
327
+ const title =
328
+ !isCustom && renderOptionContent ? (
329
+ renderOptionContent(opt)
330
+ ) : desc ? (
331
+ <View>
332
+ <Text userSelect="none" numberOfLines={1}>
333
+ {label}
334
+ </Text>
335
+ <Text size="xs" color="zinc-500" numberOfLines={1}>
336
+ {desc}
337
+ </Text>
338
+ </View>
339
+ ) : (
340
+ <Text userSelect="none" numberOfLines={1} color={isCustom ? "zinc-500" : undefined}>
341
+ {label}
342
+ </Text>
343
+ );
344
+ return (
345
+ <MenuButton
346
+ key={opt.value}
347
+ nativeID={optionId(i)}
348
+ testID={isCustom ? "combobox-custom-option" : `combobox-option-${opt.value}`}
349
+ role="option"
350
+ accessibilityLabel={label}
351
+ icon={isCustom ? <Icon name="plus" size={16} color={colors.zinc["400"]} /> : undefined}
352
+ title={title}
353
+ focused={i === activeIndex}
354
+ selected={!multi && !isCustom && single?.value === opt.value}
355
+ disabled={opt.disabled}
356
+ onPress={() => handleSelect(i)}
357
+ onHoverIn={() => setActiveIndex(i)}
358
+ />
359
+ );
360
+ })
361
+ )}
362
+ </ScrollView>
363
+ </View>
364
+ </PopoverContent>
365
+ </Popover>
366
+ </View>
141
367
  );
142
368
  }
143
369
 
144
370
  const styles = StyleSheet.create({
145
- trigger: {
371
+ multiBox: {
146
372
  flexDirection: "row",
373
+ flexWrap: "wrap",
147
374
  alignItems: "center",
148
- justifyContent: "space-between",
149
- gap: 6,
375
+ gap: 4,
150
376
  paddingHorizontal: 6,
377
+ paddingVertical: 4,
378
+ minHeight: 40,
151
379
  borderWidth: 1,
152
380
  borderColor: colors.border,
153
381
  borderRadius: 8,
154
- height: 40,
382
+ },
383
+ multiInput: {
384
+ flexGrow: 1,
385
+ flexBasis: 80,
386
+ borderWidth: 0,
387
+ height: 28,
388
+ },
389
+ chip: {
390
+ flexDirection: "row",
391
+ alignItems: "center",
392
+ gap: 4,
393
+ paddingLeft: 8,
394
+ paddingRight: 4,
395
+ paddingVertical: 2,
396
+ borderRadius: 6,
397
+ backgroundColor: colors.zinc["100"],
398
+ },
399
+ chipRemove: {
400
+ padding: 2,
401
+ borderRadius: 4,
155
402
  cursor: "pointer",
156
403
  },
157
- opened: {
158
- borderColor: colors.zinc["900"],
404
+ menu: {
405
+ gap: 2,
406
+ },
407
+ sectionHeader: {
408
+ paddingHorizontal: 8,
409
+ paddingTop: 4,
410
+ paddingBottom: 2,
159
411
  },
160
- disabled: {
161
- opacity: 0.5,
162
- cursor: "auto",
412
+ scroll: {
413
+ maxHeight: 320,
414
+ },
415
+ statusRow: {
416
+ alignItems: "center",
417
+ justifyContent: "center",
418
+ paddingVertical: 16,
163
419
  },
164
420
  });
@@ -14,8 +14,8 @@ interface LegendItemProps {
14
14
 
15
15
  /**
16
16
  * Color swatch + label + optional count. Used as the legend row below a
17
- * `StackedProgressBar`, a `ChartBar` with categorical colors, or any
18
- * other chart where consumers need to map color → meaning.
17
+ * `StackedProgressBar`, a `BarChart`/`PieChart` with per-series colors, or
18
+ * any other chart where consumers need to map color → meaning.
19
19
  *
20
20
  * Tabular nums on the value (via Metric) keep counts aligned when several
21
21
  * legend items sit in a row.
package/src/picker.tsx CHANGED
@@ -39,14 +39,11 @@ export interface PickerProps<T extends string = string, MULTI extends boolean =
39
39
  disabled?: boolean;
40
40
  autoFocus?: boolean;
41
41
  enableSelectAll?: boolean;
42
- enableSearch?: boolean;
43
42
  multi?: MULTI;
44
43
  includeEmptyOption?: boolean;
45
44
  value?: PickerValue<T, MULTI> | null;
46
45
  onValueChange?: PickerOnValueChange<T, MULTI>;
47
46
  onClose?: PickerOnClose<T, MULTI>;
48
- /** Placeholder for the search input. Pass a translated string. Default: "Search..." */
49
- searchPlaceholder?: string;
50
47
  /** Label for the "select all" link in multi-select mode. Default: "Select all" */
51
48
  selectAllLabel?: string;
52
49
  /** Label for the "deselect all" link in multi-select mode. Default: "Deselect all" */
@@ -59,7 +56,7 @@ export interface PickerProps<T extends string = string, MULTI extends boolean =
59
56
  export function Picker<T extends string, MULTI extends boolean = false>(
60
57
  props: PickerProps<T, MULTI>,
61
58
  ) {
62
- if (props.multi || props.enableSearch || props.renderOptionContent) {
59
+ if (props.multi || props.renderOptionContent) {
63
60
  return <CustomPicker {...props} />;
64
61
  }
65
62
  return <StandardPicker {...(props as PickerProps<T, false>)} />;
@@ -154,10 +151,8 @@ function CustomPicker<T extends string, MULTI extends boolean = false>(
154
151
  disabled = false,
155
152
  autoFocus = false,
156
153
  enableSelectAll = false,
157
- enableSearch = false,
158
154
  onValueChange,
159
155
  onClose,
160
- searchPlaceholder,
161
156
  selectAllLabel,
162
157
  deselectAllLabel,
163
158
  } = props;
@@ -189,7 +184,6 @@ function CustomPicker<T extends string, MULTI extends boolean = false>(
189
184
  testID={testID}
190
185
  open={open}
191
186
  style={style}
192
- enableSearch={enableSearch}
193
187
  onPress={handleToggle}
194
188
  renderOptionContent={renderOptionContent}
195
189
  selectedItems={selectedItems}
@@ -207,9 +201,7 @@ function CustomPicker<T extends string, MULTI extends boolean = false>(
207
201
  onRequestClose={handleClose}
208
202
  renderOptionContent={renderOptionContent}
209
203
  enableSelectAll={enableSelectAll}
210
- enableSearch={enableSearch}
211
204
  includeEmptyOption={includeEmptyOption}
212
- searchPlaceholder={searchPlaceholder}
213
205
  selectAllLabel={selectAllLabel}
214
206
  deselectAllLabel={deselectAllLabel}
215
207
  />
@@ -250,7 +242,6 @@ function PickerTrigger<T extends string>({
250
242
  style,
251
243
  onPress,
252
244
  renderOptionContent,
253
- enableSearch,
254
245
  selectedItems,
255
246
  placeholder,
256
247
  disabled = false,
@@ -261,7 +252,6 @@ function PickerTrigger<T extends string>({
261
252
  style?: StyleProp<ViewStyle>;
262
253
  onPress: () => void;
263
254
  renderOptionContent?: (option: PickerOption<T>) => React.ReactNode;
264
- enableSearch?: boolean;
265
255
  selectedItems: PickerOption<T>[];
266
256
  placeholder?: string;
267
257
  disabled?: boolean;
@@ -279,12 +269,7 @@ function PickerTrigger<T extends string>({
279
269
  // comes from the visible selection/placeholder text below.
280
270
  accessibilityRole="button"
281
271
  accessibilityState={{ expanded: open, disabled }}
282
- style={[
283
- styles.pressable,
284
- open && !enableSearch && styles.opened,
285
- disabled && styles.disabled,
286
- style,
287
- ]}
272
+ style={[styles.pressable, open && styles.opened, disabled && styles.disabled, style]}
288
273
  onPress={!disabled ? onPress : undefined}
289
274
  disabled={disabled}
290
275
  >