ota-components-module 1.3.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/README.md +179 -0
- package/assets/images/ic_camera.svg +3 -0
- package/assets/images/ic_close.svg +8 -0
- package/assets/images/ic_folder.svg +3 -0
- package/assets/images/placeholder.png +0 -0
- package/expo-env.d.ts +7 -0
- package/mri-manifest.json +10 -0
- package/package.json +28 -0
- package/src/button/ThemedButton.tsx +120 -0
- package/src/feedback/ActivityLoader.tsx +84 -0
- package/src/feedback/CustomAlert.tsx +143 -0
- package/src/feedback/DeleteImageConfirmationDialog.tsx +58 -0
- package/src/feedback/ProgressBar.tsx +58 -0
- package/src/image/ImagePickerBottomSheet.tsx +61 -0
- package/src/image/ImagePickerView.tsx +103 -0
- package/src/image/MultipleImagePreview.tsx +424 -0
- package/src/image/StackedImage.tsx +155 -0
- package/src/index.ts +68 -0
- package/src/input/CustomDropdown.tsx +142 -0
- package/src/input/CustomInput.tsx +101 -0
- package/src/input/FormField.tsx +358 -0
- package/src/input/KeyboardScrollView.tsx +131 -0
- package/src/input/SearchViewInput.tsx +183 -0
- package/src/layout/BottomSheetDialog.tsx +208 -0
- package/src/layout/BottomTwoButtonLayoutComponent.tsx +153 -0
- package/src/layout/CardView.tsx +101 -0
- package/src/layout/PropertyHeaderComponent.tsx +110 -0
- package/src/list/SearchableList.tsx +273 -0
- package/src/models/PropertyImage.ts +20 -0
- package/src/typography/Label.tsx +225 -0
- package/src/utils/BaseStyle.ts +46 -0
- package/src/utils/Strings.ts +1 -0
- package/src/utils/TextConstants.ts +24 -0
- package/src/utils/Utils.ts +11 -0
- package/src/webbaseview/WebBaseView.tsx +26 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Text, StyleSheet, View, TextInput, KeyboardTypeOptions } from 'react-native';
|
|
3
|
+
import CustomDropdown from './CustomDropdown';
|
|
4
|
+
import { Colors } from '../utils/BaseStyle';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// Define interfaces for our dropdown items
|
|
8
|
+
export type DropdownItem = {
|
|
9
|
+
value: string;
|
|
10
|
+
id?: number | string;
|
|
11
|
+
code?: string;
|
|
12
|
+
countryCode?: string;
|
|
13
|
+
}
|
|
14
|
+
// Define the field types
|
|
15
|
+
export type FieldType = 'text' | 'numeric' | 'date' | 'currency' | 'dropdown';
|
|
16
|
+
|
|
17
|
+
interface FormFieldProps {
|
|
18
|
+
// Common props
|
|
19
|
+
labelTestID?: string;
|
|
20
|
+
label: string;
|
|
21
|
+
required?: boolean;
|
|
22
|
+
isInline?: boolean; // Flag to determine if the layout should be inline or stacked
|
|
23
|
+
disabled?: boolean; // Flag to disable user interaction with the field
|
|
24
|
+
|
|
25
|
+
// Field type
|
|
26
|
+
fieldType?: FieldType;
|
|
27
|
+
|
|
28
|
+
// TextInput props
|
|
29
|
+
textInputTestID?: string;
|
|
30
|
+
value?: string;
|
|
31
|
+
onChangeText?: (text: string) => void;
|
|
32
|
+
placeholder?: string;
|
|
33
|
+
keyboardType?: KeyboardTypeOptions;
|
|
34
|
+
|
|
35
|
+
// Dropdown props
|
|
36
|
+
dropdownTestID?: string;
|
|
37
|
+
dropdownItems?: DropdownItem[];
|
|
38
|
+
dropdownValue?: string;
|
|
39
|
+
dropdownPlaceholder?: string;
|
|
40
|
+
onDropdownChange?: (item: DropdownItem) => void;
|
|
41
|
+
zIndex?: number;
|
|
42
|
+
|
|
43
|
+
// Style props
|
|
44
|
+
labelWidth?: number; // Width of the label when inline
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Utility functions for formatting values
|
|
48
|
+
const formatNumericValue = (text: string): string => {
|
|
49
|
+
// Only allow integer numbers
|
|
50
|
+
if (/^\d*$/.test(text)) {
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
return '';
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const formatDateValue = (text: string): string => {
|
|
57
|
+
// Remove any non-digit characters
|
|
58
|
+
if (!text || text === '') {
|
|
59
|
+
return text;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
const digitsOnly = text.replace(/\D/g, '');
|
|
64
|
+
|
|
65
|
+
// Format as dd/mm/yyyy
|
|
66
|
+
let formattedDate = '';
|
|
67
|
+
|
|
68
|
+
if (digitsOnly.length > 0) {
|
|
69
|
+
// Add day part (first 2 digits)
|
|
70
|
+
formattedDate = digitsOnly.substring(0, Math.min(2, digitsOnly.length));
|
|
71
|
+
|
|
72
|
+
// Add month part (next 2 digits)
|
|
73
|
+
if (digitsOnly.length > 2) {
|
|
74
|
+
formattedDate += '/' + digitsOnly.substring(2, Math.min(4, digitsOnly.length));
|
|
75
|
+
|
|
76
|
+
// Add year part (next 4 digits)
|
|
77
|
+
if (digitsOnly.length > 4) {
|
|
78
|
+
formattedDate += '/' + digitsOnly.substring(4, Math.min(8, digitsOnly.length));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return formattedDate;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const formatCurrencyValue = (text: string): string => {
|
|
87
|
+
// Handle empty input
|
|
88
|
+
if (!text || text === '') {
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check if the input ends with a decimal point
|
|
93
|
+
const endsWithDecimal = text.endsWith('.');
|
|
94
|
+
|
|
95
|
+
// Remove all non-digit characters except decimal points
|
|
96
|
+
let processedText = text.replace(/[^\d.]/g, '');
|
|
97
|
+
|
|
98
|
+
// Handle case where input starts with a decimal point
|
|
99
|
+
if (processedText.startsWith('.')) {
|
|
100
|
+
processedText = '0' + processedText;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Ensure only one decimal point
|
|
104
|
+
const decimalPointCount = (processedText.match(/\./g) || []).length;
|
|
105
|
+
if (decimalPointCount > 1) {
|
|
106
|
+
const parts = processedText.split('.');
|
|
107
|
+
processedText = parts[0] + '.' + parts.slice(1).join('');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Ensure only two decimal places
|
|
111
|
+
if (processedText.includes('.')) {
|
|
112
|
+
const parts = processedText.split('.');
|
|
113
|
+
if (parts[1].length > 2) {
|
|
114
|
+
processedText = parts[0] + '.' + parts[1].substring(0, 2);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Handle leading zeros in the whole number part
|
|
119
|
+
if (processedText.startsWith('0') && processedText.length > 1 && processedText[1] !== '.') {
|
|
120
|
+
processedText = processedText.replace(/^0+/, '');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Convert to number and format with commas
|
|
124
|
+
let formattedValue = '';
|
|
125
|
+
if (processedText) {
|
|
126
|
+
// Handle the case with decimal point
|
|
127
|
+
if (processedText.includes('.')) {
|
|
128
|
+
const parts = processedText.split('.');
|
|
129
|
+
// Handle case where whole number part is empty
|
|
130
|
+
const wholeNumber = parts[0] === '' ? '0' : parts[0];
|
|
131
|
+
// Format the whole number part with commas
|
|
132
|
+
formattedValue = Number(wholeNumber).toLocaleString('en-US') + '.' + parts[1];
|
|
133
|
+
} else {
|
|
134
|
+
// Format without decimal point
|
|
135
|
+
formattedValue = Number(processedText).toLocaleString('en-US');
|
|
136
|
+
|
|
137
|
+
// If the original input ended with a decimal point, add it back
|
|
138
|
+
if (endsWithDecimal) {
|
|
139
|
+
formattedValue += '.';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return formattedValue;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const FormField: React.FC<FormFieldProps> = ({
|
|
148
|
+
// Common props
|
|
149
|
+
labelTestID,
|
|
150
|
+
label,
|
|
151
|
+
required = false,
|
|
152
|
+
isInline = false, // Default to stacked layout
|
|
153
|
+
disabled = false, // Default to enabled
|
|
154
|
+
|
|
155
|
+
// Field type
|
|
156
|
+
fieldType = 'text',
|
|
157
|
+
|
|
158
|
+
// TextInput props
|
|
159
|
+
textInputTestID,
|
|
160
|
+
value = '',
|
|
161
|
+
onChangeText,
|
|
162
|
+
placeholder = '',
|
|
163
|
+
keyboardType = 'default',
|
|
164
|
+
|
|
165
|
+
// Dropdown props
|
|
166
|
+
dropdownTestID,
|
|
167
|
+
dropdownItems = [],
|
|
168
|
+
dropdownValue = 'value',
|
|
169
|
+
dropdownPlaceholder = 'Select an option',
|
|
170
|
+
onDropdownChange,
|
|
171
|
+
zIndex = 1000,
|
|
172
|
+
|
|
173
|
+
// Style props
|
|
174
|
+
labelWidth = 120, // Default label width for inline layout
|
|
175
|
+
}) => {
|
|
176
|
+
const isDropdown = fieldType === 'dropdown';
|
|
177
|
+
|
|
178
|
+
// State to hold the formatted value
|
|
179
|
+
const [formattedValue, setFormattedValue] = useState(value);
|
|
180
|
+
|
|
181
|
+
// Format the value when it changes from outside
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (value !== formattedValue) {
|
|
184
|
+
let newFormattedValue = value;
|
|
185
|
+
|
|
186
|
+
// Apply formatting based on field type
|
|
187
|
+
if (fieldType === 'numeric') {
|
|
188
|
+
newFormattedValue = formatNumericValue(value);
|
|
189
|
+
} else if (fieldType === 'date') {
|
|
190
|
+
newFormattedValue = formatDateValue(value);
|
|
191
|
+
} else if (fieldType === 'currency') {
|
|
192
|
+
newFormattedValue = formatCurrencyValue(value);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
setFormattedValue(newFormattedValue);
|
|
196
|
+
|
|
197
|
+
// If the formatted value is different from the original and we have an onChangeText handler,
|
|
198
|
+
// call it with the formatted value to update the parent component
|
|
199
|
+
if (newFormattedValue !== value && onChangeText) {
|
|
200
|
+
onChangeText(newFormattedValue);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}, [value, fieldType]);
|
|
204
|
+
|
|
205
|
+
// Handle numeric input
|
|
206
|
+
const handleNumericInput = (text: string) => {
|
|
207
|
+
if (onChangeText) {
|
|
208
|
+
const formattedText = formatNumericValue(text);
|
|
209
|
+
setFormattedValue(formattedText);
|
|
210
|
+
onChangeText(formattedText);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Handle date input in dd/mm/yyyy format
|
|
215
|
+
const handleDateInput = (text: string) => {
|
|
216
|
+
if (onChangeText) {
|
|
217
|
+
const formattedText = formatDateValue(text);
|
|
218
|
+
setFormattedValue(formattedText);
|
|
219
|
+
onChangeText(formattedText);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Handle currency input with automatic formatting
|
|
224
|
+
const handleCurrencyInput = (text: string) => {
|
|
225
|
+
if (onChangeText) {
|
|
226
|
+
const formattedText = formatCurrencyValue(text);
|
|
227
|
+
setFormattedValue(formattedText);
|
|
228
|
+
onChangeText(formattedText);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Determine the layout style based on the isInline flag
|
|
233
|
+
const containerStyle = isInline ? styles.inlineContainer : styles.stackedContainer;
|
|
234
|
+
const labelContainerStyle = isInline ?
|
|
235
|
+
[styles.labelContainer, styles.inlineLabelContainer] :
|
|
236
|
+
styles.labelContainer;
|
|
237
|
+
const inputContainerStyle = isInline ?
|
|
238
|
+
[styles.inputContainer, { flex: 1 }] :
|
|
239
|
+
styles.inputContainer;
|
|
240
|
+
|
|
241
|
+
// Apply specific styles for the input field based on layout and disabled state
|
|
242
|
+
const inputFieldStyle = [
|
|
243
|
+
styles.inputField,
|
|
244
|
+
isInline && styles.inlineInputField,
|
|
245
|
+
disabled && styles.disabledField
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<View style={[styles.formGroup, containerStyle]}>
|
|
250
|
+
{/* Label */}
|
|
251
|
+
<View style={labelContainerStyle}>
|
|
252
|
+
<Text testID={labelTestID} style={[styles.label, isDropdown && styles.dropdownLabel]}>
|
|
253
|
+
{label}
|
|
254
|
+
{required && <Text style={styles.requiredAsterisk}>*</Text>}
|
|
255
|
+
</Text>
|
|
256
|
+
</View>
|
|
257
|
+
|
|
258
|
+
{/* Input Field */}
|
|
259
|
+
<View style={inputContainerStyle}>
|
|
260
|
+
{fieldType === 'dropdown' ? (
|
|
261
|
+
<CustomDropdown
|
|
262
|
+
testID={dropdownTestID}
|
|
263
|
+
items={dropdownItems}
|
|
264
|
+
value={dropdownValue}
|
|
265
|
+
onChange={(val) => !disabled && onDropdownChange ? onDropdownChange(val) : null}
|
|
266
|
+
placeHolder={dropdownPlaceholder}
|
|
267
|
+
zIndex={zIndex}
|
|
268
|
+
disabled={disabled}
|
|
269
|
+
/>
|
|
270
|
+
) : (
|
|
271
|
+
<TextInput
|
|
272
|
+
testID={textInputTestID}
|
|
273
|
+
style={inputFieldStyle}
|
|
274
|
+
placeholder={placeholder}
|
|
275
|
+
placeholderTextColor={Colors.textMidnightColor}
|
|
276
|
+
value={formattedValue}
|
|
277
|
+
onChangeText={
|
|
278
|
+
!disabled ? (
|
|
279
|
+
fieldType === 'numeric' ? handleNumericInput :
|
|
280
|
+
fieldType === 'date' ? handleDateInput :
|
|
281
|
+
fieldType === 'currency' ? handleCurrencyInput :
|
|
282
|
+
(text) => {
|
|
283
|
+
setFormattedValue(text);
|
|
284
|
+
if (onChangeText) onChangeText(text);
|
|
285
|
+
}
|
|
286
|
+
) : undefined
|
|
287
|
+
}
|
|
288
|
+
keyboardType={
|
|
289
|
+
fieldType === 'numeric' || fieldType === 'date' ? 'number-pad' :
|
|
290
|
+
fieldType === 'currency' ? 'decimal-pad' :
|
|
291
|
+
keyboardType
|
|
292
|
+
}
|
|
293
|
+
editable={!disabled}
|
|
294
|
+
/>
|
|
295
|
+
)}
|
|
296
|
+
</View>
|
|
297
|
+
</View>
|
|
298
|
+
);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const styles = StyleSheet.create({
|
|
302
|
+
formGroup: {
|
|
303
|
+
marginBottom: 16,
|
|
304
|
+
borderBottomWidth: 0.5,
|
|
305
|
+
borderBottomColor: Colors.bottomDeviderColor,
|
|
306
|
+
},
|
|
307
|
+
// Layout styles
|
|
308
|
+
stackedContainer: {
|
|
309
|
+
// Default layout - stacked vertically
|
|
310
|
+
},
|
|
311
|
+
inlineContainer: {
|
|
312
|
+
flexDirection: 'row',
|
|
313
|
+
alignItems: 'center',
|
|
314
|
+
paddingBottom: 8,
|
|
315
|
+
},
|
|
316
|
+
labelContainer: {
|
|
317
|
+
marginBottom: 8,
|
|
318
|
+
},
|
|
319
|
+
inlineLabelContainer: {
|
|
320
|
+
marginBottom: 0,
|
|
321
|
+
marginRight: 12,
|
|
322
|
+
justifyContent: 'center',
|
|
323
|
+
},
|
|
324
|
+
inputContainer: {
|
|
325
|
+
// Container for the input field or dropdown
|
|
326
|
+
},
|
|
327
|
+
// Text and input styles
|
|
328
|
+
label: {
|
|
329
|
+
fontSize: 16,
|
|
330
|
+
color: Colors.placeholderColor,
|
|
331
|
+
fontWeight: '500',
|
|
332
|
+
},
|
|
333
|
+
dropdownLabel: {
|
|
334
|
+
marginBottom: 0,
|
|
335
|
+
},
|
|
336
|
+
requiredAsterisk: {
|
|
337
|
+
color: '#FF6161',
|
|
338
|
+
marginLeft: 4,
|
|
339
|
+
},
|
|
340
|
+
inputField: {
|
|
341
|
+
borderBottomWidth: 1,
|
|
342
|
+
borderBottomColor: Colors.bottomDeviderColor,
|
|
343
|
+
paddingVertical: 10,
|
|
344
|
+
fontSize: 16,
|
|
345
|
+
color: Colors.textMidnightColor,
|
|
346
|
+
fontWeight: '500',
|
|
347
|
+
outlineColor: 'transparent'
|
|
348
|
+
},
|
|
349
|
+
inlineInputField: {
|
|
350
|
+
borderBottomWidth: 0,
|
|
351
|
+
textAlign: 'right',
|
|
352
|
+
},
|
|
353
|
+
disabledField: {
|
|
354
|
+
opacity: 0.5,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
export default FormField;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
|
2
|
+
import {View, Platform} from 'react-native';
|
|
3
|
+
import {Keyboard, ScrollView, TextInput, StatusBar} from 'react-native';
|
|
4
|
+
|
|
5
|
+
interface KeyboardScrollViewProps extends React.ComponentProps<typeof ScrollView> {
|
|
6
|
+
additionalScrollHeight?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const KeyboardScrollView = ({
|
|
10
|
+
children,
|
|
11
|
+
additionalScrollHeight,
|
|
12
|
+
contentContainerStyle,
|
|
13
|
+
...props
|
|
14
|
+
}: KeyboardScrollViewProps) => {
|
|
15
|
+
const scrollViewRef = useRef<ScrollView>(null);
|
|
16
|
+
const scrollPositionRef = useRef<number>(0);
|
|
17
|
+
const scrollContentSizeRef = useRef<number>(0);
|
|
18
|
+
const scrollViewSizeRef = useRef<number>(0);
|
|
19
|
+
const currentYPosition = useRef<number>(0);
|
|
20
|
+
|
|
21
|
+
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
|
|
22
|
+
const [additionalPadding, setAdditionalPadding] = useState(0);
|
|
23
|
+
|
|
24
|
+
const scrollToPosition = useCallback(
|
|
25
|
+
(toPosition: number, animated?: boolean) => {
|
|
26
|
+
scrollViewRef.current?.scrollTo({y: toPosition, animated: !!animated});
|
|
27
|
+
scrollPositionRef.current = toPosition;
|
|
28
|
+
},
|
|
29
|
+
[],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const additionalScroll = useMemo(
|
|
33
|
+
() => additionalScrollHeight ?? 0,
|
|
34
|
+
[additionalScrollHeight],
|
|
35
|
+
);
|
|
36
|
+
const androidStatusBarOffset = useMemo(
|
|
37
|
+
() => StatusBar.currentHeight ?? 0,
|
|
38
|
+
[],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const didShowListener = Keyboard.addListener('keyboardDidShow', frames => {
|
|
43
|
+
const keyboardY = frames.endCoordinates.screenY;
|
|
44
|
+
const keyboardHeight = frames.endCoordinates.height;
|
|
45
|
+
setAdditionalPadding(Math.ceil(keyboardHeight));
|
|
46
|
+
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
setIsKeyboardVisible(true);
|
|
49
|
+
}, 100);
|
|
50
|
+
|
|
51
|
+
const currentlyFocusedInput = TextInput.State.currentlyFocusedInput();
|
|
52
|
+
const currentScrollY = scrollPositionRef.current;
|
|
53
|
+
currentYPosition.current = currentScrollY;
|
|
54
|
+
|
|
55
|
+
currentlyFocusedInput?.measureInWindow((_x, y, _width, height) => {
|
|
56
|
+
const endOfInputY = y + height + androidStatusBarOffset;
|
|
57
|
+
const deltaToScroll = endOfInputY - keyboardY;
|
|
58
|
+
|
|
59
|
+
if (deltaToScroll < 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const scrollPositionTarget =
|
|
64
|
+
currentScrollY + deltaToScroll + additionalScroll;
|
|
65
|
+
scrollToPosition(scrollPositionTarget, true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const didHideListener = Keyboard.addListener('keyboardDidHide', () => {
|
|
70
|
+
setAdditionalPadding(0);
|
|
71
|
+
setIsKeyboardVisible(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const willHideListener = Keyboard.addListener(
|
|
75
|
+
'keyboardWillHide',
|
|
76
|
+
frames => {
|
|
77
|
+
// iOS only, scroll back to initial position to avoid flickering
|
|
78
|
+
const keyboardHeight = frames.endCoordinates.height;
|
|
79
|
+
const currentScrollY = scrollPositionRef.current;
|
|
80
|
+
if (currentScrollY <= 0) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const scrollPositionTarget = currentScrollY - keyboardHeight;
|
|
85
|
+
scrollToPosition(currentYPosition.current, true);
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
didShowListener.remove();
|
|
91
|
+
didHideListener.remove();
|
|
92
|
+
willHideListener.remove();
|
|
93
|
+
};
|
|
94
|
+
}, [additionalScroll, androidStatusBarOffset, scrollToPosition]);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<ScrollView
|
|
98
|
+
ref={scrollViewRef}
|
|
99
|
+
contentContainerStyle={[contentContainerStyle]}
|
|
100
|
+
contentInset={{bottom: additionalPadding}}
|
|
101
|
+
keyboardShouldPersistTaps="never"
|
|
102
|
+
bounces={false}
|
|
103
|
+
onMomentumScrollEnd={event => {
|
|
104
|
+
scrollPositionRef.current = event.nativeEvent.contentOffset.y;
|
|
105
|
+
}}
|
|
106
|
+
onScrollEndDrag={event => {
|
|
107
|
+
scrollPositionRef.current = event.nativeEvent.contentOffset.y;
|
|
108
|
+
}}
|
|
109
|
+
onLayout={event => {
|
|
110
|
+
scrollViewSizeRef.current = event.nativeEvent.layout.height;
|
|
111
|
+
}}
|
|
112
|
+
onContentSizeChange={(_width, height) => {
|
|
113
|
+
const currentContentHeight = scrollContentSizeRef.current;
|
|
114
|
+
const contentSizeDelta = height - currentContentHeight;
|
|
115
|
+
scrollContentSizeRef.current = height;
|
|
116
|
+
if (!isKeyboardVisible) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const currentScrollY = scrollPositionRef.current;
|
|
120
|
+
const scrollPositionTarget = currentScrollY + contentSizeDelta;
|
|
121
|
+
scrollToPosition(scrollPositionTarget, true);
|
|
122
|
+
}}
|
|
123
|
+
{...props}>
|
|
124
|
+
<View style={{paddingBottom: Platform.OS === 'ios' ? 0 : additionalPadding}}>
|
|
125
|
+
{children}
|
|
126
|
+
</View>
|
|
127
|
+
</ScrollView>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export default KeyboardScrollView;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
TextInput,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
ViewStyle,
|
|
8
|
+
TextStyle,
|
|
9
|
+
NativeSyntheticEvent,
|
|
10
|
+
TextInputFocusEventData,
|
|
11
|
+
TextInputProps,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import { Colors } from '../utils/BaseStyle';
|
|
14
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
15
|
+
|
|
16
|
+
interface SearchViewInputProps extends Omit<TextInputProps, 'style'> {
|
|
17
|
+
/**
|
|
18
|
+
* Value of the search input
|
|
19
|
+
*/
|
|
20
|
+
value: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Callback when text changes
|
|
24
|
+
*/
|
|
25
|
+
onChangeText: (text: string) => void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Placeholder text for the search input
|
|
29
|
+
*/
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Callback when the clear button is pressed
|
|
34
|
+
*/
|
|
35
|
+
onClear?: () => void;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Callback when the search input is submitted
|
|
39
|
+
*/
|
|
40
|
+
onSubmit?: () => void;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Custom style for the container
|
|
44
|
+
*/
|
|
45
|
+
containerStyle?: ViewStyle;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Custom style for the input
|
|
49
|
+
*/
|
|
50
|
+
inputStyle?: TextStyle;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether to show the clear button when there is text
|
|
54
|
+
* @default true
|
|
55
|
+
*/
|
|
56
|
+
showClearButton?: boolean;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether to auto focus the input when mounted
|
|
60
|
+
* @default false
|
|
61
|
+
*/
|
|
62
|
+
autoFocus?: boolean;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Callback when the input is focused
|
|
66
|
+
*/
|
|
67
|
+
onFocus?: (e: NativeSyntheticEvent<TextInputFocusEventData>) => void;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Callback when the input is blurred
|
|
71
|
+
*/
|
|
72
|
+
onBlur?: (e: NativeSyntheticEvent<TextInputFocusEventData>) => void;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Test ID for the search input
|
|
76
|
+
*/
|
|
77
|
+
testID?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A reusable search input component with a clear button
|
|
82
|
+
*/
|
|
83
|
+
const SearchViewInput: React.FC<SearchViewInputProps> = ({
|
|
84
|
+
value,
|
|
85
|
+
onChangeText,
|
|
86
|
+
placeholder = 'Search',
|
|
87
|
+
onClear,
|
|
88
|
+
onSubmit,
|
|
89
|
+
containerStyle,
|
|
90
|
+
inputStyle,
|
|
91
|
+
showClearButton = true,
|
|
92
|
+
autoFocus = false,
|
|
93
|
+
onFocus,
|
|
94
|
+
onBlur,
|
|
95
|
+
testID,
|
|
96
|
+
...restProps
|
|
97
|
+
}) => {
|
|
98
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
99
|
+
const inputRef = useRef<TextInput>(null);
|
|
100
|
+
|
|
101
|
+
const handleFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
|
102
|
+
setIsFocused(true);
|
|
103
|
+
if (onFocus) {
|
|
104
|
+
onFocus(e);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const handleBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
|
109
|
+
setIsFocused(false);
|
|
110
|
+
if (onBlur) {
|
|
111
|
+
onBlur(e);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleClear = () => {
|
|
116
|
+
onChangeText('');
|
|
117
|
+
if (onClear) {
|
|
118
|
+
onClear();
|
|
119
|
+
}
|
|
120
|
+
// Focus the input after clearing
|
|
121
|
+
inputRef.current?.focus();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleSubmitEditing = () => {
|
|
125
|
+
if (onSubmit) {
|
|
126
|
+
onSubmit();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<View style={[styles.container, containerStyle]}>
|
|
132
|
+
<TextInput
|
|
133
|
+
ref={inputRef}
|
|
134
|
+
style={[styles.input, inputStyle]}
|
|
135
|
+
value={value}
|
|
136
|
+
onChangeText={onChangeText}
|
|
137
|
+
placeholder={placeholder}
|
|
138
|
+
placeholderTextColor={Colors.placeholderColor}
|
|
139
|
+
onFocus={handleFocus}
|
|
140
|
+
onBlur={handleBlur}
|
|
141
|
+
onSubmitEditing={handleSubmitEditing}
|
|
142
|
+
returnKeyType="search"
|
|
143
|
+
autoFocus={autoFocus}
|
|
144
|
+
testID={testID}
|
|
145
|
+
{...restProps}
|
|
146
|
+
/>
|
|
147
|
+
{showClearButton && value.length > 0 && (
|
|
148
|
+
<TouchableOpacity
|
|
149
|
+
style={styles.clearButton}
|
|
150
|
+
onPress={handleClear}
|
|
151
|
+
testID={`${testID}-clear-button`}
|
|
152
|
+
>
|
|
153
|
+
<Ionicons name="close-circle" size={24} color={Colors.darkGrayColor} />
|
|
154
|
+
</TouchableOpacity>
|
|
155
|
+
)}
|
|
156
|
+
</View>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const styles = StyleSheet.create({
|
|
161
|
+
container: {
|
|
162
|
+
flexDirection: 'row',
|
|
163
|
+
alignItems: 'center',
|
|
164
|
+
borderWidth: 1,
|
|
165
|
+
borderColor: Colors.borderColor,
|
|
166
|
+
borderRadius: 10,
|
|
167
|
+
paddingStart: 16,
|
|
168
|
+
paddingEnd: 8,
|
|
169
|
+
height: 50,
|
|
170
|
+
},
|
|
171
|
+
input: {
|
|
172
|
+
flex: 1,
|
|
173
|
+
height: '100%',
|
|
174
|
+
fontSize: 16,
|
|
175
|
+
color: Colors.textMidnightColor,
|
|
176
|
+
padding: 0,
|
|
177
|
+
},
|
|
178
|
+
clearButton: {
|
|
179
|
+
padding: 4,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
export default SearchViewInput;
|