rn-searchable-textinput 0.1.1 → 2.0.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.
Files changed (44) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +125 -58
  3. package/lib/module/components/Dropdown.js +67 -0
  4. package/lib/module/components/Dropdown.js.map +1 -0
  5. package/lib/module/components/SearchableTextInput.js +113 -0
  6. package/lib/module/components/SearchableTextInput.js.map +1 -0
  7. package/lib/module/docs/rn-searchable-textinput-demo.gif +0 -0
  8. package/lib/module/hooks/useDebounce.js +16 -0
  9. package/lib/module/hooks/useDebounce.js.map +1 -0
  10. package/lib/module/hooks/useDropdownLayout.js +28 -0
  11. package/lib/module/hooks/useDropdownLayout.js.map +1 -0
  12. package/lib/module/index.js +4 -0
  13. package/lib/module/index.js.map +1 -0
  14. package/lib/module/package.json +1 -0
  15. package/lib/module/styles/defaultStyles.js +68 -0
  16. package/lib/module/styles/defaultStyles.js.map +1 -0
  17. package/lib/module/types/index.js +4 -0
  18. package/lib/module/types/index.js.map +1 -0
  19. package/lib/typescript/package.json +1 -0
  20. package/lib/typescript/src/components/Dropdown.d.ts +20 -0
  21. package/lib/typescript/src/components/Dropdown.d.ts.map +1 -0
  22. package/lib/typescript/src/components/SearchableTextInput.d.ts +3 -0
  23. package/lib/typescript/src/components/SearchableTextInput.d.ts.map +1 -0
  24. package/lib/typescript/src/hooks/useDebounce.d.ts +2 -0
  25. package/lib/typescript/src/hooks/useDebounce.d.ts.map +1 -0
  26. package/lib/typescript/src/hooks/useDropdownLayout.d.ts +10 -0
  27. package/lib/typescript/src/hooks/useDropdownLayout.d.ts.map +1 -0
  28. package/lib/typescript/src/index.d.ts +3 -0
  29. package/lib/typescript/src/index.d.ts.map +1 -0
  30. package/lib/typescript/src/styles/defaultStyles.d.ts +63 -0
  31. package/lib/typescript/src/styles/defaultStyles.d.ts.map +1 -0
  32. package/lib/typescript/src/types/index.d.ts +27 -0
  33. package/lib/typescript/src/types/index.d.ts.map +1 -0
  34. package/package.json +166 -16
  35. package/src/components/Dropdown.tsx +99 -0
  36. package/src/components/SearchableTextInput.tsx +130 -0
  37. package/src/docs/rn-searchable-textinput-demo.gif +0 -0
  38. package/src/hooks/useDebounce.ts +17 -0
  39. package/src/hooks/useDropdownLayout.ts +27 -0
  40. package/src/index.tsx +2 -0
  41. package/src/styles/defaultStyles.ts +63 -0
  42. package/src/types/index.ts +36 -0
  43. package/components/searchable-text-input.js +0 -114
  44. package/index.js +0 -2
@@ -0,0 +1,63 @@
1
+ export declare const styles: Readonly<{
2
+ container: {
3
+ position: "relative";
4
+ zIndex: number;
5
+ elevation: number;
6
+ width: string;
7
+ };
8
+ inputWrapper: {
9
+ justifyContent: "center";
10
+ };
11
+ input: {
12
+ borderWidth: number;
13
+ borderColor: string;
14
+ borderRadius: number;
15
+ paddingHorizontal: number;
16
+ paddingVertical: number;
17
+ fontSize: number;
18
+ backgroundColor: string;
19
+ color: string;
20
+ };
21
+ clearButton: {
22
+ position: "absolute";
23
+ right: number;
24
+ padding: number;
25
+ };
26
+ clearIcon: {
27
+ fontSize: number;
28
+ color: string;
29
+ fontWeight: "600";
30
+ };
31
+ dropdown: {
32
+ position: "absolute";
33
+ backgroundColor: string;
34
+ borderRadius: number;
35
+ borderWidth: number;
36
+ borderColor: string;
37
+ shadowColor: string;
38
+ shadowOffset: {
39
+ width: number;
40
+ height: number;
41
+ };
42
+ shadowOpacity: number;
43
+ shadowRadius: number;
44
+ elevation: number;
45
+ overflow: "hidden";
46
+ };
47
+ item: {
48
+ paddingHorizontal: number;
49
+ paddingVertical: number;
50
+ borderBottomWidth: number;
51
+ borderBottomColor: string;
52
+ };
53
+ itemText: {
54
+ fontSize: number;
55
+ color: string;
56
+ };
57
+ loader: {
58
+ padding: number;
59
+ alignItems: "center";
60
+ justifyContent: "center";
61
+ };
62
+ }>;
63
+ //# sourceMappingURL=defaultStyles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaultStyles.d.ts","sourceRoot":"","sources":["../../../../src/styles/defaultStyles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DjB,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { TextInputProps, ViewStyle, TextStyle, StyleProp } from 'react-native';
2
+ import React from 'react';
3
+ export interface SearchableTextInputProps<T> extends Omit<TextInputProps, 'value'> {
4
+ data: T[];
5
+ searchKey: keyof T | ((item: T) => string);
6
+ value: string;
7
+ onSelect: (item: T) => void;
8
+ debounceDelay?: number;
9
+ maxDropdownHeight?: number;
10
+ containerStyle?: StyleProp<ViewStyle>;
11
+ inputStyle?: StyleProp<TextStyle>;
12
+ dropdownStyle?: StyleProp<ViewStyle>;
13
+ itemStyle?: StyleProp<ViewStyle>;
14
+ itemTextStyle?: StyleProp<TextStyle>;
15
+ renderItem?: (item: T, isSelected: boolean) => React.ReactElement;
16
+ keyExtractor?: (item: T, index: number) => string;
17
+ isLoading?: boolean;
18
+ ListEmptyComponent?: React.ReactElement;
19
+ showClearButton?: boolean;
20
+ onClear?: () => void;
21
+ }
22
+ export interface DropdownLayoutMetrics {
23
+ position: 'bottom' | 'top';
24
+ width: number;
25
+ inputHeight: number;
26
+ }
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,SAAS,EACT,SAAS,EACT,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,WAAW,wBAAwB,CAAC,CAAC,CAAE,SAAQ,IAAI,CACvD,cAAc,EACd,OAAO,CACR;IACC,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC,CAAC;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACrC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACrC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,UAAU,EAAE,OAAO,KAAK,KAAK,CAAC,YAAY,CAAC;IAClE,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kBAAkB,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC;IACxC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB"}
package/package.json CHANGED
@@ -1,27 +1,177 @@
1
1
  {
2
2
  "name": "rn-searchable-textinput",
3
- "version": "0.1.1",
4
- "description": "Searchable text input component which can be fully customized.",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
3
+ "version": "2.0.1",
4
+ "description": "A lightweight, zero-dependency, highly customizable searchable text input for React Native.",
5
+ "main": "./lib/module/index.js",
6
+ "types": "./lib/typescript/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "rn-searchable-textinput-source": "./src/index.tsx",
10
+ "types": "./lib/typescript/src/index.d.ts",
11
+ "default": "./lib/module/index.js"
12
+ },
13
+ "./package.json": "./package.json"
8
14
  },
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/pratik8442/rn-searchable-textinput.git"
15
+ "files": [
16
+ "src",
17
+ "lib",
18
+ "android",
19
+ "ios",
20
+ "cpp",
21
+ "*.podspec",
22
+ "react-native.config.js",
23
+ "!ios/build",
24
+ "!android/build",
25
+ "!android/gradle",
26
+ "!android/gradlew",
27
+ "!android/gradlew.bat",
28
+ "!android/local.properties",
29
+ "!**/__tests__",
30
+ "!**/__fixtures__",
31
+ "!**/__mocks__",
32
+ "!**/.*"
33
+ ],
34
+ "scripts": {
35
+ "example": "yarn workspace rn-searchable-textinput-example",
36
+ "clean": "del-cli lib",
37
+ "prepare": "bob build",
38
+ "typecheck": "tsc",
39
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
40
+ "test": "jest",
41
+ "release": "release-it --only-version"
12
42
  },
13
43
  "keywords": [
14
- "text-search",
15
- "text-input-search",
16
- "searchable-textinput",
17
44
  "react-native",
18
- "react native",
19
- "text-input"
45
+ "ios",
46
+ "android",
47
+ "searchable-textinput",
48
+ "autocomplete",
49
+ "searchable-dropdown",
50
+ "picker",
51
+ "searchable-picker",
52
+ "react-native-search",
53
+ "typescript"
20
54
  ],
21
- "author": "Pratik Sanap",
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/dev-pratik-sanap.git"
58
+ },
59
+ "author": "Pratik <pratiksanap007@gmail.com> (https://www.npmjs.com/~pratik8442)",
22
60
  "license": "MIT",
23
61
  "bugs": {
24
- "url": "https://github.com/pratik8442/rn-searchable-textinput/issues"
62
+ "url": "https://github.com/dev-pratik-sanap/issues"
63
+ },
64
+ "homepage": "https://github.com/dev-pratik-sanap#readme",
65
+ "publishConfig": {
66
+ "registry": "https://registry.npmjs.org/"
67
+ },
68
+ "devDependencies": {
69
+ "@commitlint/config-conventional": "^21.0.2",
70
+ "@eslint/compat": "^2.1.0",
71
+ "@eslint/eslintrc": "^3.3.5",
72
+ "@eslint/js": "^10.0.1",
73
+ "@jest/globals": "^29.7.0",
74
+ "@react-native/babel-preset": "0.85.0",
75
+ "@react-native/eslint-config": "0.85.0",
76
+ "@react-native/jest-preset": "0.85.0",
77
+ "@release-it/conventional-changelog": "^11.0.1",
78
+ "@types/react": "^19.2.0",
79
+ "commitlint": "^21.0.2",
80
+ "del-cli": "^7.0.0",
81
+ "eslint": "^9.39.4",
82
+ "eslint-config-prettier": "^10.1.8",
83
+ "eslint-plugin-ft-flow": "^3.0.11",
84
+ "eslint-plugin-prettier": "^5.5.6",
85
+ "jest": "^29.7.0",
86
+ "lefthook": "^2.1.9",
87
+ "prettier": "^3.8.3",
88
+ "react": "19.2.0",
89
+ "react-native": "0.83.10",
90
+ "react-native-builder-bob": "^0.43.0",
91
+ "release-it": "^20.2.0",
92
+ "turbo": "^2.9.16",
93
+ "typescript": "^6.0.3"
94
+ },
95
+ "peerDependencies": {
96
+ "react": "*",
97
+ "react-native": "*"
98
+ },
99
+ "workspaces": [
100
+ "example"
101
+ ],
102
+ "packageManager": "yarn@4.11.0",
103
+ "react-native-builder-bob": {
104
+ "source": "src",
105
+ "output": "lib",
106
+ "targets": [
107
+ [
108
+ "module",
109
+ {
110
+ "esm": true
111
+ }
112
+ ],
113
+ [
114
+ "typescript",
115
+ {
116
+ "project": "tsconfig.build.json"
117
+ }
118
+ ]
119
+ ]
120
+ },
121
+ "prettier": {
122
+ "quoteProps": "consistent",
123
+ "singleQuote": true,
124
+ "tabWidth": 2,
125
+ "trailingComma": "es5",
126
+ "useTabs": false
127
+ },
128
+ "jest": {
129
+ "preset": "@react-native/jest-preset",
130
+ "testEnvironmentOptions": {
131
+ "customExportConditions": [
132
+ "require",
133
+ "react-native",
134
+ "<%- project.sourceCondition -%>"
135
+ ]
136
+ },
137
+ "modulePathIgnorePatterns": [
138
+ "<rootDir>/example/node_modules",
139
+ "<rootDir>/lib/"
140
+ ]
141
+ },
142
+ "commitlint": {
143
+ "extends": [
144
+ "@commitlint/config-conventional"
145
+ ]
146
+ },
147
+ "release-it": {
148
+ "git": {
149
+ "commitMessage": "chore: release ${version}",
150
+ "tagName": "v${version}"
151
+ },
152
+ "npm": {
153
+ "publish": true
154
+ },
155
+ "github": {
156
+ "release": true
157
+ },
158
+ "plugins": {
159
+ "@release-it/conventional-changelog": {
160
+ "preset": {
161
+ "name": "angular"
162
+ }
163
+ }
164
+ }
25
165
  },
26
- "homepage": "https://github.com/pratik8442/rn-searchable-textinput#readme"
166
+ "create-react-native-library": {
167
+ "type": "library",
168
+ "languages": "js",
169
+ "tools": [
170
+ "eslint",
171
+ "jest",
172
+ "lefthook",
173
+ "release-it"
174
+ ],
175
+ "version": "0.63.0"
176
+ }
27
177
  }
@@ -0,0 +1,99 @@
1
+ import React from 'react';
2
+ import {
3
+ View,
4
+ FlatList,
5
+ TouchableOpacity,
6
+ Text,
7
+ ActivityIndicator,
8
+ } from 'react-native';
9
+ import { styles } from '../styles/defaultStyles';
10
+ import type { DropdownLayoutMetrics } from '../types';
11
+
12
+ interface DropdownProps<T> {
13
+ visible: boolean;
14
+ data: T[];
15
+ metrics: DropdownLayoutMetrics | null;
16
+ maxHeight: number;
17
+ onSelect: (item: T) => void;
18
+ getLabel: (item: T) => string;
19
+ dropdownStyle?: any;
20
+ itemStyle?: any;
21
+ itemTextStyle?: any;
22
+ renderItem?: (item: T, isSelected: boolean) => React.ReactElement;
23
+ keyExtractor?: (item: T, index: number) => string;
24
+ isLoading?: boolean;
25
+ ListEmptyComponent?: React.ReactElement;
26
+ }
27
+
28
+ export function Dropdown<T>({
29
+ visible,
30
+ data,
31
+ metrics,
32
+ maxHeight,
33
+ onSelect,
34
+ getLabel,
35
+ dropdownStyle,
36
+ itemStyle,
37
+ itemTextStyle,
38
+ renderItem,
39
+ keyExtractor,
40
+ isLoading,
41
+ ListEmptyComponent,
42
+ }: DropdownProps<T>) {
43
+ if (
44
+ !visible ||
45
+ !metrics ||
46
+ (!isLoading && data.length === 0 && !ListEmptyComponent)
47
+ )
48
+ return null;
49
+
50
+ const dynamicStyle =
51
+ metrics.position === 'bottom'
52
+ ? { top: metrics.inputHeight + 4 }
53
+ : { bottom: metrics.inputHeight + 4 };
54
+
55
+ return (
56
+ <View
57
+ style={[
58
+ styles.dropdown,
59
+ dynamicStyle,
60
+ dropdownStyle,
61
+ { maxHeight, width: metrics.width },
62
+ ]}
63
+ >
64
+ {isLoading ? (
65
+ <View style={styles.loader}>
66
+ <ActivityIndicator size="small" color="#6b7280" />
67
+ </View>
68
+ ) : (
69
+ <FlatList
70
+ data={data}
71
+ keyExtractor={keyExtractor || ((_, index) => index.toString())}
72
+ keyboardShouldPersistTaps="handled"
73
+ showsVerticalScrollIndicator={false}
74
+ nestedScrollEnabled={true}
75
+ ListEmptyComponent={ListEmptyComponent}
76
+ renderItem={({ item }) => {
77
+ if (renderItem) {
78
+ return (
79
+ <TouchableOpacity onPress={() => onSelect(item)}>
80
+ {renderItem(item, false)}
81
+ </TouchableOpacity>
82
+ );
83
+ }
84
+ return (
85
+ <TouchableOpacity
86
+ style={[styles.item, itemStyle]}
87
+ onPress={() => onSelect(item)}
88
+ >
89
+ <Text style={[styles.itemText, itemTextStyle]}>
90
+ {getLabel(item)}
91
+ </Text>
92
+ </TouchableOpacity>
93
+ );
94
+ }}
95
+ />
96
+ )}
97
+ </View>
98
+ );
99
+ }
@@ -0,0 +1,130 @@
1
+ import { useState, useRef, useMemo, useCallback, useEffect } from 'react';
2
+ import { View, TextInput, TouchableOpacity, Text } from 'react-native';
3
+ import { Dropdown } from './Dropdown';
4
+ import { useDebounce } from '../hooks/useDebounce';
5
+ import { useDropdownLayout } from '../hooks/useDropdownLayout';
6
+ import { styles } from '../styles/defaultStyles';
7
+ import type { SearchableTextInputProps } from '../types';
8
+
9
+ export function SearchableTextInput<T>(props: SearchableTextInputProps<T>) {
10
+ const {
11
+ data,
12
+ searchKey,
13
+ value,
14
+ onSelect,
15
+ placeholder = 'Search...',
16
+ debounceDelay = 200,
17
+ maxDropdownHeight = 250,
18
+ containerStyle,
19
+ inputStyle,
20
+ isLoading,
21
+ ListEmptyComponent,
22
+ showClearButton,
23
+ onClear,
24
+ ...rest
25
+ } = props;
26
+
27
+ const inputRef = useRef<any>(null);
28
+ const [searchText, setSearchText] = useState(value);
29
+ const [isFocused, setIsFocused] = useState(false);
30
+
31
+ const searchKeyRef = useRef(searchKey);
32
+ useEffect(() => {
33
+ searchKeyRef.current = searchKey;
34
+ }, [searchKey]);
35
+
36
+ const debouncedSearchText = useDebounce(searchText, debounceDelay);
37
+ const { metrics, measureLayout, setMetrics } = useDropdownLayout(
38
+ inputRef,
39
+ maxDropdownHeight
40
+ );
41
+
42
+ useEffect(() => setSearchText(value), [value]);
43
+
44
+ const getLabel = useCallback((item: T) => {
45
+ if (typeof searchKeyRef.current === 'function')
46
+ return searchKeyRef.current(item);
47
+ return String(item[searchKeyRef.current] || '');
48
+ }, []);
49
+
50
+ const filteredData = useMemo(() => {
51
+ if (!debouncedSearchText) return data;
52
+ const lowerSearch = debouncedSearchText.toLowerCase();
53
+ return data.filter((item) =>
54
+ getLabel(item).toLowerCase().includes(lowerSearch)
55
+ );
56
+ }, [data, debouncedSearchText, getLabel]);
57
+
58
+ const handleFocus = () => {
59
+ measureLayout();
60
+ setIsFocused(true);
61
+ };
62
+
63
+ const handleBlur = () => {
64
+ // Allows tap events on the list to register before unmounting
65
+ setTimeout(() => {
66
+ setIsFocused(false);
67
+ setMetrics(null);
68
+ }, 200);
69
+ };
70
+
71
+ const handleSelect = (item: T) => {
72
+ setSearchText(getLabel(item));
73
+ setIsFocused(false);
74
+ onSelect(item);
75
+ };
76
+
77
+ const handleClear = () => {
78
+ setSearchText('');
79
+ if (onClear) onClear();
80
+ };
81
+
82
+ return (
83
+ <View style={[styles.container, containerStyle]} ref={inputRef}>
84
+ <View style={styles.inputWrapper}>
85
+ <TextInput
86
+ style={[
87
+ styles.input,
88
+ inputStyle,
89
+ showClearButton && { paddingRight: 40 },
90
+ ]}
91
+ value={searchText}
92
+ onChangeText={(text) => {
93
+ setSearchText(text);
94
+ if (!isFocused) setIsFocused(true);
95
+ }}
96
+ onFocus={handleFocus}
97
+ onBlur={handleBlur}
98
+ placeholder={placeholder}
99
+ placeholderTextColor="#9ca3af"
100
+ />
101
+
102
+ {showClearButton && searchText.length > 0 && (
103
+ <TouchableOpacity style={styles.clearButton} onPress={handleClear}>
104
+ <Text style={styles.clearIcon}>✕</Text>
105
+ </TouchableOpacity>
106
+ )}
107
+ </View>
108
+
109
+ <Dropdown
110
+ visible={
111
+ isFocused &&
112
+ ((searchText.length > 0 && debouncedSearchText.length > 0) ||
113
+ isLoading === true)
114
+ }
115
+ data={filteredData}
116
+ metrics={metrics}
117
+ maxHeight={maxDropdownHeight}
118
+ onSelect={handleSelect}
119
+ getLabel={getLabel}
120
+ dropdownStyle={rest.dropdownStyle}
121
+ itemStyle={rest.itemStyle}
122
+ itemTextStyle={rest.itemTextStyle}
123
+ renderItem={rest.renderItem}
124
+ keyExtractor={rest.keyExtractor}
125
+ isLoading={isLoading}
126
+ ListEmptyComponent={ListEmptyComponent}
127
+ />
128
+ </View>
129
+ );
130
+ }
@@ -0,0 +1,17 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ export function useDebounce<T>(value: T, delay: number): T {
4
+ const [debouncedValue, setDebouncedValue] = useState<T>(value);
5
+
6
+ useEffect(() => {
7
+ const handler = setTimeout(() => {
8
+ setDebouncedValue(value);
9
+ }, delay);
10
+
11
+ return () => {
12
+ clearTimeout(handler);
13
+ };
14
+ }, [value, delay]);
15
+
16
+ return debouncedValue;
17
+ }
@@ -0,0 +1,27 @@
1
+ import React, { useState, useCallback, type RefObject } from 'react';
2
+ import { View, useWindowDimensions } from 'react-native';
3
+ import type { DropdownLayoutMetrics } from '../types';
4
+
5
+ export const useDropdownLayout = (
6
+ inputRef: RefObject<React.ElementRef<typeof View>>, // 👈 Fix is here
7
+ maxDropdownHeight: number
8
+ ) => {
9
+ const [metrics, setMetrics] = useState<DropdownLayoutMetrics | null>(null);
10
+ const { height: screenHeight } = useWindowDimensions();
11
+
12
+ const measureLayout = useCallback(() => {
13
+ inputRef.current?.measure((_fx, _fy, width, height, _px, py) => {
14
+ const spaceBelow = screenHeight - (py + height);
15
+ const spaceAbove = py;
16
+
17
+ const position =
18
+ spaceBelow < maxDropdownHeight && spaceAbove > spaceBelow
19
+ ? 'top'
20
+ : 'bottom';
21
+
22
+ setMetrics({ position, width, inputHeight: height });
23
+ });
24
+ }, [inputRef, screenHeight, maxDropdownHeight]);
25
+
26
+ return { metrics, measureLayout, setMetrics };
27
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,2 @@
1
+ export { SearchableTextInput } from './components/SearchableTextInput';
2
+ export type { SearchableTextInputProps } from './types';
@@ -0,0 +1,63 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export const styles = StyleSheet.create({
4
+ container: {
5
+ position: 'relative',
6
+ zIndex: 1000,
7
+ elevation: 1000,
8
+ width: '100%',
9
+ },
10
+ inputWrapper: {
11
+ justifyContent: 'center',
12
+ },
13
+ input: {
14
+ borderWidth: 1,
15
+ borderColor: '#d1d5db',
16
+ borderRadius: 8,
17
+ paddingHorizontal: 16,
18
+ paddingVertical: 14,
19
+ fontSize: 16,
20
+ backgroundColor: '#ffffff',
21
+ color: '#1f2937',
22
+ },
23
+ clearButton: {
24
+ position: 'absolute',
25
+ right: 12,
26
+ padding: 6,
27
+ },
28
+ clearIcon: {
29
+ fontSize: 16,
30
+ color: '#9ca3af',
31
+ fontWeight: '600',
32
+ },
33
+ dropdown: {
34
+ position: 'absolute',
35
+ backgroundColor: '#ffffff',
36
+ borderRadius: 8,
37
+ borderWidth: 1,
38
+ borderColor: '#e5e7eb',
39
+ // iOS Shadow
40
+ shadowColor: '#000',
41
+ shadowOffset: { width: 0, height: 4 },
42
+ shadowOpacity: 0.1,
43
+ shadowRadius: 12,
44
+ // Android Shadow
45
+ elevation: 5,
46
+ overflow: 'hidden',
47
+ },
48
+ item: {
49
+ paddingHorizontal: 16,
50
+ paddingVertical: 14,
51
+ borderBottomWidth: 1,
52
+ borderBottomColor: '#f3f4f6',
53
+ },
54
+ itemText: {
55
+ fontSize: 16,
56
+ color: '#374151',
57
+ },
58
+ loader: {
59
+ padding: 20,
60
+ alignItems: 'center',
61
+ justifyContent: 'center',
62
+ },
63
+ });
@@ -0,0 +1,36 @@
1
+ import type {
2
+ TextInputProps,
3
+ ViewStyle,
4
+ TextStyle,
5
+ StyleProp,
6
+ } from 'react-native';
7
+ import React from 'react';
8
+
9
+ export interface SearchableTextInputProps<T> extends Omit<
10
+ TextInputProps,
11
+ 'value'
12
+ > {
13
+ data: T[];
14
+ searchKey: keyof T | ((item: T) => string);
15
+ value: string;
16
+ onSelect: (item: T) => void;
17
+ debounceDelay?: number;
18
+ maxDropdownHeight?: number;
19
+ containerStyle?: StyleProp<ViewStyle>;
20
+ inputStyle?: StyleProp<TextStyle>;
21
+ dropdownStyle?: StyleProp<ViewStyle>;
22
+ itemStyle?: StyleProp<ViewStyle>;
23
+ itemTextStyle?: StyleProp<TextStyle>;
24
+ renderItem?: (item: T, isSelected: boolean) => React.ReactElement;
25
+ keyExtractor?: (item: T, index: number) => string;
26
+ isLoading?: boolean;
27
+ ListEmptyComponent?: React.ReactElement;
28
+ showClearButton?: boolean;
29
+ onClear?: () => void;
30
+ }
31
+
32
+ export interface DropdownLayoutMetrics {
33
+ position: 'bottom' | 'top';
34
+ width: number;
35
+ inputHeight: number;
36
+ }