react-native-auto-positioned-popup 1.0.12 → 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,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,26 +461,55 @@ 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');
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
507
  refAutoPositionedPopup.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
472
- console.log('AutoPositionedPopup measureInWindow x=', x, ' y=', y, ' width=', width, ' height=', height);
508
+ console.log('AutoPositionedPopup useTextInput measureInWindow=', {x, y, width, height});
473
509
  // SIMPLE CENTER-BASED POSITIONING STRATEGY
474
510
  const screenHeight = Dimensions.get('screen').height;
475
511
  const screenCenter = screenHeight / 2;
476
- console.log('AutoPositionedPopup screenHeight=', screenHeight, ' screenCenter=', screenCenter, ' componentY=', y);
512
+ console.log('AutoPositionedPopup useTextInput measureInWindow =', {screenHeight, screenCenter, componentY: y});
477
513
 
478
514
  // Simple rule: if component Y > screen center, show popup above; otherwise show below
479
515
  if (y > screenCenter) {
@@ -483,7 +519,7 @@ const AutoPositionedPopup = memo(
483
519
  console.log('AutoPositionedPopup with keyboard: showing below (Y <= center)');
484
520
  ref_listPos.current = {x: x, y: y + height, width: width};
485
521
  }
486
- console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
522
+ console.log('AutoPositionedPopup useTextInput ref_listPos.current=', ref_listPos.current);
487
523
  setRootViewNativeStyle(tag, {
488
524
  top: ref_listPos.current?.y,
489
525
  left: popUpViewStyle?.left,
@@ -493,12 +529,11 @@ const AutoPositionedPopup = memo(
493
529
  });
494
530
  hasShownRootView.current = true;
495
531
  });
496
- } 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
497
534
  console.log(
498
- 'AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView tag=',
499
- tag,
500
- ' forceRemoveAllRootViewOnItemSelected=',
501
- forceRemoveAllRootViewOnItemSelected
535
+ 'AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView (keyboard state changed)=',
536
+ {tag, forceRemoveAllRootViewOnItemSelected, keyboardStateChanged}
502
537
  );
503
538
  removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
504
539
  setState((prevState) => {
@@ -514,11 +549,10 @@ const AutoPositionedPopup = memo(
514
549
  } else {
515
550
  if (state.isFocus) {
516
551
  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
-
552
+ console.log('AutoPositionedPopup !useTextInput measureInWindow=', {x, y, width, height});
519
553
  // INTELLIGENT POSITION CALCULATION - MODIFIED VERSION WITH STATUS BAR SAFETY
520
554
  const calculateOptimalPosition = (componentY: number, componentHeight: number, popupHeight: number) => {
521
- console.log('🔥🔥🔥 NEW CALCULATE OPTIMAL POSITION FUNCTION EXECUTING 🔥🔥🔥');
555
+ console.log('AutoPositionedPopup 🔥🔥🔥 NEW CALCULATE OPTIMAL POSITION FUNCTION EXECUTING 🔥🔥🔥');
522
556
 
523
557
  // Use window height (visible area) instead of screen height (includes status bar)
524
558
  const windowHeight = Dimensions.get('window').height;
@@ -536,7 +570,7 @@ const AutoPositionedPopup = memo(
536
570
  }
537
571
  };
538
572
  const statusBarHeight = getStatusBarHeight();
539
- console.log('🔥 Cross-platform StatusBar height:', statusBarHeight, 'Platform:', Platform.OS);
573
+ console.log('AutoPositionedPopup 🔥 Cross-platform StatusBar height:', statusBarHeight, 'Platform:', Platform.OS);
540
574
 
541
575
  // Calculate component center point as requested
542
576
  const componentCenterY = componentY + componentHeight / 2;
@@ -597,7 +631,7 @@ const AutoPositionedPopup = memo(
597
631
 
598
632
  const finalSpacing = Math.max(baseSpacing, relativeSpacing) * edgeProximityFactor * platformMultiplier;
599
633
 
600
- console.log('🔥 Advanced spacing calculation:', {
634
+ console.log('AutoPositionedPopup 🔥 Advanced spacing calculation:', {
601
635
  componentCenter,
602
636
  screenCenter,
603
637
  distanceFromCenter,
@@ -634,41 +668,41 @@ const AutoPositionedPopup = memo(
634
668
  // 'usableSpaceAbove >= needed': usableSpaceAbove >= popupHeight + POPUP_SPACING
635
669
  // });
636
670
 
637
- if (isInBottomHalf && usableSpaceAbove >= popupHeight ) {
671
+ if (isInBottomHalf && usableSpaceAbove >= popupHeight) {
638
672
  // Component in bottom half + enough space above = FORCE ABOVE
639
673
  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 ) {
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) {
643
677
  // Component in top half + enough space below = show below
644
678
  showAbove = false;
645
- finalY = componentY + componentHeight*2;
679
+ finalY = componentY + componentHeight * 2;
646
680
  console.log('🔥 AutoPositionedPopup: Showing below - top half component with enough space, finalY=', finalY);
647
- } else if (usableSpaceAbove >= popupHeight ) {
681
+ } else if (usableSpaceAbove >= popupHeight) {
648
682
  // Fallback: enough space above
649
683
  showAbove = true;
650
- finalY = componentY - popupHeight ;
684
+ finalY = componentY - popupHeight;
651
685
  console.log('🔥 AutoPositionedPopup: Showing above - enough space available (fallback), finalY=', finalY);
652
- } else if (spaceBelow >= popupHeight ) {
686
+ } else if (spaceBelow >= popupHeight) {
653
687
  // Fallback: enough space below
654
688
  showAbove = false;
655
- finalY = componentY + componentHeight ;
689
+ finalY = componentY + componentHeight;
656
690
  console.log('🔥 AutoPositionedPopup: Showing below - enough space available (fallback), finalY=', finalY);
657
691
  } else {
658
692
  // Emergency fallback: choose larger space
659
693
  if (usableSpaceAbove >= spaceBelow) {
660
694
  showAbove = true;
661
- finalY = Math.max(statusBarHeight, componentY - popupHeight );
695
+ finalY = Math.max(statusBarHeight, componentY - popupHeight);
662
696
  console.log('🔥 AutoPositionedPopup: Emergency above - larger space, finalY=', finalY);
663
697
  } else {
664
698
  showAbove = false;
665
- finalY = componentY + componentHeight ;
699
+ finalY = componentY + componentHeight;
666
700
  console.log('🔥 AutoPositionedPopup: Emergency below - larger space, finalY=', finalY);
667
701
  }
668
702
  }
669
703
 
670
704
  // Enhanced boundary check with detailed logging
671
- console.log('🔥 Pre-boundary check:', {
705
+ console.log('AutoPositionedPopup 🔥 Pre-boundary check:', {
672
706
  originalFinalY: finalY,
673
707
  showAbove,
674
708
  statusBarHeight,
@@ -681,25 +715,25 @@ const AutoPositionedPopup = memo(
681
715
  if (showAbove && finalY < statusBarHeight) {
682
716
  const oldFinalY = finalY;
683
717
  finalY = statusBarHeight;
684
- 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);
685
719
  }
686
720
 
687
721
  if (!showAbove && finalY + popupHeight > windowHeight) {
688
722
  const oldFinalY = finalY;
689
723
  finalY = windowHeight - popupHeight;
690
- 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);
691
725
  }
692
726
 
693
727
  // 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!');
728
+ if (showAbove && finalY + popupHeight > componentY) {
729
+ console.log('AutoPositionedPopup 🚨 WARNING: Above positioning may overlap with component!');
696
730
  }
697
731
 
698
- if (!showAbove && finalY < componentY + componentHeight ) {
699
- 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!');
700
734
  }
701
735
 
702
- console.log('🔥 Post-boundary check final result:', {
736
+ console.log('AutoPositionedPopup 🔥 Post-boundary check final result:', {
703
737
  finalY,
704
738
  showAbove,
705
739
  'popupTop': finalY,
@@ -715,21 +749,15 @@ const AutoPositionedPopup = memo(
715
749
  const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
716
750
  ? CustomPopViewStyle.height
717
751
  : listLayout.height;
718
-
719
- console.log('🔥 Using actualPopupHeight for calculation:', actualPopupHeight, 'CustomPopView:', !!CustomPopView);
720
-
752
+ console.log('AutoPositionedPopup 🔥 Using actualPopupHeight for calculation:', {actualPopupHeight, CustomPopView: !!CustomPopView});
721
753
  const positionResult = calculateOptimalPosition(y, height, actualPopupHeight);
722
754
  console.log('AutoPositionedPopup FINAL position result:', positionResult);
723
-
724
755
  ref_listPos.current = {x: x, y: positionResult.finalY, width: width};
725
- console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
726
-
756
+ console.log('AutoPositionedPopup !useTextInput ref_listPos.current=', ref_listPos.current);
727
757
  if (CustomPopView && CustomPopViewStyle) {
728
- console.log('AutoPositionedPopup CustomPopViewStyle=', CustomPopViewStyle);
729
758
  // Position already calculated correctly above, no need to recalculate
730
759
  const PopViewComponent = CustomPopView();
731
- console.log('AutoPositionedPopup addRootView PopViewComponent=', PopViewComponent);
732
- console.log('AutoPositionedPopup addRootView state.selectedItem=', state.selectedItem);
760
+ console.log('AutoPositionedPopup !useTextInput addRootView=', {CustomPopViewStyle, PopViewComponent, 'state.selectedItem': state.selectedItem});
733
761
  addRootView({
734
762
  id: tag,
735
763
  style: !centerDisplay
@@ -761,7 +789,7 @@ const AutoPositionedPopup = memo(
761
789
  centerDisplay,
762
790
  });
763
791
  } else {
764
- console.log('AutoPositionedPopup addRootView tag=', tag);
792
+ console.log('AutoPositionedPopup !useTextInput addRootView tag=', tag);
765
793
  addRootView({
766
794
  id: tag,
767
795
  style: {
@@ -817,8 +845,8 @@ const AutoPositionedPopup = memo(
817
845
  CustomPopView,
818
846
  CustomPopViewStyle,
819
847
  forceRemoveAllRootViewOnItemSelected,
820
- tag,
821
- state.selectedItem,showListEmptyComponent
848
+ tag, TextInputProps,
849
+ state.selectedItem, showListEmptyComponent
822
850
  ]);
823
851
  // Imperative handle for parent component access
824
852
  useImperativeHandle(
@@ -837,7 +865,7 @@ const AutoPositionedPopup = memo(
837
865
  []
838
866
  );
839
867
  const updateState = (key: string, value: SelectedItem) => {
840
- console.log('AutoPositionedPopup updateState key=', key, ' value=', value);
868
+ console.log('AutoPositionedPopup updateState=', {key, value});
841
869
  setState((prevState) => ({
842
870
  ...prevState,
843
871
  [key]: value,
@@ -858,6 +886,225 @@ const AutoPositionedPopup = memo(
858
886
  setSearchQuery('');
859
887
  }
860
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
+
861
1108
  // Render the component following project implementation
862
1109
  return useMemo(() => {
863
1110
  console.log('AutoPositionedPopup render tag=', tag); // Now safe - circular dependency fixed
@@ -869,22 +1116,15 @@ const AutoPositionedPopup = memo(
869
1116
  style={[styles.AutoPositionedPopupBtn, AutoPositionedPopupBtnStyle]}
870
1117
  disabled={AutoPositionedPopupBtnDisabled}
871
1118
  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);
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
+ });
888
1128
  setState((prevState) => {
889
1129
  return {
890
1130
  ...prevState,
@@ -892,6 +1132,7 @@ const AutoPositionedPopup = memo(
892
1132
  };
893
1133
  });
894
1134
  if (!hasAddedRootView.current && useTextInput) {
1135
+ // TextInput version: hide first, show after keyboard is fully displayed
895
1136
  hasAddedRootView.current = true;
896
1137
  hasShownRootView.current = false;
897
1138
  addRootView({
@@ -919,6 +1160,7 @@ const AutoPositionedPopup = memo(
919
1160
  useModal: false,
920
1161
  });
921
1162
  }
1163
+ console.log('AutoPositionedPopup onPress done')
922
1164
  }}
923
1165
  >
924
1166
  {!btwChildren ? (
@@ -938,141 +1180,38 @@ const AutoPositionedPopup = memo(
938
1180
  )}
939
1181
  </TouchableOpacity>
940
1182
  ) : (
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
- )
1183
+ memoizedTextInput
1050
1184
  )}
1051
1185
  </View>
1052
1186
  </CustomRow>
1053
1187
  );
1054
- }, [tag,
1055
- fetchData,
1056
- renderItem,
1057
- onItemSelected,
1058
- 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
1059
1196
  localSearch,
1060
- placeholder,
1061
- textAlign,
1197
+ // placeholder, // ❌ Removed: may change
1198
+ // textAlign, // ❌ Removed: may change
1062
1199
  pageSize,
1063
1200
  selectedItem,
1064
- CustomRow,
1201
+ // CustomRow, // ❌ Removed: inline function, new reference each time
1065
1202
  useTextInput,
1066
- btwChildren,
1067
- selectedItem,
1068
- keyExtractor,
1069
- AutoPositionedPopupBtnStyle,
1070
- CustomPopView,
1071
- 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
1072
1208
  forceRemoveAllRootViewOnItemSelected,
1073
- inputStyle,
1074
- TextInputProps,
1075
- state.isFocus,showListEmptyComponent,emptyText]);
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
+ ]);
1076
1215
  }
1077
1216
  )
1078
1217
  );