react-native-auto-positioned-popup 1.0.11 → 1.0.13

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,8 +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);
91
+ // console.log('AutoPositionedPopup.tsx ListItem=', {index, item, selectedItem});
93
92
  const isSelected = item.id === selectedItem?.id;
94
93
  return (
95
94
  <TouchableOpacity
@@ -131,6 +130,8 @@ interface AutoPositionedPopupListProps {
131
130
  selectedItem?: SelectedItem;
132
131
  localSearch?: boolean;
133
132
  pageSize?: number;
133
+ showListEmptyComponent?: boolean;
134
+ emptyText?: string;
134
135
  }
135
136
 
136
137
  const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
@@ -142,7 +143,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
142
143
  renderItem,
143
144
  selectedItem,
144
145
  localSearch,
145
- pageSize,
146
+ pageSize, showListEmptyComponent, emptyText
146
147
  }: AutoPositionedPopupListProps): React.JSX.Element => {
147
148
  const [state, setState] = useState<{
148
149
  selectedItem?: SelectedItem;
@@ -173,8 +174,8 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
173
174
  };
174
175
  }, []);
175
176
  // useEffect(() => {
176
- // // 監聽 TextInput 事件,收到就刷新列表,不依賴 global searchQuery
177
- // // 將最新的 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
178
179
  // ref_searchQuery.current = searchQuery;
179
180
  // console.log('AutoPositionedPopupList useEffect searchQuery=', searchQuery);
180
181
  // console.log('AutoPositionedPopupList useEffect state.localData=', state.localData);
@@ -209,10 +210,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
209
210
  pageIndex,
210
211
  pageSize: currentPageSize,
211
212
  }: FetchDataParams): Promise<ListData | null> => {
212
- console.log('AutoPositionedPopupList _fetchData pageIndex=', pageIndex, ' pageSize=', currentPageSize);
213
- console.log('AutoPositionedPopupList _fetchData state.localData=', state.localData);
214
- console.log('AutoPositionedPopupList _fetchData ref_searchQuery.current=', ref_searchQuery.current);
215
- 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});
216
214
  if (localSearch && state.localData.length > 0) {
217
215
  const result: SelectedItem[] = state.localData.filter((item: SelectedItem) => {
218
216
  return item.title?.toLowerCase().includes(ref_searchQuery.current.toLowerCase());
@@ -273,7 +271,8 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
273
271
  keyboardShouldPersistTaps={'always'}
274
272
  fetchData={_fetchData}
275
273
  renderItem={renderItem ? ({item, index}) => renderItem({item: item as SelectedItem, index}) : ({item, index}) => _renderItem({item: item as SelectedItem, index})}
276
- showListEmptyComponent={false}
274
+ showListEmptyComponent={showListEmptyComponent}
275
+ emptyText={emptyText}
277
276
  />
278
277
  </View>
279
278
  );
@@ -287,7 +286,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
287
286
  searchQuery,
288
287
  localSearch,
289
288
  pageSize,
290
- rootViewsRef,
289
+ rootViewsRef, showListEmptyComponent, emptyText
291
290
  ]);
292
291
  }
293
292
  );
@@ -314,7 +313,7 @@ const AutoPositionedPopup = memo(
314
313
  AutoPositionedPopupBtnStyle,
315
314
  placeholder = 'Please Select',
316
315
  onSubmitEditing,
317
- TextInputProps = {},
316
+ TextInputProps = {autoFocus: true},
318
317
  inputStyle,
319
318
  labelStyle,
320
319
  popUpViewStyle = {left: '5%', width: '90%'},
@@ -356,7 +355,7 @@ const AutoPositionedPopup = memo(
356
355
  centerDisplay = false,
357
356
  selectedItemBackgroundColor = 'rgba(116, 116, 128, 0.08)',
358
357
  textAlign = 'right',
359
- CustomPopView = undefined, CustomPopViewStyle
358
+ CustomPopView = undefined, CustomPopViewStyle, showListEmptyComponent = true, emptyText = ''
360
359
  } = props;
361
360
  // State management similar to project implementation
362
361
  const [state, setState] = useState<StateProps>({
@@ -377,6 +376,20 @@ const AutoPositionedPopup = memo(
377
376
  const keyboardVisibleRef = useRef(false);
378
377
  const refAutoPositionedPopup = useRef<View>(null);
379
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);
380
393
  // Simple keyboard status tracking (alternative to useKeyboardStatus hook)
381
394
  // Legacy state for compatibility
382
395
  const [isVisible, setIsVisible] = useState(false);
@@ -402,8 +415,7 @@ const AutoPositionedPopup = memo(
402
415
  useEffect(() => {
403
416
  (async () => {
404
417
  })();
405
- console.log(`AutoPositionedPopup componentDidMount tag=`, tag);
406
- console.log('AutoPositionedPopup componentDidMount CustomPopView=', CustomPopView);
418
+ console.log(`AutoPositionedPopup componentDidMount=`, {tag, CustomPopView});
407
419
  //componentWillUnmount
408
420
  return () => {
409
421
  console.log(`AutoPositionedPopup componentWillUnmount tag=`, tag);
@@ -436,8 +448,7 @@ const AutoPositionedPopup = memo(
436
448
  }
437
449
  }, [rootViews]);
438
450
  useEffect(() => {
439
- console.log('AutoPositionedPopup useEffect tag=', tag);
440
- console.log('AutoPositionedPopup useEffect selectedItem=', selectedItem);
451
+ console.log('AutoPositionedPopup useEffect [selectedItem, state.selectedItem, tag]=', {tag, selectedItem, 'state.selectedItem': state.selectedItem});
441
452
  console.log('AutoPositionedPopup useEffect state.selectedItem=', state.selectedItem);
442
453
  if (state.selectedItem?.id !== selectedItem?.id || state.selectedItem?.title !== selectedItem?.title) {
443
454
  console.log('AutoPositionedPopup useEffect selectedItem!=state.selectedItem');
@@ -450,26 +461,55 @@ const AutoPositionedPopup = memo(
450
461
  }
451
462
  }, [selectedItem, state.selectedItem, tag]);
452
463
  useEffect(() => {
453
- console.log('AutoPositionedPopup useEffect tag=', tag);
454
- console.log('AutoPositionedPopup useEffect state.isFocus=', state.isFocus);
455
- console.log('AutoPositionedPopup useEffect isKeyboardFullyShown=', isKeyboardFullyShown);
456
- console.log('AutoPositionedPopup useEffect ref_isFocus.current=', ref_isFocus.current);
457
- console.log(
458
- 'AutoPositionedPopup useEffect ref_isKeyboardFullyShown.current=',
459
- ref_isKeyboardFullyShown.current
460
- );
461
- console.log('AutoPositionedPopup useEffect useTextInput=', useTextInput);
462
- console.log('AutoPositionedPopup useEffect TextInputProps=', TextInputProps);
463
- console.log('AutoPositionedPopup useEffect hasAddedRootView.current=', hasAddedRootView.current);
464
- 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');
500
+ // if (!ref_isFocus.current) {
501
+ // textInputRef.current?.focus()
502
+ // }
503
+ return;
504
+ }
465
505
  if (useTextInput) {
466
506
  if (isKeyboardFullyShown && hasAddedRootView.current && !hasShownRootView.current && state.isFocus) {
467
507
  refAutoPositionedPopup.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
468
- console.log('AutoPositionedPopup measureInWindow x=', x, ' y=', y, ' width=', width, ' height=', height);
508
+ console.log('AutoPositionedPopup useTextInput measureInWindow=', {x, y, width, height});
469
509
  // SIMPLE CENTER-BASED POSITIONING STRATEGY
470
510
  const screenHeight = Dimensions.get('screen').height;
471
511
  const screenCenter = screenHeight / 2;
472
- console.log('AutoPositionedPopup screenHeight=', screenHeight, ' screenCenter=', screenCenter, ' componentY=', y);
512
+ console.log('AutoPositionedPopup useTextInput measureInWindow =', {screenHeight, screenCenter, componentY: y});
473
513
 
474
514
  // Simple rule: if component Y > screen center, show popup above; otherwise show below
475
515
  if (y > screenCenter) {
@@ -479,7 +519,7 @@ const AutoPositionedPopup = memo(
479
519
  console.log('AutoPositionedPopup with keyboard: showing below (Y <= center)');
480
520
  ref_listPos.current = {x: x, y: y + height, width: width};
481
521
  }
482
- console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
522
+ console.log('AutoPositionedPopup useTextInput ref_listPos.current=', ref_listPos.current);
483
523
  setRootViewNativeStyle(tag, {
484
524
  top: ref_listPos.current?.y,
485
525
  left: popUpViewStyle?.left,
@@ -489,12 +529,11 @@ const AutoPositionedPopup = memo(
489
529
  });
490
530
  hasShownRootView.current = true;
491
531
  });
492
- } else if (!isKeyboardFullyShown && ref_isFocus.current) {
532
+ } else if (!isKeyboardFullyShown && ref_isFocus.current && keyboardStateChanged) {
533
+ // Only execute close logic when keyboard state actually changes from true to false
493
534
  console.log(
494
- 'AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView tag=',
495
- tag,
496
- ' forceRemoveAllRootViewOnItemSelected=',
497
- forceRemoveAllRootViewOnItemSelected
535
+ 'AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView (keyboard state changed)=',
536
+ {tag, forceRemoveAllRootViewOnItemSelected, keyboardStateChanged}
498
537
  );
499
538
  removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
500
539
  setState((prevState) => {
@@ -510,11 +549,10 @@ const AutoPositionedPopup = memo(
510
549
  } else {
511
550
  if (state.isFocus) {
512
551
  refAutoPositionedPopup.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
513
- console.log('AutoPositionedPopup measureInWindow x=', x, ' y=', y, ' width=', width, ' height=', height);
514
-
552
+ console.log('AutoPositionedPopup !useTextInput measureInWindow=', {x, y, width, height});
515
553
  // INTELLIGENT POSITION CALCULATION - MODIFIED VERSION WITH STATUS BAR SAFETY
516
554
  const calculateOptimalPosition = (componentY: number, componentHeight: number, popupHeight: number) => {
517
- console.log('🔥🔥🔥 NEW CALCULATE OPTIMAL POSITION FUNCTION EXECUTING 🔥🔥🔥');
555
+ console.log('AutoPositionedPopup 🔥🔥🔥 NEW CALCULATE OPTIMAL POSITION FUNCTION EXECUTING 🔥🔥🔥');
518
556
 
519
557
  // Use window height (visible area) instead of screen height (includes status bar)
520
558
  const windowHeight = Dimensions.get('window').height;
@@ -532,7 +570,7 @@ const AutoPositionedPopup = memo(
532
570
  }
533
571
  };
534
572
  const statusBarHeight = getStatusBarHeight();
535
- console.log('🔥 Cross-platform StatusBar height:', statusBarHeight, 'Platform:', Platform.OS);
573
+ console.log('AutoPositionedPopup 🔥 Cross-platform StatusBar height:', statusBarHeight, 'Platform:', Platform.OS);
536
574
 
537
575
  // Calculate component center point as requested
538
576
  const componentCenterY = componentY + componentHeight / 2;
@@ -593,7 +631,7 @@ const AutoPositionedPopup = memo(
593
631
 
594
632
  const finalSpacing = Math.max(baseSpacing, relativeSpacing) * edgeProximityFactor * platformMultiplier;
595
633
 
596
- console.log('🔥 Advanced spacing calculation:', {
634
+ console.log('AutoPositionedPopup 🔥 Advanced spacing calculation:', {
597
635
  componentCenter,
598
636
  screenCenter,
599
637
  distanceFromCenter,
@@ -630,41 +668,41 @@ const AutoPositionedPopup = memo(
630
668
  // 'usableSpaceAbove >= needed': usableSpaceAbove >= popupHeight + POPUP_SPACING
631
669
  // });
632
670
 
633
- if (isInBottomHalf && usableSpaceAbove >= popupHeight ) {
671
+ if (isInBottomHalf && usableSpaceAbove >= popupHeight) {
634
672
  // Component in bottom half + enough space above = FORCE ABOVE
635
673
  showAbove = true;
636
- finalY = componentY - popupHeight +componentHeight/2;
637
- console.log('🔥 AutoPositionedPopup: FORCE ABOVE - bottom half component with enough space, finalY=', finalY);
638
- } else if (!isInBottomHalf && spaceBelow >= popupHeight ) {
674
+ finalY = componentY - popupHeight + componentHeight / 2;
675
+ console.log('AutoPositionedPopup 🔥 AutoPositionedPopup: FORCE ABOVE - bottom half component with enough space, finalY=', finalY);
676
+ } else if (!isInBottomHalf && spaceBelow >= popupHeight) {
639
677
  // Component in top half + enough space below = show below
640
678
  showAbove = false;
641
- finalY = componentY + componentHeight*2;
679
+ finalY = componentY + componentHeight * 2;
642
680
  console.log('🔥 AutoPositionedPopup: Showing below - top half component with enough space, finalY=', finalY);
643
- } else if (usableSpaceAbove >= popupHeight ) {
681
+ } else if (usableSpaceAbove >= popupHeight) {
644
682
  // Fallback: enough space above
645
683
  showAbove = true;
646
- finalY = componentY - popupHeight ;
684
+ finalY = componentY - popupHeight;
647
685
  console.log('🔥 AutoPositionedPopup: Showing above - enough space available (fallback), finalY=', finalY);
648
- } else if (spaceBelow >= popupHeight ) {
686
+ } else if (spaceBelow >= popupHeight) {
649
687
  // Fallback: enough space below
650
688
  showAbove = false;
651
- finalY = componentY + componentHeight ;
689
+ finalY = componentY + componentHeight;
652
690
  console.log('🔥 AutoPositionedPopup: Showing below - enough space available (fallback), finalY=', finalY);
653
691
  } else {
654
692
  // Emergency fallback: choose larger space
655
693
  if (usableSpaceAbove >= spaceBelow) {
656
694
  showAbove = true;
657
- finalY = Math.max(statusBarHeight, componentY - popupHeight );
695
+ finalY = Math.max(statusBarHeight, componentY - popupHeight);
658
696
  console.log('🔥 AutoPositionedPopup: Emergency above - larger space, finalY=', finalY);
659
697
  } else {
660
698
  showAbove = false;
661
- finalY = componentY + componentHeight ;
699
+ finalY = componentY + componentHeight;
662
700
  console.log('🔥 AutoPositionedPopup: Emergency below - larger space, finalY=', finalY);
663
701
  }
664
702
  }
665
703
 
666
704
  // Enhanced boundary check with detailed logging
667
- console.log('🔥 Pre-boundary check:', {
705
+ console.log('AutoPositionedPopup 🔥 Pre-boundary check:', {
668
706
  originalFinalY: finalY,
669
707
  showAbove,
670
708
  statusBarHeight,
@@ -677,25 +715,25 @@ const AutoPositionedPopup = memo(
677
715
  if (showAbove && finalY < statusBarHeight) {
678
716
  const oldFinalY = finalY;
679
717
  finalY = statusBarHeight;
680
- console.log('🔥 BOUNDARY FIX: Above display adjusted for status bar:', oldFinalY, '->', finalY);
718
+ console.log('AutoPositionedPopup 🔥 BOUNDARY : Above display adjusted for status bar:', oldFinalY, '->', finalY);
681
719
  }
682
720
 
683
721
  if (!showAbove && finalY + popupHeight > windowHeight) {
684
722
  const oldFinalY = finalY;
685
723
  finalY = windowHeight - popupHeight;
686
- console.log('🔥 BOUNDARY FIX: Below display adjusted to fit window:', oldFinalY, '->', finalY);
724
+ console.log('AutoPositionedPopup 🔥 BOUNDARY : Below display adjusted to fit window:', oldFinalY, '->', finalY);
687
725
  }
688
726
 
689
727
  // CRITICAL CHECK: Detect if boundary check is changing display direction
690
- if (showAbove && finalY + popupHeight > componentY ) {
691
- console.log('🚨 WARNING: Above positioning may overlap with component!');
728
+ if (showAbove && finalY + popupHeight > componentY) {
729
+ console.log('AutoPositionedPopup 🚨 WARNING: Above positioning may overlap with component!');
692
730
  }
693
731
 
694
- if (!showAbove && finalY < componentY + componentHeight ) {
695
- console.log('🚨 WARNING: Below positioning may overlap with component!');
732
+ if (!showAbove && finalY < componentY + componentHeight) {
733
+ console.log('AutoPositionedPopup 🚨 WARNING: Below positioning may overlap with component!');
696
734
  }
697
735
 
698
- console.log('🔥 Post-boundary check final result:', {
736
+ console.log('AutoPositionedPopup 🔥 Post-boundary check final result:', {
699
737
  finalY,
700
738
  showAbove,
701
739
  'popupTop': finalY,
@@ -711,21 +749,15 @@ const AutoPositionedPopup = memo(
711
749
  const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
712
750
  ? CustomPopViewStyle.height
713
751
  : listLayout.height;
714
-
715
- console.log('🔥 Using actualPopupHeight for calculation:', actualPopupHeight, 'CustomPopView:', !!CustomPopView);
716
-
752
+ console.log('AutoPositionedPopup 🔥 Using actualPopupHeight for calculation:', {actualPopupHeight, CustomPopView: !!CustomPopView});
717
753
  const positionResult = calculateOptimalPosition(y, height, actualPopupHeight);
718
754
  console.log('AutoPositionedPopup FINAL position result:', positionResult);
719
-
720
755
  ref_listPos.current = {x: x, y: positionResult.finalY, width: width};
721
- console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
722
-
756
+ console.log('AutoPositionedPopup !useTextInput ref_listPos.current=', ref_listPos.current);
723
757
  if (CustomPopView && CustomPopViewStyle) {
724
- console.log('AutoPositionedPopup CustomPopViewStyle=', CustomPopViewStyle);
725
758
  // Position already calculated correctly above, no need to recalculate
726
759
  const PopViewComponent = CustomPopView();
727
- console.log('AutoPositionedPopup addRootView PopViewComponent=', PopViewComponent);
728
- console.log('AutoPositionedPopup addRootView state.selectedItem=', state.selectedItem);
760
+ console.log('AutoPositionedPopup !useTextInput addRootView=', {CustomPopViewStyle, PopViewComponent, 'state.selectedItem': state.selectedItem});
729
761
  addRootView({
730
762
  id: tag,
731
763
  style: !centerDisplay
@@ -757,7 +789,7 @@ const AutoPositionedPopup = memo(
757
789
  centerDisplay,
758
790
  });
759
791
  } else {
760
- console.log('AutoPositionedPopup addRootView tag=', tag);
792
+ console.log('AutoPositionedPopup !useTextInput addRootView tag=', tag);
761
793
  addRootView({
762
794
  id: tag,
763
795
  style: {
@@ -776,6 +808,8 @@ const AutoPositionedPopup = memo(
776
808
  renderItem={renderItem}
777
809
  selectedItem={state.selectedItem}
778
810
  localSearch={localSearch}
811
+ showListEmptyComponent={showListEmptyComponent}
812
+ emptyText={emptyText}
779
813
  />
780
814
  ),
781
815
  useModal: true,
@@ -811,8 +845,8 @@ const AutoPositionedPopup = memo(
811
845
  CustomPopView,
812
846
  CustomPopViewStyle,
813
847
  forceRemoveAllRootViewOnItemSelected,
814
- tag,
815
- state.selectedItem,
848
+ tag, TextInputProps,
849
+ state.selectedItem, showListEmptyComponent
816
850
  ]);
817
851
  // Imperative handle for parent component access
818
852
  useImperativeHandle(
@@ -831,7 +865,7 @@ const AutoPositionedPopup = memo(
831
865
  []
832
866
  );
833
867
  const updateState = (key: string, value: SelectedItem) => {
834
- console.log('AutoPositionedPopup updateState key=', key, ' value=', value);
868
+ console.log('AutoPositionedPopup updateState=', {key, value});
835
869
  setState((prevState) => ({
836
870
  ...prevState,
837
871
  [key]: value,
@@ -852,6 +886,225 @@ const AutoPositionedPopup = memo(
852
886
  setSearchQuery('');
853
887
  }
854
888
  };
889
+
890
+ // Simple deep comparison function (for style objects only)
891
+ const shallowEqual = (obj1: any, obj2: any): boolean => {
892
+ if (obj1 === obj2) return true;
893
+ if (!obj1 || !obj2) return false;
894
+ if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false;
895
+
896
+ const keys1 = Object.keys(obj1);
897
+ const keys2 = Object.keys(obj2);
898
+ if (keys1.length !== keys2.length) return false;
899
+
900
+ for (const key of keys1) {
901
+ if (obj1[key] !== obj2[key]) return false;
902
+ }
903
+ return true;
904
+ };
905
+
906
+ // Use useMemo to create stable props reference
907
+ // Only update when deep comparison detects real changes to avoid TextInput recreation due to reference changes during parent component redraws
908
+ const stableInputStyle = useMemo(() => {
909
+ if (!shallowEqual(stableInputStyleRef.current, inputStyle)) {
910
+ console.log(`AutoPositionedPopup inputStyle deep change detected, updating stable reference - tag: ${tag}`);
911
+ stableInputStyleRef.current = inputStyle;
912
+ }
913
+ return stableInputStyleRef.current;
914
+ }, [inputStyle, tag]);
915
+
916
+ const stableTextInputProps = useMemo(() => {
917
+ if (!shallowEqual(stableTextInputPropsRef.current, TextInputProps)) {
918
+ console.log(`AutoPositionedPopup TextInputProps deep change detected, updating stable reference - tag: ${tag}`);
919
+ stableTextInputPropsRef.current = TextInputProps;
920
+ }
921
+ return stableTextInputPropsRef.current;
922
+ }, [TextInputProps, tag]);
923
+
924
+ // Use useCallback to stabilize onFocus and onBlur callback references
925
+ // Prevent creating new callback functions during parent component redraws to avoid TextInput re-triggering focus
926
+ // Use ref to store latest state values to avoid adding frequently changing values to dependencies
927
+ const stateRef = useRef(state);
928
+ stateRef.current = state;
929
+
930
+ const handleTextInputFocus = useCallback(() => {
931
+ const currentTime = Date.now();
932
+ const timeSinceLastFocus = currentTime - lastFocusTimeRef.current;
933
+ console.log(
934
+ 'AutoPositionedPopup onFocus=',
935
+ {
936
+ tag,
937
+ 'state.selectedItem': stateRef.current.selectedItem,
938
+ 'hasTriggeredFocus.current=': hasTriggeredFocus.current,
939
+ 'textInputRef.current=': textInputRef.current,
940
+ 'ref_searchQuery.current=': ref_searchQuery.current,
941
+ 'timeSinceLastFocus': timeSinceLastFocus,
942
+ 'isKeyboardFullyShown': isKeyboardFullyShown,
943
+ 'isFocusEventProcessing': isFocusEventProcessingRef.current
944
+ }
945
+ );
946
+ // Prevent rapid repeated triggers (repeated events within 300ms are ignored)
947
+ if (timeSinceLastFocus < 300) {
948
+ console.log('AutoPositionedPopup onFocus: Skip - event triggered too quickly (< 300ms)');
949
+ return;
950
+ }
951
+ // Skip if keyboard is already open and focus has been handled
952
+ if (isKeyboardFullyShown && hasTriggeredFocus.current) {
953
+ console.log('AutoPositionedPopup onFocus: Skip - keyboard already open and focus handled');
954
+ return;
955
+ }
956
+ // Prevent concurrent processing
957
+ if (isFocusEventProcessingRef.current) {
958
+ console.log('AutoPositionedPopup onFocus: Skip - processing another focus event');
959
+ return;
960
+ }
961
+ isFocusEventProcessingRef.current = true;
962
+ lastFocusTimeRef.current = currentTime;
963
+ if (!hasTriggeredFocus.current) {
964
+ hasTriggeredFocus.current = true;
965
+ ref_isFocus.current = true;
966
+ if (stateRef.current.selectedItem) {
967
+ ref_searchQuery.current = stateRef.current.selectedItem.title;
968
+ }
969
+ if (textInputRef.current && ref_searchQuery.current) {
970
+ textInputRef.current.setNativeProps({
971
+ text: ref_searchQuery.current,
972
+ });
973
+ }
974
+ }
975
+ // Delay resetting processing flag to avoid blocking subsequent legitimate focus events
976
+ setTimeout(() => {
977
+ isFocusEventProcessingRef.current = false;
978
+ }, 100);
979
+ }, [tag, isKeyboardFullyShown]); // Remove state.selectedItem, use stateRef instead
980
+
981
+ const handleTextInputBlur = useCallback(() => {
982
+ console.log(
983
+ 'AutoPositionedPopup onBlur=',
984
+ {
985
+ tag,
986
+ 'textInputRef.current': textInputRef.current,
987
+ 'isKeyboardFullyShown': isKeyboardFullyShown,
988
+ 'hasTriggeredFocus.current': hasTriggeredFocus.current
989
+ }
990
+ );
991
+ // If keyboard is still open, this is a false trigger caused by parent component re-render, should not reset
992
+ if (isKeyboardFullyShown && hasTriggeredFocus.current) {
993
+ console.log('AutoPositionedPopup onBlur: Skip - keyboard still open, possibly caused by parent component re-render');
994
+ return;
995
+ }
996
+
997
+ // Only reset internal state, do not actively close keyboard
998
+ // Keyboard will close naturally when TextInput loses focus, no need to manually call Keyboard.dismiss()
999
+ hasTriggeredFocus.current = false;
1000
+ hasAddedRootView.current = false;
1001
+ hasShownRootView.current = false;
1002
+ ref_isFocus.current = false;
1003
+ setState((prevState) => {
1004
+ return {
1005
+ ...prevState,
1006
+ isFocus: false,
1007
+ };
1008
+ });
1009
+ removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
1010
+ setSearchQuery('');
1011
+ if (textInputRef.current) {
1012
+ textInputRef.current.setNativeProps({text: ''});
1013
+ ref_searchQuery.current = '';
1014
+ // Remove textInputRef.current.blur() - avoid forcing blur causing keyboard to close
1015
+ }
1016
+ // Remove Keyboard.dismiss() - let keyboard close naturally to avoid triggering keyboardDidHide event
1017
+ }, [tag, isKeyboardFullyShown, forceRemoveAllRootViewOnItemSelected]);
1018
+
1019
+ // Wrap TextInput independently in useMemo to recreate only when key props change
1020
+ // This avoids repeated ref callback triggers due to other props changes during parent component redraws
1021
+ const memoizedTextInput = useMemo(() => {
1022
+ console.log(`AutoPositionedPopup useMemo creating TextInput - tag: ${tag}, isFocus: ${state.isFocus}`);
1023
+ if (!useTextInput || !state.isFocus) {
1024
+ return null;
1025
+ }
1026
+ return (
1027
+ <RNTextInput
1028
+ ref={(ref) => {
1029
+ // Monitor TextInput mounting and unmounting
1030
+ if (ref && !textInputRef.current) {
1031
+ console.log(`AutoPositionedPopup TextInput created/mounted - tag: ${tag}, ref:`, ref);
1032
+ } else if (!ref && textInputRef.current) {
1033
+ console.log(`AutoPositionedPopup TextInput unmounted - tag: ${tag}`);
1034
+ } else if (ref && textInputRef.current && ref !== textInputRef.current) {
1035
+ console.log(`AutoPositionedPopup TextInput replaced - tag: ${tag}, oldRef:`, textInputRef.current, 'newRef:', ref);
1036
+ }
1037
+ textInputRef.current = ref;
1038
+ }}
1039
+ key={`textinput-${tag}`}
1040
+ style={[
1041
+ styles.inputStyle,
1042
+ stableInputStyle,
1043
+ ]}
1044
+ textAlign={stableTextInputProps['textAlign'] || 'left'}
1045
+ multiline={stableTextInputProps['multiline'] || false}
1046
+ numberOfLines={stableTextInputProps['numberOfLines'] || 1}
1047
+ onChangeText={(searchQuery) => {
1048
+ ref_searchQuery.current = searchQuery;
1049
+ console.log('AutoPositionedPopup onChangeText rootViews=', rootViews);
1050
+ if (!localSearch) {
1051
+ if (debounceTimerRef.current) {
1052
+ clearTimeout(debounceTimerRef.current);
1053
+ }
1054
+ debounceTimerRef.current = setTimeout(() => {
1055
+ emitQueryChange(ref_searchQuery.current);
1056
+ }, 500);
1057
+ } else {
1058
+ emitQueryChange(ref_searchQuery.current);
1059
+ }
1060
+ }}
1061
+ placeholderTextColor={theme.colors.placeholderText}
1062
+ placeholder={placeholder}
1063
+ onKeyPress={(e) => {
1064
+ if (e.nativeEvent.key === 'Enter') {
1065
+ Keyboard.dismiss();
1066
+ }
1067
+ }}
1068
+ keyboardType={stableTextInputProps['keyboardType'] || 'default'}
1069
+ clearButtonMode="while-editing"
1070
+ returnKeyType={stableTextInputProps['returnKeyType'] || 'done'}
1071
+ maxLength={stableTextInputProps['maxLength'] || 100}
1072
+ accessibilityLabel="selectInput"
1073
+ accessible={true}
1074
+ autoFocus={stableTextInputProps['autoFocus'] || false}
1075
+ autoCorrect={false}
1076
+ underlineColorAndroid="transparent"
1077
+ editable={stableTextInputProps['editable'] || true}
1078
+ secureTextEntry={stableTextInputProps['secureTextEntry'] || false}
1079
+ defaultValue=""
1080
+ caretHidden={false}
1081
+ enablesReturnKeyAutomatically
1082
+ onFocus={handleTextInputFocus}
1083
+ onBlur={handleTextInputBlur}
1084
+ selectTextOnFocus={stableTextInputProps['selectTextOnFocus'] || false}
1085
+ onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
1086
+ console.log(
1087
+ 'AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=',
1088
+ e.nativeEvent.text
1089
+ );
1090
+ onSubmitEditing && onSubmitEditing(e);
1091
+ }}
1092
+ />
1093
+ );
1094
+ }, [
1095
+ tag, // tag 是稳定的
1096
+ useTextInput, // useTextInput 是稳定的
1097
+ state.isFocus, // isFocus 控制显示/隐藏
1098
+ handleTextInputFocus, // useCallback wrapped, reference stable
1099
+ handleTextInputBlur, // useCallback wrapped, reference stable
1100
+ stableInputStyle, // Use stable inputStyle reference (after deep comparison)
1101
+ stableTextInputProps, // Use stable TextInputProps reference (after deep comparison)
1102
+ placeholder, // placeholder usually stable
1103
+ onSubmitEditing, // onSubmitEditing usually stable
1104
+ // No longer use original inputStyle and TextInputProps, use stable references instead
1105
+ // Stable references only update when deep comparison detects actual content changes, avoiding frequent TextInput recreation during parent component redraws
1106
+ ]);
1107
+
855
1108
  // Render the component following project implementation
856
1109
  return useMemo(() => {
857
1110
  console.log('AutoPositionedPopup render tag=', tag); // Now safe - circular dependency fixed
@@ -863,22 +1116,15 @@ const AutoPositionedPopup = memo(
863
1116
  style={[styles.AutoPositionedPopupBtn, AutoPositionedPopupBtnStyle]}
864
1117
  disabled={AutoPositionedPopupBtnDisabled}
865
1118
  onPress={() => {
866
- console.log('AutoPositionedPopup onPress tag=', tag);
867
- console.log('AutoPositionedPopup onPress state.isFocus=', state.isFocus);
868
- console.log('AutoPositionedPopup onPress useTextInput=', useTextInput);
869
- console.log(
870
- 'AutoPositionedPopup onPress hasAddedRootView.current=',
871
- hasAddedRootView.current
872
- );
873
- console.log(
874
- 'AutoPositionedPopup onPress hasShownRootView.current=',
875
- hasShownRootView.current
876
- );
877
- console.log(
878
- 'AutoPositionedPopup onPress hasTriggeredFocus.current=',
879
- hasTriggeredFocus.current
880
- );
881
- console.log('AutoPositionedPopup onPress state.selectedItem=', state.selectedItem);
1119
+ console.log('AutoPositionedPopup onPress=', {
1120
+ tag,
1121
+ 'state.isFocus': state.isFocus,
1122
+ useTextInput,
1123
+ 'hasAddedRootView.current': hasAddedRootView.current,
1124
+ 'hasShownRootView.current': hasShownRootView.current,
1125
+ 'hasTriggeredFocus.current': hasTriggeredFocus.current,
1126
+ 'state.selectedItem': state.selectedItem
1127
+ });
882
1128
  setState((prevState) => {
883
1129
  return {
884
1130
  ...prevState,
@@ -886,6 +1132,7 @@ const AutoPositionedPopup = memo(
886
1132
  };
887
1133
  });
888
1134
  if (!hasAddedRootView.current && useTextInput) {
1135
+ // TextInput version: hide first, show after keyboard is fully displayed
889
1136
  hasAddedRootView.current = true;
890
1137
  hasShownRootView.current = false;
891
1138
  addRootView({
@@ -906,11 +1153,14 @@ const AutoPositionedPopup = memo(
906
1153
  renderItem={renderItem}
907
1154
  selectedItem={state.selectedItem}
908
1155
  localSearch={localSearch}
1156
+ showListEmptyComponent={showListEmptyComponent}
1157
+ emptyText={emptyText}
909
1158
  />
910
1159
  ),
911
1160
  useModal: false,
912
1161
  });
913
1162
  }
1163
+ console.log('AutoPositionedPopup onPress done')
914
1164
  }}
915
1165
  >
916
1166
  {!btwChildren ? (
@@ -930,141 +1180,38 @@ const AutoPositionedPopup = memo(
930
1180
  )}
931
1181
  </TouchableOpacity>
932
1182
  ) : (
933
- useTextInput &&
934
- state.isFocus && (
935
- <RNTextInput
936
- ref={textInputRef}
937
- key="fixed-textinput-key"
938
- style={[
939
- styles.inputStyle,
940
- inputStyle,
941
- ]}
942
- textAlign={TextInputProps['textAlign'] || 'left'}
943
- multiline={TextInputProps['multiline'] || false}
944
- numberOfLines={TextInputProps['numberOfLines'] || 1}
945
- onChangeText={(searchQuery) => {
946
- ref_searchQuery.current = searchQuery;
947
- console.log('AutoPositionedPopup onChangeText rootViews=', rootViews);
948
- if (!localSearch) {
949
- if (debounceTimerRef.current) {
950
- clearTimeout(debounceTimerRef.current);
951
- }
952
- debounceTimerRef.current = setTimeout(() => {
953
- emitQueryChange(ref_searchQuery.current);
954
- }, 500);
955
- } else {
956
- emitQueryChange(ref_searchQuery.current);
957
- }
958
- }}
959
- placeholderTextColor={theme.colors.placeholderText}
960
- placeholder={placeholder}
961
- onKeyPress={(e) => {
962
- if (e.nativeEvent.key === 'Enter') {
963
- Keyboard.dismiss();
964
- }
965
- }}
966
- keyboardType={TextInputProps['keyboardType'] || 'default'}
967
- clearButtonMode="while-editing"
968
- returnKeyType={TextInputProps['returnKeyType'] || 'done'}
969
- maxLength={TextInputProps['maxLength'] || 100}
970
- accessibilityLabel="selectInput"
971
- accessible={true}
972
- autoFocus={TextInputProps['autoFocus'] || false}
973
- autoCorrect={false}
974
- underlineColorAndroid="transparent"
975
- editable={TextInputProps['editable'] || true}
976
- secureTextEntry={TextInputProps['secureTextEntry'] || false}
977
- defaultValue=""
978
- caretHidden={false}
979
- enablesReturnKeyAutomatically
980
- onFocus={() => {
981
- console.log(
982
- 'AutoPositionedPopup onFocus tag=',
983
- tag,
984
- ' selectedItem=',
985
- state.selectedItem,
986
- ' hasTriggeredFocus.current=',
987
- hasTriggeredFocus.current,
988
- ' textInputRef.current=',
989
- textInputRef.current,
990
- ' ref_searchQuery.current=',
991
- ref_searchQuery.current
992
- );
993
- if (!hasTriggeredFocus.current) {
994
- hasTriggeredFocus.current = true;
995
- ref_isFocus.current = true;
996
- if (state.selectedItem) {
997
- ref_searchQuery.current = state.selectedItem.title;
998
- }
999
- if (textInputRef.current && ref_searchQuery.current) {
1000
- textInputRef.current.setNativeProps({
1001
- text: ref_searchQuery.current,
1002
- });
1003
- }
1004
- }
1005
- }}
1006
- onBlur={() => {
1007
- console.log(
1008
- 'AutoPositionedPopup onBlur tag=',
1009
- tag,
1010
- 'textInputRef.current=',
1011
- textInputRef.current
1012
- );
1013
- hasTriggeredFocus.current = false;
1014
- hasAddedRootView.current = false; // 重置 RootView 狀態
1015
- hasShownRootView.current = false;
1016
- ref_isFocus.current = false;
1017
- setState((prevState) => {
1018
- return {
1019
- ...prevState,
1020
- isFocus: false,
1021
- };
1022
- });
1023
- removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
1024
- setSearchQuery('');
1025
- if (textInputRef.current) {
1026
- textInputRef.current.setNativeProps({text: ''});
1027
- ref_searchQuery.current = '';
1028
- textInputRef.current.blur();
1029
- }
1030
- Keyboard.dismiss();
1031
- }}
1032
- selectTextOnFocus={TextInputProps['selectTextOnFocus'] || false}
1033
- onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
1034
- console.log(
1035
- 'AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=',
1036
- e.nativeEvent.text
1037
- );
1038
- onSubmitEditing && onSubmitEditing(e);
1039
- }}
1040
- />
1041
- )
1183
+ memoizedTextInput
1042
1184
  )}
1043
1185
  </View>
1044
1186
  </CustomRow>
1045
1187
  );
1046
- }, [tag,
1047
- fetchData,
1048
- renderItem,
1049
- onItemSelected,
1050
- onSubmitEditing,
1188
+ }, [
1189
+ tag,
1190
+ // ✅ CRITICAL FIX: Remove all props that may change frequently or are inline functions
1191
+ // Changes to these props should not cause the entire component tree to recreate, especially TextInput
1192
+ // fetchData, // ❌ Removed: inline function
1193
+ // renderItem, // ❌ Removed: possibly inline function
1194
+ // onItemSelected, // ❌ Removed: possibly inline function
1195
+ // onSubmitEditing, // ❌ Removed: possibly inline function
1051
1196
  localSearch,
1052
- placeholder,
1053
- textAlign,
1197
+ // placeholder, // ❌ Removed: may change
1198
+ // textAlign, // ❌ Removed: may change
1054
1199
  pageSize,
1055
1200
  selectedItem,
1056
- CustomRow,
1201
+ // CustomRow, // ❌ Removed: inline function, new reference each time
1057
1202
  useTextInput,
1058
- btwChildren,
1059
- selectedItem,
1060
- keyExtractor,
1061
- AutoPositionedPopupBtnStyle,
1062
- CustomPopView,
1063
- CustomPopViewStyle,
1203
+ // btwChildren, // ❌ Removed: inline function
1204
+ // keyExtractor, // ❌ Removed: possibly inline function
1205
+ // AutoPositionedPopupBtnStyle, // ❌ Removed: possibly inline object
1206
+ // CustomPopView, // ❌ Removed: may change
1207
+ // CustomPopViewStyle, // ❌ Removed: may change
1064
1208
  forceRemoveAllRootViewOnItemSelected,
1065
- inputStyle,
1066
- TextInputProps,
1067
- state.isFocus,]);
1209
+ state.isFocus,
1210
+ showListEmptyComponent,
1211
+ emptyText,
1212
+ // ✅ Removed most dependencies that may cause re-rendering, keeping only core dependencies that truly affect component structure
1213
+ // This prevents TextInput recreation due to inline functions/objects during parent component redraws
1214
+ ]);
1068
1215
  }
1069
1216
  )
1070
1217
  );