react-native-auto-positioned-popup 1.0.5 → 1.0.7

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.
@@ -3,7 +3,7 @@ import React, {
3
3
  forwardRef,
4
4
  ForwardRefExoticComponent,
5
5
  memo,
6
- MemoExoticComponent,
6
+ MemoExoticComponent, MutableRefObject,
7
7
  useCallback,
8
8
  useEffect,
9
9
  useImperativeHandle,
@@ -19,12 +19,17 @@ import {
19
19
  TouchableOpacity,
20
20
  View,
21
21
  } from 'react-native';
22
- import {AdvancedFlatList} from 'react-native-advanced-flatlist';
22
+ // @ts-ignore - Skip type checking for third-party library with type issues
23
+ import {AdvancedFlatList as AdvancedFlatListLib} from 'react-native-advanced-flatlist';
24
+ // @ts-ignore - Direct import from source when using fake data
25
+ import AdvancedFlatListSource from 'react-native-advanced-flatlist/src/AdvancedFlatList.tsx';
23
26
  import {TextInputSubmitEditingEventData} from 'react-native/Libraries/Components/TextInput/TextInput';
24
- import {NativeSyntheticEvent} from 'react-native/Libraries/Types/CoreEventTypes';
27
+ import {LayoutRectangle, NativeSyntheticEvent} from 'react-native/Libraries/Types/CoreEventTypes';
25
28
  import {AutoPositionedPopupProps, Data, SelectedItem} from './AutoPositionedPopupProps';
26
29
  import styles from './AutoPositionedPopup.style';
27
30
  import {useRootView} from './RootViewContext';
31
+ import {useKeyboardStatus} from './KeyboardManager';
32
+ import {useSafeAreaInsets} from "react-native-safe-area-context";
28
33
 
29
34
  // Lightweight emitter to decouple TextInput and list without re-rendering context
30
35
  type QueryListener = (query: string) => void;
@@ -63,144 +68,227 @@ const defaultTheme: Theme = {
63
68
 
64
69
  // List item component for rendering individual items
65
70
  const ListItem: React.FC<{
71
+ updateState: (key: string, value: SelectedItem) => void;
66
72
  item: SelectedItem;
67
73
  index: number;
68
74
  selectedItem?: SelectedItem;
69
- onItemPress: (item: SelectedItem) => void;
70
- theme: Theme;
71
- rootViewsRef?: React.MutableRefObject<any[]>;
72
- selectedItemBackgroundColor?: string;
73
- }> = memo(({item, index, selectedItem, onItemPress, theme, rootViewsRef, selectedItemBackgroundColor = 'rgba(116, 116, 128, 0.08)'}) => {
74
- const isSelected = item.id === selectedItem?.id;
75
-
76
- return useMemo(() => (
77
- <TouchableOpacity
78
- key={item.id}
79
- style={[
80
- styles.commonModalRow,
81
- {
82
- backgroundColor: isSelected ? selectedItemBackgroundColor : 'transparent',
83
- borderColor: theme.colors.border,
84
- },
85
- ]}
86
- onPress={() => {
87
- console.log('AutoPositionedPopup.tsx ListItem onPress item=', item);
88
- if (rootViewsRef) {
89
- console.log('AutoPositionedPopup.tsx ListItem onPress rootViews=', rootViewsRef.current);
90
- }
91
- onItemPress(item);
92
- }}
93
- >
94
- <Text
95
- style={[styles.ListItemCode, {color: theme.colors.text}]}
96
- numberOfLines={1}
97
- ellipsizeMode="tail"
98
- >
99
- {item.title}
100
- </Text>
101
- </TouchableOpacity>
102
- ), [item, index, selectedItem, onItemPress, theme, rootViewsRef, isSelected, selectedItemBackgroundColor]);
103
- });
75
+ }> = memo(
76
+ ({
77
+ updateState,
78
+ item,
79
+ index,
80
+ selectedItem,
81
+ }: {
82
+ updateState: (key: string, value: SelectedItem) => void;
83
+ item: SelectedItem;
84
+ index: number;
85
+ selectedItem?: SelectedItem;
86
+ }): React.JSX.Element => {
87
+ const {addRootView, setRootViewNativeStyle, removeRootView, rootViews} = useRootView();
88
+ const rootViewsRef = useRef(rootViews);
89
+ useEffect(() => {
90
+ rootViewsRef.current = rootViews;
91
+ }, [rootViews]);
92
+ return useMemo(() => {
93
+ // console.log('AutoPositionedPopup.tsx ListItem index=', index);
94
+ // console.log('AutoPositionedPopup.tsx ListItem item=', item);
95
+ const isSelected = item.id === selectedItem?.id;
96
+ return (
97
+ <TouchableOpacity
98
+ key={item.id}
99
+ style={[
100
+ styles.commonModalRow,
101
+ {backgroundColor: isSelected ? 'rgba(116, 116, 128, 0.08)' : 'transparent'},
102
+ ]}
103
+ onPress={() => {
104
+ // console.log('AutoPositionedPopup.tsx ListItem onPress item=', item); // Commented to prevent spam
105
+ // console.log('AutoPositionedPopup.tsx ListItem onPress rootViews=', rootViewsRef.current); // Commented to prevent spam
106
+ updateState('selectedItem', item);
107
+ }}
108
+ >
109
+ <Text style={styles.ListItemCode} numberOfLines={1} ellipsizeMode="tail">
110
+ {item.title}
111
+ </Text>
112
+ </TouchableOpacity>
113
+ );
114
+ }, [updateState, item, index, selectedItem, rootViewsRef]);
115
+ }
116
+ );
104
117
 
105
118
  // Popup list component with AdvancedFlatList
106
- interface PopupListProps {
107
- data: SelectedItem[];
108
- selectedItem?: SelectedItem;
109
- onItemPress: (item: SelectedItem) => void;
119
+ interface AutoPositionedPopupListProps {
120
+ tag: string;
121
+ updateState: (key: string, value: any) => void;
122
+ fetchData: ({
123
+ pageIndex,
124
+ pageSize,
125
+ searchQuery,
126
+ }: {
127
+ pageIndex: number;
128
+ pageSize: number;
129
+ searchQuery?: string;
130
+ }) => Promise<Data | null>;
131
+ keyExtractor?: (item: SelectedItem) => string; //keyExtractor={item => item?.id}
110
132
  renderItem?: ({item, index}: { item: SelectedItem; index: number }) => React.ReactElement;
111
- keyExtractor?: (item: SelectedItem) => string;
112
- theme: Theme;
113
- rootViewsRef?: React.MutableRefObject<any[]>;
114
- fetchData?: (params: { pageIndex: number; pageSize: number; searchQuery?: string }) => Promise<Data | null>;
133
+ selectedItem?: SelectedItem;
115
134
  localSearch?: boolean;
116
135
  pageSize?: number;
117
- onDataUpdate?: (newData: SelectedItem[]) => void;
118
- selectedItemBackgroundColor?: string;
119
136
  }
120
137
 
121
- const PopupList: React.FC<PopupListProps> = memo(({
122
- data,
123
- selectedItem,
124
- onItemPress,
125
- renderItem,
126
- keyExtractor = (item: SelectedItem) => String(item.id),
127
- theme,
128
- rootViewsRef,
129
- fetchData,
130
- localSearch = false,
131
- pageSize = 20,
132
- onDataUpdate,
133
- selectedItemBackgroundColor,
134
- }) => {
135
- const [internalData, setInternalData] = useState<SelectedItem[]>(data);
136
- const searchQueryRef = useRef<string>('');
137
-
138
- // Sync external data changes
139
- useEffect(() => {
140
- setInternalData(data);
141
- }, [data]);
142
-
143
- // Listen to search query changes
144
- useEffect(() => {
145
- const unsubscribe = subscribeQueryChange(async (newQuery: string) => {
146
- console.log('PopupList subscribeQueryChange newQuery=', newQuery);
147
- searchQueryRef.current = newQuery;
148
-
149
- if (fetchData) {
150
- try {
151
- const result = await fetchData({
152
- pageIndex: 0,
153
- pageSize,
154
- searchQuery: newQuery,
138
+ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
139
+ ({
140
+ tag,
141
+ updateState,
142
+ fetchData,
143
+ keyExtractor = (item) => String(item.id),
144
+ renderItem,
145
+ selectedItem,
146
+ localSearch,
147
+ pageSize,
148
+ }: AutoPositionedPopupListProps): React.JSX.Element => {
149
+ const [state, setState] = useState<{
150
+ selectedItem?: SelectedItem;
151
+ localData: SelectedItem[];
152
+ }>({
153
+ selectedItem: selectedItem,
154
+ localData: [],
155
+ });
156
+ // Define an interface that matches the methods we need from CsxFlatList
157
+ const ref_list = useRef<{ scrollToTop: () => void; refresh: () => void } | null>(null);
158
+ const ref_searchQuery = useRef<string>('');
159
+ const {searchQuery, setSearchQuery, rootViews} = useRootView();
160
+ const rootViewsRef = useRef(rootViews);
161
+ useEffect(() => {
162
+ rootViewsRef.current = rootViews;
163
+ }, [rootViews]);
164
+ /**
165
+ * componentDidMount && componentWillUnmount
166
+ */
167
+ useEffect(() => {
168
+ (async () => {
169
+ })();
170
+ console.log(`AutoPositionedPopupList componentDidMount`);
171
+ //componentWillUnmount
172
+ return () => {
173
+ console.log(`AutoPositionedPopupList componentWillUnmount`);
174
+ setSearchQuery('');
175
+ };
176
+ }, []);
177
+ // useEffect(() => {
178
+ // // 監聽 TextInput 事件,收到就刷新列表,不依賴 global searchQuery
179
+ // // 將最新的 searchQuery 同步到 list 專用的 ref,供 _fetchData 使用
180
+ // ref_searchQuery.current = searchQuery;
181
+ // console.log('AutoPositionedPopupList useEffect searchQuery=', searchQuery);
182
+ // console.log('AutoPositionedPopupList useEffect state.localData=', state.localData);
183
+ // console.log('AutoPositionedPopupList useEffect ref_list.current=', ref_list.current);
184
+ // console.log('AutoPositionedPopupList useEffect localSearch=', localSearch);
185
+ // if (ref_list.current && (localSearch && state.localData.length > 0 || !localSearch)) {
186
+ // ref_list.current.scrollToTop();
187
+ // ref_list.current.refresh();
188
+ // }
189
+ // }, [searchQuery, state.localData, localSearch]);
190
+ useEffect(() => {
191
+ const unsubscribe = subscribeQueryChange((newQuery: string) => {
192
+ console.log('AutoPositionedPopupList useEffect subscribeQueryChange newQuery=', newQuery);
193
+ ref_searchQuery.current = newQuery;
194
+ if (ref_list.current) {
195
+ ref_list.current.scrollToTop();
196
+ ref_list.current.refresh();
197
+ }
198
+ });
199
+ return unsubscribe;
200
+ }, []);
201
+ const _updateState = (key: string, value: SelectedItem) => {
202
+ console.log('AutoPositionedPopupList _updateState key=', key, ' value=', value);
203
+ setState((prevState) => ({
204
+ ...prevState,
205
+ [key]: value,
206
+ }));
207
+ console.log('AutoPositionedPopupList _updateState rootViews=', rootViewsRef.current);
208
+ updateState(key, value);
209
+ };
210
+ const _fetchData = async ({
211
+ pageIndex,
212
+ pageSize: currentPageSize,
213
+ }: {
214
+ pageIndex: number;
215
+ pageSize: number;
216
+ }): Promise<Data | null> => {
217
+ console.log('AutoPositionedPopupList _fetchData pageIndex=', pageIndex, ' pageSize=', currentPageSize);
218
+ console.log('AutoPositionedPopupList _fetchData state.localData=', state.localData);
219
+ console.log('AutoPositionedPopupList _fetchData ref_searchQuery.current=', ref_searchQuery.current);
220
+ console.log('AutoPositionedPopupList _fetchData localSearch=', localSearch);
221
+ if (localSearch && state.localData.length > 0) {
222
+ const result: SelectedItem[] = state.localData.filter((item: SelectedItem) => {
223
+ return item.title?.toLowerCase().includes(ref_searchQuery.current.toLowerCase());
224
+ });
225
+ console.log('AutoPositionedPopupList _fetchData localSearch result=', result);
226
+ return Promise.resolve({
227
+ items: result,
228
+ pageIndex: 0,
229
+ needLoadMore: false,
230
+ });
231
+ }
232
+ try {
233
+ const res: Data | null = await fetchData({
234
+ pageIndex,
235
+ pageSize: pageSize || 10,
236
+ searchQuery: ref_searchQuery.current,
237
+ });
238
+ console.log('AutoPositionedPopupList _fetchData res=', res);
239
+ if (res?.items && localSearch) {
240
+ setState((prevState) => {
241
+ return {
242
+ ...prevState,
243
+ localData: res.items,
244
+ };
155
245
  });
156
-
157
- if (result?.items) {
158
- setInternalData(result.items);
159
- onDataUpdate?.(result.items);
160
- }
161
- } catch (error) {
162
- console.error('PopupList fetchData error:', error);
163
246
  }
164
- } else if (localSearch) {
165
- // Local filtering
166
- const filtered = data.filter(item =>
167
- item.title.toLowerCase().includes(newQuery.toLowerCase())
168
- );
169
- setInternalData(filtered);
170
- onDataUpdate?.(filtered);
247
+ return Promise.resolve(res);
248
+ } catch (e) {
249
+ console.warn('Error in fetchData:', e);
171
250
  }
172
- });
173
-
174
- return unsubscribe;
175
- }, [fetchData, localSearch, pageSize, data, onDataUpdate]);
176
- const defaultRenderItem = useCallback(
177
- ({item, index}: { item: SelectedItem; index: number }) => (
178
- <ListItem
179
- item={item}
180
- index={index}
181
- selectedItem={selectedItem}
182
- onItemPress={onItemPress}
183
- theme={theme}
184
- rootViewsRef={rootViewsRef}
185
- selectedItemBackgroundColor={selectedItemBackgroundColor}
186
- />
187
- ),
188
- [selectedItem, onItemPress, theme, rootViewsRef, selectedItemBackgroundColor]
189
- );
190
-
191
- return (
192
- <View style={[styles.autoPositionedPopupList, {backgroundColor: theme.colors.background}]}>
193
- <AdvancedFlatList
194
- data={internalData}
195
- keyExtractor={keyExtractor}
196
- renderItem={renderItem || defaultRenderItem}
197
- keyboardShouldPersistTaps="always"
198
- showsVerticalScrollIndicator={true}
199
- nestedScrollEnabled={true}
200
- />
201
- </View>
202
- );
203
- });
251
+ console.log('AutoPositionedPopupList _fetchData res=', null);
252
+ return null;
253
+ };
254
+ const _renderItem = useCallback(
255
+ ({item, index}: { item: SelectedItem; index: number }) => {
256
+ return <ListItem item={item} index={index} updateState={_updateState} selectedItem={state.selectedItem} />;
257
+ },
258
+ [state.selectedItem]
259
+ );
260
+ return useMemo(() => {
261
+ console.log('AutoPositionedPopupList (global as any)?.$fake=', (global as any)?.$fake);
262
+ // Choose AdvancedFlatList version based on global.$fake
263
+ const AdvancedFlatListComponent = (global as any)?.$fake ? AdvancedFlatListSource : AdvancedFlatListLib;
264
+
265
+ return (
266
+ <View style={[styles.baseModalView, styles.autoPositionedPopupList]}>
267
+ {/* @ts-ignore - Type assertion to bypass third-party library type issues */}
268
+ <AdvancedFlatListComponent
269
+ style={[{borderRadius: 0}]}
270
+ {...(ref_list && { ref: ref_list })}
271
+ keyExtractor={keyExtractor}
272
+ keyboardShouldPersistTaps={'always'}
273
+ {...({ fetchData: _fetchData })}
274
+ renderItem={renderItem || _renderItem}
275
+ />
276
+ </View>
277
+ );
278
+ }, [tag,
279
+ updateState,
280
+ fetchData,
281
+ keyExtractor,
282
+ renderItem,
283
+ state.selectedItem,
284
+ state.localData,
285
+ searchQuery,
286
+ localSearch,
287
+ pageSize,
288
+ rootViewsRef,
289
+ ]);
290
+ }
291
+ );
204
292
 
205
293
  // State interface for AutoPositionedPopup
206
294
  interface StateProps {
@@ -230,7 +318,30 @@ const AutoPositionedPopup: MemoExoticComponent<
230
318
  inputStyle,
231
319
  labelStyle,
232
320
  popUpViewStyle = {left: '5%', width: '90%'},
233
- fetchData,
321
+ fetchData = async ({
322
+ pageIndex,
323
+ pageSize,
324
+ searchQuery,
325
+ }: {
326
+ pageIndex: number;
327
+ pageSize: number;
328
+ searchQuery?: string;
329
+ }): Promise<Data | null> => {
330
+ const res = {
331
+ items: [] as any[],
332
+ pageIndex,
333
+ needLoadMore: false,
334
+ };
335
+ try {
336
+ // const res1: any[] = await $api.xxx(pageSize)
337
+ // console.log('${NAME} xxx res=', res)
338
+ // res.items = res1
339
+ // res.needLoadMore = res1.length === pageSize
340
+ } catch (e) {
341
+ console.warn('Error in fetch operation:', e);
342
+ }
343
+ return res;
344
+ },
234
345
  renderItem,
235
346
  onItemSelected,
236
347
  localSearch = false,
@@ -239,60 +350,83 @@ const AutoPositionedPopup: MemoExoticComponent<
239
350
  useTextInput = false,
240
351
  btwChildren,
241
352
  CustomRow = ({children}) => <View>{children}</View>,
242
- keyExtractor = (item: any) => item?.id,
353
+ keyExtractor = (item: any) => String(item?.id || ''),
243
354
  AutoPositionedPopupBtnDisabled = false,
244
355
  forceRemoveAllRootViewOnItemSelected = false,
245
356
  centerDisplay = false,
246
357
  selectedItemBackgroundColor = 'rgba(116, 116, 128, 0.08)',
358
+ textAlign = 'right',
359
+ CustomPopView = undefined, CustomPopViewStyle
247
360
  } = props;
248
-
249
361
  // State management similar to project implementation
250
362
  const [state, setState] = useState<StateProps>({
251
363
  isFocus: false,
252
364
  selectedItem: selectedItem,
253
365
  });
254
-
255
366
  // Use RootView context
256
- const {addRootView, removeRootView, rootViews, setSearchQuery: setContextSearchQuery, setRootViewNativeStyle} = useRootView();
367
+ const {addRootView, setRootViewNativeStyle, removeRootView, rootViews, setSearchQuery} = useRootView();
368
+ const insets = useSafeAreaInsets();
257
369
  const rootViewsRef = useRef(rootViews);
258
-
259
370
  // Track TextInput focus and RootView states like project implementation
260
371
  const hasTriggeredFocus = useRef(false);
261
372
  const hasAddedRootView = useRef(false);
262
373
  const hasShownRootView = useRef(false);
263
-
264
374
  // Additional refs for keyboard and position tracking
265
375
  const ref_isFocus = useRef<boolean>();
266
376
  const ref_isKeyboardFullyShown = useRef<boolean>();
267
- const ref_listPos = useRef<any>();
377
+ const ref_listPos: MutableRefObject<any> = useRef<LayoutRectangle>()
268
378
  const keyboardVisibleRef = useRef(false);
269
379
  const refAutoPositionedPopup = useRef<View>(null);
270
-
380
+ const ref_searchQuery = useRef<string>('');
271
381
  // Simple keyboard status tracking (alternative to useKeyboardStatus hook)
272
- const [isKeyboardFullyShown, setIsKeyboardFullyShown] = useState(false);
273
-
274
- // Keyboard listeners
382
+ // Legacy state for compatibility
383
+ const [isVisible, setIsVisible] = useState(false);
384
+ const [data, setData] = useState<SelectedItem[]>([]);
385
+ const [loading, setLoading] = useState(false);
386
+ const [popupPosition, setPopupPosition] = useState<{
387
+ top: number;
388
+ left: number;
389
+ width: number;
390
+ }>({top: 0, left: 0, width: 0});
391
+ // Refs for performance optimization
392
+ const containerRef = useRef<View>(null);
393
+ const textInputRef = useRef<RNTextInput>(null);
394
+ const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
395
+ const searchQueryRef = useRef<string>(''); // Use ref instead of state to avoid re-renders
396
+ // Refs to store latest values for useEffect without adding to dependency array
397
+ const dataRef = useRef<SelectedItem[]>(data);
398
+ const isKeyboardFullyShown = useKeyboardStatus();
399
+ const theme = defaultTheme;
400
+ /**
401
+ * componentDidMount && componentWillUnmount
402
+ */
275
403
  useEffect(() => {
276
- const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
277
- setIsKeyboardFullyShown(true);
278
- });
279
-
280
- const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
281
- setIsKeyboardFullyShown(false);
282
- });
283
-
404
+ (async () => {
405
+ })();
406
+ console.log(`AutoPositionedPopup componentDidMount tag=`, tag);
407
+ console.log('AutoPositionedPopup componentDidMount CustomPopView=', CustomPopView);
408
+ //componentWillUnmount
284
409
  return () => {
285
- keyboardDidShowListener?.remove();
286
- keyboardDidHideListener?.remove();
410
+ console.log(`AutoPositionedPopup componentWillUnmount tag=`, tag);
411
+ removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
412
+ setSearchQuery('');
413
+ if (textInputRef.current) {
414
+ textInputRef.current.blur();
415
+ hasTriggeredFocus.current = false;
416
+ hasAddedRootView.current = false;
417
+ hasShownRootView.current = false;
418
+ ref_isFocus.current = false;
419
+ }
287
420
  };
288
421
  }, []);
289
-
290
422
  useEffect(() => {
291
423
  console.log('AutoPositionedPopup rootViews=', rootViews);
292
424
  rootViewsRef.current = rootViews;
293
425
  if (rootViews.length === 0) {
294
426
  hasAddedRootView.current = false;
295
427
  hasShownRootView.current = false;
428
+ ref_isFocus.current = false;
429
+ ref_isKeyboardFullyShown.current = false;
296
430
  hasTriggeredFocus.current = false;
297
431
  setState((prevState) => {
298
432
  return {
@@ -302,8 +436,6 @@ const AutoPositionedPopup: MemoExoticComponent<
302
436
  });
303
437
  }
304
438
  }, [rootViews]);
305
-
306
- // Sync selectedItem changes like project implementation
307
439
  useEffect(() => {
308
440
  console.log('AutoPositionedPopup useEffect tag=', tag);
309
441
  console.log('AutoPositionedPopup useEffect selectedItem=', selectedItem);
@@ -318,323 +450,6 @@ const AutoPositionedPopup: MemoExoticComponent<
318
450
  });
319
451
  }
320
452
  }, [selectedItem, state.selectedItem, tag]);
321
-
322
-
323
- // Legacy state for compatibility
324
- const [isVisible, setIsVisible] = useState(false);
325
- const [data, setData] = useState<SelectedItem[]>([]);
326
- const [loading, setLoading] = useState(false);
327
- const [popupPosition, setPopupPosition] = useState<{
328
- top: number;
329
- left: number;
330
- width: number;
331
- }>({top: 0, left: 0, width: 0});
332
- const popupId = useRef(`popup-${tag}-${Date.now()}`);
333
-
334
- // Refs for performance optimization
335
- const containerRef = useRef<View>(null);
336
- const textInputRef = useRef<RNTextInput>(null);
337
- const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
338
- const searchQueryRef = useRef<string>(''); // Use ref instead of state to avoid re-renders
339
-
340
- // Constants
341
- const LIST_HEIGHT = 200;
342
- const theme = defaultTheme;
343
-
344
- // Fetch data function
345
- const loadData = useCallback(async (query: string = '') => {
346
- if (!fetchData) return;
347
-
348
- setLoading(true);
349
- try {
350
- const result = await fetchData({
351
- pageIndex: 0,
352
- pageSize,
353
- searchQuery: query,
354
- });
355
-
356
- if (result?.items) {
357
- setData(result.items);
358
- }
359
- } catch (error) {
360
- console.error('Error loading data:', error);
361
- } finally {
362
- setLoading(false);
363
- }
364
- }, [fetchData, pageSize]);
365
-
366
- // Handle search query change with debounce and event emission
367
- const handleSearchChange = useCallback((query: string) => {
368
- // Store in ref to avoid re-renders
369
- searchQueryRef.current = query;
370
-
371
- // Update TextInput value directly if needed
372
- if (textInputRef.current) {
373
- // The TextInput's value will be controlled by its own state
374
- }
375
-
376
- // Clear previous debounce timer
377
- if (debounceTimerRef.current) {
378
- clearTimeout(debounceTimerRef.current);
379
- }
380
-
381
- // Use debounce for performance optimization
382
- debounceTimerRef.current = setTimeout(() => {
383
- // Emit query change event to decouple components and avoid context re-rendering
384
- emitQueryChange(searchQueryRef.current);
385
- }, 300); // Use 300ms debounce like the original
386
- }, []);
387
-
388
- // Calculate popup position
389
- const calculatePosition = useCallback(() => {
390
- if (!containerRef.current) return;
391
-
392
- containerRef.current.measureInWindow((x, y, width, height) => {
393
- const screenHeight = Dimensions.get('screen').height;
394
- const screenWidth = Dimensions.get('screen').width;
395
-
396
- let top = y + height;
397
- let left = x;
398
- let popupWidth = width;
399
-
400
- // Check if popup should appear above the input
401
- if (y + height + LIST_HEIGHT > screenHeight) {
402
- top = y - LIST_HEIGHT;
403
- }
404
-
405
- // Adjust horizontal position if needed
406
- if (popUpViewStyle?.left && popUpViewStyle?.width) {
407
- const leftPercent = parseFloat(String(popUpViewStyle.left).replace('%', '')) / 100;
408
- const widthPercent = parseFloat(String(popUpViewStyle.width).replace('%', '')) / 100;
409
- left = screenWidth * leftPercent;
410
- popupWidth = screenWidth * widthPercent;
411
- }
412
-
413
- setPopupPosition({top, left, width: popupWidth});
414
- });
415
- }, [popUpViewStyle]);
416
-
417
- // Hide popup using RootView
418
- const hidePopup = useCallback(() => {
419
- setIsVisible(false);
420
- // Reset search query
421
- searchQueryRef.current = '';
422
- if (textInputRef.current) {
423
- textInputRef.current.blur();
424
- textInputRef.current.clear?.(); // Clear the TextInput
425
- }
426
- removeRootView(popupId.current, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
427
- }, [removeRootView, forceRemoveAllRootViewOnItemSelected]);
428
-
429
- // Handle data updates from PopupList
430
- const handleDataUpdate = useCallback((newData: SelectedItem[]) => {
431
- setData(newData);
432
- }, []);
433
-
434
- // Handle item selection
435
- const handleItemPress = useCallback((item: SelectedItem) => {
436
- onItemSelected?.(item);
437
- hidePopup();
438
- }, [onItemSelected, hidePopup]);
439
-
440
- // Show popup using RootView
441
- const showPopup = useCallback(() => {
442
- calculatePosition();
443
- setIsVisible(true);
444
- loadData(searchQueryRef.current);
445
-
446
- // Wait for position to be calculated
447
- setTimeout(() => {
448
- const popupComponent = (
449
- <TouchableOpacity
450
- style={{
451
- flex: 1,
452
- backgroundColor: 'rgba(0, 0, 0, 0.3)',
453
- }}
454
- activeOpacity={1}
455
- onPress={hidePopup}
456
- >
457
- <View
458
- style={{
459
- position: 'absolute',
460
- top: popupPosition.top,
461
- left: popupPosition.left,
462
- width: popupPosition.width,
463
- height: LIST_HEIGHT,
464
- backgroundColor: theme.colors.background,
465
- borderRadius: 8,
466
- shadowColor: '#000',
467
- shadowOffset: {width: 0, height: 2},
468
- shadowOpacity: 0.25,
469
- shadowRadius: 3.84,
470
- elevation: 5,
471
- }}
472
- >
473
- {useTextInput && (
474
- <RNTextInput
475
- ref={textInputRef}
476
- style={[
477
- styles.inputStyle,
478
- {
479
- height: 40,
480
- borderBottomWidth: 1,
481
- borderBottomColor: theme.colors.border,
482
- paddingHorizontal: 12,
483
- color: theme.colors.text,
484
- },
485
- inputStyle,
486
- ]}
487
- placeholder={placeholder}
488
- placeholderTextColor={theme.colors.placeholderText}
489
- defaultValue={searchQueryRef.current}
490
- onChangeText={handleSearchChange}
491
- onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
492
- onSubmitEditing?.(e);
493
- Keyboard.dismiss();
494
- }}
495
- returnKeyType="done"
496
- {...TextInputProps}
497
- />
498
- )}
499
-
500
- <PopupList
501
- data={data}
502
- selectedItem={selectedItem}
503
- onItemPress={handleItemPress}
504
- renderItem={renderItem}
505
- keyExtractor={keyExtractor}
506
- theme={theme}
507
- rootViewsRef={rootViewsRef}
508
- fetchData={fetchData}
509
- localSearch={localSearch}
510
- pageSize={pageSize}
511
- onDataUpdate={handleDataUpdate}
512
- selectedItemBackgroundColor={selectedItemBackgroundColor}
513
- />
514
- </View>
515
- </TouchableOpacity>
516
- );
517
-
518
- addRootView({
519
- id: popupId.current,
520
- style: {
521
- position: 'absolute',
522
- top: 0,
523
- left: 0,
524
- right: 0,
525
- bottom: 0,
526
- },
527
- component: popupComponent,
528
- useModal: true,
529
- onModalClose: hidePopup,
530
- centerDisplay: centerDisplay,
531
- });
532
- }, 100);
533
- }, [calculatePosition, loadData, popupPosition, useTextInput, placeholder, theme, inputStyle, TextInputProps, data, selectedItem, renderItem, keyExtractor, centerDisplay, addRootView, hidePopup, handleSearchChange, handleItemPress, LIST_HEIGHT, selectedItemBackgroundColor]);
534
-
535
- // Handle button press - following project implementation logic
536
- const handleButtonPress = useCallback(() => {
537
- if (AutoPositionedPopupBtnDisabled) return;
538
-
539
- console.log('AutoPositionedPopup onPress tag=', tag);
540
- console.log('AutoPositionedPopup onPress state.isFocus=', state.isFocus);
541
- console.log('AutoPositionedPopup onPress useTextInput=', useTextInput);
542
- console.log(
543
- 'AutoPositionedPopup onPress hasAddedRootView.current=',
544
- hasAddedRootView.current
545
- );
546
- console.log(
547
- 'AutoPositionedPopup onPress hasShownRootView.current=',
548
- hasShownRootView.current
549
- );
550
- console.log(
551
- 'AutoPositionedPopup onPress hasTriggeredFocus.current=',
552
- hasTriggeredFocus.current
553
- );
554
- console.log('AutoPositionedPopup onPress state.selectedItem=', state.selectedItem);
555
-
556
- setState((prevState) => {
557
- return {
558
- ...prevState,
559
- isFocus: true,
560
- };
561
- });
562
-
563
- if (!hasAddedRootView.current && useTextInput) {
564
- // TextInput version: hide first, show after keyboard fully appears
565
- hasAddedRootView.current = true;
566
- hasShownRootView.current = false;
567
- addRootView({
568
- id: tag,
569
- style: {
570
- top: 0,
571
- left: 0,
572
- width: popUpViewStyle?.width,
573
- height: listLayout.height,
574
- opacity: 0,
575
- },
576
- component: (
577
- <PopupList
578
- data={data}
579
- selectedItem={state.selectedItem}
580
- onItemPress={handleItemPress}
581
- renderItem={renderItem}
582
- keyExtractor={keyExtractor}
583
- theme={theme}
584
- rootViewsRef={rootViewsRef}
585
- fetchData={fetchData}
586
- localSearch={localSearch}
587
- pageSize={pageSize}
588
- onDataUpdate={handleDataUpdate}
589
- selectedItemBackgroundColor={selectedItemBackgroundColor}
590
- />
591
- ),
592
- useModal: false,
593
- });
594
- }
595
- }, [AutoPositionedPopupBtnDisabled, useTextInput, state.isFocus, state.selectedItem, tag, hasAddedRootView, hasShownRootView, hasTriggeredFocus, addRootView, popUpViewStyle, data, handleItemPress, renderItem, keyExtractor, theme, rootViewsRef, fetchData, localSearch, pageSize, handleDataUpdate, selectedItemBackgroundColor]);
596
-
597
- // Imperative handle for parent component access
598
- useImperativeHandle(
599
- parentRef,
600
- () => ({
601
- clearSelectedItem: () => {
602
- // Clear selection logic can be implemented here
603
- console.log('Clearing selected item for:', tag);
604
- },
605
- showPopup,
606
- hidePopup,
607
- }),
608
- [tag, showPopup, hidePopup]
609
- );
610
-
611
- // Component lifecycle management like project implementation
612
- useEffect(() => {
613
- console.log(`AutoPositionedPopup componentDidMount tag=`, tag);
614
-
615
- //componentWillUnmount
616
- return () => {
617
- console.log(`AutoPositionedPopup componentWillUnmount tag=`, tag);
618
- removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
619
- setContextSearchQuery('');
620
- if (textInputRef.current) {
621
- textInputRef.current.blur();
622
- hasTriggeredFocus.current = false;
623
- hasAddedRootView.current = false;
624
- hasShownRootView.current = false;
625
- }
626
- };
627
- }, [tag, removeRootView, forceRemoveAllRootViewOnItemSelected, setContextSearchQuery]);
628
-
629
- // Cleanup debounce timer
630
- useEffect(() => {
631
- return () => {
632
- if (debounceTimerRef.current) {
633
- clearTimeout(debounceTimerRef.current);
634
- }
635
- };
636
- }, []);
637
-
638
453
  useEffect(() => {
639
454
  console.log('AutoPositionedPopup useEffect tag=', tag);
640
455
  console.log('AutoPositionedPopup useEffect state.isFocus=', state.isFocus);
@@ -648,26 +463,26 @@ const AutoPositionedPopup: MemoExoticComponent<
648
463
  console.log('AutoPositionedPopup useEffect TextInputProps=', TextInputProps);
649
464
  console.log('AutoPositionedPopup useEffect hasAddedRootView.current=', hasAddedRootView.current);
650
465
  console.log('AutoPositionedPopup useEffect hasShownRootView.current=', hasShownRootView.current);
651
-
652
466
  if (useTextInput) {
653
467
  if (isKeyboardFullyShown && hasAddedRootView.current && !hasShownRootView.current && state.isFocus) {
654
468
  refAutoPositionedPopup.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
655
469
  console.log('AutoPositionedPopup measureInWindow x=', x, ' y=', y, ' width=', width, ' height=', height);
656
- // Get the full screen height (including the status bar and navigation bar)
470
+ // SIMPLE CENTER-BASED POSITIONING STRATEGY
657
471
  const screenHeight = Dimensions.get('screen').height;
658
- console.log('AutoPositionedPopup screenHeight=', screenHeight);
472
+ const screenCenter = screenHeight / 2;
473
+ console.log('AutoPositionedPopup screenHeight=', screenHeight, ' screenCenter=', screenCenter, ' componentY=', y);
659
474
 
660
- if (y + height < screenHeight / 2 - listLayout.height / 2) {
661
- console.log('AutoPositionedPopup y + height < screenHeight / 2');
662
- ref_listPos.current = { x: x, y: y + height, width: width };
475
+ // Simple rule: if component Y > screen center, show popup above; otherwise show below
476
+ if (y > screenCenter) {
477
+ console.log('AutoPositionedPopup with keyboard: showing above (Y > center)');
478
+ ref_listPos.current = {x: x, y: y - listLayout.height, width: width};
663
479
  } else {
664
- console.log('AutoPositionedPopup y + height >= screenHeight / 2');
665
- ref_listPos.current = { x: x, y: y - listLayout.height, width: width };
480
+ console.log('AutoPositionedPopup with keyboard: showing below (Y <= center)');
481
+ ref_listPos.current = {x: x, y: y + height, width: width};
666
482
  }
667
483
  console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
668
-
669
484
  setRootViewNativeStyle(tag, {
670
- top: ref_listPos.current.y,
485
+ top: ref_listPos.current?.y,
671
486
  left: popUpViewStyle?.left,
672
487
  width: popUpViewStyle?.width,
673
488
  height: listLayout.height,
@@ -689,7 +504,7 @@ const AutoPositionedPopup: MemoExoticComponent<
689
504
  isFocus: false,
690
505
  };
691
506
  });
692
- setContextSearchQuery('');
507
+ setSearchQuery('');
693
508
  hasAddedRootView.current = false;
694
509
  hasShownRootView.current = false;
695
510
  }
@@ -697,107 +512,170 @@ const AutoPositionedPopup: MemoExoticComponent<
697
512
  if (state.isFocus) {
698
513
  refAutoPositionedPopup.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
699
514
  console.log('AutoPositionedPopup measureInWindow x=', x, ' y=', y, ' width=', width, ' height=', height);
700
- // Get the full screen height (including the status bar and navigation bar)
515
+ // SIMPLE CENTER-BASED POSITIONING STRATEGY
701
516
  const screenHeight = Dimensions.get('screen').height;
702
- console.log('AutoPositionedPopup screenHeight=', screenHeight);
517
+ const screenCenter = screenHeight / 2;
518
+ console.log('AutoPositionedPopup screenHeight=', screenHeight, ' screenCenter=', screenCenter, ' componentY=', y);
703
519
 
704
- if (y + height < screenHeight / 2 - listLayout.height / 2) {
705
- console.log('AutoPositionedPopup y + height < screenHeight / 2');
706
- ref_listPos.current = { x: x, y: y + height, width: width };
520
+ // Simple rule: if component Y > screen center, show popup above; otherwise show below
521
+ if (y+insets. top > screenCenter) {
522
+ console.log('AutoPositionedPopup: showing above (Y > center)');
523
+ ref_listPos.current = {x: x, y: y - listLayout.height, width: width};
707
524
  } else {
708
- console.log('AutoPositionedPopup y + height >= screenHeight / 2');
709
- ref_listPos.current = { x: x, y: y - listLayout.height, width: width };
525
+ console.log('AutoPositionedPopup: showing below (Y <= center)');
526
+ ref_listPos.current = {x: x, y: y + height+insets.top, width: width};
710
527
  }
711
528
  console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
712
-
713
- console.log('AutoPositionedPopup addRootView tag=', tag);
714
- addRootView({
715
- id: tag,
716
- style: {
717
- top: ref_listPos.current.y,
718
- left: popUpViewStyle?.left,
719
- width: popUpViewStyle?.width,
720
- height: listLayout.height,
721
- opacity: 1,
722
- },
723
- component: (
724
- <PopupList
725
- data={data}
726
- selectedItem={state.selectedItem}
727
- onItemPress={handleItemPress}
728
- renderItem={renderItem}
729
- keyExtractor={keyExtractor}
730
- theme={theme}
731
- rootViewsRef={rootViewsRef}
732
- fetchData={fetchData}
733
- localSearch={localSearch}
734
- pageSize={pageSize}
735
- onDataUpdate={handleDataUpdate}
736
- selectedItemBackgroundColor={selectedItemBackgroundColor}
737
- />
738
- ),
739
- useModal: true,
740
- onModalClose: () => {
741
- console.log('AutoPositionedPopup onModalClose tag=', tag);
742
- removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
743
- setState((prevState) => {
744
- return {
745
- ...prevState,
746
- isFocus: false,
747
- };
748
- });
749
- hasAddedRootView.current = false;
750
- hasShownRootView.current = false;
751
- hasTriggeredFocus.current = false;
752
- setContextSearchQuery('');
753
- },
754
- centerDisplay,
755
- });
529
+ if (CustomPopView && CustomPopViewStyle) {
530
+ console.log('AutoPositionedPopup CustomPopViewStyle=', CustomPopViewStyle);
531
+ // Ensure CustomPopViewStyle.height is a number before using it in calculations
532
+ const customHeight =
533
+ typeof CustomPopViewStyle.height === 'number' ? CustomPopViewStyle.height : listLayout.height;
534
+
535
+ // Apply same simple center-based strategy for CustomPopView
536
+ console.log('AutoPositionedPopup CustomPopView using center-based positioning, customHeight=', customHeight);
537
+
538
+ // Simple rule: if component Y > screen center, show popup above; otherwise show below
539
+ if (y > screenCenter) {
540
+ console.log('AutoPositionedPopup CustomPopView: showing above (Y > center), tag=', tag);
541
+ ref_listPos.current = {x: x, y: y - customHeight, width: width};
542
+ } else {
543
+ console.log('AutoPositionedPopup CustomPopView: showing below (Y <= center), tag=', tag);
544
+ ref_listPos.current = {x: x, y: y + height, width: width};
545
+ }
546
+ const PopViewComponent = CustomPopView();
547
+ console.log('AutoPositionedPopup addRootView PopViewComponent=', PopViewComponent);
548
+ console.log('AutoPositionedPopup addRootView state.selectedItem=', state.selectedItem);
549
+ addRootView({
550
+ id: tag,
551
+ style: !centerDisplay
552
+ ? {
553
+ top: ref_listPos.current.y,
554
+ left: popUpViewStyle?.left,
555
+ width: popUpViewStyle?.width,
556
+ height: listLayout.height,
557
+ opacity: 1,
558
+ ...CustomPopViewStyle,
559
+ }
560
+ : {width: popUpViewStyle?.width, height: listLayout.height, ...CustomPopViewStyle},
561
+ component: <PopViewComponent selectedItem={state.selectedItem}></PopViewComponent>,
562
+ useModal: true,
563
+ onModalClose: () => {
564
+ console.log('AutoPositionedPopup onModalClose');
565
+ removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
566
+ setState((prevState) => {
567
+ return {
568
+ ...prevState,
569
+ isFocus: false,
570
+ };
571
+ });
572
+ hasAddedRootView.current = false;
573
+ hasShownRootView.current = false;
574
+ hasTriggeredFocus.current = false;
575
+ setSearchQuery('');
576
+ },
577
+ centerDisplay,
578
+ });
579
+ } else {
580
+ console.log('AutoPositionedPopup addRootView tag=', tag);
581
+ addRootView({
582
+ id: tag,
583
+ style: {
584
+ top: ref_listPos.current.y,
585
+ left: popUpViewStyle?.left,
586
+ width: popUpViewStyle?.width,
587
+ height: listLayout.height,
588
+ opacity: 1,
589
+ },
590
+ component: (
591
+ <AutoPositionedPopupList
592
+ tag={tag}
593
+ updateState={updateState}
594
+ fetchData={fetchData}
595
+ pageSize={pageSize}
596
+ renderItem={renderItem}
597
+ selectedItem={state.selectedItem}
598
+ localSearch={localSearch}
599
+ />
600
+ ),
601
+ useModal: true,
602
+ onModalClose: () => {
603
+ console.log('AutoPositionedPopup onModalClose tag=', tag);
604
+ removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
605
+ setState((prevState) => {
606
+ return {
607
+ ...prevState,
608
+ };
609
+ });
610
+ setSearchQuery('');
611
+ },
612
+ });
613
+ }
756
614
  });
757
615
  }
758
616
  }
759
-
760
617
  if (isKeyboardFullyShown) {
761
618
  ref_isFocus.current = state.isFocus;
762
619
  if (isKeyboardFullyShown !== keyboardVisibleRef.current) {
763
620
  keyboardVisibleRef.current = isKeyboardFullyShown;
764
- // Ensure TextInput has correct focus when keyboard is fully shown
765
621
  if (isKeyboardFullyShown && textInputRef.current) {
766
- // Force ensure TextInput displays correct value
767
- if (searchQueryRef.current) {
768
- textInputRef.current.setNativeProps({ text: searchQueryRef.current });
622
+ if (ref_searchQuery.current) {
623
+ textInputRef.current.setNativeProps({text: ref_searchQuery.current});
769
624
  }
770
625
  }
771
626
  }
772
627
  }
773
- }, [
628
+ }, [insets,
774
629
  isKeyboardFullyShown,
775
630
  state.isFocus,
776
631
  useTextInput,
632
+ CustomPopView,
633
+ CustomPopViewStyle,
777
634
  forceRemoveAllRootViewOnItemSelected,
778
635
  tag,
779
636
  state.selectedItem,
780
- popUpViewStyle,
781
- data,
782
- handleItemPress,
783
- renderItem,
784
- keyExtractor,
785
- theme,
786
- rootViewsRef,
787
- fetchData,
788
- localSearch,
789
- pageSize,
790
- handleDataUpdate,
791
- selectedItemBackgroundColor,
792
- removeRootView,
793
- setContextSearchQuery,
794
- addRootView,
795
- centerDisplay,
796
- setRootViewNativeStyle,
797
637
  ]);
798
-
638
+ // Imperative handle for parent component access
639
+ useImperativeHandle(
640
+ parentRef,
641
+ () => ({
642
+ clearSelectedItem: () => {
643
+ console.log('AutoPositionedPopup clearSelectedItem tag=', tag);
644
+ setState((prevState) => {
645
+ return {
646
+ ...prevState,
647
+ selectedItem: undefined,
648
+ };
649
+ });
650
+ },
651
+ }),
652
+ []
653
+ );
654
+ const updateState = (key: string, value: SelectedItem) => {
655
+ console.log('AutoPositionedPopup updateState key=', key, ' value=', value);
656
+ setState((prevState) => ({
657
+ ...prevState,
658
+ [key]: value,
659
+ }));
660
+ if (key === 'selectedItem' && onItemSelected) {
661
+ onItemSelected(value);
662
+ console.log('AutoPositionedPopup updateState onItemSelected rootViewsRef.current=', rootViewsRef.current);
663
+ removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
664
+ hasAddedRootView.current = false;
665
+ hasShownRootView.current = false;
666
+ hasTriggeredFocus.current = false;
667
+ setState((prevState) => {
668
+ return {
669
+ ...prevState,
670
+ isFocus: false,
671
+ };
672
+ });
673
+ setSearchQuery('');
674
+ }
675
+ };
799
676
  // Render the component following project implementation
800
677
  return useMemo(() => {
678
+ console.log('AutoPositionedPopup render tag=', tag); // Now safe - circular dependency fixed
801
679
  return (
802
680
  <CustomRow>
803
681
  <View style={[styles.contain, style]} ref={refAutoPositionedPopup}>
@@ -805,13 +683,62 @@ const AutoPositionedPopup: MemoExoticComponent<
805
683
  <TouchableOpacity
806
684
  style={[styles.AutoPositionedPopupBtn, AutoPositionedPopupBtnStyle]}
807
685
  disabled={AutoPositionedPopupBtnDisabled}
808
- onPress={handleButtonPress}
686
+ onPress={() => {
687
+ console.log('AutoPositionedPopup onPress tag=', tag);
688
+ console.log('AutoPositionedPopup onPress state.isFocus=', state.isFocus);
689
+ console.log('AutoPositionedPopup onPress useTextInput=', useTextInput);
690
+ console.log(
691
+ 'AutoPositionedPopup onPress hasAddedRootView.current=',
692
+ hasAddedRootView.current
693
+ );
694
+ console.log(
695
+ 'AutoPositionedPopup onPress hasShownRootView.current=',
696
+ hasShownRootView.current
697
+ );
698
+ console.log(
699
+ 'AutoPositionedPopup onPress hasTriggeredFocus.current=',
700
+ hasTriggeredFocus.current
701
+ );
702
+ console.log('AutoPositionedPopup onPress state.selectedItem=', state.selectedItem);
703
+ setState((prevState) => {
704
+ return {
705
+ ...prevState,
706
+ isFocus: true,
707
+ };
708
+ });
709
+ if (!hasAddedRootView.current && useTextInput) {
710
+ hasAddedRootView.current = true;
711
+ hasShownRootView.current = false;
712
+ addRootView({
713
+ id: tag,
714
+ style: {
715
+ top: 0,
716
+ left: 0,
717
+ width: popUpViewStyle?.width,
718
+ height: listLayout.height,
719
+ opacity: 0,
720
+ },
721
+ component: (
722
+ <AutoPositionedPopupList
723
+ tag={tag}
724
+ updateState={updateState}
725
+ fetchData={fetchData}
726
+ pageSize={pageSize}
727
+ renderItem={renderItem}
728
+ selectedItem={state.selectedItem}
729
+ localSearch={localSearch}
730
+ />
731
+ ),
732
+ useModal: false,
733
+ });
734
+ }
735
+ }}
809
736
  >
810
737
  {!btwChildren ? (
811
738
  <Text
812
739
  style={[
813
740
  styles.searchQueryTxt,
814
- state.selectedItem && { color: theme.colors.text },
741
+ state.selectedItem && {color: theme.colors.text},
815
742
  labelStyle,
816
743
  ]}
817
744
  numberOfLines={1}
@@ -831,33 +758,134 @@ const AutoPositionedPopup: MemoExoticComponent<
831
758
  key="fixed-textinput-key"
832
759
  style={[
833
760
  styles.inputStyle,
834
- {
835
- textAlignVertical: 'center',
836
- paddingVertical: 0,
837
- paddingHorizontal: 0,
838
- },
839
761
  inputStyle,
840
762
  ]}
841
763
  textAlign={TextInputProps['textAlign'] || 'left'}
842
764
  multiline={TextInputProps['multiline'] || false}
843
765
  numberOfLines={TextInputProps['numberOfLines'] || 1}
844
- placeholder={placeholder}
766
+ onChangeText={(searchQuery) => {
767
+ ref_searchQuery.current = searchQuery;
768
+ console.log('AutoPositionedPopup onChangeText rootViews=', rootViews);
769
+ if (!localSearch) {
770
+ if (debounceTimerRef.current) {
771
+ clearTimeout(debounceTimerRef.current);
772
+ }
773
+ debounceTimerRef.current = setTimeout(() => {
774
+ emitQueryChange(ref_searchQuery.current);
775
+ }, 500);
776
+ } else {
777
+ emitQueryChange(ref_searchQuery.current);
778
+ }
779
+ }}
845
780
  placeholderTextColor={theme.colors.placeholderText}
846
- defaultValue={searchQueryRef.current}
847
- onChangeText={handleSearchChange}
848
- onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
849
- onSubmitEditing?.(e);
781
+ placeholder={placeholder}
782
+ onKeyPress={(e) => {
783
+ if (e.nativeEvent.key === 'Enter') {
784
+ Keyboard.dismiss();
785
+ }
786
+ }}
787
+ keyboardType={TextInputProps['keyboardType'] || 'default'}
788
+ clearButtonMode="while-editing"
789
+ returnKeyType={TextInputProps['returnKeyType'] || 'done'}
790
+ maxLength={TextInputProps['maxLength'] || 100}
791
+ accessibilityLabel="selectInput"
792
+ accessible={true}
793
+ autoFocus={TextInputProps['autoFocus'] || false}
794
+ autoCorrect={false}
795
+ underlineColorAndroid="transparent"
796
+ editable={TextInputProps['editable'] || true}
797
+ secureTextEntry={TextInputProps['secureTextEntry'] || false}
798
+ defaultValue=""
799
+ caretHidden={false}
800
+ enablesReturnKeyAutomatically
801
+ onFocus={() => {
802
+ console.log(
803
+ 'AutoPositionedPopup onFocus tag=',
804
+ tag,
805
+ ' selectedItem=',
806
+ state.selectedItem,
807
+ ' hasTriggeredFocus.current=',
808
+ hasTriggeredFocus.current,
809
+ ' textInputRef.current=',
810
+ textInputRef.current,
811
+ ' ref_searchQuery.current=',
812
+ ref_searchQuery.current
813
+ );
814
+ if (!hasTriggeredFocus.current) {
815
+ hasTriggeredFocus.current = true;
816
+ ref_isFocus.current = true;
817
+ if (state.selectedItem) {
818
+ ref_searchQuery.current = state.selectedItem.title;
819
+ }
820
+ if (textInputRef.current && ref_searchQuery.current) {
821
+ textInputRef.current.setNativeProps({
822
+ text: ref_searchQuery.current,
823
+ });
824
+ }
825
+ }
826
+ }}
827
+ onBlur={() => {
828
+ console.log(
829
+ 'AutoPositionedPopup onBlur tag=',
830
+ tag,
831
+ 'textInputRef.current=',
832
+ textInputRef.current
833
+ );
834
+ hasTriggeredFocus.current = false;
835
+ hasAddedRootView.current = false; // 重置 RootView 狀態
836
+ hasShownRootView.current = false;
837
+ ref_isFocus.current = false;
838
+ setState((prevState) => {
839
+ return {
840
+ ...prevState,
841
+ isFocus: false,
842
+ };
843
+ });
844
+ removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
845
+ setSearchQuery('');
846
+ if (textInputRef.current) {
847
+ textInputRef.current.setNativeProps({text: ''});
848
+ ref_searchQuery.current = '';
849
+ textInputRef.current.blur();
850
+ }
850
851
  Keyboard.dismiss();
851
852
  }}
852
- returnKeyType="done"
853
- {...TextInputProps}
853
+ selectTextOnFocus={TextInputProps['selectTextOnFocus'] || false}
854
+ onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
855
+ console.log(
856
+ 'AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=',
857
+ e.nativeEvent.text
858
+ );
859
+ onSubmitEditing && onSubmitEditing(e);
860
+ }}
854
861
  />
855
862
  )
856
863
  )}
857
864
  </View>
858
865
  </CustomRow>
859
866
  );
860
- }, [state.isFocus, useTextInput, AutoPositionedPopupBtnStyle, AutoPositionedPopupBtnDisabled, handleButtonPress, btwChildren, state.selectedItem, theme.colors.text, labelStyle, placeholder, textInputRef, inputStyle, TextInputProps, searchQueryRef, handleSearchChange, onSubmitEditing, style, refAutoPositionedPopup]);
867
+ }, [tag,
868
+ fetchData,
869
+ renderItem,
870
+ onItemSelected,
871
+ onSubmitEditing,
872
+ localSearch,
873
+ placeholder,
874
+ textAlign,
875
+ pageSize,
876
+ selectedItem,
877
+ CustomRow,
878
+ useTextInput,
879
+ btwChildren,
880
+ selectedItem,
881
+ keyExtractor,
882
+ AutoPositionedPopupBtnStyle,
883
+ CustomPopView,
884
+ CustomPopViewStyle,
885
+ forceRemoveAllRootViewOnItemSelected,
886
+ inputStyle,
887
+ TextInputProps,
888
+ state.isFocus,]);
861
889
  }
862
890
  )
863
891
  );