react-native-auto-positioned-popup 1.2.16 → 1.2.18

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.
@@ -1,3 +1,25 @@
1
+ // Module load marker - unique ID for tracking code version
2
+ // V19f (2025-01-04): CORRECT direction for coordinate adjustment - ADD statusBarHeight to move popup DOWN
3
+ // Wait 1 second for KeyboardAwareScrollView to stabilize, then use measureInWindow to get trigger's FINAL position
4
+ // NOTE: Parent component (KeyboardAwareScrollView) is responsible for scrolling trigger into view
5
+ // DEBUG FLAG: Set to false to disable all console logs for better performance
6
+ const POPUP_DEBUG = false; // DISABLED: Too many logs cause app freeze
7
+ const POPUP_POSITION_DEBUG = true; // Only log positioning calculations
8
+ const debugLog = (...args: any[]) => {
9
+ if (POPUP_DEBUG) {
10
+ console.log(...args);
11
+ }
12
+ };
13
+ // Separate logging function for position-related logs only
14
+ const positionDebugLog = (...args: any[]) => {
15
+ if (POPUP_POSITION_DEBUG) {
16
+ console.log(...args);
17
+ }
18
+ };
19
+
20
+ // Only log module load in debug mode
21
+ positionDebugLog('POPUP_MODULE_V19f_LOADED at ' + new Date().toISOString() + ' (Parent handles scroll)');
22
+
1
23
  import React, {
2
24
  ForwardedRef,
3
25
  forwardRef,
@@ -35,7 +57,7 @@ import {useKeyboardStatus} from './KeyboardManager';
35
57
  type QueryListener = (query: string) => void;
36
58
  const queryChangeListeners: QueryListener[] = [];
37
59
  const emitQueryChange = (query: string) => {
38
- console.log('AutoPositionedPopup.tsx emitQueryChange query=', query, ' listeners=', queryChangeListeners.length);
60
+ debugLog('AutoPositionedPopup.tsx emitQueryChange query=', query, ' listeners=', queryChangeListeners.length);
39
61
  queryChangeListeners.forEach((l) => l(query));
40
62
  };
41
63
  const subscribeQueryChange = (listener: QueryListener) => {
@@ -92,7 +114,7 @@ const ListItem: React.FC<{
92
114
  rootViewsRef.current = rootViews;
93
115
  }, [rootViews]);
94
116
  return useMemo(() => {
95
- // console.log('AutoPositionedPopup.tsx ListItem=', {index, item, selectedItem});
117
+ // debugLog('AutoPositionedPopup.tsx ListItem=', {index, item, selectedItem});
96
118
  const isSelected = item.id === selectedItem?.id || item.title == selectedItem?.title;
97
119
  return (
98
120
  <TouchableOpacity
@@ -102,8 +124,8 @@ const ListItem: React.FC<{
102
124
  {backgroundColor: isSelected ? (themeMode === 'light' ? 'rgba(116, 116, 128, 0.08)' : 'rgba(120, 120, 128, 0.36)') : 'transparent'},
103
125
  ]}
104
126
  onPress={() => {
105
- // console.log('AutoPositionedPopup.tsx ListItem onPress item=', item); // Commented to prevent spam
106
- // console.log('AutoPositionedPopup.tsx ListItem onPress rootViews=', rootViewsRef.current); // Commented to prevent spam
127
+ // debugLog('AutoPositionedPopup.tsx ListItem onPress item=', item); // Commented to prevent spam
128
+ // debugLog('AutoPositionedPopup.tsx ListItem onPress rootViews=', rootViewsRef.current); // Commented to prevent spam
107
129
  updateState('selectedItem', item);
108
130
  }}
109
131
  >
@@ -171,16 +193,16 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
171
193
  useEffect(() => {
172
194
  (async () => {
173
195
  })();
174
- console.log(`AutoPositionedPopupList componentDidMount`);
196
+ debugLog(`AutoPositionedPopupList componentDidMount`);
175
197
  //componentWillUnmount
176
198
  return () => {
177
- console.log(`AutoPositionedPopupList componentWillUnmount`);
199
+ debugLog(`AutoPositionedPopupList componentWillUnmount`);
178
200
  setSearchQuery('');
179
201
  };
180
202
  }, []);
181
203
  useEffect(() => {
182
204
  const unsubscribe = subscribeQueryChange((newQuery: string) => {
183
- console.log('AutoPositionedPopupList useEffect subscribeQueryChange newQuery=', newQuery);
205
+ debugLog('AutoPositionedPopupList useEffect subscribeQueryChange newQuery=', newQuery);
184
206
  ref_searchQuery.current = newQuery;
185
207
  if (ref_list.current) {
186
208
  ref_list.current.scrollToTop();
@@ -190,24 +212,24 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
190
212
  return unsubscribe;
191
213
  }, []);
192
214
  const _updateState = (key: string, value: SelectedItem) => {
193
- console.log('AutoPositionedPopupList _updateState key=', key, ' value=', value);
215
+ debugLog('AutoPositionedPopupList _updateState key=', key, ' value=', value);
194
216
  setState((prevState) => ({
195
217
  ...prevState,
196
218
  [key]: value,
197
219
  }));
198
- console.log('AutoPositionedPopupList _updateState rootViews=', rootViewsRef.current);
220
+ debugLog('AutoPositionedPopupList _updateState rootViews=', rootViewsRef.current);
199
221
  updateState(key, value);
200
222
  };
201
223
  const _fetchData = async ({
202
224
  pageIndex,
203
225
  pageSize: currentPageSize,
204
226
  }: FetchDataParams): Promise<ListData | null> => {
205
- console.log('AutoPositionedPopupList _fetchData=', {pageIndex, pageSize: currentPageSize, 'state.localData': state.localData, 'ref_searchQuery.current': ref_searchQuery.current, localSearch});
227
+ debugLog('AutoPositionedPopupList _fetchData=', {pageIndex, pageSize: currentPageSize, 'state.localData': state.localData, 'ref_searchQuery.current': ref_searchQuery.current, localSearch});
206
228
  if (localSearch && state.localData.length > 0) {
207
229
  const result: SelectedItem[] = state.localData.filter((item: SelectedItem) => {
208
230
  return `${item.title}`?.toLowerCase().includes(ref_searchQuery.current.toLowerCase());
209
231
  });
210
- console.log('AutoPositionedPopupList _fetchData localSearch result=', result);
232
+ debugLog('AutoPositionedPopupList _fetchData localSearch result=', result);
211
233
  return Promise.resolve({
212
234
  items: result,
213
235
  pageIndex: 0,
@@ -220,7 +242,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
220
242
  pageSize: pageSize || 10,
221
243
  searchQuery: ref_searchQuery.current,
222
244
  });
223
- console.log('AutoPositionedPopupList _fetchData res=', res);
245
+ debugLog('AutoPositionedPopupList _fetchData res=', res);
224
246
  if (res?.items && localSearch) {
225
247
  setState((prevState) => {
226
248
  return {
@@ -239,9 +261,9 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
239
261
  }
240
262
  return null;
241
263
  } catch (e) {
242
- console.warn('Error in fetchData:', e);
264
+ debugLog('Error in fetchData:', e);
243
265
  }
244
- console.log('AutoPositionedPopupList _fetchData res=', null);
266
+ debugLog('AutoPositionedPopupList _fetchData res=', null);
245
267
  return null;
246
268
  };
247
269
  const _renderItem = useCallback(
@@ -251,7 +273,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
251
273
  [state.selectedItem, themeMode]
252
274
  );
253
275
  return useMemo(() => {
254
- console.log('AutoPositionedPopupList (global as any)?.$fake=', (global as any)?.$fake);
276
+ debugLog('AutoPositionedPopupList (global as any)?.$fake=', (global as any)?.$fake);
255
277
  // Babel configuration handles the path redirection based on global.$fake
256
278
  // No need for conditional import here
257
279
  return (
@@ -298,7 +320,7 @@ const listLayout = {
298
320
  const AutoPositionedPopup = memo(
299
321
  forwardRef<unknown, AutoPositionedPopupProps>(
300
322
  (props: AutoPositionedPopupProps, parentRef: ForwardedRef<unknown>): React.JSX.Element => {
301
- console.log('AutoPositionedPopup props=', props);
323
+ debugLog('AutoPositionedPopup props=', props);
302
324
  const {
303
325
  tag,
304
326
  style,
@@ -325,11 +347,11 @@ const AutoPositionedPopup = memo(
325
347
  };
326
348
  try {
327
349
  // const res1: any[] = await $api.xxx(pageSize)
328
- // console.log('${NAME} xxx res=', res)
350
+ // debugLog('${NAME} xxx res=', res)
329
351
  // res.items = res1
330
352
  // res.needLoadMore = res1.length === pageSize
331
353
  } catch (e) {
332
- console.warn('Error in fetch operation:', e);
354
+ debugLog('Error in fetch operation:', e);
333
355
  }
334
356
  return res;
335
357
  },
@@ -356,7 +378,7 @@ const AutoPositionedPopup = memo(
356
378
  selectedItem: selectedItem,
357
379
  });
358
380
  // Use RootView context
359
- const {addRootView, setRootViewNativeStyle, removeRootView, rootViews, setSearchQuery} = useRootView();
381
+ const {addRootView, setRootViewNativeStyle, updateRootView, removeRootView, rootViews, setSearchQuery} = useRootView();
360
382
  const rootViewsRef = useRef(rootViews);
361
383
  // Track TextInput focus and RootView states like project implementation
362
384
  const hasTriggeredFocus = useRef(false);
@@ -370,6 +392,8 @@ const AutoPositionedPopup = memo(
370
392
  const ref_searchQuery = useRef<string>('');
371
393
  // Store trigger button position when clicked (before it's replaced by TextInput)
372
394
  const triggerPositionRef = useRef<{x: number; y: number; width: number; height: number} | null>(null);
395
+ // V19: Track keyboard height for accurate popup positioning
396
+ const keyboardHeightRef = useRef<number>(0);
373
397
  // Add ref to track previous keyboard state to avoid false triggers during parent component re-renders
374
398
  const prevIsKeyboardFullyShownRef = useRef<boolean>(false);
375
399
  const prevPropsRef = useRef<{
@@ -404,11 +428,16 @@ const AutoPositionedPopup = memo(
404
428
  const searchQueryRef = useRef<string>(''); // Use ref instead of state to avoid re-renders
405
429
  // Refs to store latest values for useEffect without adding to dependency array
406
430
  const dataRef = useRef<SelectedItem[]>(data);
407
- const isKeyboardFullyShown = useKeyboardStatus();
431
+ // V19: useKeyboardStatus now returns { isShown, height } for accurate positioning
432
+ const keyboardStatus = useKeyboardStatus();
433
+ const isKeyboardFullyShown = keyboardStatus.isShown;
408
434
  const ref_isKeyboardFullyShown = useRef<boolean>(isKeyboardFullyShown);
409
435
  useEffect(() => {
410
436
  ref_isKeyboardFullyShown.current = isKeyboardFullyShown;
411
- }, [isKeyboardFullyShown])
437
+ // V19: Store keyboard height for popup positioning calculations
438
+ keyboardHeightRef.current = keyboardStatus.height;
439
+ positionDebugLog(`KEYBOARD_HEIGHT_UPDATE: height=${keyboardStatus.height} isShown=${isKeyboardFullyShown}`);
440
+ }, [keyboardStatus.isShown, keyboardStatus.height])
412
441
  const theme = defaultTheme;
413
442
 
414
443
  /**
@@ -418,7 +447,7 @@ const AutoPositionedPopup = memo(
418
447
  */
419
448
  const scrollParentToTrigger = useCallback(() => {
420
449
  if (!parentScrollViewRef?.current || !triggerBtnRef.current) {
421
- console.log('AutoPositionedPopup scrollParentToTrigger: No parentScrollViewRef or triggerBtnRef available');
450
+ debugLog('AutoPositionedPopup scrollParentToTrigger: No parentScrollViewRef or triggerBtnRef available');
422
451
  return;
423
452
  }
424
453
 
@@ -428,18 +457,18 @@ const AutoPositionedPopup = memo(
428
457
  const nodeHandle = findNodeHandle(triggerBtnRef.current);
429
458
 
430
459
  if (nodeHandle && scrollView) {
431
- console.log('AutoPositionedPopup scrollParentToTrigger: Scrolling to trigger button with extraHeight=', scrollExtraHeight);
460
+ debugLog('AutoPositionedPopup scrollParentToTrigger: Scrolling to trigger button with extraHeight=', scrollExtraHeight);
432
461
 
433
462
  // KeyboardAwareScrollView has a scrollToFocusedInput method that handles this
434
463
  // However, it requires a ReactNode. We'll use scrollToPosition as an alternative.
435
464
  // First, measure the trigger button position relative to the ScrollView
436
465
  triggerBtnRef.current.measureInWindow((x, y, width, height) => {
437
466
  if (y === undefined || height === undefined) {
438
- console.log('AutoPositionedPopup scrollParentToTrigger: measureInWindow returned undefined');
467
+ debugLog('AutoPositionedPopup scrollParentToTrigger: measureInWindow returned undefined');
439
468
  return;
440
469
  }
441
470
 
442
- console.log('AutoPositionedPopup scrollParentToTrigger: trigger position=', { x, y, width, height });
471
+ debugLog('AutoPositionedPopup scrollParentToTrigger: trigger position=', { x, y, width, height });
443
472
 
444
473
  // Get keyboard height from Keyboard API
445
474
  // On keyboard show, scroll to position that keeps trigger above keyboard
@@ -451,7 +480,7 @@ const AutoPositionedPopup = memo(
451
480
  const triggerBottom = y + height;
452
481
  const visibleAreaBottom = screenHeight - keyboardHeight;
453
482
 
454
- console.log('AutoPositionedPopup scrollParentToTrigger: keyboard data=', {
483
+ debugLog('AutoPositionedPopup scrollParentToTrigger: keyboard data=', {
455
484
  keyboardHeight,
456
485
  screenHeight,
457
486
  triggerBottom,
@@ -462,7 +491,7 @@ const AutoPositionedPopup = memo(
462
491
  if (triggerBottom > visibleAreaBottom) {
463
492
  // Calculate how much to scroll
464
493
  const scrollAmount = triggerBottom - visibleAreaBottom + scrollExtraHeight;
465
- console.log('AutoPositionedPopup scrollParentToTrigger: scrolling by', scrollAmount);
494
+ debugLog('AutoPositionedPopup scrollParentToTrigger: scrolling by', scrollAmount);
466
495
 
467
496
  // Use scrollForExtraHeightOnAndroid or scrollToPosition
468
497
  if (typeof scrollView.scrollToPosition === 'function') {
@@ -470,7 +499,7 @@ const AutoPositionedPopup = memo(
470
499
  scrollView.scrollToPosition(0, scrollAmount, true);
471
500
  } else if (typeof scrollView.scrollToEnd === 'function') {
472
501
  // Fallback: scroll to end might help in some cases
473
- console.log('AutoPositionedPopup scrollParentToTrigger: using scrollToEnd fallback');
502
+ debugLog('AutoPositionedPopup scrollParentToTrigger: using scrollToEnd fallback');
474
503
  }
475
504
  }
476
505
  });
@@ -483,21 +512,21 @@ const AutoPositionedPopup = memo(
483
512
  * Uses stored trigger position (captured before TextInput replaces the trigger button)
484
513
  */
485
514
  const scrollToTriggerWithMeasure = useCallback(() => {
486
- console.log('AutoPositionedPopup scrollToTriggerWithMeasure called, tag=', tag, {
515
+ debugLog('AutoPositionedPopup scrollToTriggerWithMeasure called, tag=', tag, {
487
516
  hasParentScrollViewRef: !!parentScrollViewRef?.current,
488
517
  hasTriggerPosition: !!triggerPositionRef.current,
489
518
  triggerPosition: triggerPositionRef.current
490
519
  });
491
520
 
492
521
  if (!parentScrollViewRef?.current) {
493
- console.log('AutoPositionedPopup scrollToTriggerWithMeasure: parentScrollViewRef not available, tag=', tag);
522
+ debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: parentScrollViewRef not available, tag=', tag);
494
523
  return;
495
524
  }
496
525
 
497
526
  // Use stored trigger position (captured when trigger was clicked)
498
527
  const storedPosition = triggerPositionRef.current;
499
528
  if (!storedPosition) {
500
- console.log('AutoPositionedPopup scrollToTriggerWithMeasure: no stored trigger position, tag=', tag);
529
+ debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: no stored trigger position, tag=', tag);
501
530
  return;
502
531
  }
503
532
 
@@ -513,7 +542,7 @@ const AutoPositionedPopup = memo(
513
542
  const visibleAreaBottom = screenHeight - keyboardApproxHeight;
514
543
  const triggerBottom = triggerY + triggerHeight;
515
544
 
516
- console.log('AutoPositionedPopup scrollToTriggerWithMeasure: calculations=', {
545
+ debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: calculations=', {
517
546
  tag,
518
547
  triggerY,
519
548
  triggerHeight,
@@ -527,7 +556,7 @@ const AutoPositionedPopup = memo(
527
556
  if (triggerBottom > visibleAreaBottom) {
528
557
  // Calculate scroll amount to bring trigger above keyboard
529
558
  const scrollAmount = triggerBottom - visibleAreaBottom + scrollExtraHeight;
530
- console.log('AutoPositionedPopup scrollToTriggerWithMeasure: scrolling, amount=', scrollAmount, 'tag=', tag);
559
+ debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: scrolling, amount=', scrollAmount, 'tag=', tag);
531
560
 
532
561
  // Use scrollForExtraHeightOnAndroid for KeyboardAwareScrollView
533
562
  if (typeof scrollView.scrollForExtraHeightOnAndroid === 'function') {
@@ -538,10 +567,10 @@ const AutoPositionedPopup = memo(
538
567
  // Fallback to standard ScrollView method
539
568
  (scrollView as any).scrollTo({ y: scrollAmount, animated: true });
540
569
  } else {
541
- console.log('AutoPositionedPopup scrollToTriggerWithMeasure: no scroll method available on scrollView');
570
+ debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: no scroll method available on scrollView');
542
571
  }
543
572
  } else {
544
- console.log('AutoPositionedPopup scrollToTriggerWithMeasure: trigger already visible, no scroll needed, tag=', tag);
573
+ debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: trigger already visible, no scroll needed, tag=', tag);
545
574
  }
546
575
  }, [parentScrollViewRef, scrollExtraHeight, tag]);
547
576
 
@@ -551,10 +580,10 @@ const AutoPositionedPopup = memo(
551
580
  useEffect(() => {
552
581
  (async () => {
553
582
  })();
554
- console.log(`AutoPositionedPopup componentDidMount=`, {tag, CustomPopView});
583
+ debugLog(`AutoPositionedPopup componentDidMount=`, {tag, CustomPopView});
555
584
  //componentWillUnmount
556
585
  return () => {
557
- console.log(`AutoPositionedPopup componentWillUnmount tag=`, tag);
586
+ debugLog(`AutoPositionedPopup componentWillUnmount tag=`, tag);
558
587
  removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
559
588
  setSearchQuery('');
560
589
  if (textInputRef.current) {
@@ -567,7 +596,7 @@ const AutoPositionedPopup = memo(
567
596
  };
568
597
  }, []);
569
598
  useEffect(() => {
570
- console.log('AutoPositionedPopup rootViews=', {tag, rootViews});
599
+ debugLog('AutoPositionedPopup rootViews=', {tag, rootViews});
571
600
  rootViewsRef.current = rootViews;
572
601
  if (rootViews.length === 0) {
573
602
  hasAddedRootView.current = false;
@@ -584,10 +613,10 @@ const AutoPositionedPopup = memo(
584
613
  }
585
614
  }, [rootViews]);
586
615
  useEffect(() => {
587
- console.log('AutoPositionedPopup useEffect [selectedItem, state.selectedItem, tag]=', {tag, selectedItem, 'state.selectedItem': state.selectedItem});
588
- console.log('AutoPositionedPopup useEffect state.selectedItem=', state.selectedItem);
616
+ debugLog('AutoPositionedPopup useEffect [selectedItem, state.selectedItem, tag]=', {tag, selectedItem, 'state.selectedItem': state.selectedItem});
617
+ debugLog('AutoPositionedPopup useEffect state.selectedItem=', state.selectedItem);
589
618
  if (state.selectedItem?.id !== selectedItem?.id || state.selectedItem?.title != selectedItem?.title) {
590
- console.log('AutoPositionedPopup useEffect selectedItem!=state.selectedItem');
619
+ debugLog('AutoPositionedPopup useEffect selectedItem!=state.selectedItem');
591
620
  setState((prevState) => {
592
621
  return {
593
622
  ...prevState,
@@ -603,7 +632,7 @@ const AutoPositionedPopup = memo(
603
632
  prevPropsRef.current.CustomPopView !== CustomPopView ||
604
633
  prevPropsRef.current.CustomPopViewStyle !== CustomPopViewStyle ||
605
634
  (prevPropsRef.current.TextInputProps !== TextInputProps && useTextInput);
606
- console.log('AutoPositionedPopup useEffect [isKeyboardFullyShown,\n' +
635
+ debugLog('AutoPositionedPopup useEffect [isKeyboardFullyShown,\n' +
607
636
  ' state.isFocus,\n' +
608
637
  ' useTextInput,\n' +
609
638
  ' CustomPopView,\n' +
@@ -634,13 +663,19 @@ const AutoPositionedPopup = memo(
634
663
  TextInputProps
635
664
  };
636
665
  // Only execute logic when keyboard state actually changes or user actively operates
637
- if (!keyboardStateChanged && hasAddedRootView.current) {
638
- console.log('AutoPositionedPopup: Skip execution - parent component re-rendered but keyboard state unchanged textInputRef.current=', textInputRef.current);
639
- // if (!ref_isFocus.current) {
640
- // textInputRef.current?.focus()
641
- // }
666
+ // CRITICAL FIX: Also allow execution when popup needs initial positioning
667
+ // hasAddedRootView.current = true means popup container exists
668
+ // hasShownRootView.current = false means positioning not done yet
669
+ // We MUST allow execution when popup needs positioning, even if keyboard state unchanged
670
+ if (!keyboardStateChanged && hasAddedRootView.current && hasShownRootView.current) {
671
+ debugLog('AutoPositionedPopup: Skip execution - already positioned and keyboard state unchanged');
642
672
  return;
643
673
  }
674
+
675
+ // Log when we're allowing execution for initial positioning
676
+ if (!keyboardStateChanged && hasAddedRootView.current && !hasShownRootView.current) {
677
+ debugLog('AutoPositionedPopup: ALLOWING execution for initial positioning (popup added but not positioned yet)');
678
+ }
644
679
  const getStatusBarHeight = (): number => {
645
680
  if (Platform.OS === 'android') {
646
681
  // Android: Use StatusBar.currentHeight API
@@ -656,130 +691,163 @@ const AutoPositionedPopup = memo(
656
691
  const statusBarHeight = getStatusBarHeight();
657
692
  if (useTextInput) {
658
693
  if (isKeyboardFullyShown && hasAddedRootView.current && !hasShownRootView.current && state.isFocus) {
659
- // KEYBOARD AVOIDANCE FIX: Scroll parent ScrollView to keep trigger visible
660
- // When keyboard appears, the trigger button may be covered. If parentScrollViewRef
661
- // is provided, scroll the parent to keep the trigger visible above the keyboard.
662
- if (parentScrollViewRef?.current) {
663
- console.log('AutoPositionedPopup: Keyboard appeared, scrolling parent to keep trigger visible');
664
- // Use a slight delay to ensure keyboard animation has started
665
- setTimeout(() => {
666
- scrollToTriggerWithMeasure();
667
- }, 100);
694
+ // KEYBOARD AVOIDANCE FIX: Use KeyboardAwareScrollView's native scrollToFocusedInput method
695
+ // This properly scrolls to the dynamically created TextInput without causing double scrolling.
696
+ // The previous custom scrollToTriggerWithMeasure() caused over-scrolling issues.
697
+ if (parentScrollViewRef?.current && textInputRef.current) {
698
+ debugLog('AutoPositionedPopup: Keyboard appeared, using scrollToFocusedInput to scroll parent');
699
+ // Use KeyboardAwareScrollView's native method to scroll to the focused TextInput
700
+ // This is more reliable than custom scroll calculations
701
+ const scrollView = parentScrollViewRef.current;
702
+ if (typeof scrollView.scrollToFocusedInput === 'function') {
703
+ // findNodeHandle is needed to get the native node reference
704
+ const nodeHandle = findNodeHandle(textInputRef.current);
705
+ if (nodeHandle) {
706
+ // scrollToFocusedInput expects a ReactNode, use the TextInput ref
707
+ scrollView.scrollToFocusedInput(textInputRef.current, scrollExtraHeight);
708
+ debugLog('AutoPositionedPopup: Called scrollToFocusedInput with extraHeight=', scrollExtraHeight);
709
+ }
710
+ } else {
711
+ debugLog('AutoPositionedPopup: scrollToFocusedInput not available, skipping scroll');
712
+ }
668
713
  }
669
714
 
670
715
  // CRITICAL FIX FOR KEYBOARD POSITION CALCULATION
671
716
  // Problem: When keyboard appears, the page shifts up but measureInWindow executes too early
672
- // Solution: Wait for keyboard animation (300ms) + use requestAnimationFrame for next render frame
717
+ // Solution: Wait for keyboard animation + page scroll to complete before measuring
673
718
  //
674
719
  // Timing breakdown:
675
720
  // 1. Keyboard animation: ~250-300ms (iOS/Android)
676
- // 2. Page shift animation: ~200-300ms (KeyboardAvoidingView)
677
- // 3. Layout tree update: ~50-100ms (React Native)
678
- // Total: ~500-700ms needed for stable layout
721
+ // 2. Page shift animation: ~300-500ms (KeyboardAwareScrollView)
722
+ // 3. Layout tree update: ~100-200ms (React Native)
723
+ // Total: ~700-1000ms needed for stable layout
724
+ //
725
+ // USER REQUEST (2025-01-04): Wait 1 second (1000ms) after keyboard appears
726
+ // to ensure trigger component position has fully stabilized after scroll
679
727
  //
680
- // Strategy: setTimeout(300ms) waits for most animations to complete,
728
+ // Strategy: setTimeout(1000ms) waits for all animations to complete,
681
729
  // then requestAnimationFrame ensures measurement happens after next render frame
730
+ const KEYBOARD_STABILIZATION_DELAY = 500; // 500ms as requested by user
731
+ positionDebugLog(`POPUP_WAIT: Waiting ${KEYBOARD_STABILIZATION_DELAY}ms for keyboard/scroll stabilization, tag=${tag}`);
682
732
  setTimeout(() => {
733
+ positionDebugLog(`POPUP_MEASURE_START: ${KEYBOARD_STABILIZATION_DELAY}ms elapsed, now measuring position for tag=${tag}`);
683
734
  requestAnimationFrame(() => {
684
- // CRITICAL FIX: Use triggerBtnRef (the actual TouchableOpacity) for measurement
685
- // instead of refAutoPositionedPopup (the outer View with flex:1/height:100%)
686
- const measureTarget = triggerBtnRef.current || refAutoPositionedPopup.current;
687
- measureTarget?.measureInWindow((x: number | undefined, y: number | undefined, width: number | undefined, height: number | undefined) => {
688
- console.log('AutoPositionedPopup useTextInput measureInWindow (after 300ms + RAF, layout stable)=', {x, y, width, height, usingTriggerRef: !!triggerBtnRef.current});
689
- // CRITICAL FIX: Handle undefined values from measureInWindow
690
- // This can happen during navigation transitions or when view is not yet mounted
691
- if (x === undefined || y === undefined || width === undefined || height === undefined) {
692
- console.warn('AutoPositionedPopup useTextInput: measureInWindow returned undefined values, using fallback position');
693
- const screenHeightFallback = Dimensions.get('window').height;
694
- const screenWidthFallback = Dimensions.get('window').width;
695
- const fallbackY = (screenHeightFallback - listLayout.height) / 2;
696
- const fallbackX = screenWidthFallback * 0.05;
697
- const fallbackWidth = screenWidthFallback * 0.9;
698
- x = fallbackX;
699
- y = fallbackY;
700
- width = fallbackWidth;
701
- height = 50;
702
- }
703
- // CRITICAL FIX: Coordinate system mismatch issue
704
- // Problem: measureInWindow returns coordinates relative to window (fixed reference),
705
- // but popup uses absolute positioning relative to App container (which shifts when keyboard appears)
706
- //
707
- // When keyboard appears:
708
- // 1. measureInWindow returns y relative to window (e.g., y=400 after shifting)
709
- // 2. But popup's absolute positioning is relative to App container
710
- // 3. If App container shifted up by 200px, setting top=200 will display at window.y=0 (wrong!)
711
- //
712
- // Solution: Since popup is rendered at root level and uses absolute positioning,
713
- // we should directly use measureInWindow's y value without additional calculations
714
- // The popup container is at the same level as the page content
715
- const screenHeight = Dimensions.get('window').height; // Use window height, not screen
716
- console.log('AutoPositionedPopup useTextInput positioning data=', {
717
- screenHeight,
718
- componentY: y,
719
- componentHeight: height,
720
- listHeight: listLayout.height
735
+ // CRITICAL FIX: Measure CURRENT position AFTER keyboard animation completes
736
+ // DO NOT use stored triggerPositionRef because keyboard may have shifted the view up
737
+ // Instead, measure the outer wrapper (refAutoPositionedPopup)
738
+ // which reflects the ACTUAL current position after keyboard shift
739
+
740
+ // DEBUG: Log both refs to compare their positions
741
+ positionDebugLog(`POPUP_REFS: textInputRef=${!!textInputRef.current} refAutoPositionedPopup=${!!refAutoPositionedPopup.current}`);
742
+
743
+ // Measure BOTH refs for comparison
744
+ if (textInputRef.current && refAutoPositionedPopup.current) {
745
+ textInputRef.current.measureInWindow((tx: number | undefined, ty: number | undefined, tw: number | undefined, th: number | undefined) => {
746
+ debugLog('AutoPositionedPopup DEBUG: textInputRef position=', {x: tx, y: ty, width: tw, height: th});
747
+ });
748
+ refAutoPositionedPopup.current.measureInWindow((rx: number | undefined, ry: number | undefined, rw: number | undefined, rh: number | undefined) => {
749
+ debugLog('AutoPositionedPopup DEBUG: refAutoPositionedPopup position=', {x: rx, y: ry, width: rw, height: rh});
721
750
  });
722
- // FIXED POSITIONING LOGIC (with keyboard):
723
- // measureInWindow returns coordinates relative to the window (screen)
724
- // The popup uses position: 'absolute' relative to RootViewProvider
725
- // So we should NOT add statusBarHeight to the position calculation
726
- //
727
- // 1. Default: show popup ABOVE the input field
728
- // Position popup so that the trigger remains VISIBLE below the popup
729
- // Use (y + height * 0.7) as reference to compensate for measurement offset
730
- // while still leaving trigger visible (30% of trigger height exposed)
731
- let popupY = y + (height * 0.7) - listLayout.height;
732
-
733
- console.log('AutoPositionedPopup with keyboard: initial calculation for ABOVE position:', {
734
- componentY: y,
735
- componentHeight: height,
736
- popupHeight: listLayout.height,
737
- popupY,
738
- popupBottom: popupY + listLayout.height,
739
- triggerTop: y,
740
- statusBarHeight
751
+ }
752
+
753
+ // CRITICAL FIX: Use textInputRef as primary measurement target
754
+ // refAutoPositionedPopup.measureInWindow() returns undefined values
755
+ // because the outer wrapper View uses flex:1/height:100% which makes it unmeasurable
756
+ // textInputRef reliably returns the actual position of the input field
757
+ const measureTarget = textInputRef.current || refAutoPositionedPopup.current;
758
+
759
+ if (!measureTarget) {
760
+ debugLog('AutoPositionedPopup useTextInput: no measureTarget available, using fallback');
761
+ const screenHeightFallback = Dimensions.get('window').height;
762
+ const screenWidthFallback = Dimensions.get('window').width;
763
+ const fallbackY = (screenHeightFallback - listLayout.height) / 2;
764
+ ref_listPos.current = {x: screenWidthFallback * 0.05, y: fallbackY, width: screenWidthFallback * 0.9};
765
+ updateRootView(tag, {
766
+ style: {
767
+ top: ref_listPos.current?.y,
768
+ left: popUpViewStyle?.left,
769
+ width: popUpViewStyle?.width,
770
+ height: listLayout.height,
771
+ opacity: 1,
772
+ }
741
773
  });
774
+ hasShownRootView.current = true;
775
+ return;
776
+ }
777
+
778
+ // Determine which ref is actually being used (for logging)
779
+ const usingTextInputRef = measureTarget === textInputRef.current;
780
+ debugLog('AutoPositionedPopup useTextInput: using measureTarget=', usingTextInputRef ? 'textInputRef' : 'refAutoPositionedPopup');
781
+
782
+ // V19f: Position popup above trigger
783
+ // Parent KeyboardAwareScrollView is responsible for scrolling trigger into view
784
+ // This component only handles popup positioning relative to trigger's FINAL position
785
+ const screenHeight = Dimensions.get('window').height;
786
+ const screenWidth = Dimensions.get('window').width;
787
+ const currentKeyboardHeight = keyboardHeightRef.current;
788
+ const popupHeight = listLayout.height; // 200px
789
+
790
+ positionDebugLog(`V19f_SCREEN: height=${screenHeight} width=${screenWidth} keyboardH=${currentKeyboardHeight} statusBarH=${statusBarHeight}`);
791
+
792
+ measureTarget.measureInWindow((x: number | undefined, y: number | undefined, width: number | undefined, height: number | undefined) => {
793
+ positionDebugLog(`V19f_MEASURE: triggerX=${x} triggerY=${y} triggerW=${width} triggerH=${height}`);
742
794
 
743
- // 2. Check if showing above would go behind status bar
744
- if (popupY < statusBarHeight) {
745
- console.log('AutoPositionedPopup with keyboard: would go behind status bar, showing BELOW instead');
746
- // Show BELOW the input field
747
- // Since y + height represents the trigger's "reference bottom" (accounting for measurement offset),
748
- // we need to add another height to position popup BELOW the actual trigger
749
- popupY = y + height + height;
750
- console.log('AutoPositionedPopup with keyboard: BELOW position calculated:', {
751
- formula: 'y + 2*height',
752
- y,
753
- height,
754
- popupY
795
+ // Handle undefined values
796
+ if (x === undefined || y === undefined || width === undefined || height === undefined) {
797
+ positionDebugLog('V19f: undefined values, using center fallback');
798
+ const fallbackY = (screenHeight - currentKeyboardHeight - popupHeight) / 2;
799
+ updateRootView(tag, {
800
+ style: { top: fallbackY, left: popUpViewStyle?.left, width: popUpViewStyle?.width, height: popupHeight, opacity: 1 }
755
801
  });
802
+ hasShownRootView.current = true;
803
+ return;
804
+ }
756
805
 
757
- // 3. Also check if showing below would go off the bottom
758
- const maxY = screenHeight - listLayout.height;
806
+ const triggerTop = y;
807
+ const triggerHeight = height;
808
+ const triggerBottom = y + height;
809
+ const keyboardTop = screenHeight - currentKeyboardHeight;
810
+
811
+ positionDebugLog(`V19f_ANALYSIS: triggerTop=${triggerTop} triggerBottom=${triggerBottom} keyboardTop=${keyboardTop}`);
812
+
813
+ // V19f: Position popup DIRECTLY above trigger
814
+ // ADD statusBarHeight to close the gap (coordinates adjustment)
815
+ let popupY = triggerTop - popupHeight + statusBarHeight;
816
+ let position = 'ABOVE';
817
+
818
+ positionDebugLog(`V19f_CALC: base=${triggerTop - popupHeight} + statusBarH=${statusBarHeight} = popupY=${popupY}`);
819
+
820
+ // Safety check: ensure popup doesn't go above screen top
821
+ if (popupY < 0) {
822
+ // If popup would go off screen top, position it BELOW trigger instead
823
+ popupY = triggerBottom + statusBarHeight;
824
+ position = 'BELOW';
825
+ // Clamp to stay above keyboard
826
+ const maxY = keyboardTop - popupHeight;
759
827
  if (popupY > maxY) {
760
- // If both positions are problematic, clamp to visible area
761
- console.log('AutoPositionedPopup with keyboard: both positions problematic, clamping to visible area');
762
- popupY = Math.min(Math.max(statusBarHeight, y - listLayout.height), maxY);
828
+ popupY = maxY;
763
829
  }
764
- } else {
765
- console.log('AutoPositionedPopup with keyboard: showing ABOVE input field (preferred position)');
830
+ positionDebugLog(`V19f_BELOW: popupY=${popupY} (clamped to stay above keyboard)`);
766
831
  }
767
- ref_listPos.current = {x: x, y: popupY, width: width};
768
- console.log('AutoPositionedPopup useTextInput final position=', ref_listPos.current);
769
- setRootViewNativeStyle(tag, {
770
- top: ref_listPos.current?.y,
771
- left: popUpViewStyle?.left,
772
- width: popUpViewStyle?.width,
773
- height: listLayout.height,
774
- opacity: 1,
832
+
833
+ // V19f: Verification
834
+ const popupBottom = popupY + popupHeight;
835
+ const gapPixels = triggerTop - popupBottom;
836
+
837
+ positionDebugLog(`V19f_RESULT: position=${position} popupY=${popupY} popupBottom=${popupBottom}`);
838
+ positionDebugLog(`V19f_GAP: trigger_top=${triggerTop} - popup_bottom=${popupBottom} = gap=${gapPixels}px`);
839
+
840
+ ref_listPos.current = {x, y: popupY, width};
841
+ updateRootView(tag, {
842
+ style: { top: popupY, left: popUpViewStyle?.left, width: popUpViewStyle?.width, height: listLayout.height, opacity: 1 }
775
843
  });
776
844
  hasShownRootView.current = true;
777
845
  });
778
846
  });
779
- }, 300) // 300ms is sufficient for keyboard animation, as proven by user testing (even 3000ms didn't fix wrong logic)
847
+ }, KEYBOARD_STABILIZATION_DELAY) // 1000ms to wait for keyboard + scroll stabilization (user request 2025-01-04)
780
848
  } else if (!isKeyboardFullyShown && ref_isFocus.current && keyboardStateChanged) {
781
849
  // Only execute close logic when keyboard state actually changes from true to false
782
- console.log(
850
+ debugLog(
783
851
  'AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView (keyboard state changed)=',
784
852
  {tag, forceRemoveAllRootViewOnItemSelected, keyboardStateChanged}
785
853
  );
@@ -795,217 +863,83 @@ const AutoPositionedPopup = memo(
795
863
  hasShownRootView.current = false;
796
864
  }
797
865
  } else {
866
+ // V17 SIMPLIFICATION: When useTextInput=false, ALWAYS show popup in CENTER of screen
867
+ // User request: "只要传入的 useTextInput 是 false, 弹框都显示在屏幕中间"
868
+ // This avoids all complex positioning calculations that kept failing
798
869
  if (state.isFocus) {
799
870
  if (isKeyboardFullyShown) {
800
871
  Keyboard.dismiss();
801
872
  return;
802
873
  }
803
- // CRITICAL FIX: Use triggerBtnRef (the actual TouchableOpacity) for measurement
804
- // instead of refAutoPositionedPopup (the outer View with flex:1/height:100%)
805
- // This ensures accurate position when component is inside complex layouts like KeyboardAwareScrollView
806
- const measureTarget = triggerBtnRef.current || refAutoPositionedPopup.current;
807
- measureTarget?.measureInWindow((x: number | undefined, y: number | undefined, width: number | undefined, height: number | undefined) => {
808
- console.log('AutoPositionedPopup !useTextInput measureInWindow=', {x, y, width, height, usingTriggerRef: !!triggerBtnRef.current});
809
- // CRITICAL FIX: Handle undefined values from measureInWindow
810
- // This can happen during navigation transitions or when view is not yet mounted
811
- if (x === undefined || y === undefined || width === undefined || height === undefined) {
812
- console.warn('AutoPositionedPopup: measureInWindow returned undefined values, using fallback position');
813
- // Use screen center as fallback position
814
- const screenHeight = Dimensions.get('window').height;
815
- const screenWidth = Dimensions.get('window').width;
816
- const fallbackY = (screenHeight - listLayout.height) / 2;
817
- const fallbackX = screenWidth * 0.05; // 5% from left
818
- const fallbackWidth = screenWidth * 0.9; // 90% width
819
- ref_listPos.current = { x: fallbackX, y: fallbackY, width: fallbackWidth };
820
- console.log('AutoPositionedPopup !useTextInput using fallback position=', ref_listPos.current);
821
- // Proceed with fallback values
822
- x = fallbackX;
823
- y = fallbackY;
824
- width = fallbackWidth;
825
- height = 50; // Default height for the trigger element
826
- }
827
- // CORRECT POSITIONING LOGIC (as per user requirement)
828
- // Default: show popup ABOVE the input field
829
- // Only if that goes off the top of screen (considering status bar), show BELOW instead
830
- const calculateOptimalPosition = (componentY: number, componentHeight: number, popupHeight: number) => {
831
- console.log('AutoPositionedPopup calculateOptimalPosition executing');
832
- // Use window height (visible area) instead of screen height
833
- const screenHeight = Dimensions.get('window').height;
834
- console.log('AutoPositionedPopup positioning data:', {
835
- screenHeight,
836
- componentY,
837
- componentHeight,
838
- popupHeight,
839
- statusBarHeight,
840
- platform: Platform.OS
841
- });
842
- // FIXED POSITIONING LOGIC:
843
- // The popup uses position: 'absolute' relative to the RootViewProvider container
844
- // measureInWindow returns coordinates relative to the window (screen)
845
- // So we should NOT add statusBarHeight to the position calculation
846
- //
847
- // 1. Default: show popup ABOVE the trigger element
848
- // FIX: Use (componentY + componentHeight) as the trigger's bottom edge reference point
849
- // This compensates for measurement inaccuracies when trigger is inside complex layouts (FlatList, ScrollView)
850
- // The popup's bottom should be at the trigger's top with minimal gap (≤5px)
851
- // Formula: popup_top = trigger_bottom - componentHeight - popupHeight
852
- // popup_bottom = trigger_bottom - componentHeight = trigger_top
853
- let popupY = componentY + componentHeight - popupHeight;
854
-
855
- console.log('AutoPositionedPopup: initial calculation for ABOVE position:', {
856
- componentY,
857
- componentHeight,
858
- popupHeight,
859
- popupY,
860
- triggerBottom: componentY + componentHeight,
861
- statusBarHeight
862
- });
863
874
 
864
- // 2. Check if showing above would go off the top of screen (behind status bar)
865
- if (popupY < statusBarHeight) {
866
- console.log('AutoPositionedPopup: would go behind status bar, showing BELOW instead');
867
- // Show BELOW the trigger element
868
- // Since componentY + componentHeight represents the trigger's "reference bottom" (accounting for measurement offset),
869
- // we need to add another componentHeight to position popup BELOW the actual trigger
870
- // Formula: popup top = componentY + (2 * componentHeight)
871
- // - (componentY + componentHeight) = trigger's actual top (compensated)
872
- // - + componentHeight = skip past trigger height to get to trigger's actual bottom
873
- popupY = componentY + componentHeight + componentHeight;
874
- console.log('AutoPositionedPopup: BELOW position calculated:', {
875
- formula: 'componentY + 2*componentHeight',
876
- componentY,
877
- componentHeight,
878
- popupY
879
- });
875
+ debugLog('🟢🟢🟢 POPUP_V17 useTextInput=false, showing popup in CENTER of screen');
880
876
 
881
- // 3. Also check if showing below would go off the bottom of screen
882
- const maxY = screenHeight - popupHeight;
883
- if (popupY > maxY) {
884
- // If both positions are problematic, clamp to visible area
885
- // Prioritize showing as close to trigger as possible
886
- console.log('AutoPositionedPopup: both positions problematic, clamping to visible area');
887
- popupY = Math.min(Math.max(statusBarHeight, componentY - popupHeight), maxY);
888
- }
889
- } else {
890
- console.log('AutoPositionedPopup: showing ABOVE input field (preferred position)');
891
- }
892
- console.log('AutoPositionedPopup final position:', {
893
- popupY,
894
- 'showing above': popupY < componentY,
895
- 'below status bar': popupY >= statusBarHeight
896
- });
897
- return {finalY: popupY, showAbove: popupY < componentY};
898
- };
899
- // Calculate position ONCE based on actual popup height
900
- const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
901
- ? CustomPopViewStyle.height
902
- : listLayout.height;
903
- console.log('AutoPositionedPopup 🔥 Using actualPopupHeight for calculation:', {actualPopupHeight, CustomPopView: !!CustomPopView});
904
- const positionResult = calculateOptimalPosition(y, height, actualPopupHeight);
905
- console.log('AutoPositionedPopup FINAL position result:', positionResult);
906
- ref_listPos.current = {x: x, y: positionResult.finalY, width: width};
907
- console.log('AutoPositionedPopup !useTextInput ref_listPos.current=', ref_listPos.current);
908
- if (CustomPopView && CustomPopViewStyle) {
909
- // Position already calculated correctly above, no need to recalculate
910
- const PopViewComponent = CustomPopView();
911
- console.log('AutoPositionedPopup !useTextInput addRootView=', {CustomPopViewStyle, PopViewComponent, 'state.selectedItem': state.selectedItem});
912
- addRootView({
913
- id: tag,
914
- style: !centerDisplay
915
- ? {
916
- top: ref_listPos.current.y,
917
- left: popUpViewStyle?.left,
918
- width: popUpViewStyle?.width,
919
- height: listLayout.height,
920
- opacity: 1,
921
- ...CustomPopViewStyle,
922
- }
923
- : {width: popUpViewStyle?.width, height: listLayout.height, ...CustomPopViewStyle},
924
- component: <PopViewComponent selectedItem={state.selectedItem}></PopViewComponent>,
925
- useModal: true,
926
- onModalClose: () => {
927
- console.log('AutoPositionedPopup onModalClose');
928
- removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
929
- setState((prevState) => {
930
- return {
931
- ...prevState,
932
- isFocus: false,
933
- };
934
- });
935
- hasAddedRootView.current = false;
936
- hasShownRootView.current = false;
937
- hasTriggeredFocus.current = false;
938
- setSearchQuery('');
939
- },
940
- centerDisplay,
941
- });
942
- } else {
943
- console.log('AutoPositionedPopup !useTextInput addRootView tag=', tag);
944
- addRootView({
945
- id: tag,
946
- style: {
947
- top: ref_listPos.current.y,
948
- left: popUpViewStyle?.left,
949
- width: popUpViewStyle?.width,
950
- height: listLayout.height,
951
- opacity: 1,
952
- },
953
- component: (
954
- <AutoPositionedPopupList
955
- tag={tag}
956
- updateState={updateState}
957
- fetchData={fetchData}
958
- pageSize={pageSize}
959
- renderItem={renderItem}
960
- selectedItem={state.selectedItem}
961
- localSearch={localSearch}
962
- showListEmptyComponent={showListEmptyComponent}
963
- emptyText={emptyText}
964
- themeMode={themeMode}
965
- />
966
- ),
967
- useModal: true,
968
- onModalClose: () => {
969
- console.log('AutoPositionedPopup onModalClose tag=', tag);
970
- removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
971
- setState((prevState) => {
972
- return {
973
- ...prevState,
974
- };
975
- });
976
- setSearchQuery('');
977
- },
978
- });
979
- }
980
- });
981
- }
982
- }
983
- if (isKeyboardFullyShown) {
984
- ref_isFocus.current = state.isFocus ?? false;
985
- if (isKeyboardFullyShown !== keyboardVisibleRef.current) {
986
- keyboardVisibleRef.current = isKeyboardFullyShown;
987
- if (isKeyboardFullyShown && textInputRef.current) {
988
- if (ref_searchQuery.current) {
989
- textInputRef.current.setNativeProps({text: ref_searchQuery.current});
990
- }
877
+ const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
878
+ ? CustomPopViewStyle.height
879
+ : listLayout.height;
880
+
881
+ if (CustomPopView && CustomPopViewStyle) {
882
+ const PopViewComponent = CustomPopView();
883
+ debugLog('🔵🔵🔵 POPUP_V17 CustomPopView centerDisplay=true');
884
+ addRootView({
885
+ id: tag,
886
+ style: { width: popUpViewStyle?.width, ...CustomPopViewStyle },
887
+ component: <PopViewComponent selectedItem={state.selectedItem}></PopViewComponent>,
888
+ useModal: true,
889
+ centerDisplay: true, // V17: Force center display for useTextInput=false
890
+ onModalClose: () => {
891
+ debugLog('AutoPositionedPopup V17 onModalClose tag=', tag);
892
+ removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
893
+ setState((prevState) => ({ ...prevState }));
894
+ setSearchQuery('');
895
+ },
896
+ });
897
+ } else {
898
+ debugLog('🔵🔵🔵 POPUP_V17 List centerDisplay=true, height=', listLayout.height);
899
+ addRootView({
900
+ id: tag,
901
+ style: { width: popUpViewStyle?.width, height: listLayout.height, opacity: 1 },
902
+ component: (
903
+ <AutoPositionedPopupList
904
+ tag={tag} updateState={updateState} fetchData={fetchData} pageSize={pageSize}
905
+ renderItem={renderItem} selectedItem={state.selectedItem} localSearch={localSearch}
906
+ showListEmptyComponent={showListEmptyComponent} emptyText={emptyText} themeMode={themeMode}
907
+ />
908
+ ),
909
+ useModal: true,
910
+ centerDisplay: true, // V17: Force center display for useTextInput=false
911
+ onModalClose: () => {
912
+ debugLog('AutoPositionedPopup V17 onModalClose tag=', tag);
913
+ removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
914
+ setState((prevState) => ({ ...prevState }));
915
+ setSearchQuery('');
916
+ },
917
+ });
991
918
  }
919
+ return; // V17: Early return after handling !useTextInput case
992
920
  }
993
921
  }
994
922
  }, [isKeyboardFullyShown,
995
- state.isFocus,
996
- useTextInput,
997
- CustomPopView,
998
- CustomPopViewStyle,
999
- forceRemoveAllRootViewOnItemSelected,
1000
- tag, TextInputProps,
1001
- state.selectedItem, showListEmptyComponent, themeMode
1002
- ]);
1003
- // Imperative handle for parent component access
1004
- useImperativeHandle(
923
+ state.isFocus,
924
+ useTextInput,
925
+ CustomPopView,
926
+ CustomPopViewStyle,
927
+ forceRemoveAllRootViewOnItemSelected,
928
+ tag, TextInputProps,
929
+ state.selectedItem, showListEmptyComponent, themeMode
930
+ ]);
931
+
932
+ // V18: All positioning logic is now in the useEffect above
933
+ // V18 FIX (2025-01-04): Wait 1000ms after keyboard appears before measuring position
934
+ // This ensures trigger position is stable after KeyboardAwareScrollView scrolls
935
+ // Formula: top = componentY - popupHeight (popup bottom touches trigger top exactly)
936
+
937
+ // Imperative handle for parent component access
938
+ useImperativeHandle(
1005
939
  parentRef,
1006
940
  () => ({
1007
941
  clearSelectedItem: () => {
1008
- console.log('AutoPositionedPopup clearSelectedItem tag=', tag);
942
+ debugLog('AutoPositionedPopup clearSelectedItem tag=', tag);
1009
943
  setState((prevState) => {
1010
944
  return {
1011
945
  ...prevState,
@@ -1027,14 +961,14 @@ const AutoPositionedPopup = memo(
1027
961
  []
1028
962
  );
1029
963
  const updateState = (key: string, value: SelectedItem) => {
1030
- console.log('AutoPositionedPopup updateState=', {key, value});
964
+ debugLog('AutoPositionedPopup updateState=', {key, value});
1031
965
  setState((prevState) => ({
1032
966
  ...prevState,
1033
967
  [key]: value,
1034
968
  }));
1035
969
  if (key === 'selectedItem' && onItemSelected) {
1036
970
  onItemSelected(value);
1037
- console.log('AutoPositionedPopup updateState onItemSelected rootViewsRef.current=', rootViewsRef.current);
971
+ debugLog('AutoPositionedPopup updateState onItemSelected rootViewsRef.current=', rootViewsRef.current);
1038
972
  removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
1039
973
  hasAddedRootView.current = false;
1040
974
  hasShownRootView.current = false;
@@ -1069,7 +1003,7 @@ const AutoPositionedPopup = memo(
1069
1003
  // Only update when deep comparison detects real changes to avoid TextInput recreation due to reference changes during parent component redraws
1070
1004
  const stableInputStyle = useMemo(() => {
1071
1005
  if (!shallowEqual(stableInputStyleRef.current, inputStyle)) {
1072
- console.log(`AutoPositionedPopup stableInputStyle: `, {tag, inputStyle, themeMode});
1006
+ debugLog(`AutoPositionedPopup stableInputStyle: `, {tag, inputStyle, themeMode});
1073
1007
  stableInputStyleRef.current = inputStyle;
1074
1008
  }
1075
1009
  return stableInputStyleRef.current;
@@ -1077,10 +1011,10 @@ const AutoPositionedPopup = memo(
1077
1011
 
1078
1012
  const stableTextInputProps = useMemo(() => {
1079
1013
  if (!shallowEqual(stableTextInputPropsRef.current, TextInputProps)) {
1080
- console.log(`AutoPositionedPopup TextInputProps deep change detected, updating stable reference - tag: ${tag}`);
1014
+ debugLog(`AutoPositionedPopup TextInputProps deep change detected, updating stable reference - tag: ${tag}`);
1081
1015
  stableTextInputPropsRef.current = TextInputProps;
1082
1016
  }
1083
- console.log('AutoPositionedPopup stableTextInputProps=', {tag, TextInputProps, 'stableTextInputPropsRef.current': stableTextInputPropsRef.current})
1017
+ debugLog('AutoPositionedPopup stableTextInputProps=', {tag, TextInputProps, 'stableTextInputPropsRef.current': stableTextInputPropsRef.current})
1084
1018
  return stableTextInputPropsRef.current;
1085
1019
  }, [TextInputProps, tag]);
1086
1020
 
@@ -1093,7 +1027,7 @@ const AutoPositionedPopup = memo(
1093
1027
  const handleTextInputFocus = useCallback(() => {
1094
1028
  const currentTime = Date.now();
1095
1029
  const timeSinceLastFocus = currentTime - lastFocusTimeRef.current;
1096
- console.log(
1030
+ debugLog(
1097
1031
  'AutoPositionedPopup onFocus=',
1098
1032
  {
1099
1033
  tag,
@@ -1108,17 +1042,17 @@ const AutoPositionedPopup = memo(
1108
1042
  );
1109
1043
  // Prevent rapid repeated triggers (repeated events within 300ms are ignored)
1110
1044
  if (timeSinceLastFocus < 300) {
1111
- console.log('AutoPositionedPopup onFocus: Skip - event triggered too quickly (< 300ms)');
1045
+ debugLog('AutoPositionedPopup onFocus: Skip - event triggered too quickly (< 300ms)');
1112
1046
  return;
1113
1047
  }
1114
1048
  // Skip if keyboard is already open and focus has been handled
1115
1049
  if (isKeyboardFullyShown && hasTriggeredFocus.current) {
1116
- console.log('AutoPositionedPopup onFocus: Skip - keyboard already open and focus handled');
1050
+ debugLog('AutoPositionedPopup onFocus: Skip - keyboard already open and focus handled');
1117
1051
  return;
1118
1052
  }
1119
1053
  // Prevent concurrent processing
1120
1054
  if (isFocusEventProcessingRef.current) {
1121
- console.log('AutoPositionedPopup onFocus: Skip - processing another focus event');
1055
+ debugLog('AutoPositionedPopup onFocus: Skip - processing another focus event');
1122
1056
  return;
1123
1057
  }
1124
1058
  isFocusEventProcessingRef.current = true;
@@ -1142,7 +1076,7 @@ const AutoPositionedPopup = memo(
1142
1076
  }, [tag, isKeyboardFullyShown]); // Remove state.selectedItem, use stateRef instead
1143
1077
 
1144
1078
  const handleTextInputBlur = useCallback(() => {
1145
- console.log(
1079
+ debugLog(
1146
1080
  'AutoPositionedPopup onBlur=',
1147
1081
  {
1148
1082
  tag,
@@ -1153,7 +1087,7 @@ const AutoPositionedPopup = memo(
1153
1087
  );
1154
1088
  // If keyboard is still open, this is a false trigger caused by parent component re-render, should not reset
1155
1089
  if (isKeyboardFullyShown && hasTriggeredFocus.current) {
1156
- console.log('AutoPositionedPopup onBlur: Skip - keyboard still open, possibly caused by parent component re-render');
1090
+ debugLog('AutoPositionedPopup onBlur: Skip - keyboard still open, possibly caused by parent component re-render');
1157
1091
  return;
1158
1092
  }
1159
1093
 
@@ -1182,7 +1116,7 @@ const AutoPositionedPopup = memo(
1182
1116
  // Wrap TextInput independently in useMemo to recreate only when key props change
1183
1117
  // This avoids repeated ref callback triggers due to other props changes during parent component redraws
1184
1118
  const memoizedTextInput = useMemo(() => {
1185
- console.log('AutoPositionedPopup memoizedTextInput=', {tag, useTextInput, 'state.isFocus': state.isFocus, stableTextInputProps});
1119
+ debugLog('AutoPositionedPopup memoizedTextInput=', {tag, useTextInput, 'state.isFocus': state.isFocus, stableTextInputProps});
1186
1120
  if (!useTextInput || !state.isFocus) {
1187
1121
  return null;
1188
1122
  }
@@ -1191,11 +1125,11 @@ const AutoPositionedPopup = memo(
1191
1125
  ref={(ref) => {
1192
1126
  // Monitor TextInput mounting and unmounting
1193
1127
  if (ref && !textInputRef.current) {
1194
- console.log(`AutoPositionedPopup TextInput created/mounted - tag: ${tag}, ref:`, ref);
1128
+ debugLog(`AutoPositionedPopup TextInput created/mounted - tag: ${tag}, ref:`, ref);
1195
1129
  } else if (!ref && textInputRef.current) {
1196
- console.log(`AutoPositionedPopup TextInput unmounted - tag: ${tag}`);
1130
+ debugLog(`AutoPositionedPopup TextInput unmounted - tag: ${tag}`);
1197
1131
  } else if (ref && textInputRef.current && ref !== textInputRef.current) {
1198
- console.log(`AutoPositionedPopup TextInput replaced - tag: ${tag}, oldRef:`, textInputRef.current, 'newRef:', ref);
1132
+ debugLog(`AutoPositionedPopup TextInput replaced - tag: ${tag}, oldRef:`, textInputRef.current, 'newRef:', ref);
1199
1133
  }
1200
1134
  textInputRef.current = ref;
1201
1135
  }}
@@ -1203,14 +1137,14 @@ const AutoPositionedPopup = memo(
1203
1137
  style={[
1204
1138
  styles.inputStyle,
1205
1139
  stableInputStyle,
1206
- (themeMode==='dark' && {color:'#fff'})
1140
+ (themeMode==='dark' && {color:'#fff'}),
1207
1141
  ]}
1208
1142
  textAlign={stableTextInputProps && stableTextInputProps['textAlign'] || 'left'}
1209
1143
  multiline={stableTextInputProps && stableTextInputProps['multiline'] || false}
1210
1144
  numberOfLines={stableTextInputProps && stableTextInputProps['numberOfLines'] || 1}
1211
1145
  onChangeText={(searchQuery) => {
1212
1146
  ref_searchQuery.current = searchQuery;
1213
- console.log('AutoPositionedPopup onChangeText rootViews=', rootViews);
1147
+ debugLog('AutoPositionedPopup onChangeText rootViews=', rootViews);
1214
1148
  if (!localSearch) {
1215
1149
  if (debounceTimerRef.current) {
1216
1150
  clearTimeout(debounceTimerRef.current);
@@ -1249,7 +1183,7 @@ const AutoPositionedPopup = memo(
1249
1183
  onBlur={handleTextInputBlur}
1250
1184
  selectTextOnFocus={stableTextInputProps && stableTextInputProps['selectTextOnFocus'] || false}
1251
1185
  onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
1252
- console.log(
1186
+ debugLog(
1253
1187
  'AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=',
1254
1188
  e.nativeEvent.text
1255
1189
  );
@@ -1273,7 +1207,7 @@ const AutoPositionedPopup = memo(
1273
1207
 
1274
1208
  // Render the component following project implementation
1275
1209
  return useMemo(() => {
1276
- console.log('AutoPositionedPopup render tag=', tag); // Now safe - circular dependency fixed
1210
+ debugLog('AutoPositionedPopup render tag=', tag); // Now safe - circular dependency fixed
1277
1211
  return (
1278
1212
  <CustomRow>
1279
1213
  <View style={[styles.contain, style]} ref={refAutoPositionedPopup}>
@@ -1283,7 +1217,7 @@ const AutoPositionedPopup = memo(
1283
1217
  style={[styles.AutoPositionedPopupBtn, AutoPositionedPopupBtnStyle]}
1284
1218
  disabled={AutoPositionedPopupBtnDisabled}
1285
1219
  onPress={() => {
1286
- console.log('AutoPositionedPopup onPress=', {
1220
+ debugLog('AutoPositionedPopup onPress=', {
1287
1221
  tag,
1288
1222
  'state.isFocus': state.isFocus,
1289
1223
  useTextInput,
@@ -1296,10 +1230,13 @@ const AutoPositionedPopup = memo(
1296
1230
 
1297
1231
  // Capture trigger button position BEFORE switching to TextInput
1298
1232
  // This is critical because triggerBtnRef will become null after isFocus=true
1299
- if (triggerBtnRef.current && parentScrollViewRef?.current) {
1233
+ // IMPORTANT: Always capture position regardless of parentScrollViewRef
1234
+ if (triggerBtnRef.current) {
1300
1235
  triggerBtnRef.current.measureInWindow((x, y, width, height) => {
1301
- console.log('AutoPositionedPopup onPress: captured trigger position=', {tag, x, y, width, height});
1302
- triggerPositionRef.current = {x, y, width, height};
1236
+ debugLog('AutoPositionedPopup onPress: captured trigger position=', {tag, x, y, width, height});
1237
+ if (x !== undefined && y !== undefined && width !== undefined && height !== undefined) {
1238
+ triggerPositionRef.current = {x, y, width, height};
1239
+ }
1303
1240
  });
1304
1241
  }
1305
1242
 
@@ -1357,6 +1294,9 @@ const AutoPositionedPopup = memo(
1357
1294
  _addRootView()
1358
1295
  }
1359
1296
  } else {
1297
+ // V17 SIMPLIFICATION: For useTextInput=false, popup will be centered
1298
+ // No need for complex position measurement - just trigger focus
1299
+ debugLog('🔵🔵🔵 POPUP_V17 onPress useTextInput=false, will show centered popup');
1360
1300
  setState((prevState) => {
1361
1301
  return {
1362
1302
  ...prevState,
@@ -1364,7 +1304,7 @@ const AutoPositionedPopup = memo(
1364
1304
  };
1365
1305
  });
1366
1306
  }
1367
- console.log('AutoPositionedPopup onPress done')
1307
+ debugLog('AutoPositionedPopup onPress done')
1368
1308
  }}
1369
1309
  >
1370
1310
  {!btwChildren ? (
@@ -1391,29 +1331,29 @@ const AutoPositionedPopup = memo(
1391
1331
  );
1392
1332
  }, [
1393
1333
  tag,
1394
- // CRITICAL FIX: Remove all props that may change frequently or are inline functions
1334
+ // CRITICAL FIX: Remove all props that may change frequently or are inline functions
1395
1335
  // Changes to these props should not cause the entire component tree to recreate, especially TextInput
1396
- // fetchData, // ❌ Removed: inline function
1397
- // renderItem, // ❌ Removed: possibly inline function
1398
- // onItemSelected, // ❌ Removed: possibly inline function
1399
- // onSubmitEditing, // ❌ Removed: possibly inline function
1336
+ // fetchData, // ❌Removed: inline function
1337
+ // renderItem, // ❌Removed: possibly inline function
1338
+ // onItemSelected, // ❌Removed: possibly inline function
1339
+ // onSubmitEditing, // ❌Removed: possibly inline function
1400
1340
  localSearch,
1401
- // placeholder, // ❌ Removed: may change
1402
- // textAlign, // ❌ Removed: may change
1341
+ // placeholder, // ❌Removed: may change
1342
+ // textAlign, // ❌Removed: may change
1403
1343
  pageSize,
1404
1344
  selectedItem,
1405
- // CustomRow, // ❌ Removed: inline function, new reference each time
1345
+ // CustomRow, // ❌Removed: inline function, new reference each time
1406
1346
  useTextInput,
1407
- // btwChildren, // ❌ Removed: inline function
1408
- // keyExtractor, // ❌ Removed: possibly inline function
1409
- // AutoPositionedPopupBtnStyle, // ❌ Removed: possibly inline object
1410
- // CustomPopView, // ❌ Removed: may change
1411
- // CustomPopViewStyle, // ❌ Removed: may change
1347
+ // btwChildren, // ❌Removed: inline function
1348
+ // keyExtractor, // ❌Removed: possibly inline function
1349
+ // AutoPositionedPopupBtnStyle, // ❌Removed: possibly inline object
1350
+ // CustomPopView, // ❌Removed: may change
1351
+ // CustomPopViewStyle, // ❌Removed: may change
1412
1352
  forceRemoveAllRootViewOnItemSelected,
1413
1353
  state.isFocus,
1414
1354
  showListEmptyComponent,
1415
1355
  emptyText,
1416
- // Removed most dependencies that may cause re-rendering, keeping only core dependencies that truly affect component structure
1356
+ // Removed most dependencies that may cause re-rendering, keeping only core dependencies that truly affect component structure
1417
1357
  // This prevents TextInput recreation due to inline functions/objects during parent component redraws
1418
1358
  ]);
1419
1359
  }