react-native-form-controls 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/react-native-form-controls.svg)](https://www.npmjs.com/package/react-native-form-controls)
4
4
 
5
- A highly customizable input component for React Native that works in **any project**. Use it for text, password, textarea out of the box; optionally add date/time picker and Google Places autocomplete by installing extra packages.
5
+ A highly customizable input component for React Native. **Date/time**: uses **react-native-date-picker** when installed (recommended); otherwise falls back to a built-in picker. **Autocomplete**: built-in Google Places (pass `googleApiKey`).
6
6
 
7
7
  ## Features
8
8
 
9
- - **Core types (no extra deps)**: text, password, textarea
10
- - **Optional date/time**: Install `react-native-date-picker` for date, time, and datetime types
11
- - **Optional autocomplete**: Install `react-native-google-places-autocomplete` and pass `googleApiKey` for location search
9
+ - **Text, password, textarea**: Standard inputs
10
+ - **Date, time, datetime**: Uses **react-native-date-picker** when installed (best UX); otherwise built-in modal picker
11
+ - **Autocomplete**: Built-in Google Places search (pass `googleApiKey`; uses Places API)
12
12
  - **Customizable styles**: container, label, input
13
13
  - **Error handling**: error message and error border
14
14
  - **Icons**: left and right icons with actions
@@ -16,7 +16,7 @@ A highly customizable input component for React Native that works in **any proje
16
16
 
17
17
  ## Installation
18
18
 
19
- **Base (works in any project):**
19
+ **Required:** react-native
20
20
 
21
21
  ```bash
22
22
  npm install react-native-form-controls
@@ -24,7 +24,7 @@ npm install react-native-form-controls
24
24
  yarn add react-native-form-controls
25
25
  ```
26
26
 
27
- **Optional date/time picker** (for `type="date"`, `type="time"`, `type="datetime"`):
27
+ **Optional (recommended for date/time):** For the best date/time picker experience, install **react-native-date-picker**. The package will use it automatically when present; otherwise a built-in picker is used.
28
28
 
29
29
  ```bash
30
30
  npm install react-native-date-picker
@@ -32,15 +32,8 @@ npm install react-native-date-picker
32
32
  yarn add react-native-date-picker
33
33
  ```
34
34
 
35
- **Optional – Google Places autocomplete** (for `type="autocomplete"`):
36
35
 
37
- ```bash
38
- npm install react-native-google-places-autocomplete
39
- # or
40
- yarn add react-native-google-places-autocomplete
41
- ```
42
36
 
43
- If you don’t install the optional packages, the component still works: text, password, and textarea work as usual; date/time and autocomplete show a fallback message asking you to install the corresponding package.
44
37
 
45
38
  ## Example app
46
39
 
@@ -66,13 +59,13 @@ import { Input, COLORS } from 'react-native-form-controls';
66
59
  // Textarea
67
60
  <Input type="textarea" label="Notes" placeholder="Enter notes" />
68
61
 
69
- // Date (requires react-native-date-picker)
62
+ // Date (built-in picker)
70
63
  <Input type="date" label="Birth date" onChangeText={(iso) => setDate(iso)} />
71
64
 
72
- // Time (requires react-native-date-picker)
65
+ // Time (built-in picker)
73
66
  <Input type="time" label="Time" onChangeText={(iso) => setTime(iso)} />
74
67
 
75
- // Autocomplete (requires react-native-google-places-autocomplete + googleApiKey)
68
+ // Autocomplete (built-in; requires googleApiKey Google Places API key)
76
69
  <Input
77
70
  type="autocomplete"
78
71
  label="Location"
package/lib/Input.d.ts CHANGED
@@ -21,13 +21,12 @@ export declare const COLORS: {
21
21
  interface InputProps extends TextInputProps {
22
22
  type: 'text' | 'password' | 'date' | 'time' | 'datetime' | 'textarea' | 'autocomplete';
23
23
  leftIcon?: ImageSourcePropType;
24
- rightIcon?: React.ReactNode;
24
+ rightIcon?: ImageSourcePropType;
25
25
  label?: string;
26
26
  error?: string;
27
27
  containerStyle?: StyleProp<ViewStyle>;
28
28
  labelStyle?: StyleProp<TextStyle>;
29
29
  inputStyle?: StyleProp<TextStyle>;
30
- /** Required for type="autocomplete". Install react-native-google-places-autocomplete to use. */
31
30
  googleApiKey?: string;
32
31
  }
33
32
  declare const Input: React.FC<InputProps>;
package/lib/Input.js CHANGED
@@ -43,6 +43,42 @@ var __importStar = (this && this.__importStar) || (function () {
43
43
  return result;
44
44
  };
45
45
  })();
46
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
47
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
48
+ return new (P || (P = Promise))(function (resolve, reject) {
49
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
50
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
51
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
52
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
53
+ });
54
+ };
55
+ var __generator = (this && this.__generator) || function (thisArg, body) {
56
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
57
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
58
+ function verb(n) { return function (v) { return step([n, v]); }; }
59
+ function step(op) {
60
+ if (f) throw new TypeError("Generator is already executing.");
61
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
62
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
63
+ if (y = 0, t) op = [op[0] & 2, t.value];
64
+ switch (op[0]) {
65
+ case 0: case 1: t = op; break;
66
+ case 4: _.label++; return { value: op[1], done: false };
67
+ case 5: _.label++; y = op[1]; op = [0]; continue;
68
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
69
+ default:
70
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
71
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
72
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
73
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
74
+ if (t[2]) _.ops.pop();
75
+ _.trys.pop(); continue;
76
+ }
77
+ op = body.call(thisArg, _);
78
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
79
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
80
+ }
81
+ };
46
82
  var __rest = (this && this.__rest) || function (s, e) {
47
83
  var t = {};
48
84
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
@@ -54,35 +90,36 @@ var __rest = (this && this.__rest) || function (s, e) {
54
90
  }
55
91
  return t;
56
92
  };
57
- var _a;
58
93
  Object.defineProperty(exports, "__esModule", { value: true });
59
94
  exports.COLORS = void 0;
60
95
  var react_1 = __importStar(require("react"));
61
96
  var react_native_1 = require("react-native");
62
- // Optional dependencies only loaded when used; safe for projects that don't install them
63
- var DatePicker = null;
64
- var GooglePlacesAutocomplete = null;
65
- try {
66
- DatePicker = require('react-native-date-picker').default;
67
- }
68
- catch (_b) {
69
- // react-native-date-picker not installed – date/time/datetime will show fallback
97
+ // On Android, require() returns a number but RCTImageView expects an object convert it.
98
+ function getImageSource(source) {
99
+ if (typeof source === 'number') {
100
+ if (react_native_1.Platform.OS === 'android') {
101
+ var resolved = react_native_1.Image.resolveAssetSource(source);
102
+ if (resolved === null || resolved === void 0 ? void 0 : resolved.uri)
103
+ return { uri: resolved.uri, width: resolved.width, height: resolved.height };
104
+ }
105
+ return source;
106
+ }
107
+ return source;
70
108
  }
109
+ // Prefer react-native-date-picker when installed (better native UX)
110
+ var RNDatePicker = null;
71
111
  try {
72
- var GooglePlaces = require('react-native-google-places-autocomplete');
73
- GooglePlacesAutocomplete = (_a = GooglePlaces.GooglePlacesAutocomplete) !== null && _a !== void 0 ? _a : GooglePlaces.default;
112
+ RNDatePicker = require('react-native-date-picker').default;
74
113
  }
75
- catch (_c) {
76
- // react-native-google-places-autocomplete not installed – autocomplete will show fallback
114
+ catch (_a) {
115
+ // Use built-in picker when not installed
77
116
  }
78
117
  exports.COLORS = {
79
- // text color
80
118
  black: '#252F40',
81
119
  white: '#FFFFFF',
82
- background: '#FFFFF',
120
+ background: '#FFFFFF',
83
121
  darkGray: '#A9A9A9',
84
122
  lightGray: '#D3D3D3',
85
- // base colors
86
123
  primary: '#007bff',
87
124
  secondary: '#627594',
88
125
  success: '#28a745',
@@ -95,69 +132,16 @@ exports.COLORS = {
95
132
  link: '#007BFF',
96
133
  error: '#EB5757',
97
134
  };
98
- var TextInputField = function (_a) {
99
- var isPasswordVisible = _a.isPasswordVisible, inputStyle = _a.inputStyle, error = _a.error, type = _a.type, props = __rest(_a, ["isPasswordVisible", "inputStyle", "error", "type"]);
100
- return (react_1.default.createElement(react_native_1.TextInput, __assign({ style: [styles.input, inputStyle, error ? styles.errorBorder : {}], secureTextEntry: type === 'password' && !isPasswordVisible, placeholderTextColor: exports.COLORS.secondary }, props)));
101
- };
102
- var TextareaInput = function (_a) {
103
- var inputStyle = _a.inputStyle, error = _a.error, props = __rest(_a, ["inputStyle", "error"]);
104
- return (react_1.default.createElement(react_native_1.TextInput, __assign({ style: [styles.textarea, inputStyle, error ? styles.errorBorder : {}], multiline: true, placeholderTextColor: exports.COLORS.secondary }, props)));
105
- };
106
- var AutocompleteInput = function (_a) {
107
- var inputStyle = _a.inputStyle, error = _a.error, googleApiKey = _a.googleApiKey, _b = _a.placeholder, placeholder = _b === void 0 ? 'Search' : _b, props = __rest(_a, ["inputStyle", "error", "googleApiKey", "placeholder"]);
108
- var getAddress = (0, react_1.useCallback)(function (details) {
109
- var addressComponent = details === null || details === void 0 ? void 0 : details.address_components;
110
- var address = {};
111
- addressComponent === null || addressComponent === void 0 ? void 0 : addressComponent.forEach(function (item) {
112
- var _a, _b, _c, _d;
113
- if ((_a = item === null || item === void 0 ? void 0 : item.types) === null || _a === void 0 ? void 0 : _a.includes('locality')) {
114
- address.city = item === null || item === void 0 ? void 0 : item.long_name;
115
- }
116
- if ((_b = item === null || item === void 0 ? void 0 : item.types) === null || _b === void 0 ? void 0 : _b.includes('country')) {
117
- address.country = item === null || item === void 0 ? void 0 : item.long_name;
118
- address.countryCode = item === null || item === void 0 ? void 0 : item.short_name;
119
- }
120
- if ((_c = item === null || item === void 0 ? void 0 : item.types) === null || _c === void 0 ? void 0 : _c.includes('postal_code')) {
121
- address.pincode = item === null || item === void 0 ? void 0 : item.long_name;
122
- }
123
- if ((_d = item === null || item === void 0 ? void 0 : item.types) === null || _d === void 0 ? void 0 : _d.includes('administrative_area_level_1')) {
124
- address.state = item === null || item === void 0 ? void 0 : item.long_name;
125
- }
126
- });
127
- return address;
128
- }, []);
129
- var handlePlacePress = (0, react_1.useCallback)(function (data, details) {
130
- var _a, _b, _c, _d;
131
- if (props.onChangeText) {
132
- var addressObj = {
133
- addresstext: data.description,
134
- latitude: (_b = (_a = details === null || details === void 0 ? void 0 : details.geometry) === null || _a === void 0 ? void 0 : _a.location) === null || _b === void 0 ? void 0 : _b.lat,
135
- longitude: (_d = (_c = details === null || details === void 0 ? void 0 : details.geometry) === null || _c === void 0 ? void 0 : _c.location) === null || _d === void 0 ? void 0 : _d.lng,
136
- location: getAddress(details),
137
- };
138
- props.onChangeText(addressObj);
139
- }
140
- }, [getAddress, props.onChangeText]);
141
- if (!GooglePlacesAutocomplete) {
142
- return (react_1.default.createElement(react_native_1.TextInput, { style: [styles.input, styles.inputContainer, inputStyle, error ? styles.errorBorder : {}], placeholder: "Install react-native-google-places-autocomplete for location search", placeholderTextColor: exports.COLORS.secondary, editable: false }));
143
- }
144
- if (!googleApiKey) {
145
- return (react_1.default.createElement(react_native_1.TextInput, { style: [styles.input, styles.inputContainer, inputStyle, error ? styles.errorBorder : {}], placeholder: "Provide googleApiKey for location search", placeholderTextColor: exports.COLORS.secondary, editable: false }));
146
- }
147
- var PlacesComponent = GooglePlacesAutocomplete;
148
- return (react_1.default.createElement(PlacesComponent, __assign({ placeholder: placeholder, fetchDetails: true, onPress: handlePlacePress, query: {
149
- key: googleApiKey,
150
- language: 'en',
151
- }, textInputProps: {
152
- placeholderTextColor: exports.COLORS.secondary,
153
- }, styles: {
154
- textInput: [
155
- styles.inputContainer,
156
- inputStyle,
157
- error ? styles.errorBorder : {},
158
- ],
159
- } }, props)));
160
- };
135
+ var MONTHS = [
136
+ 'January', 'February', 'March', 'April', 'May', 'June',
137
+ 'July', 'August', 'September', 'October', 'November', 'December',
138
+ ];
139
+ var PICKER_ITEM_HEIGHT = 36;
140
+ var PICKER_VISIBLE_ITEMS = 5;
141
+ var PICKER_COLUMN_HEIGHT = PICKER_ITEM_HEIGHT * PICKER_VISIBLE_ITEMS;
142
+ function getDaysInMonth(year, month) {
143
+ return new Date(year, month + 1, 0).getDate();
144
+ }
161
145
  function parseDateValue(value) {
162
146
  if (value === undefined || value === null || value === '')
163
147
  return undefined;
@@ -173,6 +157,278 @@ function formatDateByType(date, type) {
173
157
  return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
174
158
  return date.toLocaleString();
175
159
  }
160
+ // Wheel column: same design as react-native-date-picker (scrollable, center highlight)
161
+ var PickerWheelColumn = function (_a) {
162
+ var items = _a.items, selectedIndex = _a.selectedIndex, onSelect = _a.onSelect, _b = _a.itemHeight, itemHeight = _b === void 0 ? PICKER_ITEM_HEIGHT : _b;
163
+ var scrollRef = (0, react_1.useRef)(null);
164
+ var padding = (PICKER_VISIBLE_ITEMS - 1) * 0.5 * itemHeight;
165
+ var centerOffset = padding + selectedIndex * itemHeight - (PICKER_COLUMN_HEIGHT - itemHeight) / 2;
166
+ (0, react_1.useEffect)(function () {
167
+ var timeoutId = null;
168
+ var task = react_native_1.InteractionManager.runAfterInteractions(function () {
169
+ timeoutId = setTimeout(function () {
170
+ var _a;
171
+ try {
172
+ (_a = scrollRef.current) === null || _a === void 0 ? void 0 : _a.scrollTo({
173
+ y: Math.max(0, centerOffset),
174
+ animated: false,
175
+ });
176
+ }
177
+ catch (_) { }
178
+ }, 100);
179
+ });
180
+ return function () {
181
+ task.cancel();
182
+ if (timeoutId != null)
183
+ clearTimeout(timeoutId);
184
+ };
185
+ }, [selectedIndex, itemHeight, centerOffset]);
186
+ return (react_1.default.createElement(react_native_1.View, { style: pickerStyles.wheelColumn },
187
+ react_1.default.createElement(react_native_1.View, { style: [pickerStyles.wheelHighlight, { height: itemHeight }], pointerEvents: "none" }),
188
+ react_1.default.createElement(react_native_1.ScrollView, { ref: scrollRef, style: { height: PICKER_COLUMN_HEIGHT }, contentContainerStyle: { paddingVertical: padding }, showsVerticalScrollIndicator: false, snapToInterval: itemHeight, snapToAlignment: "center", decelerationRate: "fast", onMomentumScrollEnd: function (e) {
189
+ var y = e.nativeEvent.contentOffset.y + (PICKER_COLUMN_HEIGHT - itemHeight) / 2 - padding;
190
+ var i = Math.round(y / itemHeight);
191
+ if (i >= 0 && i < items.length)
192
+ onSelect(i);
193
+ } }, items.map(function (item, i) { return (react_1.default.createElement(react_native_1.Pressable, { key: i, style: [pickerStyles.wheelItem, { height: itemHeight }, i === selectedIndex && pickerStyles.wheelItemSelected], onPress: function () { return onSelect(i); } },
194
+ react_1.default.createElement(react_native_1.Text, { style: [pickerStyles.wheelItemText, i === selectedIndex && pickerStyles.wheelItemTextSelected] }, String(item)))); }))));
195
+ };
196
+ // Built-in date/time picker – same design as react-native-date-picker (header + wheel)
197
+ var BuiltInDateTimePicker = function (_a) {
198
+ var visible = _a.visible, mode = _a.mode, initialDate = _a.initialDate, onConfirm = _a.onConfirm, onCancel = _a.onCancel;
199
+ var _b = (0, react_1.useState)(initialDate), date = _b[0], setDate = _b[1];
200
+ var year = date.getFullYear();
201
+ var month = date.getMonth();
202
+ var day = date.getDate();
203
+ var hours = date.getHours();
204
+ var minutes = date.getMinutes();
205
+ (0, react_1.useEffect)(function () {
206
+ if (visible)
207
+ setDate(initialDate);
208
+ }, [visible, initialDate.getTime()]);
209
+ var years = Array.from({ length: 100 }, function (_, i) { return new Date().getFullYear() - 80 + i; });
210
+ var months = MONTHS;
211
+ var days = Array.from({ length: getDaysInMonth(year, month) }, function (_, i) { return i + 1; });
212
+ var hourItems = Array.from({ length: 24 }, function (_, i) { return i.toString().padStart(2, '0'); });
213
+ var minuteItems = Array.from({ length: 60 }, function (_, i) { return i.toString().padStart(2, '0'); });
214
+ var handleConfirm = function () {
215
+ var d = new Date(year, month, Math.min(day, getDaysInMonth(year, month)), hours, minutes);
216
+ onConfirm(d);
217
+ };
218
+ if (!visible)
219
+ return null;
220
+ return (react_1.default.createElement(react_native_1.Modal, { visible: true, transparent: true, animationType: "slide", onRequestClose: onCancel },
221
+ react_1.default.createElement(react_native_1.Pressable, { style: pickerStyles.overlay, onPress: onCancel },
222
+ react_1.default.createElement(react_native_1.Pressable, { style: pickerStyles.modal, onPress: function () { } },
223
+ react_1.default.createElement(react_native_1.View, { style: pickerStyles.modalContent, collapsable: false },
224
+ react_1.default.createElement(react_native_1.View, { style: pickerStyles.header },
225
+ react_1.default.createElement(react_native_1.Pressable, { onPress: onCancel, style: pickerStyles.headerBtn },
226
+ react_1.default.createElement(react_native_1.Text, { style: pickerStyles.headerCancel }, "Cancel")),
227
+ react_1.default.createElement(react_native_1.Text, { style: pickerStyles.headerTitle, numberOfLines: 1 }, mode === 'date' ? 'Date' : mode === 'time' ? 'Time' : 'Date & Time'),
228
+ react_1.default.createElement(react_native_1.Pressable, { onPress: handleConfirm, style: pickerStyles.headerBtn },
229
+ react_1.default.createElement(react_native_1.Text, { style: pickerStyles.headerConfirm }, "Confirm"))),
230
+ react_1.default.createElement(react_native_1.View, { style: pickerStyles.wheelRow },
231
+ (mode === 'date' || mode === 'datetime') && (react_1.default.createElement(react_1.default.Fragment, null,
232
+ react_1.default.createElement(PickerWheelColumn, { items: months, selectedIndex: month, onSelect: function (i) { return setDate(new Date(year, i, Math.min(day, getDaysInMonth(year, i)), hours, minutes)); } }),
233
+ react_1.default.createElement(PickerWheelColumn, { items: days, selectedIndex: day - 1, onSelect: function (i) { return setDate(new Date(year, month, i + 1, hours, minutes)); } }),
234
+ react_1.default.createElement(PickerWheelColumn, { items: years, selectedIndex: years.indexOf(year), onSelect: function (i) { return setDate(new Date(years[i], month, Math.min(day, getDaysInMonth(years[i], month)), hours, minutes)); } }))),
235
+ (mode === 'time' || mode === 'datetime') && (react_1.default.createElement(react_1.default.Fragment, null,
236
+ react_1.default.createElement(PickerWheelColumn, { items: hourItems, selectedIndex: hours, onSelect: function (i) { return setDate(new Date(year, month, day, i, minutes)); } }),
237
+ react_1.default.createElement(PickerWheelColumn, { items: minuteItems, selectedIndex: minutes, onSelect: function (i) { return setDate(new Date(year, month, day, hours, i)); } })))))))));
238
+ };
239
+ var pickerStyles = react_native_1.StyleSheet.create({
240
+ overlay: {
241
+ flex: 1,
242
+ backgroundColor: 'rgba(0,0,0,0.5)',
243
+ justifyContent: 'flex-end',
244
+ },
245
+ modal: {
246
+ backgroundColor: exports.COLORS.white,
247
+ borderTopLeftRadius: 16,
248
+ borderTopRightRadius: 16,
249
+ width: '100%',
250
+ minHeight: Math.min(340, react_native_1.Dimensions.get('window').height * 0.45),
251
+ maxHeight: '65%',
252
+ },
253
+ modalContent: {},
254
+ header: {
255
+ flexDirection: 'row',
256
+ alignItems: 'center',
257
+ justifyContent: 'space-between',
258
+ paddingHorizontal: 16,
259
+ paddingVertical: 14,
260
+ borderBottomWidth: react_native_1.StyleSheet.hairlineWidth,
261
+ borderBottomColor: exports.COLORS.lightGray,
262
+ },
263
+ headerBtn: { minWidth: 70 },
264
+ headerCancel: { fontSize: 16, color: exports.COLORS.secondary },
265
+ headerTitle: { fontSize: 16, fontWeight: '600', color: exports.COLORS.black, flex: 1, textAlign: 'center' },
266
+ headerConfirm: { fontSize: 16, fontWeight: '600', color: exports.COLORS.primary, textAlign: 'right' },
267
+ wheelRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 8 },
268
+ wheelColumn: { flex: 1, maxWidth: 100, height: PICKER_COLUMN_HEIGHT, justifyContent: 'center' },
269
+ wheelHighlight: {
270
+ position: 'absolute',
271
+ left: 8,
272
+ right: 8,
273
+ top: (PICKER_COLUMN_HEIGHT - PICKER_ITEM_HEIGHT) / 2,
274
+ backgroundColor: 'rgba(0,122,255,0.08)',
275
+ borderRadius: 8,
276
+ zIndex: 0,
277
+ },
278
+ wheelItem: {
279
+ justifyContent: 'center',
280
+ alignItems: 'center',
281
+ zIndex: 1,
282
+ },
283
+ wheelItemSelected: {},
284
+ wheelItemText: { fontSize: 18, color: exports.COLORS.darkGray },
285
+ wheelItemTextSelected: { fontSize: 20, fontWeight: '600', color: exports.COLORS.black },
286
+ });
287
+ // Built-in Google Places autocomplete (no external lib) – uses Places API
288
+ function useDebounce(value, delay) {
289
+ var _a = (0, react_1.useState)(value), debouncedValue = _a[0], setDebouncedValue = _a[1];
290
+ (0, react_1.useEffect)(function () {
291
+ var t = setTimeout(function () { return setDebouncedValue(value); }, delay);
292
+ return function () { return clearTimeout(t); };
293
+ }, [value, delay]);
294
+ return debouncedValue;
295
+ }
296
+ var AutocompleteInput = function (_a) {
297
+ var inputStyle = _a.inputStyle, error = _a.error, googleApiKey = _a.googleApiKey, _b = _a.placeholder, placeholder = _b === void 0 ? 'Search location' : _b, props = __rest(_a, ["inputStyle", "error", "googleApiKey", "placeholder"]);
298
+ var _c = (0, react_1.useState)(''), query = _c[0], setQuery = _c[1];
299
+ var _d = (0, react_1.useState)([]), predictions = _d[0], setPredictions = _d[1];
300
+ var _e = (0, react_1.useState)(false), loading = _e[0], setLoading = _e[1];
301
+ var _f = (0, react_1.useState)(false), listVisible = _f[0], setListVisible = _f[1];
302
+ var debouncedQuery = useDebounce(query, 300);
303
+ var fetchPredictions = (0, react_1.useCallback)(function (q) { return __awaiter(void 0, void 0, void 0, function () {
304
+ var url, res, data, _a;
305
+ return __generator(this, function (_b) {
306
+ switch (_b.label) {
307
+ case 0:
308
+ if (!googleApiKey || !q.trim()) {
309
+ setPredictions([]);
310
+ return [2 /*return*/];
311
+ }
312
+ setLoading(true);
313
+ _b.label = 1;
314
+ case 1:
315
+ _b.trys.push([1, 4, 5, 6]);
316
+ url = "https://maps.googleapis.com/maps/api/place/autocomplete/json?input=".concat(encodeURIComponent(q), "&key=").concat(googleApiKey);
317
+ return [4 /*yield*/, fetch(url)];
318
+ case 2:
319
+ res = _b.sent();
320
+ return [4 /*yield*/, res.json()];
321
+ case 3:
322
+ data = _b.sent();
323
+ if (data.predictions && Array.isArray(data.predictions)) {
324
+ setPredictions(data.predictions.map(function (p) { return ({ description: p.description, place_id: p.place_id }); }));
325
+ setListVisible(true);
326
+ }
327
+ else {
328
+ setPredictions([]);
329
+ }
330
+ return [3 /*break*/, 6];
331
+ case 4:
332
+ _a = _b.sent();
333
+ setPredictions([]);
334
+ return [3 /*break*/, 6];
335
+ case 5:
336
+ setLoading(false);
337
+ return [7 /*endfinally*/];
338
+ case 6: return [2 /*return*/];
339
+ }
340
+ });
341
+ }); }, [googleApiKey]);
342
+ (0, react_1.useEffect)(function () {
343
+ fetchPredictions(debouncedQuery);
344
+ }, [debouncedQuery, fetchPredictions]);
345
+ var getAddressFromDetails = (0, react_1.useCallback)(function (details) {
346
+ var _a;
347
+ var address = {};
348
+ var components = ((_a = details === null || details === void 0 ? void 0 : details.result) === null || _a === void 0 ? void 0 : _a.address_components) || [];
349
+ components.forEach(function (item) {
350
+ var _a, _b, _c, _d;
351
+ if ((_a = item.types) === null || _a === void 0 ? void 0 : _a.includes('locality'))
352
+ address.city = item.long_name;
353
+ if ((_b = item.types) === null || _b === void 0 ? void 0 : _b.includes('country')) {
354
+ address.country = item.long_name;
355
+ address.countryCode = item.short_name;
356
+ }
357
+ if ((_c = item.types) === null || _c === void 0 ? void 0 : _c.includes('postal_code'))
358
+ address.pincode = item.long_name;
359
+ if ((_d = item.types) === null || _d === void 0 ? void 0 : _d.includes('administrative_area_level_1'))
360
+ address.state = item.long_name;
361
+ });
362
+ return address;
363
+ }, []);
364
+ var onSelectPlace = (0, react_1.useCallback)(function (placeId, description) { return __awaiter(void 0, void 0, void 0, function () {
365
+ var url, res, data, result, loc, addressObj, _a;
366
+ var _b;
367
+ return __generator(this, function (_c) {
368
+ switch (_c.label) {
369
+ case 0:
370
+ setListVisible(false);
371
+ setQuery(description);
372
+ if (!props.onChangeText || !googleApiKey)
373
+ return [2 /*return*/];
374
+ setLoading(true);
375
+ _c.label = 1;
376
+ case 1:
377
+ _c.trys.push([1, 4, 5, 6]);
378
+ url = "https://maps.googleapis.com/maps/api/place/details/json?place_id=".concat(encodeURIComponent(placeId), "&key=").concat(googleApiKey);
379
+ return [4 /*yield*/, fetch(url)];
380
+ case 2:
381
+ res = _c.sent();
382
+ return [4 /*yield*/, res.json()];
383
+ case 3:
384
+ data = _c.sent();
385
+ result = data === null || data === void 0 ? void 0 : data.result;
386
+ loc = (_b = result === null || result === void 0 ? void 0 : result.geometry) === null || _b === void 0 ? void 0 : _b.location;
387
+ addressObj = {
388
+ addresstext: description,
389
+ latitude: loc === null || loc === void 0 ? void 0 : loc.lat,
390
+ longitude: loc === null || loc === void 0 ? void 0 : loc.lng,
391
+ location: getAddressFromDetails({ result: result }),
392
+ };
393
+ props.onChangeText(addressObj);
394
+ return [3 /*break*/, 6];
395
+ case 4:
396
+ _a = _c.sent();
397
+ props.onChangeText({ addresstext: description, latitude: undefined, longitude: undefined, location: {} });
398
+ return [3 /*break*/, 6];
399
+ case 5:
400
+ setLoading(false);
401
+ return [7 /*endfinally*/];
402
+ case 6: return [2 /*return*/];
403
+ }
404
+ });
405
+ }); }, [googleApiKey, props.onChangeText, getAddressFromDetails]);
406
+ if (!googleApiKey) {
407
+ return (react_1.default.createElement(react_native_1.TextInput, { style: [styles.input, styles.inputContainer, inputStyle, error ? styles.errorBorder : {}], placeholder: "Provide googleApiKey for location search", placeholderTextColor: exports.COLORS.secondary, editable: false }));
408
+ }
409
+ return (react_1.default.createElement(react_native_1.View, { style: styles.autocompleteWrap },
410
+ react_1.default.createElement(react_native_1.View, { style: [styles.inputContainer, error ? styles.errorBorder : {}] },
411
+ react_1.default.createElement(react_native_1.TextInput, { style: [styles.input, inputStyle], placeholder: placeholder, placeholderTextColor: exports.COLORS.secondary, value: query, onChangeText: function (t) {
412
+ setQuery(t);
413
+ if (!t.trim())
414
+ setListVisible(false);
415
+ }, onFocus: function () { return predictions.length > 0 && setListVisible(true); }, onBlur: function () { return setTimeout(function () { return setListVisible(false); }, 200); } }),
416
+ loading && react_1.default.createElement(react_native_1.ActivityIndicator, { size: "small", color: exports.COLORS.primary, style: styles.autocompleteLoader })),
417
+ listVisible && predictions.length > 0 && (react_1.default.createElement(react_native_1.View, { style: styles.predictionsList },
418
+ react_1.default.createElement(react_native_1.FlatList, { data: predictions, keyExtractor: function (item) { return item.place_id; }, keyboardShouldPersistTaps: "handled", renderItem: function (_a) {
419
+ var item = _a.item;
420
+ return (react_1.default.createElement(react_native_1.Pressable, { style: styles.predictionItem, onPress: function () { return onSelectPlace(item.place_id, item.description); } },
421
+ react_1.default.createElement(react_native_1.Text, { style: styles.predictionText, numberOfLines: 2 }, item.description)));
422
+ } })))));
423
+ };
424
+ var TextInputField = function (_a) {
425
+ var isPasswordVisible = _a.isPasswordVisible, inputStyle = _a.inputStyle, error = _a.error, type = _a.type, props = __rest(_a, ["isPasswordVisible", "inputStyle", "error", "type"]);
426
+ return (react_1.default.createElement(react_native_1.TextInput, __assign({ style: [styles.input, inputStyle, error ? styles.errorBorder : {}], secureTextEntry: type === 'password' && !isPasswordVisible, placeholderTextColor: exports.COLORS.secondary }, props)));
427
+ };
428
+ var TextareaInput = function (_a) {
429
+ var inputStyle = _a.inputStyle, error = _a.error, props = __rest(_a, ["inputStyle", "error"]);
430
+ return (react_1.default.createElement(react_native_1.TextInput, __assign({ style: [styles.textarea, inputStyle, error ? styles.errorBorder : {}], multiline: true, placeholderTextColor: exports.COLORS.secondary }, props)));
431
+ };
176
432
  var Input = function (_a) {
177
433
  var _b;
178
434
  var type = _a.type, leftIcon = _a.leftIcon, rightIcon = _a.rightIcon, label = _a.label, error = _a.error, containerStyle = _a.containerStyle, labelStyle = _a.labelStyle, inputStyle = _a.inputStyle, valueProp = _a.value, props = __rest(_a, ["type", "leftIcon", "rightIcon", "label", "error", "containerStyle", "labelStyle", "inputStyle", "value"]);
@@ -199,55 +455,56 @@ var Input = function (_a) {
199
455
  setIsDatePickerVisible(true);
200
456
  }
201
457
  };
458
+ var closeDatePicker = (0, react_1.useCallback)(function () {
459
+ setIsDatePickerVisible(false);
460
+ }, []);
461
+ var handleDateConfirm = (0, react_1.useCallback)(function (selectedDate) {
462
+ closeDatePicker();
463
+ // Defer date update so Modal can unmount first (avoids dispatchCommand on TextInput)
464
+ setTimeout(function () {
465
+ setDate(selectedDate);
466
+ if (props.onChangeText)
467
+ props.onChangeText(selectedDate.toISOString());
468
+ }, 200);
469
+ }, [props.onChangeText, closeDatePicker]);
202
470
  var renderInput = function () {
203
471
  switch (type) {
204
472
  case 'textarea':
205
473
  return (react_1.default.createElement(TextareaInput, __assign({ inputStyle: inputStyle, error: error, value: valueProp }, props)));
206
474
  case 'autocomplete':
207
- return (react_1.default.createElement(AutocompleteInput, __assign({ type: 'text', inputStyle: inputStyle, error: error, googleApiKey: props.googleApiKey }, props)));
475
+ return (react_1.default.createElement(AutocompleteInput, __assign({ type: "text", inputStyle: inputStyle, error: error, googleApiKey: props.googleApiKey }, props)));
208
476
  case 'date':
209
477
  case 'time':
210
478
  case 'datetime':
211
- if (!DatePicker) {
212
- return (react_1.default.createElement(react_native_1.TextInput, { style: [styles.input, inputStyle, error ? styles.errorBorder : {}], placeholder: "Install react-native-date-picker for date/time selection", placeholderTextColor: exports.COLORS.secondary, editable: false }));
213
- }
214
- return (react_1.default.createElement(TextInputField, __assign({ type: type, value: displayValue, editable: false, inputStyle: inputStyle, error: error }, props)));
479
+ return (react_1.default.createElement(react_native_1.Pressable, { onPress: function () { return setIsDatePickerVisible(true); }, style: styles.dateInputPressable },
480
+ react_1.default.createElement(react_native_1.Text, { style: [
481
+ styles.input,
482
+ styles.dateDisplayText,
483
+ inputStyle,
484
+ error ? styles.errorBorder : {},
485
+ !displayValue && { color: exports.COLORS.secondary },
486
+ ], numberOfLines: 1 }, displayValue || props.placeholder || '')));
215
487
  default:
216
488
  return (react_1.default.createElement(TextInputField, __assign({ type: type, isPasswordVisible: isPasswordVisible, inputStyle: inputStyle, error: error, value: valueProp }, props)));
217
489
  }
218
490
  };
219
- var renderDatePicker = function () {
220
- if (!DatePicker || !isDateType || !isDatePickerVisible)
221
- return null;
222
- return (react_1.default.createElement(DatePicker, { modal: true, mode: type === 'datetime' ? 'datetime' : type, open: isDatePickerVisible, date: displayDate, onConfirm: function (selectedDate) {
223
- setIsDatePickerVisible(false);
224
- setDate(selectedDate);
225
- if (props.onChangeText) {
226
- props.onChangeText(selectedDate.toISOString());
227
- }
228
- }, onCancel: function () { return setIsDatePickerVisible(false); } }));
229
- };
230
491
  return (react_1.default.createElement(react_native_1.View, { style: [styles.container, containerStyle] },
231
492
  label && react_1.default.createElement(react_native_1.Text, { style: [styles.label, labelStyle] }, label),
232
493
  react_1.default.createElement(react_native_1.View, { style: [
233
494
  type !== 'autocomplete' && styles.inputContainer,
234
495
  error ? styles.errorBorder : {},
235
496
  ] },
236
- leftIcon && react_1.default.createElement(react_native_1.Image, { source: leftIcon, style: styles.icon }),
497
+ leftIcon != null && (react_1.default.createElement(react_native_1.Image, { source: getImageSource(leftIcon), style: styles.icon, resizeMode: "contain" })),
237
498
  renderInput(),
238
- rightIcon && (react_1.default.createElement(react_native_1.TouchableOpacity, { onPress: handleIconPress }, rightIcon))),
499
+ rightIcon != null && (react_1.default.createElement(react_native_1.Pressable, { onPress: handleIconPress },
500
+ react_1.default.createElement(react_native_1.Image, { source: getImageSource(rightIcon), style: styles.icon, resizeMode: "contain" })))),
239
501
  error && react_1.default.createElement(react_native_1.Text, { style: styles.errorText }, error),
240
- renderDatePicker()));
502
+ isDateType && RNDatePicker && (react_1.default.createElement(RNDatePicker, { modal: true, open: isDatePickerVisible, date: displayDate, mode: type === 'datetime' ? 'datetime' : type, onConfirm: function (d) { return handleDateConfirm(d); }, onCancel: closeDatePicker })),
503
+ isDateType && !RNDatePicker && (react_1.default.createElement(BuiltInDateTimePicker, { visible: isDatePickerVisible, mode: type === 'datetime' ? 'datetime' : type, initialDate: displayDate, onConfirm: handleDateConfirm, onCancel: closeDatePicker }))));
241
504
  };
242
505
  var styles = react_native_1.StyleSheet.create({
243
- container: {
244
- marginVertical: 5,
245
- },
246
- label: {
247
- marginBottom: 4,
248
- color: exports.COLORS.black,
249
- fontSize: 16,
250
- },
506
+ container: { marginVertical: 5 },
507
+ label: { marginBottom: 4, color: exports.COLORS.black, fontSize: 16 },
251
508
  inputContainer: {
252
509
  flexDirection: 'row',
253
510
  alignItems: 'center',
@@ -259,12 +516,9 @@ var styles = react_native_1.StyleSheet.create({
259
516
  backgroundColor: exports.COLORS.white,
260
517
  minHeight: 40,
261
518
  },
262
- input: {
263
- flex: 1,
264
- color: exports.COLORS.black,
265
- padding: 0,
266
- fontSize: 14,
267
- },
519
+ input: { flex: 1, color: exports.COLORS.black, padding: 0, fontSize: 14 },
520
+ dateInputPressable: { flex: 1 },
521
+ dateDisplayText: { lineHeight: 20 },
268
522
  textarea: {
269
523
  flex: 1,
270
524
  color: exports.COLORS.black,
@@ -273,17 +527,25 @@ var styles = react_native_1.StyleSheet.create({
273
527
  textAlignVertical: 'top',
274
528
  height: 100,
275
529
  },
276
- icon: {
277
- width: 16,
278
- height: 16,
279
- },
280
- errorBorder: {
281
- borderColor: exports.COLORS.error,
282
- },
283
- errorText: {
284
- color: exports.COLORS.error,
285
- fontSize: 12,
530
+ icon: { width: 24, height: 24 },
531
+ errorBorder: { borderColor: exports.COLORS.error },
532
+ errorText: { color: exports.COLORS.error, fontSize: 12, marginTop: 4 },
533
+ autocompleteWrap: { position: 'relative' },
534
+ autocompleteLoader: { position: 'absolute', right: 12, top: 10 },
535
+ predictionsList: {
536
+ position: 'absolute',
537
+ top: '100%',
538
+ left: 0,
539
+ right: 0,
540
+ backgroundColor: exports.COLORS.white,
541
+ borderWidth: 1,
542
+ borderColor: exports.COLORS.lightGray,
543
+ borderRadius: 8,
286
544
  marginTop: 4,
545
+ maxHeight: 200,
546
+ zIndex: 1000,
287
547
  },
548
+ predictionItem: { padding: 12, borderBottomWidth: 1, borderBottomColor: exports.COLORS.light },
549
+ predictionText: { fontSize: 14, color: exports.COLORS.black },
288
550
  });
289
551
  exports.default = Input;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-form-controls",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A highly customizable and versatile input component for React Native.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -25,20 +25,13 @@
25
25
  ],
26
26
  "author": "Tarun Jain",
27
27
  "license": "MIT",
28
- "dependencies": {
29
- "react-native": "^0.74.3"
30
- },
31
28
  "peerDependencies": {
32
29
  "react-native": ">=0.70.0",
33
- "react-native-date-picker": "^4.0.0",
34
- "react-native-google-places-autocomplete": "^2.0.0"
30
+ "react-native-date-picker": "^4.0.0"
35
31
  },
36
32
  "peerDependenciesMeta": {
37
33
  "react-native-date-picker": {
38
34
  "optional": true
39
- },
40
- "react-native-google-places-autocomplete": {
41
- "optional": true
42
35
  }
43
36
  },
44
37
  "devDependencies": {