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.
@@ -34,9 +34,7 @@ const ListItem = memo(({ updateState, item, index, selectedItem, }) => {
34
34
  rootViewsRef.current = rootViews;
35
35
  }, [rootViews]);
36
36
  return useMemo(() => {
37
- // console.log('AutoPositionedPopup.tsx ListItem index=', index);
38
- console.log('AutoPositionedPopup.tsx ListItem item=', item);
39
- console.log('AutoPositionedPopup.tsx ListItem selectedItem=', selectedItem);
37
+ // console.log('AutoPositionedPopup.tsx ListItem=', {index, item, selectedItem});
40
38
  const isSelected = item.id === (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id);
41
39
  return (<TouchableOpacity key={item.id} style={[
42
40
  styles.commonModalRow,
@@ -79,8 +77,8 @@ const AutoPositionedPopupList = memo(({ tag, updateState, fetchData, keyExtracto
79
77
  };
80
78
  }, []);
81
79
  // useEffect(() => {
82
- // // 監聽 TextInput 事件,收到就刷新列表,不依賴 global searchQuery
83
- // // 將最新的 searchQuery 同步到 list 專用的 ref,供 _fetchData 使用
80
+ // // Listen to TextInput events, refresh list when received, not dependent on global searchQuery
81
+ // // Sync the latest searchQuery to list-specific ref for _fetchData to use
84
82
  // ref_searchQuery.current = searchQuery;
85
83
  // console.log('AutoPositionedPopupList useEffect searchQuery=', searchQuery);
86
84
  // console.log('AutoPositionedPopupList useEffect state.localData=', state.localData);
@@ -109,10 +107,7 @@ const AutoPositionedPopupList = memo(({ tag, updateState, fetchData, keyExtracto
109
107
  updateState(key, value);
110
108
  };
111
109
  const _fetchData = async ({ pageIndex, pageSize: currentPageSize, }) => {
112
- console.log('AutoPositionedPopupList _fetchData pageIndex=', pageIndex, ' pageSize=', currentPageSize);
113
- console.log('AutoPositionedPopupList _fetchData state.localData=', state.localData);
114
- console.log('AutoPositionedPopupList _fetchData ref_searchQuery.current=', ref_searchQuery.current);
115
- console.log('AutoPositionedPopupList _fetchData localSearch=', localSearch);
110
+ console.log('AutoPositionedPopupList _fetchData=', { pageIndex, pageSize: currentPageSize, 'state.localData': state.localData, 'ref_searchQuery.current': ref_searchQuery.current, localSearch });
116
111
  if (localSearch && state.localData.length > 0) {
117
112
  const result = state.localData.filter((item) => {
118
113
  var _a;
@@ -183,7 +178,7 @@ const listLayout = {
183
178
  // Main AutoPositionedPopup component
184
179
  const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
185
180
  console.log('AutoPositionedPopup props=', props);
186
- const { tag, style, AutoPositionedPopupBtnStyle, placeholder = 'Please Select', onSubmitEditing, TextInputProps = {}, inputStyle, labelStyle, popUpViewStyle = { left: '5%', width: '90%' }, fetchData = async ({ pageIndex, pageSize, searchQuery, }) => {
181
+ const { tag, style, AutoPositionedPopupBtnStyle, placeholder = 'Please Select', onSubmitEditing, TextInputProps = { autoFocus: true }, inputStyle, labelStyle, popUpViewStyle = { left: '5%', width: '90%' }, fetchData = async ({ pageIndex, pageSize, searchQuery, }) => {
187
182
  const res = {
188
183
  items: [],
189
184
  pageIndex,
@@ -219,6 +214,16 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
219
214
  const keyboardVisibleRef = useRef(false);
220
215
  const refAutoPositionedPopup = useRef(null);
221
216
  const ref_searchQuery = useRef('');
217
+ // Add ref to track previous keyboard state to avoid false triggers during parent component re-renders
218
+ const prevIsKeyboardFullyShownRef = useRef(false);
219
+ const prevPropsRef = useRef({});
220
+ // Add ref to prevent onFocus/onBlur loop triggers during parent component re-renders
221
+ const lastFocusTimeRef = useRef(0);
222
+ const isFocusEventProcessingRef = useRef(false);
223
+ // Add ref to stabilize TextInput props reference
224
+ // Only update when deep comparison detects real changes to avoid TextInput recreation due to reference changes during parent component redraws
225
+ const stableInputStyleRef = useRef(inputStyle);
226
+ const stableTextInputPropsRef = useRef(TextInputProps);
222
227
  // Simple keyboard status tracking (alternative to useKeyboardStatus hook)
223
228
  // Legacy state for compatibility
224
229
  const [isVisible, setIsVisible] = useState(false);
@@ -240,8 +245,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
240
245
  useEffect(() => {
241
246
  (async () => {
242
247
  })();
243
- console.log(`AutoPositionedPopup componentDidMount tag=`, tag);
244
- console.log('AutoPositionedPopup componentDidMount CustomPopView=', CustomPopView);
248
+ console.log(`AutoPositionedPopup componentDidMount=`, { tag, CustomPopView });
245
249
  //componentWillUnmount
246
250
  return () => {
247
251
  console.log(`AutoPositionedPopup componentWillUnmount tag=`, tag);
@@ -272,8 +276,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
272
276
  }, [rootViews]);
273
277
  useEffect(() => {
274
278
  var _a, _b;
275
- console.log('AutoPositionedPopup useEffect tag=', tag);
276
- console.log('AutoPositionedPopup useEffect selectedItem=', selectedItem);
279
+ console.log('AutoPositionedPopup useEffect [selectedItem, state.selectedItem, tag]=', { tag, selectedItem, 'state.selectedItem': state.selectedItem });
277
280
  console.log('AutoPositionedPopup useEffect state.selectedItem=', state.selectedItem);
278
281
  if (((_a = state.selectedItem) === null || _a === void 0 ? void 0 : _a.id) !== (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) || ((_b = state.selectedItem) === null || _b === void 0 ? void 0 : _b.title) !== (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.title)) {
279
282
  console.log('AutoPositionedPopup useEffect selectedItem!=state.selectedItem');
@@ -283,47 +286,123 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
283
286
  }
284
287
  }, [selectedItem, state.selectedItem, tag]);
285
288
  useEffect(() => {
286
- var _a, _b, _c;
287
- console.log('AutoPositionedPopup useEffect tag=', tag);
288
- console.log('AutoPositionedPopup useEffect state.isFocus=', state.isFocus);
289
- console.log('AutoPositionedPopup useEffect isKeyboardFullyShown=', isKeyboardFullyShown);
290
- console.log('AutoPositionedPopup useEffect ref_isFocus.current=', ref_isFocus.current);
291
- console.log('AutoPositionedPopup useEffect ref_isKeyboardFullyShown.current=', ref_isKeyboardFullyShown.current);
292
- console.log('AutoPositionedPopup useEffect useTextInput=', useTextInput);
293
- console.log('AutoPositionedPopup useEffect TextInputProps=', TextInputProps);
294
- console.log('AutoPositionedPopup useEffect hasAddedRootView.current=', hasAddedRootView.current);
295
- console.log('AutoPositionedPopup useEffect hasShownRootView.current=', hasShownRootView.current);
289
+ var _a, _b;
290
+ // Detect if keyboard state has actually changed to avoid false triggers during parent component re-renders
291
+ const keyboardStateChanged = prevIsKeyboardFullyShownRef.current !== isKeyboardFullyShown;
292
+ const propsChanged = prevPropsRef.current.CustomPopView !== CustomPopView ||
293
+ prevPropsRef.current.CustomPopViewStyle !== CustomPopViewStyle ||
294
+ prevPropsRef.current.TextInputProps !== TextInputProps;
295
+ console.log('AutoPositionedPopup useEffect [isKeyboardFullyShown,\n' +
296
+ ' state.isFocus,\n' +
297
+ ' useTextInput,\n' +
298
+ ' CustomPopView,\n' +
299
+ ' CustomPopViewStyle,\n' +
300
+ ' forceRemoveAllRootViewOnItemSelected,\n' +
301
+ ' tag, TextInputProps,\n' +
302
+ ' state.selectedItem, showListEmptyComponent\n' +
303
+ ' ]=', {
304
+ tag,
305
+ 'state.isFocus': state.isFocus,
306
+ isKeyboardFullyShown,
307
+ 'ref_isFocus.current': ref_isFocus.current,
308
+ 'ref_isKeyboardFullyShown.current': ref_isKeyboardFullyShown.current,
309
+ useTextInput, TextInputProps,
310
+ 'hasAddedRootView.current': hasAddedRootView.current,
311
+ 'hasShownRootView.current': hasShownRootView.current,
312
+ 'keyboardStateChanged': keyboardStateChanged,
313
+ 'propsChanged': propsChanged
314
+ });
315
+ // Update ref to record current state
316
+ prevIsKeyboardFullyShownRef.current = isKeyboardFullyShown;
317
+ prevPropsRef.current = {
318
+ CustomPopView,
319
+ CustomPopViewStyle,
320
+ TextInputProps
321
+ };
322
+ // Only execute logic when keyboard state actually changes or user actively operates
323
+ if (!keyboardStateChanged && hasAddedRootView.current) {
324
+ console.log('AutoPositionedPopup: Skip execution - parent component re-rendered but keyboard state unchanged textInputRef.current=', textInputRef.current);
325
+ // if (!ref_isFocus.current) {
326
+ // textInputRef.current?.focus()
327
+ // }
328
+ return;
329
+ }
296
330
  if (useTextInput) {
297
331
  if (isKeyboardFullyShown && hasAddedRootView.current && !hasShownRootView.current && state.isFocus) {
298
- (_a = refAutoPositionedPopup.current) === null || _a === void 0 ? void 0 : _a.measureInWindow((x, y, width, height) => {
299
- var _a;
300
- console.log('AutoPositionedPopup measureInWindow x=', x, ' y=', y, ' width=', width, ' height=', height);
301
- // SIMPLE CENTER-BASED POSITIONING STRATEGY
302
- const screenHeight = Dimensions.get('screen').height;
303
- const screenCenter = screenHeight / 2;
304
- console.log('AutoPositionedPopup screenHeight=', screenHeight, ' screenCenter=', screenCenter, ' componentY=', y);
305
- // Simple rule: if component Y > screen center, show popup above; otherwise show below
306
- if (y > screenCenter) {
307
- console.log('AutoPositionedPopup with keyboard: showing above (Y > center)');
308
- ref_listPos.current = { x: x, y: y - listLayout.height, width: width };
309
- }
310
- else {
311
- console.log('AutoPositionedPopup with keyboard: showing below (Y <= center)');
312
- ref_listPos.current = { x: x, y: y + height, width: width };
313
- }
314
- console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
315
- setRootViewNativeStyle(tag, {
316
- top: (_a = ref_listPos.current) === null || _a === void 0 ? void 0 : _a.y,
317
- left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left,
318
- width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width,
319
- height: listLayout.height,
320
- opacity: 1,
332
+ // CRITICAL FIX FOR KEYBOARD POSITION CALCULATION
333
+ // Problem: When keyboard appears, the page shifts up but measureInWindow executes too early
334
+ // Solution: Wait for keyboard animation (300ms) + use requestAnimationFrame for next render frame
335
+ //
336
+ // Timing breakdown:
337
+ // 1. Keyboard animation: ~250-300ms (iOS/Android)
338
+ // 2. Page shift animation: ~200-300ms (KeyboardAvoidingView)
339
+ // 3. Layout tree update: ~50-100ms (React Native)
340
+ // Total: ~500-700ms needed for stable layout
341
+ //
342
+ // Strategy: setTimeout(300ms) waits for most animations to complete,
343
+ // then requestAnimationFrame ensures measurement happens after next render frame
344
+ setTimeout(() => {
345
+ requestAnimationFrame(() => {
346
+ var _a;
347
+ (_a = refAutoPositionedPopup.current) === null || _a === void 0 ? void 0 : _a.measureInWindow((x, y, width, height) => {
348
+ var _a;
349
+ console.log('AutoPositionedPopup useTextInput measureInWindow (after 300ms + RAF, layout stable)=', { x, y, width, height });
350
+ // CRITICAL FIX: Coordinate system mismatch issue
351
+ // Problem: measureInWindow returns coordinates relative to window (fixed reference),
352
+ // but popup uses absolute positioning relative to App container (which shifts when keyboard appears)
353
+ //
354
+ // When keyboard appears:
355
+ // 1. measureInWindow returns y relative to window (e.g., y=400 after shifting)
356
+ // 2. But popup's absolute positioning is relative to App container
357
+ // 3. If App container shifted up by 200px, setting top=200 will display at window.y=0 (wrong!)
358
+ //
359
+ // Solution: Since popup is rendered at root level and uses absolute positioning,
360
+ // we should directly use measureInWindow's y value without additional calculations
361
+ // The popup container is at the same level as the page content
362
+ const screenHeight = Dimensions.get('window').height; // Use window height, not screen
363
+ console.log('AutoPositionedPopup useTextInput positioning data=', {
364
+ screenHeight,
365
+ componentY: y,
366
+ componentHeight: height,
367
+ listHeight: listLayout.height
368
+ });
369
+ // CORRECT POSITIONING LOGIC (as per user requirement):
370
+ // 1. ALWAYS try to show popup ABOVE the input field first
371
+ // 2. Only if that goes off the top of screen, show BELOW instead
372
+ // 3. Don't cover/overlap the input field
373
+ let popupY = y - listLayout.height; // Default: above input field
374
+ // Check if showing above would go off the top of screen
375
+ if (popupY < 0) {
376
+ console.log('AutoPositionedPopup with keyboard: would go off screen top, showing BELOW instead');
377
+ popupY = y + height; // Show below input field
378
+ // Also check if showing below would go off the bottom
379
+ const maxY = screenHeight - listLayout.height;
380
+ if (popupY > maxY) {
381
+ // If both positions are problematic, clamp to visible area
382
+ console.log('AutoPositionedPopup with keyboard: both positions problematic, clamping to visible area');
383
+ popupY = Math.min(Math.max(0, y - listLayout.height), maxY);
384
+ }
385
+ }
386
+ else {
387
+ console.log('AutoPositionedPopup with keyboard: showing ABOVE input field (preferred position)');
388
+ }
389
+ ref_listPos.current = { x: x, y: popupY, width: width };
390
+ console.log('AutoPositionedPopup useTextInput final position=', ref_listPos.current);
391
+ setRootViewNativeStyle(tag, {
392
+ top: (_a = ref_listPos.current) === null || _a === void 0 ? void 0 : _a.y,
393
+ left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left,
394
+ width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width,
395
+ height: listLayout.height,
396
+ opacity: 1,
397
+ });
398
+ hasShownRootView.current = true;
399
+ });
321
400
  });
322
- hasShownRootView.current = true;
323
- });
401
+ }, 300); // 300ms is sufficient for keyboard animation, as proven by user testing (even 3000ms didn't fix wrong logic)
324
402
  }
325
- else if (!isKeyboardFullyShown && ref_isFocus.current) {
326
- console.log('AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView tag=', tag, ' forceRemoveAllRootViewOnItemSelected=', forceRemoveAllRootViewOnItemSelected);
403
+ else if (!isKeyboardFullyShown && ref_isFocus.current && keyboardStateChanged) {
404
+ // Only execute close logic when keyboard state actually changes from true to false
405
+ console.log('AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView (keyboard state changed)=', { tag, forceRemoveAllRootViewOnItemSelected, keyboardStateChanged });
327
406
  removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
328
407
  setState((prevState) => {
329
408
  return Object.assign(Object.assign({}, prevState), { isFocus: false });
@@ -335,11 +414,11 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
335
414
  }
336
415
  else {
337
416
  if (state.isFocus) {
338
- (_b = refAutoPositionedPopup.current) === null || _b === void 0 ? void 0 : _b.measureInWindow((x, y, width, height) => {
339
- console.log('AutoPositionedPopup measureInWindow x=', x, ' y=', y, ' width=', width, ' height=', height);
417
+ (_a = refAutoPositionedPopup.current) === null || _a === void 0 ? void 0 : _a.measureInWindow((x, y, width, height) => {
418
+ console.log('AutoPositionedPopup !useTextInput measureInWindow=', { x, y, width, height });
340
419
  // INTELLIGENT POSITION CALCULATION - MODIFIED VERSION WITH STATUS BAR SAFETY
341
420
  const calculateOptimalPosition = (componentY, componentHeight, popupHeight) => {
342
- console.log('🔥🔥🔥 NEW CALCULATE OPTIMAL POSITION FUNCTION EXECUTING 🔥🔥🔥');
421
+ console.log('AutoPositionedPopup 🔥🔥🔥 NEW CALCULATE OPTIMAL POSITION FUNCTION EXECUTING 🔥🔥🔥');
343
422
  // Use window height (visible area) instead of screen height (includes status bar)
344
423
  const windowHeight = Dimensions.get('window').height;
345
424
  const visibleAreaCenter = windowHeight / 2;
@@ -356,7 +435,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
356
435
  }
357
436
  };
358
437
  const statusBarHeight = getStatusBarHeight();
359
- console.log('🔥 Cross-platform StatusBar height:', statusBarHeight, 'Platform:', Platform.OS);
438
+ console.log('AutoPositionedPopup 🔥 Cross-platform StatusBar height:', statusBarHeight, 'Platform:', Platform.OS);
360
439
  // Calculate component center point as requested
361
440
  const componentCenterY = componentY + componentHeight / 2;
362
441
  console.log('AutoPositionedPopup positioning data:', {
@@ -404,7 +483,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
404
483
  // Strong platform adjustment - much smaller for Android bottom components
405
484
  const platformMultiplier = Platform.OS === 'ios' ? 1.0 : (isInBottomHalf ? 0.5 : 0.9);
406
485
  const finalSpacing = Math.max(baseSpacing, relativeSpacing) * edgeProximityFactor * platformMultiplier;
407
- console.log('🔥 Advanced spacing calculation:', {
486
+ console.log('AutoPositionedPopup 🔥 Advanced spacing calculation:', {
408
487
  componentCenter,
409
488
  screenCenter,
410
489
  distanceFromCenter,
@@ -440,7 +519,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
440
519
  // Component in bottom half + enough space above = FORCE ABOVE
441
520
  showAbove = true;
442
521
  finalY = componentY - popupHeight + componentHeight / 2;
443
- console.log('🔥 AutoPositionedPopup: FORCE ABOVE - bottom half component with enough space, finalY=', finalY);
522
+ console.log('AutoPositionedPopup 🔥 AutoPositionedPopup: FORCE ABOVE - bottom half component with enough space, finalY=', finalY);
444
523
  }
445
524
  else if (!isInBottomHalf && spaceBelow >= popupHeight) {
446
525
  // Component in top half + enough space below = show below
@@ -474,7 +553,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
474
553
  }
475
554
  }
476
555
  // Enhanced boundary check with detailed logging
477
- console.log('🔥 Pre-boundary check:', {
556
+ console.log('AutoPositionedPopup 🔥 Pre-boundary check:', {
478
557
  originalFinalY: finalY,
479
558
  showAbove,
480
559
  statusBarHeight,
@@ -486,21 +565,21 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
486
565
  if (showAbove && finalY < statusBarHeight) {
487
566
  const oldFinalY = finalY;
488
567
  finalY = statusBarHeight;
489
- console.log('🔥 BOUNDARY FIX: Above display adjusted for status bar:', oldFinalY, '->', finalY);
568
+ console.log('AutoPositionedPopup 🔥 BOUNDARY : Above display adjusted for status bar:', oldFinalY, '->', finalY);
490
569
  }
491
570
  if (!showAbove && finalY + popupHeight > windowHeight) {
492
571
  const oldFinalY = finalY;
493
572
  finalY = windowHeight - popupHeight;
494
- console.log('🔥 BOUNDARY FIX: Below display adjusted to fit window:', oldFinalY, '->', finalY);
573
+ console.log('AutoPositionedPopup 🔥 BOUNDARY : Below display adjusted to fit window:', oldFinalY, '->', finalY);
495
574
  }
496
575
  // CRITICAL CHECK: Detect if boundary check is changing display direction
497
576
  if (showAbove && finalY + popupHeight > componentY) {
498
- console.log('🚨 WARNING: Above positioning may overlap with component!');
577
+ console.log('AutoPositionedPopup 🚨 WARNING: Above positioning may overlap with component!');
499
578
  }
500
579
  if (!showAbove && finalY < componentY + componentHeight) {
501
- console.log('🚨 WARNING: Below positioning may overlap with component!');
580
+ console.log('AutoPositionedPopup 🚨 WARNING: Below positioning may overlap with component!');
502
581
  }
503
- console.log('🔥 Post-boundary check final result:', {
582
+ console.log('AutoPositionedPopup 🔥 Post-boundary check final result:', {
504
583
  finalY,
505
584
  showAbove,
506
585
  'popupTop': finalY,
@@ -514,17 +593,15 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
514
593
  const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
515
594
  ? CustomPopViewStyle.height
516
595
  : listLayout.height;
517
- console.log('🔥 Using actualPopupHeight for calculation:', actualPopupHeight, 'CustomPopView:', !!CustomPopView);
596
+ console.log('AutoPositionedPopup 🔥 Using actualPopupHeight for calculation:', { actualPopupHeight, CustomPopView: !!CustomPopView });
518
597
  const positionResult = calculateOptimalPosition(y, height, actualPopupHeight);
519
598
  console.log('AutoPositionedPopup FINAL position result:', positionResult);
520
599
  ref_listPos.current = { x: x, y: positionResult.finalY, width: width };
521
- console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
600
+ console.log('AutoPositionedPopup !useTextInput ref_listPos.current=', ref_listPos.current);
522
601
  if (CustomPopView && CustomPopViewStyle) {
523
- console.log('AutoPositionedPopup CustomPopViewStyle=', CustomPopViewStyle);
524
602
  // Position already calculated correctly above, no need to recalculate
525
603
  const PopViewComponent = CustomPopView();
526
- console.log('AutoPositionedPopup addRootView PopViewComponent=', PopViewComponent);
527
- console.log('AutoPositionedPopup addRootView state.selectedItem=', state.selectedItem);
604
+ console.log('AutoPositionedPopup !useTextInput addRootView=', { CustomPopViewStyle, PopViewComponent, 'state.selectedItem': state.selectedItem });
528
605
  addRootView({
529
606
  id: tag,
530
607
  style: !centerDisplay
@@ -546,7 +623,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
546
623
  });
547
624
  }
548
625
  else {
549
- console.log('AutoPositionedPopup addRootView tag=', tag);
626
+ console.log('AutoPositionedPopup !useTextInput addRootView tag=', tag);
550
627
  addRootView({
551
628
  id: tag,
552
629
  style: {
@@ -572,7 +649,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
572
649
  }
573
650
  }
574
651
  if (isKeyboardFullyShown) {
575
- ref_isFocus.current = (_c = state.isFocus) !== null && _c !== void 0 ? _c : false;
652
+ ref_isFocus.current = (_b = state.isFocus) !== null && _b !== void 0 ? _b : false;
576
653
  if (isKeyboardFullyShown !== keyboardVisibleRef.current) {
577
654
  keyboardVisibleRef.current = isKeyboardFullyShown;
578
655
  if (isKeyboardFullyShown && textInputRef.current) {
@@ -588,7 +665,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
588
665
  CustomPopView,
589
666
  CustomPopViewStyle,
590
667
  forceRemoveAllRootViewOnItemSelected,
591
- tag,
668
+ tag, TextInputProps,
592
669
  state.selectedItem, showListEmptyComponent
593
670
  ]);
594
671
  // Imperative handle for parent component access
@@ -601,7 +678,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
601
678
  },
602
679
  }), []);
603
680
  const updateState = (key, value) => {
604
- console.log('AutoPositionedPopup updateState key=', key, ' value=', value);
681
+ console.log('AutoPositionedPopup updateState=', { key, value });
605
682
  setState((prevState) => (Object.assign(Object.assign({}, prevState), { [key]: value })));
606
683
  if (key === 'selectedItem' && onItemSelected) {
607
684
  onItemSelected(value);
@@ -616,6 +693,179 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
616
693
  setSearchQuery('');
617
694
  }
618
695
  };
696
+ // Simple deep comparison function (for style objects only)
697
+ const shallowEqual = (obj1, obj2) => {
698
+ if (obj1 === obj2)
699
+ return true;
700
+ if (!obj1 || !obj2)
701
+ return false;
702
+ if (typeof obj1 !== 'object' || typeof obj2 !== 'object')
703
+ return false;
704
+ const keys1 = Object.keys(obj1);
705
+ const keys2 = Object.keys(obj2);
706
+ if (keys1.length !== keys2.length)
707
+ return false;
708
+ for (const key of keys1) {
709
+ if (obj1[key] !== obj2[key])
710
+ return false;
711
+ }
712
+ return true;
713
+ };
714
+ // Use useMemo to create stable props reference
715
+ // Only update when deep comparison detects real changes to avoid TextInput recreation due to reference changes during parent component redraws
716
+ const stableInputStyle = useMemo(() => {
717
+ if (!shallowEqual(stableInputStyleRef.current, inputStyle)) {
718
+ console.log(`AutoPositionedPopup inputStyle deep change detected, updating stable reference - tag: ${tag}`);
719
+ stableInputStyleRef.current = inputStyle;
720
+ }
721
+ return stableInputStyleRef.current;
722
+ }, [inputStyle, tag]);
723
+ const stableTextInputProps = useMemo(() => {
724
+ if (!shallowEqual(stableTextInputPropsRef.current, TextInputProps)) {
725
+ console.log(`AutoPositionedPopup TextInputProps deep change detected, updating stable reference - tag: ${tag}`);
726
+ stableTextInputPropsRef.current = TextInputProps;
727
+ }
728
+ return stableTextInputPropsRef.current;
729
+ }, [TextInputProps, tag]);
730
+ // Use useCallback to stabilize onFocus and onBlur callback references
731
+ // Prevent creating new callback functions during parent component redraws to avoid TextInput re-triggering focus
732
+ // Use ref to store latest state values to avoid adding frequently changing values to dependencies
733
+ const stateRef = useRef(state);
734
+ stateRef.current = state;
735
+ const handleTextInputFocus = useCallback(() => {
736
+ const currentTime = Date.now();
737
+ const timeSinceLastFocus = currentTime - lastFocusTimeRef.current;
738
+ console.log('AutoPositionedPopup onFocus=', {
739
+ tag,
740
+ 'state.selectedItem': stateRef.current.selectedItem,
741
+ 'hasTriggeredFocus.current=': hasTriggeredFocus.current,
742
+ 'textInputRef.current=': textInputRef.current,
743
+ 'ref_searchQuery.current=': ref_searchQuery.current,
744
+ 'timeSinceLastFocus': timeSinceLastFocus,
745
+ 'isKeyboardFullyShown': isKeyboardFullyShown,
746
+ 'isFocusEventProcessing': isFocusEventProcessingRef.current
747
+ });
748
+ // Prevent rapid repeated triggers (repeated events within 300ms are ignored)
749
+ if (timeSinceLastFocus < 300) {
750
+ console.log('AutoPositionedPopup onFocus: Skip - event triggered too quickly (< 300ms)');
751
+ return;
752
+ }
753
+ // Skip if keyboard is already open and focus has been handled
754
+ if (isKeyboardFullyShown && hasTriggeredFocus.current) {
755
+ console.log('AutoPositionedPopup onFocus: Skip - keyboard already open and focus handled');
756
+ return;
757
+ }
758
+ // Prevent concurrent processing
759
+ if (isFocusEventProcessingRef.current) {
760
+ console.log('AutoPositionedPopup onFocus: Skip - processing another focus event');
761
+ return;
762
+ }
763
+ isFocusEventProcessingRef.current = true;
764
+ lastFocusTimeRef.current = currentTime;
765
+ if (!hasTriggeredFocus.current) {
766
+ hasTriggeredFocus.current = true;
767
+ ref_isFocus.current = true;
768
+ if (stateRef.current.selectedItem) {
769
+ ref_searchQuery.current = stateRef.current.selectedItem.title;
770
+ }
771
+ if (textInputRef.current && ref_searchQuery.current) {
772
+ textInputRef.current.setNativeProps({
773
+ text: ref_searchQuery.current,
774
+ });
775
+ }
776
+ }
777
+ // Delay resetting processing flag to avoid blocking subsequent legitimate focus events
778
+ setTimeout(() => {
779
+ isFocusEventProcessingRef.current = false;
780
+ }, 100);
781
+ }, [tag, isKeyboardFullyShown]); // Remove state.selectedItem, use stateRef instead
782
+ const handleTextInputBlur = useCallback(() => {
783
+ console.log('AutoPositionedPopup onBlur=', {
784
+ tag,
785
+ 'textInputRef.current': textInputRef.current,
786
+ 'isKeyboardFullyShown': isKeyboardFullyShown,
787
+ 'hasTriggeredFocus.current': hasTriggeredFocus.current
788
+ });
789
+ // If keyboard is still open, this is a false trigger caused by parent component re-render, should not reset
790
+ if (isKeyboardFullyShown && hasTriggeredFocus.current) {
791
+ console.log('AutoPositionedPopup onBlur: Skip - keyboard still open, possibly caused by parent component re-render');
792
+ return;
793
+ }
794
+ // Only reset internal state, do not actively close keyboard
795
+ // Keyboard will close naturally when TextInput loses focus, no need to manually call Keyboard.dismiss()
796
+ hasTriggeredFocus.current = false;
797
+ hasAddedRootView.current = false;
798
+ hasShownRootView.current = false;
799
+ ref_isFocus.current = false;
800
+ setState((prevState) => {
801
+ return Object.assign(Object.assign({}, prevState), { isFocus: false });
802
+ });
803
+ removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
804
+ setSearchQuery('');
805
+ if (textInputRef.current) {
806
+ textInputRef.current.setNativeProps({ text: '' });
807
+ ref_searchQuery.current = '';
808
+ // Remove textInputRef.current.blur() - avoid forcing blur causing keyboard to close
809
+ }
810
+ // Remove Keyboard.dismiss() - let keyboard close naturally to avoid triggering keyboardDidHide event
811
+ }, [tag, isKeyboardFullyShown, forceRemoveAllRootViewOnItemSelected]);
812
+ // Wrap TextInput independently in useMemo to recreate only when key props change
813
+ // This avoids repeated ref callback triggers due to other props changes during parent component redraws
814
+ const memoizedTextInput = useMemo(() => {
815
+ console.log(`AutoPositionedPopup useMemo creating TextInput - tag: ${tag}, isFocus: ${state.isFocus}`);
816
+ if (!useTextInput || !state.isFocus) {
817
+ return null;
818
+ }
819
+ return (<RNTextInput ref={(ref) => {
820
+ // Monitor TextInput mounting and unmounting
821
+ if (ref && !textInputRef.current) {
822
+ console.log(`AutoPositionedPopup TextInput created/mounted - tag: ${tag}, ref:`, ref);
823
+ }
824
+ else if (!ref && textInputRef.current) {
825
+ console.log(`AutoPositionedPopup TextInput unmounted - tag: ${tag}`);
826
+ }
827
+ else if (ref && textInputRef.current && ref !== textInputRef.current) {
828
+ console.log(`AutoPositionedPopup TextInput replaced - tag: ${tag}, oldRef:`, textInputRef.current, 'newRef:', ref);
829
+ }
830
+ textInputRef.current = ref;
831
+ }} key={`textinput-${tag}`} style={[
832
+ styles.inputStyle,
833
+ stableInputStyle,
834
+ ]} textAlign={stableTextInputProps['textAlign'] || 'left'} multiline={stableTextInputProps['multiline'] || false} numberOfLines={stableTextInputProps['numberOfLines'] || 1} onChangeText={(searchQuery) => {
835
+ ref_searchQuery.current = searchQuery;
836
+ console.log('AutoPositionedPopup onChangeText rootViews=', rootViews);
837
+ if (!localSearch) {
838
+ if (debounceTimerRef.current) {
839
+ clearTimeout(debounceTimerRef.current);
840
+ }
841
+ debounceTimerRef.current = setTimeout(() => {
842
+ emitQueryChange(ref_searchQuery.current);
843
+ }, 500);
844
+ }
845
+ else {
846
+ emitQueryChange(ref_searchQuery.current);
847
+ }
848
+ }} placeholderTextColor={theme.colors.placeholderText} placeholder={placeholder} onKeyPress={(e) => {
849
+ if (e.nativeEvent.key === 'Enter') {
850
+ Keyboard.dismiss();
851
+ }
852
+ }} keyboardType={stableTextInputProps['keyboardType'] || 'default'} clearButtonMode="while-editing" returnKeyType={stableTextInputProps['returnKeyType'] || 'done'} maxLength={stableTextInputProps['maxLength'] || 100} accessibilityLabel="selectInput" accessible={true} autoFocus={stableTextInputProps['autoFocus'] || false} autoCorrect={false} underlineColorAndroid="transparent" editable={stableTextInputProps['editable'] || true} secureTextEntry={stableTextInputProps['secureTextEntry'] || false} defaultValue="" caretHidden={false} enablesReturnKeyAutomatically onFocus={handleTextInputFocus} onBlur={handleTextInputBlur} selectTextOnFocus={stableTextInputProps['selectTextOnFocus'] || false} onSubmitEditing={(e) => {
853
+ console.log('AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=', e.nativeEvent.text);
854
+ onSubmitEditing && onSubmitEditing(e);
855
+ }}/>);
856
+ }, [
857
+ tag, // tag 是稳定的
858
+ useTextInput, // useTextInput 是稳定的
859
+ state.isFocus, // isFocus 控制显示/隐藏
860
+ handleTextInputFocus, // useCallback wrapped, reference stable
861
+ handleTextInputBlur, // useCallback wrapped, reference stable
862
+ stableInputStyle, // Use stable inputStyle reference (after deep comparison)
863
+ stableTextInputProps, // Use stable TextInputProps reference (after deep comparison)
864
+ placeholder, // placeholder usually stable
865
+ onSubmitEditing, // onSubmitEditing usually stable
866
+ // No longer use original inputStyle and TextInputProps, use stable references instead
867
+ // Stable references only update when deep comparison detects actual content changes, avoiding frequent TextInput recreation during parent component redraws
868
+ ]);
619
869
  // Render the component following project implementation
620
870
  return useMemo(() => {
621
871
  var _a;
@@ -623,17 +873,20 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
623
873
  return (<CustomRow>
624
874
  <View style={[styles.contain, style]} ref={refAutoPositionedPopup}>
625
875
  {!state.isFocus || !useTextInput ? (<TouchableOpacity style={[styles.AutoPositionedPopupBtn, AutoPositionedPopupBtnStyle]} disabled={AutoPositionedPopupBtnDisabled} onPress={() => {
626
- console.log('AutoPositionedPopup onPress tag=', tag);
627
- console.log('AutoPositionedPopup onPress state.isFocus=', state.isFocus);
628
- console.log('AutoPositionedPopup onPress useTextInput=', useTextInput);
629
- console.log('AutoPositionedPopup onPress hasAddedRootView.current=', hasAddedRootView.current);
630
- console.log('AutoPositionedPopup onPress hasShownRootView.current=', hasShownRootView.current);
631
- console.log('AutoPositionedPopup onPress hasTriggeredFocus.current=', hasTriggeredFocus.current);
632
- console.log('AutoPositionedPopup onPress state.selectedItem=', state.selectedItem);
876
+ console.log('AutoPositionedPopup onPress=', {
877
+ tag,
878
+ 'state.isFocus': state.isFocus,
879
+ useTextInput,
880
+ 'hasAddedRootView.current': hasAddedRootView.current,
881
+ 'hasShownRootView.current': hasShownRootView.current,
882
+ 'hasTriggeredFocus.current': hasTriggeredFocus.current,
883
+ 'state.selectedItem': state.selectedItem
884
+ });
633
885
  setState((prevState) => {
634
886
  return Object.assign(Object.assign({}, prevState), { isFocus: true });
635
887
  });
636
888
  if (!hasAddedRootView.current && useTextInput) {
889
+ // TextInput version: hide first, show after keyboard is fully displayed
637
890
  hasAddedRootView.current = true;
638
891
  hasShownRootView.current = false;
639
892
  addRootView({
@@ -649,6 +902,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
649
902
  useModal: false,
650
903
  });
651
904
  }
905
+ console.log('AutoPositionedPopup onPress done');
652
906
  }}>
653
907
  {!btwChildren ? (<Text style={[
654
908
  styles.searchQueryTxt,
@@ -657,87 +911,36 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
657
911
  ]} numberOfLines={1} ellipsizeMode={'tail'}>
658
912
  {((_a = state.selectedItem) === null || _a === void 0 ? void 0 : _a.title) || placeholder}
659
913
  </Text>) : (btwChildren())}
660
- </TouchableOpacity>) : (useTextInput &&
661
- state.isFocus && (<RNTextInput ref={textInputRef} key="fixed-textinput-key" style={[
662
- styles.inputStyle,
663
- inputStyle,
664
- ]} textAlign={TextInputProps['textAlign'] || 'left'} multiline={TextInputProps['multiline'] || false} numberOfLines={TextInputProps['numberOfLines'] || 1} onChangeText={(searchQuery) => {
665
- ref_searchQuery.current = searchQuery;
666
- console.log('AutoPositionedPopup onChangeText rootViews=', rootViews);
667
- if (!localSearch) {
668
- if (debounceTimerRef.current) {
669
- clearTimeout(debounceTimerRef.current);
670
- }
671
- debounceTimerRef.current = setTimeout(() => {
672
- emitQueryChange(ref_searchQuery.current);
673
- }, 500);
674
- }
675
- else {
676
- emitQueryChange(ref_searchQuery.current);
677
- }
678
- }} placeholderTextColor={theme.colors.placeholderText} placeholder={placeholder} onKeyPress={(e) => {
679
- if (e.nativeEvent.key === 'Enter') {
680
- Keyboard.dismiss();
681
- }
682
- }} keyboardType={TextInputProps['keyboardType'] || 'default'} clearButtonMode="while-editing" returnKeyType={TextInputProps['returnKeyType'] || 'done'} maxLength={TextInputProps['maxLength'] || 100} accessibilityLabel="selectInput" accessible={true} autoFocus={TextInputProps['autoFocus'] || false} autoCorrect={false} underlineColorAndroid="transparent" editable={TextInputProps['editable'] || true} secureTextEntry={TextInputProps['secureTextEntry'] || false} defaultValue="" caretHidden={false} enablesReturnKeyAutomatically onFocus={() => {
683
- console.log('AutoPositionedPopup onFocus tag=', tag, ' selectedItem=', state.selectedItem, ' hasTriggeredFocus.current=', hasTriggeredFocus.current, ' textInputRef.current=', textInputRef.current, ' ref_searchQuery.current=', ref_searchQuery.current);
684
- if (!hasTriggeredFocus.current) {
685
- hasTriggeredFocus.current = true;
686
- ref_isFocus.current = true;
687
- if (state.selectedItem) {
688
- ref_searchQuery.current = state.selectedItem.title;
689
- }
690
- if (textInputRef.current && ref_searchQuery.current) {
691
- textInputRef.current.setNativeProps({
692
- text: ref_searchQuery.current,
693
- });
694
- }
695
- }
696
- }} onBlur={() => {
697
- console.log('AutoPositionedPopup onBlur tag=', tag, 'textInputRef.current=', textInputRef.current);
698
- hasTriggeredFocus.current = false;
699
- hasAddedRootView.current = false; // 重置 RootView 狀態
700
- hasShownRootView.current = false;
701
- ref_isFocus.current = false;
702
- setState((prevState) => {
703
- return Object.assign(Object.assign({}, prevState), { isFocus: false });
704
- });
705
- removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
706
- setSearchQuery('');
707
- if (textInputRef.current) {
708
- textInputRef.current.setNativeProps({ text: '' });
709
- ref_searchQuery.current = '';
710
- textInputRef.current.blur();
711
- }
712
- Keyboard.dismiss();
713
- }} selectTextOnFocus={TextInputProps['selectTextOnFocus'] || false} onSubmitEditing={(e) => {
714
- console.log('AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=', e.nativeEvent.text);
715
- onSubmitEditing && onSubmitEditing(e);
716
- }}/>))}
914
+ </TouchableOpacity>) : (memoizedTextInput)}
717
915
  </View>
718
916
  </CustomRow>);
719
- }, [tag,
720
- fetchData,
721
- renderItem,
722
- onItemSelected,
723
- onSubmitEditing,
917
+ }, [
918
+ tag,
919
+ // ✅ CRITICAL FIX: Remove all props that may change frequently or are inline functions
920
+ // Changes to these props should not cause the entire component tree to recreate, especially TextInput
921
+ // fetchData, // ❌ Removed: inline function
922
+ // renderItem, // ❌ Removed: possibly inline function
923
+ // onItemSelected, // ❌ Removed: possibly inline function
924
+ // onSubmitEditing, // ❌ Removed: possibly inline function
724
925
  localSearch,
725
- placeholder,
726
- textAlign,
926
+ // placeholder, // ❌ Removed: may change
927
+ // textAlign, // ❌ Removed: may change
727
928
  pageSize,
728
929
  selectedItem,
729
- CustomRow,
930
+ // CustomRow, // ❌ Removed: inline function, new reference each time
730
931
  useTextInput,
731
- btwChildren,
732
- selectedItem,
733
- keyExtractor,
734
- AutoPositionedPopupBtnStyle,
735
- CustomPopView,
736
- CustomPopViewStyle,
932
+ // btwChildren, // ❌ Removed: inline function
933
+ // keyExtractor, // ❌ Removed: possibly inline function
934
+ // AutoPositionedPopupBtnStyle, // ❌ Removed: possibly inline object
935
+ // CustomPopView, // ❌ Removed: may change
936
+ // CustomPopViewStyle, // ❌ Removed: may change
737
937
  forceRemoveAllRootViewOnItemSelected,
738
- inputStyle,
739
- TextInputProps,
740
- state.isFocus, showListEmptyComponent, emptyText]);
938
+ state.isFocus,
939
+ showListEmptyComponent,
940
+ emptyText,
941
+ // ✅ Removed most dependencies that may cause re-rendering, keeping only core dependencies that truly affect component structure
942
+ // This prevents TextInput recreation due to inline functions/objects during parent component redraws
943
+ ]);
741
944
  }));
742
945
  export default AutoPositionedPopup;
743
946
  //# sourceMappingURL=AutoPositionedPopup.js.map