react-native-auto-positioned-popup 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/LICENSE +21 -0
- package/README.md +425 -0
- package/README_zh.md +425 -0
- package/lib/AutoPositionedPopup.d.ts +5 -0
- package/lib/AutoPositionedPopup.d.ts.map +1 -0
- package/lib/AutoPositionedPopup.js +306 -0
- package/lib/AutoPositionedPopup.style.d.ts +80 -0
- package/lib/AutoPositionedPopup.style.d.ts.map +1 -0
- package/lib/AutoPositionedPopup.style.js +79 -0
- package/lib/AutoPositionedPopupProps.d.ts +58 -0
- package/lib/AutoPositionedPopupProps.d.ts.map +1 -0
- package/lib/AutoPositionedPopupProps.js +1 -0
- package/lib/RootViewContext.d.ts +31 -0
- package/lib/RootViewContext.d.ts.map +1 -0
- package/lib/RootViewContext.js +136 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +7 -0
- package/package.json +82 -0
- package/src/AutoPositionedPopup.style.ts +80 -0
- package/src/AutoPositionedPopup.tsx +529 -0
- package/src/AutoPositionedPopupProps.ts +61 -0
- package/src/RootViewContext.tsx +186 -0
- package/src/index.ts +16 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
ForwardedRef,
|
|
3
|
+
forwardRef,
|
|
4
|
+
ForwardRefExoticComponent,
|
|
5
|
+
memo,
|
|
6
|
+
MemoExoticComponent,
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useImperativeHandle,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from 'react';
|
|
14
|
+
import {
|
|
15
|
+
Dimensions,
|
|
16
|
+
Keyboard,
|
|
17
|
+
Text,
|
|
18
|
+
TextInput as RNTextInput,
|
|
19
|
+
TouchableOpacity,
|
|
20
|
+
View,
|
|
21
|
+
ViewStyle,
|
|
22
|
+
} from 'react-native';
|
|
23
|
+
import {AdvancedFlatList} from 'react-native-advanced-flatlist';
|
|
24
|
+
import {TextInputSubmitEditingEventData} from 'react-native/Libraries/Components/TextInput/TextInput';
|
|
25
|
+
import {LayoutRectangle, NativeSyntheticEvent} from 'react-native/Libraries/Types/CoreEventTypes';
|
|
26
|
+
import {AutoPositionedPopupProps, Data, SelectedItem} from './AutoPositionedPopupProps';
|
|
27
|
+
import styles from './AutoPositionedPopup.style';
|
|
28
|
+
import {useRootView} from './RootViewContext';
|
|
29
|
+
|
|
30
|
+
// Lightweight emitter to decouple TextInput and list without re-rendering context
|
|
31
|
+
type QueryListener = (query: string) => void;
|
|
32
|
+
const queryChangeListeners: QueryListener[] = [];
|
|
33
|
+
const emitQueryChange = (query: string) => {
|
|
34
|
+
console.log('AutoPositionedPopup.tsx emitQueryChange query=', query, ' listeners=', queryChangeListeners.length);
|
|
35
|
+
queryChangeListeners.forEach((l) => l(query));
|
|
36
|
+
};
|
|
37
|
+
const subscribeQueryChange = (listener: QueryListener) => {
|
|
38
|
+
queryChangeListeners.push(listener);
|
|
39
|
+
return () => {
|
|
40
|
+
const idx = queryChangeListeners.indexOf(listener);
|
|
41
|
+
if (idx !== -1) queryChangeListeners.splice(idx, 1);
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Default theme colors interface
|
|
46
|
+
interface Theme {
|
|
47
|
+
colors: {
|
|
48
|
+
text: string;
|
|
49
|
+
placeholderText: string;
|
|
50
|
+
background: string;
|
|
51
|
+
border: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Default light theme
|
|
56
|
+
const defaultTheme: Theme = {
|
|
57
|
+
colors: {
|
|
58
|
+
text: '#333333',
|
|
59
|
+
placeholderText: '#999999',
|
|
60
|
+
background: '#FFFFFF',
|
|
61
|
+
border: '#E0E0E0',
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// List item component for rendering individual items
|
|
66
|
+
const ListItem: React.FC<{
|
|
67
|
+
item: SelectedItem;
|
|
68
|
+
index: number;
|
|
69
|
+
selectedItem?: SelectedItem;
|
|
70
|
+
onItemPress: (item: SelectedItem) => void;
|
|
71
|
+
theme: Theme;
|
|
72
|
+
rootViewsRef?: React.MutableRefObject<any[]>;
|
|
73
|
+
selectedItemBackgroundColor?: string;
|
|
74
|
+
}> = memo(({item, index, selectedItem, onItemPress, theme, rootViewsRef, selectedItemBackgroundColor = 'rgba(116, 116, 128, 0.08)'}) => {
|
|
75
|
+
const isSelected = item.id === selectedItem?.id;
|
|
76
|
+
|
|
77
|
+
return useMemo(() => (
|
|
78
|
+
<TouchableOpacity
|
|
79
|
+
key={item.id}
|
|
80
|
+
style={[
|
|
81
|
+
styles.commonModalRow,
|
|
82
|
+
{
|
|
83
|
+
backgroundColor: isSelected ? selectedItemBackgroundColor : 'transparent',
|
|
84
|
+
borderColor: theme.colors.border,
|
|
85
|
+
},
|
|
86
|
+
]}
|
|
87
|
+
onPress={() => {
|
|
88
|
+
console.log('AutoPositionedPopup.tsx ListItem onPress item=', item);
|
|
89
|
+
if (rootViewsRef) {
|
|
90
|
+
console.log('AutoPositionedPopup.tsx ListItem onPress rootViews=', rootViewsRef.current);
|
|
91
|
+
}
|
|
92
|
+
onItemPress(item);
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<Text
|
|
96
|
+
style={[styles.ListItemCode, {color: theme.colors.text}]}
|
|
97
|
+
numberOfLines={1}
|
|
98
|
+
ellipsizeMode="tail"
|
|
99
|
+
>
|
|
100
|
+
{item.title}
|
|
101
|
+
</Text>
|
|
102
|
+
</TouchableOpacity>
|
|
103
|
+
), [item, index, selectedItem, onItemPress, theme, rootViewsRef, isSelected, selectedItemBackgroundColor]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Popup list component with AdvancedFlatList
|
|
107
|
+
interface PopupListProps {
|
|
108
|
+
data: SelectedItem[];
|
|
109
|
+
selectedItem?: SelectedItem;
|
|
110
|
+
onItemPress: (item: SelectedItem) => void;
|
|
111
|
+
renderItem?: ({item, index}: { item: SelectedItem; index: number }) => React.ReactElement;
|
|
112
|
+
keyExtractor?: (item: SelectedItem) => string;
|
|
113
|
+
theme: Theme;
|
|
114
|
+
rootViewsRef?: React.MutableRefObject<any[]>;
|
|
115
|
+
fetchData?: (params: { pageIndex: number; pageSize: number; searchQuery?: string }) => Promise<Data | null>;
|
|
116
|
+
localSearch?: boolean;
|
|
117
|
+
pageSize?: number;
|
|
118
|
+
onDataUpdate?: (newData: SelectedItem[]) => void;
|
|
119
|
+
selectedItemBackgroundColor?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const PopupList: React.FC<PopupListProps> = memo(({
|
|
123
|
+
data,
|
|
124
|
+
selectedItem,
|
|
125
|
+
onItemPress,
|
|
126
|
+
renderItem,
|
|
127
|
+
keyExtractor = (item: SelectedItem) => String(item.id),
|
|
128
|
+
theme,
|
|
129
|
+
rootViewsRef,
|
|
130
|
+
fetchData,
|
|
131
|
+
localSearch = false,
|
|
132
|
+
pageSize = 20,
|
|
133
|
+
onDataUpdate,
|
|
134
|
+
selectedItemBackgroundColor,
|
|
135
|
+
}) => {
|
|
136
|
+
const [internalData, setInternalData] = useState<SelectedItem[]>(data);
|
|
137
|
+
const searchQueryRef = useRef<string>('');
|
|
138
|
+
|
|
139
|
+
// Sync external data changes
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
setInternalData(data);
|
|
142
|
+
}, [data]);
|
|
143
|
+
|
|
144
|
+
// Listen to search query changes
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const unsubscribe = subscribeQueryChange(async (newQuery: string) => {
|
|
147
|
+
console.log('PopupList subscribeQueryChange newQuery=', newQuery);
|
|
148
|
+
searchQueryRef.current = newQuery;
|
|
149
|
+
|
|
150
|
+
if (fetchData) {
|
|
151
|
+
try {
|
|
152
|
+
const result = await fetchData({
|
|
153
|
+
pageIndex: 0,
|
|
154
|
+
pageSize,
|
|
155
|
+
searchQuery: newQuery,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (result?.items) {
|
|
159
|
+
setInternalData(result.items);
|
|
160
|
+
onDataUpdate?.(result.items);
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('PopupList fetchData error:', error);
|
|
164
|
+
}
|
|
165
|
+
} else if (localSearch) {
|
|
166
|
+
// Local filtering
|
|
167
|
+
const filtered = data.filter(item =>
|
|
168
|
+
item.title.toLowerCase().includes(newQuery.toLowerCase())
|
|
169
|
+
);
|
|
170
|
+
setInternalData(filtered);
|
|
171
|
+
onDataUpdate?.(filtered);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return unsubscribe;
|
|
176
|
+
}, [fetchData, localSearch, pageSize, data, onDataUpdate]);
|
|
177
|
+
const defaultRenderItem = useCallback(
|
|
178
|
+
({item, index}: { item: SelectedItem; index: number }) => (
|
|
179
|
+
<ListItem
|
|
180
|
+
item={item}
|
|
181
|
+
index={index}
|
|
182
|
+
selectedItem={selectedItem}
|
|
183
|
+
onItemPress={onItemPress}
|
|
184
|
+
theme={theme}
|
|
185
|
+
rootViewsRef={rootViewsRef}
|
|
186
|
+
selectedItemBackgroundColor={selectedItemBackgroundColor}
|
|
187
|
+
/>
|
|
188
|
+
),
|
|
189
|
+
[selectedItem, onItemPress, theme, rootViewsRef, selectedItemBackgroundColor]
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<View style={[styles.autoPositionedPopupList, {backgroundColor: theme.colors.background}]}>
|
|
194
|
+
<AdvancedFlatList
|
|
195
|
+
data={internalData}
|
|
196
|
+
keyExtractor={keyExtractor}
|
|
197
|
+
renderItem={renderItem || defaultRenderItem}
|
|
198
|
+
keyboardShouldPersistTaps="always"
|
|
199
|
+
showsVerticalScrollIndicator={true}
|
|
200
|
+
nestedScrollEnabled={true}
|
|
201
|
+
/>
|
|
202
|
+
</View>
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Main AutoPositionedPopup component
|
|
207
|
+
const AutoPositionedPopup: MemoExoticComponent<
|
|
208
|
+
ForwardRefExoticComponent<AutoPositionedPopupProps>
|
|
209
|
+
> = memo(
|
|
210
|
+
forwardRef<unknown, AutoPositionedPopupProps>(
|
|
211
|
+
(props: AutoPositionedPopupProps, parentRef: ForwardedRef<unknown>): React.JSX.Element => {
|
|
212
|
+
const {
|
|
213
|
+
tag,
|
|
214
|
+
style,
|
|
215
|
+
AutoPositionedPopupBtnStyle,
|
|
216
|
+
placeholder = 'Please Select',
|
|
217
|
+
textAlign = 'right',
|
|
218
|
+
onSubmitEditing,
|
|
219
|
+
TextInputProps = {},
|
|
220
|
+
inputStyle,
|
|
221
|
+
labelStyle,
|
|
222
|
+
popUpViewStyle = {left: '5%', width: '90%'},
|
|
223
|
+
fetchData,
|
|
224
|
+
renderItem,
|
|
225
|
+
onItemSelected,
|
|
226
|
+
localSearch = false,
|
|
227
|
+
pageSize = 20,
|
|
228
|
+
selectedItem,
|
|
229
|
+
useTextInput = false,
|
|
230
|
+
btwChildren,
|
|
231
|
+
CustomRow = ({children}) => <View>{children}</View>,
|
|
232
|
+
keyExtractor = (item: any) => item?.id,
|
|
233
|
+
AutoPositionedPopupBtnDisabled = false,
|
|
234
|
+
forceRemoveAllRootViewOnItemSelected = false,
|
|
235
|
+
centerDisplay = false,
|
|
236
|
+
selectedItemBackgroundColor = 'rgba(116, 116, 128, 0.08)',
|
|
237
|
+
} = props;
|
|
238
|
+
|
|
239
|
+
// Use RootView context
|
|
240
|
+
const {addRootView, removeRootView, rootViews, searchQuery: contextSearchQuery, setSearchQuery: setContextSearchQuery} = useRootView();
|
|
241
|
+
const rootViewsRef = useRef(rootViews);
|
|
242
|
+
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
rootViewsRef.current = rootViews;
|
|
245
|
+
}, [rootViews]);
|
|
246
|
+
|
|
247
|
+
// State management
|
|
248
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
249
|
+
const [data, setData] = useState<SelectedItem[]>([]);
|
|
250
|
+
const [loading, setLoading] = useState(false);
|
|
251
|
+
const [popupPosition, setPopupPosition] = useState<{
|
|
252
|
+
top: number;
|
|
253
|
+
left: number;
|
|
254
|
+
width: number;
|
|
255
|
+
}>({top: 0, left: 0, width: 0});
|
|
256
|
+
const popupId = useRef(`popup-${tag}-${Date.now()}`);
|
|
257
|
+
|
|
258
|
+
// Refs for performance optimization
|
|
259
|
+
const containerRef = useRef<View>(null);
|
|
260
|
+
const textInputRef = useRef<RNTextInput>(null);
|
|
261
|
+
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
262
|
+
const searchQueryRef = useRef<string>(''); // Use ref instead of state to avoid re-renders
|
|
263
|
+
|
|
264
|
+
// Constants
|
|
265
|
+
const LIST_HEIGHT = 200;
|
|
266
|
+
const theme = defaultTheme;
|
|
267
|
+
|
|
268
|
+
// Fetch data function
|
|
269
|
+
const loadData = useCallback(async (query: string = '') => {
|
|
270
|
+
if (!fetchData) return;
|
|
271
|
+
|
|
272
|
+
setLoading(true);
|
|
273
|
+
try {
|
|
274
|
+
const result = await fetchData({
|
|
275
|
+
pageIndex: 0,
|
|
276
|
+
pageSize,
|
|
277
|
+
searchQuery: query,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
if (result?.items) {
|
|
281
|
+
setData(result.items);
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error('Error loading data:', error);
|
|
285
|
+
} finally {
|
|
286
|
+
setLoading(false);
|
|
287
|
+
}
|
|
288
|
+
}, [fetchData, pageSize]);
|
|
289
|
+
|
|
290
|
+
// Handle search query change with debounce and event emission
|
|
291
|
+
const handleSearchChange = useCallback((query: string) => {
|
|
292
|
+
// Store in ref to avoid re-renders
|
|
293
|
+
searchQueryRef.current = query;
|
|
294
|
+
|
|
295
|
+
// Update TextInput value directly if needed
|
|
296
|
+
if (textInputRef.current) {
|
|
297
|
+
// The TextInput's value will be controlled by its own state
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Clear previous debounce timer
|
|
301
|
+
if (debounceTimerRef.current) {
|
|
302
|
+
clearTimeout(debounceTimerRef.current);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Use debounce for performance optimization
|
|
306
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
307
|
+
// Emit query change event to decouple components and avoid context re-rendering
|
|
308
|
+
emitQueryChange(searchQueryRef.current);
|
|
309
|
+
}, 300); // Use 300ms debounce like the original
|
|
310
|
+
}, []);
|
|
311
|
+
|
|
312
|
+
// Calculate popup position
|
|
313
|
+
const calculatePosition = useCallback(() => {
|
|
314
|
+
if (!containerRef.current) return;
|
|
315
|
+
|
|
316
|
+
containerRef.current.measureInWindow((x, y, width, height) => {
|
|
317
|
+
const screenHeight = Dimensions.get('screen').height;
|
|
318
|
+
const screenWidth = Dimensions.get('screen').width;
|
|
319
|
+
|
|
320
|
+
let top = y + height;
|
|
321
|
+
let left = x;
|
|
322
|
+
let popupWidth = width;
|
|
323
|
+
|
|
324
|
+
// Check if popup should appear above the input
|
|
325
|
+
if (y + height + LIST_HEIGHT > screenHeight) {
|
|
326
|
+
top = y - LIST_HEIGHT;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Adjust horizontal position if needed
|
|
330
|
+
if (popUpViewStyle?.left && popUpViewStyle?.width) {
|
|
331
|
+
const leftPercent = parseFloat(String(popUpViewStyle.left).replace('%', '')) / 100;
|
|
332
|
+
const widthPercent = parseFloat(String(popUpViewStyle.width).replace('%', '')) / 100;
|
|
333
|
+
left = screenWidth * leftPercent;
|
|
334
|
+
popupWidth = screenWidth * widthPercent;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
setPopupPosition({top, left, width: popupWidth});
|
|
338
|
+
});
|
|
339
|
+
}, [popUpViewStyle]);
|
|
340
|
+
|
|
341
|
+
// Hide popup using RootView
|
|
342
|
+
const hidePopup = useCallback(() => {
|
|
343
|
+
setIsVisible(false);
|
|
344
|
+
// Reset search query
|
|
345
|
+
searchQueryRef.current = '';
|
|
346
|
+
if (textInputRef.current) {
|
|
347
|
+
textInputRef.current.blur();
|
|
348
|
+
textInputRef.current.clear?.(); // Clear the TextInput
|
|
349
|
+
}
|
|
350
|
+
removeRootView(popupId.current, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
351
|
+
}, [removeRootView, forceRemoveAllRootViewOnItemSelected]);
|
|
352
|
+
|
|
353
|
+
// Handle data updates from PopupList
|
|
354
|
+
const handleDataUpdate = useCallback((newData: SelectedItem[]) => {
|
|
355
|
+
setData(newData);
|
|
356
|
+
}, []);
|
|
357
|
+
|
|
358
|
+
// Handle item selection
|
|
359
|
+
const handleItemPress = useCallback((item: SelectedItem) => {
|
|
360
|
+
onItemSelected?.(item);
|
|
361
|
+
hidePopup();
|
|
362
|
+
}, [onItemSelected, hidePopup]);
|
|
363
|
+
|
|
364
|
+
// Show popup using RootView
|
|
365
|
+
const showPopup = useCallback(() => {
|
|
366
|
+
calculatePosition();
|
|
367
|
+
setIsVisible(true);
|
|
368
|
+
loadData(searchQueryRef.current);
|
|
369
|
+
|
|
370
|
+
// Wait for position to be calculated
|
|
371
|
+
setTimeout(() => {
|
|
372
|
+
const popupComponent = (
|
|
373
|
+
<TouchableOpacity
|
|
374
|
+
style={{
|
|
375
|
+
flex: 1,
|
|
376
|
+
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
377
|
+
}}
|
|
378
|
+
activeOpacity={1}
|
|
379
|
+
onPress={hidePopup}
|
|
380
|
+
>
|
|
381
|
+
<View
|
|
382
|
+
style={{
|
|
383
|
+
position: 'absolute',
|
|
384
|
+
top: popupPosition.top,
|
|
385
|
+
left: popupPosition.left,
|
|
386
|
+
width: popupPosition.width,
|
|
387
|
+
height: LIST_HEIGHT,
|
|
388
|
+
backgroundColor: theme.colors.background,
|
|
389
|
+
borderRadius: 8,
|
|
390
|
+
shadowColor: '#000',
|
|
391
|
+
shadowOffset: {width: 0, height: 2},
|
|
392
|
+
shadowOpacity: 0.25,
|
|
393
|
+
shadowRadius: 3.84,
|
|
394
|
+
elevation: 5,
|
|
395
|
+
}}
|
|
396
|
+
>
|
|
397
|
+
{useTextInput && (
|
|
398
|
+
<RNTextInput
|
|
399
|
+
ref={textInputRef}
|
|
400
|
+
style={[
|
|
401
|
+
styles.inputStyle,
|
|
402
|
+
{
|
|
403
|
+
height: 40,
|
|
404
|
+
borderBottomWidth: 1,
|
|
405
|
+
borderBottomColor: theme.colors.border,
|
|
406
|
+
paddingHorizontal: 12,
|
|
407
|
+
color: theme.colors.text,
|
|
408
|
+
},
|
|
409
|
+
inputStyle,
|
|
410
|
+
]}
|
|
411
|
+
placeholder={placeholder}
|
|
412
|
+
placeholderTextColor={theme.colors.placeholderText}
|
|
413
|
+
defaultValue={searchQueryRef.current}
|
|
414
|
+
onChangeText={handleSearchChange}
|
|
415
|
+
onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
|
|
416
|
+
onSubmitEditing?.(e);
|
|
417
|
+
Keyboard.dismiss();
|
|
418
|
+
}}
|
|
419
|
+
returnKeyType="done"
|
|
420
|
+
{...TextInputProps}
|
|
421
|
+
/>
|
|
422
|
+
)}
|
|
423
|
+
|
|
424
|
+
<PopupList
|
|
425
|
+
data={data}
|
|
426
|
+
selectedItem={selectedItem}
|
|
427
|
+
onItemPress={handleItemPress}
|
|
428
|
+
renderItem={renderItem}
|
|
429
|
+
keyExtractor={keyExtractor}
|
|
430
|
+
theme={theme}
|
|
431
|
+
rootViewsRef={rootViewsRef}
|
|
432
|
+
fetchData={fetchData}
|
|
433
|
+
localSearch={localSearch}
|
|
434
|
+
pageSize={pageSize}
|
|
435
|
+
onDataUpdate={handleDataUpdate}
|
|
436
|
+
selectedItemBackgroundColor={selectedItemBackgroundColor}
|
|
437
|
+
/>
|
|
438
|
+
</View>
|
|
439
|
+
</TouchableOpacity>
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
addRootView({
|
|
443
|
+
id: popupId.current,
|
|
444
|
+
style: {
|
|
445
|
+
position: 'absolute',
|
|
446
|
+
top: 0,
|
|
447
|
+
left: 0,
|
|
448
|
+
right: 0,
|
|
449
|
+
bottom: 0,
|
|
450
|
+
},
|
|
451
|
+
component: popupComponent,
|
|
452
|
+
useModal: true,
|
|
453
|
+
onModalClose: hidePopup,
|
|
454
|
+
centerDisplay: centerDisplay,
|
|
455
|
+
});
|
|
456
|
+
}, 100);
|
|
457
|
+
}, [calculatePosition, loadData, popupPosition, useTextInput, placeholder, theme, inputStyle, TextInputProps, data, selectedItem, renderItem, keyExtractor, centerDisplay, addRootView, hidePopup, handleSearchChange, handleItemPress, LIST_HEIGHT, selectedItemBackgroundColor]);
|
|
458
|
+
|
|
459
|
+
// Handle button press
|
|
460
|
+
const handleButtonPress = useCallback(() => {
|
|
461
|
+
if (AutoPositionedPopupBtnDisabled) return;
|
|
462
|
+
|
|
463
|
+
if (useTextInput) {
|
|
464
|
+
showPopup();
|
|
465
|
+
// Focus text input after a short delay
|
|
466
|
+
setTimeout(() => {
|
|
467
|
+
textInputRef.current?.focus();
|
|
468
|
+
}, 100);
|
|
469
|
+
} else {
|
|
470
|
+
showPopup();
|
|
471
|
+
}
|
|
472
|
+
}, [AutoPositionedPopupBtnDisabled, useTextInput, showPopup]);
|
|
473
|
+
|
|
474
|
+
// Imperative handle for parent component access
|
|
475
|
+
useImperativeHandle(
|
|
476
|
+
parentRef,
|
|
477
|
+
() => ({
|
|
478
|
+
clearSelectedItem: () => {
|
|
479
|
+
// Clear selection logic can be implemented here
|
|
480
|
+
console.log('Clearing selected item for:', tag);
|
|
481
|
+
},
|
|
482
|
+
showPopup,
|
|
483
|
+
hidePopup,
|
|
484
|
+
}),
|
|
485
|
+
[tag, showPopup, hidePopup]
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
// Cleanup
|
|
489
|
+
useEffect(() => {
|
|
490
|
+
return () => {
|
|
491
|
+
if (debounceTimerRef.current) {
|
|
492
|
+
clearTimeout(debounceTimerRef.current);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}, []);
|
|
496
|
+
|
|
497
|
+
// Render the component
|
|
498
|
+
return (
|
|
499
|
+
<CustomRow>
|
|
500
|
+
<View style={[styles.contain, style]} ref={containerRef}>
|
|
501
|
+
<TouchableOpacity
|
|
502
|
+
style={[styles.AutoPositionedPopupBtn, AutoPositionedPopupBtnStyle]}
|
|
503
|
+
disabled={AutoPositionedPopupBtnDisabled}
|
|
504
|
+
onPress={handleButtonPress}
|
|
505
|
+
>
|
|
506
|
+
{btwChildren ? (
|
|
507
|
+
btwChildren()
|
|
508
|
+
) : (
|
|
509
|
+
<Text
|
|
510
|
+
style={[
|
|
511
|
+
styles.searchQueryTxt,
|
|
512
|
+
selectedItem && {color: theme.colors.text},
|
|
513
|
+
labelStyle,
|
|
514
|
+
]}
|
|
515
|
+
numberOfLines={1}
|
|
516
|
+
ellipsizeMode="tail"
|
|
517
|
+
>
|
|
518
|
+
{selectedItem?.title || placeholder}
|
|
519
|
+
</Text>
|
|
520
|
+
)}
|
|
521
|
+
</TouchableOpacity>
|
|
522
|
+
</View>
|
|
523
|
+
</CustomRow>
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
)
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
export default AutoPositionedPopup;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleProp, TextInputProps, TextStyle, ViewStyle } from 'react-native';
|
|
3
|
+
import { TextInputSubmitEditingEventData } from 'react-native/Libraries/Components/TextInput/TextInput';
|
|
4
|
+
import { NativeSyntheticEvent } from 'react-native/Libraries/Types/CoreEventTypes';
|
|
5
|
+
|
|
6
|
+
export interface Data {
|
|
7
|
+
items: any[];
|
|
8
|
+
pageIndex: number;
|
|
9
|
+
needLoadMore: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SelectedItem {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Props interface for AutoPositionedPopup component
|
|
19
|
+
*/
|
|
20
|
+
export interface AutoPositionedPopupProps {
|
|
21
|
+
style?: ViewStyle;
|
|
22
|
+
labelStyle?: ViewStyle;
|
|
23
|
+
tag: string;
|
|
24
|
+
tagStyle?: ViewStyle;
|
|
25
|
+
fetchData?: ({
|
|
26
|
+
pageIndex,
|
|
27
|
+
pageSize,
|
|
28
|
+
searchQuery,
|
|
29
|
+
}: {
|
|
30
|
+
pageIndex: number;
|
|
31
|
+
pageSize: number;
|
|
32
|
+
searchQuery?: string;
|
|
33
|
+
}) => Promise<Data | null>;
|
|
34
|
+
renderItem?: ({ item, index }: { item: SelectedItem; index: number }) => React.ReactElement;
|
|
35
|
+
onItemSelected?: (item: SelectedItem) => void;
|
|
36
|
+
onSubmitEditing?: (e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => void;
|
|
37
|
+
localSearch?: boolean;
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
textAlign?: 'left' | 'center' | 'right' | undefined;
|
|
40
|
+
pageSize?: number;
|
|
41
|
+
selectedItem?: SelectedItem | any;
|
|
42
|
+
CustomRow?: React.ComponentType<ViewStyle & { children?: React.ReactNode }>;
|
|
43
|
+
btwChildren?: () => React.ReactNode;
|
|
44
|
+
useTextInput?: boolean;
|
|
45
|
+
keyExtractor?: (item: SelectedItem) => string;
|
|
46
|
+
CustomPopView?: () => React.ComponentType<
|
|
47
|
+
ViewStyle & {
|
|
48
|
+
children?: React.ReactNode;
|
|
49
|
+
selectedItem?: SelectedItem | any;
|
|
50
|
+
}
|
|
51
|
+
>;
|
|
52
|
+
CustomPopViewStyle?: ViewStyle;
|
|
53
|
+
forceRemoveAllRootViewOnItemSelected?: boolean;
|
|
54
|
+
inputStyle?: StyleProp<TextStyle>;
|
|
55
|
+
TextInputProps?: TextInputProps;
|
|
56
|
+
popUpViewStyle?: ViewStyle;
|
|
57
|
+
AutoPositionedPopupBtnStyle?: ViewStyle;
|
|
58
|
+
AutoPositionedPopupBtnDisabled?: boolean;
|
|
59
|
+
centerDisplay?: boolean;
|
|
60
|
+
selectedItemBackgroundColor?: string;
|
|
61
|
+
}
|