react-native-auto-positioned-popup 1.0.12 → 1.0.14

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.
@@ -88,9 +88,7 @@ const ListItem: React.FC<{
88
88
  rootViewsRef.current = rootViews;
89
89
  }, [rootViews]);
90
90
  return useMemo(() => {
91
- // console.log('AutoPositionedPopup.tsx ListItem index=', index);
92
- console.log('AutoPositionedPopup.tsx ListItem item=', item);
93
- console.log('AutoPositionedPopup.tsx ListItem selectedItem=', selectedItem);
91
+ // console.log('AutoPositionedPopup.tsx ListItem=', {index, item, selectedItem});
94
92
  const isSelected = item.id === selectedItem?.id;
95
93
  return (
96
94
  <TouchableOpacity
@@ -132,7 +130,7 @@ interface AutoPositionedPopupListProps {
132
130
  selectedItem?: SelectedItem;
133
131
  localSearch?: boolean;
134
132
  pageSize?: number;
135
- showListEmptyComponent?:boolean;
133
+ showListEmptyComponent?: boolean;
136
134
  emptyText?: string;
137
135
  }
138
136
 
@@ -145,7 +143,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
145
143
  renderItem,
146
144
  selectedItem,
147
145
  localSearch,
148
- pageSize,showListEmptyComponent,emptyText
146
+ pageSize, showListEmptyComponent, emptyText
149
147
  }: AutoPositionedPopupListProps): React.JSX.Element => {
150
148
  const [state, setState] = useState<{
151
149
  selectedItem?: SelectedItem;
@@ -176,8 +174,8 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
176
174
  };
177
175
  }, []);
178
176
  // useEffect(() => {
179
- // // 監聽 TextInput 事件,收到就刷新列表,不依賴 global searchQuery
180
- // // 將最新的 searchQuery 同步到 list 專用的 ref,供 _fetchData 使用
177
+ // // Listen to TextInput events, refresh list when received, not dependent on global searchQuery
178
+ // // Sync the latest searchQuery to list-specific ref for _fetchData to use
181
179
  // ref_searchQuery.current = searchQuery;
182
180
  // console.log('AutoPositionedPopupList useEffect searchQuery=', searchQuery);
183
181
  // console.log('AutoPositionedPopupList useEffect state.localData=', state.localData);
@@ -212,10 +210,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
212
210
  pageIndex,
213
211
  pageSize: currentPageSize,
214
212
  }: FetchDataParams): Promise<ListData | null> => {
215
- console.log('AutoPositionedPopupList _fetchData pageIndex=', pageIndex, ' pageSize=', currentPageSize);
216
- console.log('AutoPositionedPopupList _fetchData state.localData=', state.localData);
217
- console.log('AutoPositionedPopupList _fetchData ref_searchQuery.current=', ref_searchQuery.current);
218
- console.log('AutoPositionedPopupList _fetchData localSearch=', localSearch);
213
+ console.log('AutoPositionedPopupList _fetchData=', {pageIndex, pageSize: currentPageSize, 'state.localData': state.localData, 'ref_searchQuery.current': ref_searchQuery.current, localSearch});
219
214
  if (localSearch && state.localData.length > 0) {
220
215
  const result: SelectedItem[] = state.localData.filter((item: SelectedItem) => {
221
216
  return item.title?.toLowerCase().includes(ref_searchQuery.current.toLowerCase());
@@ -291,7 +286,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
291
286
  searchQuery,
292
287
  localSearch,
293
288
  pageSize,
294
- rootViewsRef,showListEmptyComponent,emptyText
289
+ rootViewsRef, showListEmptyComponent, emptyText
295
290
  ]);
296
291
  }
297
292
  );
@@ -318,7 +313,7 @@ const AutoPositionedPopup = memo(
318
313
  AutoPositionedPopupBtnStyle,
319
314
  placeholder = 'Please Select',
320
315
  onSubmitEditing,
321
- TextInputProps = {},
316
+ TextInputProps = {autoFocus: true},
322
317
  inputStyle,
323
318
  labelStyle,
324
319
  popUpViewStyle = {left: '5%', width: '90%'},
@@ -360,7 +355,7 @@ const AutoPositionedPopup = memo(
360
355
  centerDisplay = false,
361
356
  selectedItemBackgroundColor = 'rgba(116, 116, 128, 0.08)',
362
357
  textAlign = 'right',
363
- CustomPopView = undefined, CustomPopViewStyle,showListEmptyComponent=true,emptyText=''
358
+ CustomPopView = undefined, CustomPopViewStyle, showListEmptyComponent = true, emptyText = ''
364
359
  } = props;
365
360
  // State management similar to project implementation
366
361
  const [state, setState] = useState<StateProps>({
@@ -381,6 +376,20 @@ const AutoPositionedPopup = memo(
381
376
  const keyboardVisibleRef = useRef(false);
382
377
  const refAutoPositionedPopup = useRef<View>(null);
383
378
  const ref_searchQuery = useRef<string>('');
379
+ // Add ref to track previous keyboard state to avoid false triggers during parent component re-renders
380
+ const prevIsKeyboardFullyShownRef = useRef<boolean>(false);
381
+ const prevPropsRef = useRef<{
382
+ CustomPopView?: any;
383
+ CustomPopViewStyle?: any;
384
+ TextInputProps?: any;
385
+ }>({});
386
+ // Add ref to prevent onFocus/onBlur loop triggers during parent component re-renders
387
+ const lastFocusTimeRef = useRef<number>(0);
388
+ const isFocusEventProcessingRef = useRef<boolean>(false);
389
+ // Add ref to stabilize TextInput props reference
390
+ // Only update when deep comparison detects real changes to avoid TextInput recreation due to reference changes during parent component redraws
391
+ const stableInputStyleRef = useRef<any>(inputStyle);
392
+ const stableTextInputPropsRef = useRef<any>(TextInputProps);
384
393
  // Simple keyboard status tracking (alternative to useKeyboardStatus hook)
385
394
  // Legacy state for compatibility
386
395
  const [isVisible, setIsVisible] = useState(false);
@@ -406,8 +415,7 @@ const AutoPositionedPopup = memo(
406
415
  useEffect(() => {
407
416
  (async () => {
408
417
  })();
409
- console.log(`AutoPositionedPopup componentDidMount tag=`, tag);
410
- console.log('AutoPositionedPopup componentDidMount CustomPopView=', CustomPopView);
418
+ console.log(`AutoPositionedPopup componentDidMount=`, {tag, CustomPopView});
411
419
  //componentWillUnmount
412
420
  return () => {
413
421
  console.log(`AutoPositionedPopup componentWillUnmount tag=`, tag);
@@ -440,8 +448,7 @@ const AutoPositionedPopup = memo(
440
448
  }
441
449
  }, [rootViews]);
442
450
  useEffect(() => {
443
- console.log('AutoPositionedPopup useEffect tag=', tag);
444
- console.log('AutoPositionedPopup useEffect selectedItem=', selectedItem);
451
+ console.log('AutoPositionedPopup useEffect [selectedItem, state.selectedItem, tag]=', {tag, selectedItem, 'state.selectedItem': state.selectedItem});
445
452
  console.log('AutoPositionedPopup useEffect state.selectedItem=', state.selectedItem);
446
453
  if (state.selectedItem?.id !== selectedItem?.id || state.selectedItem?.title !== selectedItem?.title) {
447
454
  console.log('AutoPositionedPopup useEffect selectedItem!=state.selectedItem');
@@ -454,51 +461,121 @@ const AutoPositionedPopup = memo(
454
461
  }
455
462
  }, [selectedItem, state.selectedItem, tag]);
456
463
  useEffect(() => {
457
- console.log('AutoPositionedPopup useEffect tag=', tag);
458
- console.log('AutoPositionedPopup useEffect state.isFocus=', state.isFocus);
459
- console.log('AutoPositionedPopup useEffect isKeyboardFullyShown=', isKeyboardFullyShown);
460
- console.log('AutoPositionedPopup useEffect ref_isFocus.current=', ref_isFocus.current);
461
- console.log(
462
- 'AutoPositionedPopup useEffect ref_isKeyboardFullyShown.current=',
463
- ref_isKeyboardFullyShown.current
464
- );
465
- console.log('AutoPositionedPopup useEffect useTextInput=', useTextInput);
466
- console.log('AutoPositionedPopup useEffect TextInputProps=', TextInputProps);
467
- console.log('AutoPositionedPopup useEffect hasAddedRootView.current=', hasAddedRootView.current);
468
- console.log('AutoPositionedPopup useEffect hasShownRootView.current=', hasShownRootView.current);
464
+ // Detect if keyboard state has actually changed to avoid false triggers during parent component re-renders
465
+ const keyboardStateChanged = prevIsKeyboardFullyShownRef.current !== isKeyboardFullyShown;
466
+ const propsChanged =
467
+ prevPropsRef.current.CustomPopView !== CustomPopView ||
468
+ prevPropsRef.current.CustomPopViewStyle !== CustomPopViewStyle ||
469
+ prevPropsRef.current.TextInputProps !== TextInputProps;
470
+ console.log('AutoPositionedPopup useEffect [isKeyboardFullyShown,\n' +
471
+ ' state.isFocus,\n' +
472
+ ' useTextInput,\n' +
473
+ ' CustomPopView,\n' +
474
+ ' CustomPopViewStyle,\n' +
475
+ ' forceRemoveAllRootViewOnItemSelected,\n' +
476
+ ' tag, TextInputProps,\n' +
477
+ ' state.selectedItem, showListEmptyComponent\n' +
478
+ ' ]=', {
479
+ tag,
480
+ 'state.isFocus': state.isFocus,
481
+ isKeyboardFullyShown,
482
+ 'ref_isFocus.current': ref_isFocus.current,
483
+ 'ref_isKeyboardFullyShown.current': ref_isKeyboardFullyShown.current,
484
+ useTextInput, TextInputProps,
485
+ 'hasAddedRootView.current': hasAddedRootView.current,
486
+ 'hasShownRootView.current': hasShownRootView.current,
487
+ 'keyboardStateChanged': keyboardStateChanged,
488
+ 'propsChanged': propsChanged
489
+ });
490
+ // Update ref to record current state
491
+ prevIsKeyboardFullyShownRef.current = isKeyboardFullyShown;
492
+ prevPropsRef.current = {
493
+ CustomPopView,
494
+ CustomPopViewStyle,
495
+ TextInputProps
496
+ };
497
+ // Only execute logic when keyboard state actually changes or user actively operates
498
+ if (!keyboardStateChanged && hasAddedRootView.current) {
499
+ console.log('AutoPositionedPopup: Skip execution - parent component re-rendered but keyboard state unchanged textInputRef.current=',textInputRef.current);
500
+ // if (!ref_isFocus.current) {
501
+ // textInputRef.current?.focus()
502
+ // }
503
+ return;
504
+ }
469
505
  if (useTextInput) {
470
506
  if (isKeyboardFullyShown && hasAddedRootView.current && !hasShownRootView.current && state.isFocus) {
471
- refAutoPositionedPopup.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
472
- console.log('AutoPositionedPopup measureInWindow x=', x, ' y=', y, ' width=', width, ' height=', height);
473
- // SIMPLE CENTER-BASED POSITIONING STRATEGY
474
- const screenHeight = Dimensions.get('screen').height;
475
- const screenCenter = screenHeight / 2;
476
- console.log('AutoPositionedPopup screenHeight=', screenHeight, ' screenCenter=', screenCenter, ' componentY=', y);
477
-
478
- // Simple rule: if component Y > screen center, show popup above; otherwise show below
479
- if (y > screenCenter) {
480
- console.log('AutoPositionedPopup with keyboard: showing above (Y > center)');
481
- ref_listPos.current = {x: x, y: y - listLayout.height, width: width};
482
- } else {
483
- console.log('AutoPositionedPopup with keyboard: showing below (Y <= center)');
484
- ref_listPos.current = {x: x, y: y + height, width: width};
485
- }
486
- console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
487
- setRootViewNativeStyle(tag, {
488
- top: ref_listPos.current?.y,
489
- left: popUpViewStyle?.left,
490
- width: popUpViewStyle?.width,
491
- height: listLayout.height,
492
- opacity: 1,
507
+ // CRITICAL FIX FOR KEYBOARD POSITION CALCULATION
508
+ // Problem: When keyboard appears, the page shifts up but measureInWindow executes too early
509
+ // Solution: Wait for keyboard animation (300ms) + use requestAnimationFrame for next render frame
510
+ //
511
+ // Timing breakdown:
512
+ // 1. Keyboard animation: ~250-300ms (iOS/Android)
513
+ // 2. Page shift animation: ~200-300ms (KeyboardAvoidingView)
514
+ // 3. Layout tree update: ~50-100ms (React Native)
515
+ // Total: ~500-700ms needed for stable layout
516
+ //
517
+ // Strategy: setTimeout(300ms) waits for most animations to complete,
518
+ // then requestAnimationFrame ensures measurement happens after next render frame
519
+ setTimeout(() => {
520
+ requestAnimationFrame(() => {
521
+ refAutoPositionedPopup.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
522
+ console.log('AutoPositionedPopup useTextInput measureInWindow (after 300ms + RAF, layout stable)=', {x, y, width, height});
523
+ // CRITICAL FIX: Coordinate system mismatch issue
524
+ // Problem: measureInWindow returns coordinates relative to window (fixed reference),
525
+ // but popup uses absolute positioning relative to App container (which shifts when keyboard appears)
526
+ //
527
+ // When keyboard appears:
528
+ // 1. measureInWindow returns y relative to window (e.g., y=400 after shifting)
529
+ // 2. But popup's absolute positioning is relative to App container
530
+ // 3. If App container shifted up by 200px, setting top=200 will display at window.y=0 (wrong!)
531
+ //
532
+ // Solution: Since popup is rendered at root level and uses absolute positioning,
533
+ // we should directly use measureInWindow's y value without additional calculations
534
+ // The popup container is at the same level as the page content
535
+ const screenHeight = Dimensions.get('window').height; // Use window height, not screen
536
+ console.log('AutoPositionedPopup useTextInput positioning data=', {
537
+ screenHeight,
538
+ componentY: y,
539
+ componentHeight: height,
540
+ listHeight: listLayout.height
541
+ });
542
+ // CORRECT POSITIONING LOGIC (as per user requirement):
543
+ // 1. ALWAYS try to show popup ABOVE the input field first
544
+ // 2. Only if that goes off the top of screen, show BELOW instead
545
+ // 3. Don't cover/overlap the input field
546
+ let popupY = y - listLayout.height; // Default: above input field
547
+ // Check if showing above would go off the top of screen
548
+ if (popupY < 0) {
549
+ console.log('AutoPositionedPopup with keyboard: would go off screen top, showing BELOW instead');
550
+ popupY = y + height; // Show below input field
551
+ // Also check if showing below would go off the bottom
552
+ const maxY = screenHeight - listLayout.height;
553
+ if (popupY > maxY) {
554
+ // If both positions are problematic, clamp to visible area
555
+ console.log('AutoPositionedPopup with keyboard: both positions problematic, clamping to visible area');
556
+ popupY = Math.min(Math.max(0, y - listLayout.height), maxY);
557
+ }
558
+ } else {
559
+ console.log('AutoPositionedPopup with keyboard: showing ABOVE input field (preferred position)');
560
+ }
561
+ ref_listPos.current = {x: x, y: popupY, width: width};
562
+ console.log('AutoPositionedPopup useTextInput final position=', ref_listPos.current);
563
+ setRootViewNativeStyle(tag, {
564
+ top: ref_listPos.current?.y,
565
+ left: popUpViewStyle?.left,
566
+ width: popUpViewStyle?.width,
567
+ height: listLayout.height,
568
+ opacity: 1,
569
+ });
570
+ hasShownRootView.current = true;
571
+ });
493
572
  });
494
- hasShownRootView.current = true;
495
- });
496
- } else if (!isKeyboardFullyShown && ref_isFocus.current) {
573
+ }, 300) // 300ms is sufficient for keyboard animation, as proven by user testing (even 3000ms didn't fix wrong logic)
574
+ } else if (!isKeyboardFullyShown && ref_isFocus.current && keyboardStateChanged) {
575
+ // Only execute close logic when keyboard state actually changes from true to false
497
576
  console.log(
498
- 'AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView tag=',
499
- tag,
500
- ' forceRemoveAllRootViewOnItemSelected=',
501
- forceRemoveAllRootViewOnItemSelected
577
+ 'AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView (keyboard state changed)=',
578
+ {tag, forceRemoveAllRootViewOnItemSelected, keyboardStateChanged}
502
579
  );
503
580
  removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
504
581
  setState((prevState) => {
@@ -514,11 +591,10 @@ const AutoPositionedPopup = memo(
514
591
  } else {
515
592
  if (state.isFocus) {
516
593
  refAutoPositionedPopup.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
517
- console.log('AutoPositionedPopup measureInWindow x=', x, ' y=', y, ' width=', width, ' height=', height);
518
-
594
+ console.log('AutoPositionedPopup !useTextInput measureInWindow=', {x, y, width, height});
519
595
  // INTELLIGENT POSITION CALCULATION - MODIFIED VERSION WITH STATUS BAR SAFETY
520
596
  const calculateOptimalPosition = (componentY: number, componentHeight: number, popupHeight: number) => {
521
- console.log('🔥🔥🔥 NEW CALCULATE OPTIMAL POSITION FUNCTION EXECUTING 🔥🔥🔥');
597
+ console.log('AutoPositionedPopup 🔥🔥🔥 NEW CALCULATE OPTIMAL POSITION FUNCTION EXECUTING 🔥🔥🔥');
522
598
 
523
599
  // Use window height (visible area) instead of screen height (includes status bar)
524
600
  const windowHeight = Dimensions.get('window').height;
@@ -536,7 +612,7 @@ const AutoPositionedPopup = memo(
536
612
  }
537
613
  };
538
614
  const statusBarHeight = getStatusBarHeight();
539
- console.log('🔥 Cross-platform StatusBar height:', statusBarHeight, 'Platform:', Platform.OS);
615
+ console.log('AutoPositionedPopup 🔥 Cross-platform StatusBar height:', statusBarHeight, 'Platform:', Platform.OS);
540
616
 
541
617
  // Calculate component center point as requested
542
618
  const componentCenterY = componentY + componentHeight / 2;
@@ -597,7 +673,7 @@ const AutoPositionedPopup = memo(
597
673
 
598
674
  const finalSpacing = Math.max(baseSpacing, relativeSpacing) * edgeProximityFactor * platformMultiplier;
599
675
 
600
- console.log('🔥 Advanced spacing calculation:', {
676
+ console.log('AutoPositionedPopup 🔥 Advanced spacing calculation:', {
601
677
  componentCenter,
602
678
  screenCenter,
603
679
  distanceFromCenter,
@@ -634,41 +710,41 @@ const AutoPositionedPopup = memo(
634
710
  // 'usableSpaceAbove >= needed': usableSpaceAbove >= popupHeight + POPUP_SPACING
635
711
  // });
636
712
 
637
- if (isInBottomHalf && usableSpaceAbove >= popupHeight ) {
713
+ if (isInBottomHalf && usableSpaceAbove >= popupHeight) {
638
714
  // Component in bottom half + enough space above = FORCE ABOVE
639
715
  showAbove = true;
640
- finalY = componentY - popupHeight +componentHeight/2;
641
- console.log('🔥 AutoPositionedPopup: FORCE ABOVE - bottom half component with enough space, finalY=', finalY);
642
- } else if (!isInBottomHalf && spaceBelow >= popupHeight ) {
716
+ finalY = componentY - popupHeight + componentHeight / 2;
717
+ console.log('AutoPositionedPopup 🔥 AutoPositionedPopup: FORCE ABOVE - bottom half component with enough space, finalY=', finalY);
718
+ } else if (!isInBottomHalf && spaceBelow >= popupHeight) {
643
719
  // Component in top half + enough space below = show below
644
720
  showAbove = false;
645
- finalY = componentY + componentHeight*2;
721
+ finalY = componentY + componentHeight * 2;
646
722
  console.log('🔥 AutoPositionedPopup: Showing below - top half component with enough space, finalY=', finalY);
647
- } else if (usableSpaceAbove >= popupHeight ) {
723
+ } else if (usableSpaceAbove >= popupHeight) {
648
724
  // Fallback: enough space above
649
725
  showAbove = true;
650
- finalY = componentY - popupHeight ;
726
+ finalY = componentY - popupHeight;
651
727
  console.log('🔥 AutoPositionedPopup: Showing above - enough space available (fallback), finalY=', finalY);
652
- } else if (spaceBelow >= popupHeight ) {
728
+ } else if (spaceBelow >= popupHeight) {
653
729
  // Fallback: enough space below
654
730
  showAbove = false;
655
- finalY = componentY + componentHeight ;
731
+ finalY = componentY + componentHeight;
656
732
  console.log('🔥 AutoPositionedPopup: Showing below - enough space available (fallback), finalY=', finalY);
657
733
  } else {
658
734
  // Emergency fallback: choose larger space
659
735
  if (usableSpaceAbove >= spaceBelow) {
660
736
  showAbove = true;
661
- finalY = Math.max(statusBarHeight, componentY - popupHeight );
737
+ finalY = Math.max(statusBarHeight, componentY - popupHeight);
662
738
  console.log('🔥 AutoPositionedPopup: Emergency above - larger space, finalY=', finalY);
663
739
  } else {
664
740
  showAbove = false;
665
- finalY = componentY + componentHeight ;
741
+ finalY = componentY + componentHeight;
666
742
  console.log('🔥 AutoPositionedPopup: Emergency below - larger space, finalY=', finalY);
667
743
  }
668
744
  }
669
745
 
670
746
  // Enhanced boundary check with detailed logging
671
- console.log('🔥 Pre-boundary check:', {
747
+ console.log('AutoPositionedPopup 🔥 Pre-boundary check:', {
672
748
  originalFinalY: finalY,
673
749
  showAbove,
674
750
  statusBarHeight,
@@ -681,25 +757,25 @@ const AutoPositionedPopup = memo(
681
757
  if (showAbove && finalY < statusBarHeight) {
682
758
  const oldFinalY = finalY;
683
759
  finalY = statusBarHeight;
684
- console.log('🔥 BOUNDARY FIX: Above display adjusted for status bar:', oldFinalY, '->', finalY);
760
+ console.log('AutoPositionedPopup 🔥 BOUNDARY : Above display adjusted for status bar:', oldFinalY, '->', finalY);
685
761
  }
686
762
 
687
763
  if (!showAbove && finalY + popupHeight > windowHeight) {
688
764
  const oldFinalY = finalY;
689
765
  finalY = windowHeight - popupHeight;
690
- console.log('🔥 BOUNDARY FIX: Below display adjusted to fit window:', oldFinalY, '->', finalY);
766
+ console.log('AutoPositionedPopup 🔥 BOUNDARY : Below display adjusted to fit window:', oldFinalY, '->', finalY);
691
767
  }
692
768
 
693
769
  // CRITICAL CHECK: Detect if boundary check is changing display direction
694
- if (showAbove && finalY + popupHeight > componentY ) {
695
- console.log('🚨 WARNING: Above positioning may overlap with component!');
770
+ if (showAbove && finalY + popupHeight > componentY) {
771
+ console.log('AutoPositionedPopup 🚨 WARNING: Above positioning may overlap with component!');
696
772
  }
697
773
 
698
- if (!showAbove && finalY < componentY + componentHeight ) {
699
- console.log('🚨 WARNING: Below positioning may overlap with component!');
774
+ if (!showAbove && finalY < componentY + componentHeight) {
775
+ console.log('AutoPositionedPopup 🚨 WARNING: Below positioning may overlap with component!');
700
776
  }
701
777
 
702
- console.log('🔥 Post-boundary check final result:', {
778
+ console.log('AutoPositionedPopup 🔥 Post-boundary check final result:', {
703
779
  finalY,
704
780
  showAbove,
705
781
  'popupTop': finalY,
@@ -715,21 +791,15 @@ const AutoPositionedPopup = memo(
715
791
  const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
716
792
  ? CustomPopViewStyle.height
717
793
  : listLayout.height;
718
-
719
- console.log('🔥 Using actualPopupHeight for calculation:', actualPopupHeight, 'CustomPopView:', !!CustomPopView);
720
-
794
+ console.log('AutoPositionedPopup 🔥 Using actualPopupHeight for calculation:', {actualPopupHeight, CustomPopView: !!CustomPopView});
721
795
  const positionResult = calculateOptimalPosition(y, height, actualPopupHeight);
722
796
  console.log('AutoPositionedPopup FINAL position result:', positionResult);
723
-
724
797
  ref_listPos.current = {x: x, y: positionResult.finalY, width: width};
725
- console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
726
-
798
+ console.log('AutoPositionedPopup !useTextInput ref_listPos.current=', ref_listPos.current);
727
799
  if (CustomPopView && CustomPopViewStyle) {
728
- console.log('AutoPositionedPopup CustomPopViewStyle=', CustomPopViewStyle);
729
800
  // Position already calculated correctly above, no need to recalculate
730
801
  const PopViewComponent = CustomPopView();
731
- console.log('AutoPositionedPopup addRootView PopViewComponent=', PopViewComponent);
732
- console.log('AutoPositionedPopup addRootView state.selectedItem=', state.selectedItem);
802
+ console.log('AutoPositionedPopup !useTextInput addRootView=', {CustomPopViewStyle, PopViewComponent, 'state.selectedItem': state.selectedItem});
733
803
  addRootView({
734
804
  id: tag,
735
805
  style: !centerDisplay
@@ -761,7 +831,7 @@ const AutoPositionedPopup = memo(
761
831
  centerDisplay,
762
832
  });
763
833
  } else {
764
- console.log('AutoPositionedPopup addRootView tag=', tag);
834
+ console.log('AutoPositionedPopup !useTextInput addRootView tag=', tag);
765
835
  addRootView({
766
836
  id: tag,
767
837
  style: {
@@ -817,8 +887,8 @@ const AutoPositionedPopup = memo(
817
887
  CustomPopView,
818
888
  CustomPopViewStyle,
819
889
  forceRemoveAllRootViewOnItemSelected,
820
- tag,
821
- state.selectedItem,showListEmptyComponent
890
+ tag, TextInputProps,
891
+ state.selectedItem, showListEmptyComponent
822
892
  ]);
823
893
  // Imperative handle for parent component access
824
894
  useImperativeHandle(
@@ -837,7 +907,7 @@ const AutoPositionedPopup = memo(
837
907
  []
838
908
  );
839
909
  const updateState = (key: string, value: SelectedItem) => {
840
- console.log('AutoPositionedPopup updateState key=', key, ' value=', value);
910
+ console.log('AutoPositionedPopup updateState=', {key, value});
841
911
  setState((prevState) => ({
842
912
  ...prevState,
843
913
  [key]: value,
@@ -858,6 +928,225 @@ const AutoPositionedPopup = memo(
858
928
  setSearchQuery('');
859
929
  }
860
930
  };
931
+
932
+ // Simple deep comparison function (for style objects only)
933
+ const shallowEqual = (obj1: any, obj2: any): boolean => {
934
+ if (obj1 === obj2) return true;
935
+ if (!obj1 || !obj2) return false;
936
+ if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false;
937
+
938
+ const keys1 = Object.keys(obj1);
939
+ const keys2 = Object.keys(obj2);
940
+ if (keys1.length !== keys2.length) return false;
941
+
942
+ for (const key of keys1) {
943
+ if (obj1[key] !== obj2[key]) return false;
944
+ }
945
+ return true;
946
+ };
947
+
948
+ // Use useMemo to create stable props reference
949
+ // Only update when deep comparison detects real changes to avoid TextInput recreation due to reference changes during parent component redraws
950
+ const stableInputStyle = useMemo(() => {
951
+ if (!shallowEqual(stableInputStyleRef.current, inputStyle)) {
952
+ console.log(`AutoPositionedPopup inputStyle deep change detected, updating stable reference - tag: ${tag}`);
953
+ stableInputStyleRef.current = inputStyle;
954
+ }
955
+ return stableInputStyleRef.current;
956
+ }, [inputStyle, tag]);
957
+
958
+ const stableTextInputProps = useMemo(() => {
959
+ if (!shallowEqual(stableTextInputPropsRef.current, TextInputProps)) {
960
+ console.log(`AutoPositionedPopup TextInputProps deep change detected, updating stable reference - tag: ${tag}`);
961
+ stableTextInputPropsRef.current = TextInputProps;
962
+ }
963
+ return stableTextInputPropsRef.current;
964
+ }, [TextInputProps, tag]);
965
+
966
+ // Use useCallback to stabilize onFocus and onBlur callback references
967
+ // Prevent creating new callback functions during parent component redraws to avoid TextInput re-triggering focus
968
+ // Use ref to store latest state values to avoid adding frequently changing values to dependencies
969
+ const stateRef = useRef(state);
970
+ stateRef.current = state;
971
+
972
+ const handleTextInputFocus = useCallback(() => {
973
+ const currentTime = Date.now();
974
+ const timeSinceLastFocus = currentTime - lastFocusTimeRef.current;
975
+ console.log(
976
+ 'AutoPositionedPopup onFocus=',
977
+ {
978
+ tag,
979
+ 'state.selectedItem': stateRef.current.selectedItem,
980
+ 'hasTriggeredFocus.current=': hasTriggeredFocus.current,
981
+ 'textInputRef.current=': textInputRef.current,
982
+ 'ref_searchQuery.current=': ref_searchQuery.current,
983
+ 'timeSinceLastFocus': timeSinceLastFocus,
984
+ 'isKeyboardFullyShown': isKeyboardFullyShown,
985
+ 'isFocusEventProcessing': isFocusEventProcessingRef.current
986
+ }
987
+ );
988
+ // Prevent rapid repeated triggers (repeated events within 300ms are ignored)
989
+ if (timeSinceLastFocus < 300) {
990
+ console.log('AutoPositionedPopup onFocus: Skip - event triggered too quickly (< 300ms)');
991
+ return;
992
+ }
993
+ // Skip if keyboard is already open and focus has been handled
994
+ if (isKeyboardFullyShown && hasTriggeredFocus.current) {
995
+ console.log('AutoPositionedPopup onFocus: Skip - keyboard already open and focus handled');
996
+ return;
997
+ }
998
+ // Prevent concurrent processing
999
+ if (isFocusEventProcessingRef.current) {
1000
+ console.log('AutoPositionedPopup onFocus: Skip - processing another focus event');
1001
+ return;
1002
+ }
1003
+ isFocusEventProcessingRef.current = true;
1004
+ lastFocusTimeRef.current = currentTime;
1005
+ if (!hasTriggeredFocus.current) {
1006
+ hasTriggeredFocus.current = true;
1007
+ ref_isFocus.current = true;
1008
+ if (stateRef.current.selectedItem) {
1009
+ ref_searchQuery.current = stateRef.current.selectedItem.title;
1010
+ }
1011
+ if (textInputRef.current && ref_searchQuery.current) {
1012
+ textInputRef.current.setNativeProps({
1013
+ text: ref_searchQuery.current,
1014
+ });
1015
+ }
1016
+ }
1017
+ // Delay resetting processing flag to avoid blocking subsequent legitimate focus events
1018
+ setTimeout(() => {
1019
+ isFocusEventProcessingRef.current = false;
1020
+ }, 100);
1021
+ }, [tag, isKeyboardFullyShown]); // Remove state.selectedItem, use stateRef instead
1022
+
1023
+ const handleTextInputBlur = useCallback(() => {
1024
+ console.log(
1025
+ 'AutoPositionedPopup onBlur=',
1026
+ {
1027
+ tag,
1028
+ 'textInputRef.current': textInputRef.current,
1029
+ 'isKeyboardFullyShown': isKeyboardFullyShown,
1030
+ 'hasTriggeredFocus.current': hasTriggeredFocus.current
1031
+ }
1032
+ );
1033
+ // If keyboard is still open, this is a false trigger caused by parent component re-render, should not reset
1034
+ if (isKeyboardFullyShown && hasTriggeredFocus.current) {
1035
+ console.log('AutoPositionedPopup onBlur: Skip - keyboard still open, possibly caused by parent component re-render');
1036
+ return;
1037
+ }
1038
+
1039
+ // Only reset internal state, do not actively close keyboard
1040
+ // Keyboard will close naturally when TextInput loses focus, no need to manually call Keyboard.dismiss()
1041
+ hasTriggeredFocus.current = false;
1042
+ hasAddedRootView.current = false;
1043
+ hasShownRootView.current = false;
1044
+ ref_isFocus.current = false;
1045
+ setState((prevState) => {
1046
+ return {
1047
+ ...prevState,
1048
+ isFocus: false,
1049
+ };
1050
+ });
1051
+ removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
1052
+ setSearchQuery('');
1053
+ if (textInputRef.current) {
1054
+ textInputRef.current.setNativeProps({text: ''});
1055
+ ref_searchQuery.current = '';
1056
+ // Remove textInputRef.current.blur() - avoid forcing blur causing keyboard to close
1057
+ }
1058
+ // Remove Keyboard.dismiss() - let keyboard close naturally to avoid triggering keyboardDidHide event
1059
+ }, [tag, isKeyboardFullyShown, forceRemoveAllRootViewOnItemSelected]);
1060
+
1061
+ // Wrap TextInput independently in useMemo to recreate only when key props change
1062
+ // This avoids repeated ref callback triggers due to other props changes during parent component redraws
1063
+ const memoizedTextInput = useMemo(() => {
1064
+ console.log(`AutoPositionedPopup useMemo creating TextInput - tag: ${tag}, isFocus: ${state.isFocus}`);
1065
+ if (!useTextInput || !state.isFocus) {
1066
+ return null;
1067
+ }
1068
+ return (
1069
+ <RNTextInput
1070
+ ref={(ref) => {
1071
+ // Monitor TextInput mounting and unmounting
1072
+ if (ref && !textInputRef.current) {
1073
+ console.log(`AutoPositionedPopup TextInput created/mounted - tag: ${tag}, ref:`, ref);
1074
+ } else if (!ref && textInputRef.current) {
1075
+ console.log(`AutoPositionedPopup TextInput unmounted - tag: ${tag}`);
1076
+ } else if (ref && textInputRef.current && ref !== textInputRef.current) {
1077
+ console.log(`AutoPositionedPopup TextInput replaced - tag: ${tag}, oldRef:`, textInputRef.current, 'newRef:', ref);
1078
+ }
1079
+ textInputRef.current = ref;
1080
+ }}
1081
+ key={`textinput-${tag}`}
1082
+ style={[
1083
+ styles.inputStyle,
1084
+ stableInputStyle,
1085
+ ]}
1086
+ textAlign={stableTextInputProps['textAlign'] || 'left'}
1087
+ multiline={stableTextInputProps['multiline'] || false}
1088
+ numberOfLines={stableTextInputProps['numberOfLines'] || 1}
1089
+ onChangeText={(searchQuery) => {
1090
+ ref_searchQuery.current = searchQuery;
1091
+ console.log('AutoPositionedPopup onChangeText rootViews=', rootViews);
1092
+ if (!localSearch) {
1093
+ if (debounceTimerRef.current) {
1094
+ clearTimeout(debounceTimerRef.current);
1095
+ }
1096
+ debounceTimerRef.current = setTimeout(() => {
1097
+ emitQueryChange(ref_searchQuery.current);
1098
+ }, 500);
1099
+ } else {
1100
+ emitQueryChange(ref_searchQuery.current);
1101
+ }
1102
+ }}
1103
+ placeholderTextColor={theme.colors.placeholderText}
1104
+ placeholder={placeholder}
1105
+ onKeyPress={(e) => {
1106
+ if (e.nativeEvent.key === 'Enter') {
1107
+ Keyboard.dismiss();
1108
+ }
1109
+ }}
1110
+ keyboardType={stableTextInputProps['keyboardType'] || 'default'}
1111
+ clearButtonMode="while-editing"
1112
+ returnKeyType={stableTextInputProps['returnKeyType'] || 'done'}
1113
+ maxLength={stableTextInputProps['maxLength'] || 100}
1114
+ accessibilityLabel="selectInput"
1115
+ accessible={true}
1116
+ autoFocus={stableTextInputProps['autoFocus'] || false}
1117
+ autoCorrect={false}
1118
+ underlineColorAndroid="transparent"
1119
+ editable={stableTextInputProps['editable'] || true}
1120
+ secureTextEntry={stableTextInputProps['secureTextEntry'] || false}
1121
+ defaultValue=""
1122
+ caretHidden={false}
1123
+ enablesReturnKeyAutomatically
1124
+ onFocus={handleTextInputFocus}
1125
+ onBlur={handleTextInputBlur}
1126
+ selectTextOnFocus={stableTextInputProps['selectTextOnFocus'] || false}
1127
+ onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
1128
+ console.log(
1129
+ 'AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=',
1130
+ e.nativeEvent.text
1131
+ );
1132
+ onSubmitEditing && onSubmitEditing(e);
1133
+ }}
1134
+ />
1135
+ );
1136
+ }, [
1137
+ tag, // tag 是稳定的
1138
+ useTextInput, // useTextInput 是稳定的
1139
+ state.isFocus, // isFocus 控制显示/隐藏
1140
+ handleTextInputFocus, // useCallback wrapped, reference stable
1141
+ handleTextInputBlur, // useCallback wrapped, reference stable
1142
+ stableInputStyle, // Use stable inputStyle reference (after deep comparison)
1143
+ stableTextInputProps, // Use stable TextInputProps reference (after deep comparison)
1144
+ placeholder, // placeholder usually stable
1145
+ onSubmitEditing, // onSubmitEditing usually stable
1146
+ // No longer use original inputStyle and TextInputProps, use stable references instead
1147
+ // Stable references only update when deep comparison detects actual content changes, avoiding frequent TextInput recreation during parent component redraws
1148
+ ]);
1149
+
861
1150
  // Render the component following project implementation
862
1151
  return useMemo(() => {
863
1152
  console.log('AutoPositionedPopup render tag=', tag); // Now safe - circular dependency fixed
@@ -869,22 +1158,15 @@ const AutoPositionedPopup = memo(
869
1158
  style={[styles.AutoPositionedPopupBtn, AutoPositionedPopupBtnStyle]}
870
1159
  disabled={AutoPositionedPopupBtnDisabled}
871
1160
  onPress={() => {
872
- console.log('AutoPositionedPopup onPress tag=', tag);
873
- console.log('AutoPositionedPopup onPress state.isFocus=', state.isFocus);
874
- console.log('AutoPositionedPopup onPress useTextInput=', useTextInput);
875
- console.log(
876
- 'AutoPositionedPopup onPress hasAddedRootView.current=',
877
- hasAddedRootView.current
878
- );
879
- console.log(
880
- 'AutoPositionedPopup onPress hasShownRootView.current=',
881
- hasShownRootView.current
882
- );
883
- console.log(
884
- 'AutoPositionedPopup onPress hasTriggeredFocus.current=',
885
- hasTriggeredFocus.current
886
- );
887
- console.log('AutoPositionedPopup onPress state.selectedItem=', state.selectedItem);
1161
+ console.log('AutoPositionedPopup onPress=', {
1162
+ tag,
1163
+ 'state.isFocus': state.isFocus,
1164
+ useTextInput,
1165
+ 'hasAddedRootView.current': hasAddedRootView.current,
1166
+ 'hasShownRootView.current': hasShownRootView.current,
1167
+ 'hasTriggeredFocus.current': hasTriggeredFocus.current,
1168
+ 'state.selectedItem': state.selectedItem
1169
+ });
888
1170
  setState((prevState) => {
889
1171
  return {
890
1172
  ...prevState,
@@ -892,6 +1174,7 @@ const AutoPositionedPopup = memo(
892
1174
  };
893
1175
  });
894
1176
  if (!hasAddedRootView.current && useTextInput) {
1177
+ // TextInput version: hide first, show after keyboard is fully displayed
895
1178
  hasAddedRootView.current = true;
896
1179
  hasShownRootView.current = false;
897
1180
  addRootView({
@@ -919,6 +1202,7 @@ const AutoPositionedPopup = memo(
919
1202
  useModal: false,
920
1203
  });
921
1204
  }
1205
+ console.log('AutoPositionedPopup onPress done')
922
1206
  }}
923
1207
  >
924
1208
  {!btwChildren ? (
@@ -938,141 +1222,38 @@ const AutoPositionedPopup = memo(
938
1222
  )}
939
1223
  </TouchableOpacity>
940
1224
  ) : (
941
- useTextInput &&
942
- state.isFocus && (
943
- <RNTextInput
944
- ref={textInputRef}
945
- key="fixed-textinput-key"
946
- style={[
947
- styles.inputStyle,
948
- inputStyle,
949
- ]}
950
- textAlign={TextInputProps['textAlign'] || 'left'}
951
- multiline={TextInputProps['multiline'] || false}
952
- numberOfLines={TextInputProps['numberOfLines'] || 1}
953
- onChangeText={(searchQuery) => {
954
- ref_searchQuery.current = searchQuery;
955
- console.log('AutoPositionedPopup onChangeText rootViews=', rootViews);
956
- if (!localSearch) {
957
- if (debounceTimerRef.current) {
958
- clearTimeout(debounceTimerRef.current);
959
- }
960
- debounceTimerRef.current = setTimeout(() => {
961
- emitQueryChange(ref_searchQuery.current);
962
- }, 500);
963
- } else {
964
- emitQueryChange(ref_searchQuery.current);
965
- }
966
- }}
967
- placeholderTextColor={theme.colors.placeholderText}
968
- placeholder={placeholder}
969
- onKeyPress={(e) => {
970
- if (e.nativeEvent.key === 'Enter') {
971
- Keyboard.dismiss();
972
- }
973
- }}
974
- keyboardType={TextInputProps['keyboardType'] || 'default'}
975
- clearButtonMode="while-editing"
976
- returnKeyType={TextInputProps['returnKeyType'] || 'done'}
977
- maxLength={TextInputProps['maxLength'] || 100}
978
- accessibilityLabel="selectInput"
979
- accessible={true}
980
- autoFocus={TextInputProps['autoFocus'] || false}
981
- autoCorrect={false}
982
- underlineColorAndroid="transparent"
983
- editable={TextInputProps['editable'] || true}
984
- secureTextEntry={TextInputProps['secureTextEntry'] || false}
985
- defaultValue=""
986
- caretHidden={false}
987
- enablesReturnKeyAutomatically
988
- onFocus={() => {
989
- console.log(
990
- 'AutoPositionedPopup onFocus tag=',
991
- tag,
992
- ' selectedItem=',
993
- state.selectedItem,
994
- ' hasTriggeredFocus.current=',
995
- hasTriggeredFocus.current,
996
- ' textInputRef.current=',
997
- textInputRef.current,
998
- ' ref_searchQuery.current=',
999
- ref_searchQuery.current
1000
- );
1001
- if (!hasTriggeredFocus.current) {
1002
- hasTriggeredFocus.current = true;
1003
- ref_isFocus.current = true;
1004
- if (state.selectedItem) {
1005
- ref_searchQuery.current = state.selectedItem.title;
1006
- }
1007
- if (textInputRef.current && ref_searchQuery.current) {
1008
- textInputRef.current.setNativeProps({
1009
- text: ref_searchQuery.current,
1010
- });
1011
- }
1012
- }
1013
- }}
1014
- onBlur={() => {
1015
- console.log(
1016
- 'AutoPositionedPopup onBlur tag=',
1017
- tag,
1018
- 'textInputRef.current=',
1019
- textInputRef.current
1020
- );
1021
- hasTriggeredFocus.current = false;
1022
- hasAddedRootView.current = false; // 重置 RootView 狀態
1023
- hasShownRootView.current = false;
1024
- ref_isFocus.current = false;
1025
- setState((prevState) => {
1026
- return {
1027
- ...prevState,
1028
- isFocus: false,
1029
- };
1030
- });
1031
- removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
1032
- setSearchQuery('');
1033
- if (textInputRef.current) {
1034
- textInputRef.current.setNativeProps({text: ''});
1035
- ref_searchQuery.current = '';
1036
- textInputRef.current.blur();
1037
- }
1038
- Keyboard.dismiss();
1039
- }}
1040
- selectTextOnFocus={TextInputProps['selectTextOnFocus'] || false}
1041
- onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
1042
- console.log(
1043
- 'AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=',
1044
- e.nativeEvent.text
1045
- );
1046
- onSubmitEditing && onSubmitEditing(e);
1047
- }}
1048
- />
1049
- )
1225
+ memoizedTextInput
1050
1226
  )}
1051
1227
  </View>
1052
1228
  </CustomRow>
1053
1229
  );
1054
- }, [tag,
1055
- fetchData,
1056
- renderItem,
1057
- onItemSelected,
1058
- onSubmitEditing,
1230
+ }, [
1231
+ tag,
1232
+ // ✅ CRITICAL FIX: Remove all props that may change frequently or are inline functions
1233
+ // Changes to these props should not cause the entire component tree to recreate, especially TextInput
1234
+ // fetchData, // ❌ Removed: inline function
1235
+ // renderItem, // ❌ Removed: possibly inline function
1236
+ // onItemSelected, // ❌ Removed: possibly inline function
1237
+ // onSubmitEditing, // ❌ Removed: possibly inline function
1059
1238
  localSearch,
1060
- placeholder,
1061
- textAlign,
1239
+ // placeholder, // ❌ Removed: may change
1240
+ // textAlign, // ❌ Removed: may change
1062
1241
  pageSize,
1063
1242
  selectedItem,
1064
- CustomRow,
1243
+ // CustomRow, // ❌ Removed: inline function, new reference each time
1065
1244
  useTextInput,
1066
- btwChildren,
1067
- selectedItem,
1068
- keyExtractor,
1069
- AutoPositionedPopupBtnStyle,
1070
- CustomPopView,
1071
- CustomPopViewStyle,
1245
+ // btwChildren, // ❌ Removed: inline function
1246
+ // keyExtractor, // ❌ Removed: possibly inline function
1247
+ // AutoPositionedPopupBtnStyle, // ❌ Removed: possibly inline object
1248
+ // CustomPopView, // ❌ Removed: may change
1249
+ // CustomPopViewStyle, // ❌ Removed: may change
1072
1250
  forceRemoveAllRootViewOnItemSelected,
1073
- inputStyle,
1074
- TextInputProps,
1075
- state.isFocus,showListEmptyComponent,emptyText]);
1251
+ state.isFocus,
1252
+ showListEmptyComponent,
1253
+ emptyText,
1254
+ // ✅ Removed most dependencies that may cause re-rendering, keeping only core dependencies that truly affect component structure
1255
+ // This prevents TextInput recreation due to inline functions/objects during parent component redraws
1256
+ ]);
1076
1257
  }
1077
1258
  )
1078
1259
  );