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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,24 @@
1
1
  import { debugLog } from './constants';
2
+ // Module load marker - unique ID for tracking code version
3
+ // V19f (2025-01-04): CORRECT direction for coordinate adjustment - ADD statusBarHeight to move popup DOWN
4
+ // Wait 1 second for KeyboardAwareScrollView to stabilize, then use measureInWindow to get trigger's FINAL position
5
+ // NOTE: Parent component (KeyboardAwareScrollView) is responsible for scrolling trigger into view
6
+ // DEBUG FLAG: Set to false to disable all console logs for better performance
7
+ const POPUP_DEBUG = false; // DISABLED: Too many logs cause app freeze
8
+ const POPUP_POSITION_DEBUG = true; // Only log positioning calculations
9
+ const debugLog = (...args) => {
10
+ if (POPUP_DEBUG) {
11
+ debugLog(...args);
12
+ }
13
+ };
14
+ // Separate logging function for position-related logs only
15
+ const positionDebugLog = (...args) => {
16
+ if (POPUP_POSITION_DEBUG) {
17
+ debugLog(...args);
18
+ }
19
+ };
20
+ // Only log module load in debug mode
21
+ positionDebugLog('POPUP_MODULE_V19f_LOADED at ' + new Date().toISOString() + ' (Parent handles scroll)');
2
22
  import React, { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
3
23
  import { Dimensions, findNodeHandle, Keyboard, Platform, StatusBar, Text, TextInput as RNTextInput, TouchableOpacity, View, } from 'react-native';
4
24
  import { AdvancedFlatList } from 'react-native-advanced-flatlist';
@@ -131,7 +151,7 @@ const AutoPositionedPopupList = memo(({ tag, updateState, fetchData, keyExtracto
131
151
  return null;
132
152
  }
133
153
  catch (e) {
134
- console.warn('Error in fetchData:', e);
154
+ debugLog('Error in fetchData:', e);
135
155
  }
136
156
  debugLog('AutoPositionedPopupList _fetchData res=', null);
137
157
  return null;
@@ -180,7 +200,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
180
200
  // res.needLoadMore = res1.length === pageSize
181
201
  }
182
202
  catch (e) {
183
- console.warn('Error in fetch operation:', e);
203
+ debugLog('Error in fetch operation:', e);
184
204
  }
185
205
  return res;
186
206
  }, renderItem, onItemSelected, localSearch = false, pageSize = 20, selectedItem, useTextInput = false, btwChildren, CustomRow = ({ children }) => <View>{children}</View>, keyExtractor = (item) => String((item === null || item === void 0 ? void 0 : item.id) || ''), AutoPositionedPopupBtnDisabled = false, forceRemoveAllRootViewOnItemSelected = false, centerDisplay = false, selectedItemBackgroundColor = 'rgba(116, 116, 128, 0.08)',
@@ -192,7 +212,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
192
212
  selectedItem: selectedItem,
193
213
  });
194
214
  // Use RootView context
195
- const { addRootView, setRootViewNativeStyle, removeRootView, rootViews, setSearchQuery } = useRootView();
215
+ const { addRootView, setRootViewNativeStyle, updateRootView, removeRootView, rootViews, setSearchQuery } = useRootView();
196
216
  const rootViewsRef = useRef(rootViews);
197
217
  // Track TextInput focus and RootView states like project implementation
198
218
  const hasTriggeredFocus = useRef(false);
@@ -206,6 +226,8 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
206
226
  const ref_searchQuery = useRef('');
207
227
  // Store trigger button position when clicked (before it's replaced by TextInput)
208
228
  const triggerPositionRef = useRef(null);
229
+ // V19: Track keyboard height for accurate popup positioning
230
+ const keyboardHeightRef = useRef(0);
209
231
  // Add ref to track previous keyboard state to avoid false triggers during parent component re-renders
210
232
  const prevIsKeyboardFullyShownRef = useRef(false);
211
233
  const prevPropsRef = useRef({});
@@ -232,11 +254,16 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
232
254
  const searchQueryRef = useRef(''); // Use ref instead of state to avoid re-renders
233
255
  // Refs to store latest values for useEffect without adding to dependency array
234
256
  const dataRef = useRef(data);
235
- const isKeyboardFullyShown = useKeyboardStatus();
257
+ // V19: useKeyboardStatus now returns { isShown, height } for accurate positioning
258
+ const keyboardStatus = useKeyboardStatus();
259
+ const isKeyboardFullyShown = keyboardStatus.isShown;
236
260
  const ref_isKeyboardFullyShown = useRef(isKeyboardFullyShown);
237
261
  useEffect(() => {
238
262
  ref_isKeyboardFullyShown.current = isKeyboardFullyShown;
239
- }, [isKeyboardFullyShown]);
263
+ // V19: Store keyboard height for popup positioning calculations
264
+ keyboardHeightRef.current = keyboardStatus.height;
265
+ positionDebugLog(`KEYBOARD_HEIGHT_UPDATE: height=${keyboardStatus.height} isShown=${isKeyboardFullyShown}`);
266
+ }, [keyboardStatus.isShown, keyboardStatus.height]);
240
267
  const theme = defaultTheme;
241
268
  /**
242
269
  * Scrolls the parent KeyboardAwareScrollView to make the trigger button visible
@@ -405,7 +432,6 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
405
432
  }
406
433
  }, [selectedItem, state.selectedItem, tag]);
407
434
  useEffect(() => {
408
- var _a;
409
435
  // Detect if keyboard state has actually changed to avoid false triggers during parent component re-renders
410
436
  const keyboardStateChanged = prevIsKeyboardFullyShownRef.current !== isKeyboardFullyShown;
411
437
  const propsChanged = prevPropsRef.current.CustomPopView !== CustomPopView ||
@@ -442,13 +468,18 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
442
468
  TextInputProps
443
469
  };
444
470
  // Only execute logic when keyboard state actually changes or user actively operates
445
- if (!keyboardStateChanged && hasAddedRootView.current) {
446
- debugLog('AutoPositionedPopup: Skip execution - parent component re-rendered but keyboard state unchanged textInputRef.current=', textInputRef.current);
447
- // if (!ref_isFocus.current) {
448
- // textInputRef.current?.focus()
449
- // }
471
+ // CRITICAL FIX: Also allow execution when popup needs initial positioning
472
+ // hasAddedRootView.current = true means popup container exists
473
+ // hasShownRootView.current = false means positioning not done yet
474
+ // We MUST allow execution when popup needs positioning, even if keyboard state unchanged
475
+ if (!keyboardStateChanged && hasAddedRootView.current && hasShownRootView.current) {
476
+ debugLog('AutoPositionedPopup: Skip execution - already positioned and keyboard state unchanged');
450
477
  return;
451
478
  }
479
+ // Log when we're allowing execution for initial positioning
480
+ if (!keyboardStateChanged && hasAddedRootView.current && !hasShownRootView.current) {
481
+ debugLog('AutoPositionedPopup: ALLOWING execution for initial positioning (popup added but not positioned yet)');
482
+ }
452
483
  const getStatusBarHeight = () => {
453
484
  if (Platform.OS === 'android') {
454
485
  // Android: Use StatusBar.currentHeight API
@@ -465,125 +496,144 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
465
496
  const statusBarHeight = getStatusBarHeight();
466
497
  if (useTextInput) {
467
498
  if (isKeyboardFullyShown && hasAddedRootView.current && !hasShownRootView.current && state.isFocus) {
468
- // KEYBOARD AVOIDANCE FIX: Scroll parent ScrollView to keep trigger visible
469
- // When keyboard appears, the trigger button may be covered. If parentScrollViewRef
470
- // is provided, scroll the parent to keep the trigger visible above the keyboard.
471
- if (parentScrollViewRef === null || parentScrollViewRef === void 0 ? void 0 : parentScrollViewRef.current) {
472
- debugLog('AutoPositionedPopup: Keyboard appeared, scrolling parent to keep trigger visible');
473
- // Use a slight delay to ensure keyboard animation has started
474
- setTimeout(() => {
475
- scrollToTriggerWithMeasure();
476
- }, 100);
499
+ // KEYBOARD AVOIDANCE FIX: Use KeyboardAwareScrollView's native scrollToFocusedInput method
500
+ // This properly scrolls to the dynamically created TextInput without causing double scrolling.
501
+ // The previous custom scrollToTriggerWithMeasure() caused over-scrolling issues.
502
+ if ((parentScrollViewRef === null || parentScrollViewRef === void 0 ? void 0 : parentScrollViewRef.current) && textInputRef.current) {
503
+ debugLog('AutoPositionedPopup: Keyboard appeared, using scrollToFocusedInput to scroll parent');
504
+ // Use KeyboardAwareScrollView's native method to scroll to the focused TextInput
505
+ // This is more reliable than custom scroll calculations
506
+ const scrollView = parentScrollViewRef.current;
507
+ if (typeof scrollView.scrollToFocusedInput === 'function') {
508
+ // findNodeHandle is needed to get the native node reference
509
+ const nodeHandle = findNodeHandle(textInputRef.current);
510
+ if (nodeHandle) {
511
+ // scrollToFocusedInput expects a ReactNode, use the TextInput ref
512
+ scrollView.scrollToFocusedInput(textInputRef.current, scrollExtraHeight);
513
+ debugLog('AutoPositionedPopup: Called scrollToFocusedInput with extraHeight=', scrollExtraHeight);
514
+ }
515
+ }
516
+ else {
517
+ debugLog('AutoPositionedPopup: scrollToFocusedInput not available, skipping scroll');
518
+ }
477
519
  }
478
520
  // CRITICAL FIX FOR KEYBOARD POSITION CALCULATION
479
521
  // Problem: When keyboard appears, the page shifts up but measureInWindow executes too early
480
- // Solution: Wait for keyboard animation (300ms) + use requestAnimationFrame for next render frame
522
+ // Solution: Wait for keyboard animation + page scroll to complete before measuring
481
523
  //
482
524
  // Timing breakdown:
483
525
  // 1. Keyboard animation: ~250-300ms (iOS/Android)
484
- // 2. Page shift animation: ~200-300ms (KeyboardAvoidingView)
485
- // 3. Layout tree update: ~50-100ms (React Native)
486
- // Total: ~500-700ms needed for stable layout
526
+ // 2. Page shift animation: ~300-500ms (KeyboardAwareScrollView)
527
+ // 3. Layout tree update: ~100-200ms (React Native)
528
+ // Total: ~700-1000ms needed for stable layout
529
+ //
530
+ // USER REQUEST (2025-01-04): Wait 1 second (1000ms) after keyboard appears
531
+ // to ensure trigger component position has fully stabilized after scroll
487
532
  //
488
- // Strategy: setTimeout(300ms) waits for most animations to complete,
533
+ // Strategy: setTimeout(1000ms) waits for all animations to complete,
489
534
  // then requestAnimationFrame ensures measurement happens after next render frame
535
+ const KEYBOARD_STABILIZATION_DELAY = 500; // 500ms as requested by user
536
+ positionDebugLog(`POPUP_WAIT: Waiting ${KEYBOARD_STABILIZATION_DELAY}ms for keyboard/scroll stabilization, tag=${tag}`);
490
537
  setTimeout(() => {
538
+ positionDebugLog(`POPUP_MEASURE_START: ${KEYBOARD_STABILIZATION_DELAY}ms elapsed, now measuring position for tag=${tag}`);
491
539
  requestAnimationFrame(() => {
492
- // CRITICAL FIX: Use triggerBtnRef (the actual TouchableOpacity) for measurement
493
- // instead of refAutoPositionedPopup (the outer View with flex:1/height:100%)
494
- const measureTarget = triggerBtnRef.current || refAutoPositionedPopup.current;
495
- measureTarget === null || measureTarget === void 0 ? void 0 : measureTarget.measureInWindow((x, y, width, height) => {
496
- var _a;
497
- debugLog('AutoPositionedPopup useTextInput measureInWindow (after 300ms + RAF, layout stable)=', { x, y, width, height, usingTriggerRef: !!triggerBtnRef.current });
498
- // CRITICAL FIX: Handle undefined values from measureInWindow
499
- // This can happen during navigation transitions or when view is not yet mounted
500
- if (x === undefined || y === undefined || width === undefined || height === undefined) {
501
- console.warn('AutoPositionedPopup useTextInput: measureInWindow returned undefined values, using fallback position');
502
- const screenHeightFallback = Dimensions.get('window').height;
503
- const screenWidthFallback = Dimensions.get('window').width;
504
- const fallbackY = (screenHeightFallback - listLayout.height) / 2;
505
- const fallbackX = screenWidthFallback * 0.05;
506
- const fallbackWidth = screenWidthFallback * 0.9;
507
- x = fallbackX;
508
- y = fallbackY;
509
- width = fallbackWidth;
510
- height = 50;
511
- }
512
- // CRITICAL FIX: Coordinate system mismatch issue
513
- // Problem: measureInWindow returns coordinates relative to window (fixed reference),
514
- // but popup uses absolute positioning relative to App container (which shifts when keyboard appears)
515
- //
516
- // When keyboard appears:
517
- // 1. measureInWindow returns y relative to window (e.g., y=400 after shifting)
518
- // 2. But popup's absolute positioning is relative to App container
519
- // 3. If App container shifted up by 200px, setting top=200 will display at window.y=0 (wrong!)
520
- //
521
- // Solution: Since popup is rendered at root level and uses absolute positioning,
522
- // we should directly use measureInWindow's y value without additional calculations
523
- // The popup container is at the same level as the page content
524
- const screenHeight = Dimensions.get('window').height; // Use window height, not screen
525
- debugLog('AutoPositionedPopup useTextInput positioning data=', {
526
- screenHeight,
527
- componentY: y,
528
- componentHeight: height,
529
- listHeight: listLayout.height
540
+ // CRITICAL FIX: Measure CURRENT position AFTER keyboard animation completes
541
+ // DO NOT use stored triggerPositionRef because keyboard may have shifted the view up
542
+ // Instead, measure the outer wrapper (refAutoPositionedPopup)
543
+ // which reflects the ACTUAL current position after keyboard shift
544
+ var _a;
545
+ // DEBUG: Log both refs to compare their positions
546
+ positionDebugLog(`POPUP_REFS: textInputRef=${!!textInputRef.current} refAutoPositionedPopup=${!!refAutoPositionedPopup.current}`);
547
+ // Measure BOTH refs for comparison
548
+ if (textInputRef.current && refAutoPositionedPopup.current) {
549
+ textInputRef.current.measureInWindow((tx, ty, tw, th) => {
550
+ debugLog('AutoPositionedPopup DEBUG: textInputRef position=', { x: tx, y: ty, width: tw, height: th });
551
+ });
552
+ refAutoPositionedPopup.current.measureInWindow((rx, ry, rw, rh) => {
553
+ debugLog('AutoPositionedPopup DEBUG: refAutoPositionedPopup position=', { x: rx, y: ry, width: rw, height: rh });
530
554
  });
531
- // FIXED POSITIONING LOGIC (with keyboard):
532
- // measureInWindow returns coordinates relative to the window (screen)
533
- // The popup uses position: 'absolute' relative to RootViewProvider
534
- // So we should NOT add statusBarHeight to the position calculation
535
- //
536
- // 1. Default: show popup ABOVE the input field
537
- // Position popup so that the trigger remains VISIBLE below the popup
538
- // Use (y + height * 0.7) as reference to compensate for measurement offset
539
- // while still leaving trigger visible (30% of trigger height exposed)
540
- let popupY = y + (height * 0.7) - listLayout.height;
541
- debugLog('AutoPositionedPopup with keyboard: initial calculation for ABOVE position:', {
542
- componentY: y,
543
- componentHeight: height,
544
- popupHeight: listLayout.height,
545
- popupY,
546
- popupBottom: popupY + listLayout.height,
547
- triggerTop: y,
548
- statusBarHeight
555
+ }
556
+ // CRITICAL FIX: Use textInputRef as primary measurement target
557
+ // refAutoPositionedPopup.measureInWindow() returns undefined values
558
+ // because the outer wrapper View uses flex:1/height:100% which makes it unmeasurable
559
+ // textInputRef reliably returns the actual position of the input field
560
+ const measureTarget = textInputRef.current || refAutoPositionedPopup.current;
561
+ if (!measureTarget) {
562
+ debugLog('AutoPositionedPopup useTextInput: no measureTarget available, using fallback');
563
+ const screenHeightFallback = Dimensions.get('window').height;
564
+ const screenWidthFallback = Dimensions.get('window').width;
565
+ const fallbackY = (screenHeightFallback - listLayout.height) / 2;
566
+ ref_listPos.current = { x: screenWidthFallback * 0.05, y: fallbackY, width: screenWidthFallback * 0.9 };
567
+ updateRootView(tag, {
568
+ style: {
569
+ top: (_a = ref_listPos.current) === null || _a === void 0 ? void 0 : _a.y,
570
+ left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left,
571
+ width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width,
572
+ height: listLayout.height,
573
+ opacity: 1,
574
+ }
549
575
  });
550
- // 2. Check if showing above would go behind status bar
551
- if (popupY < statusBarHeight) {
552
- debugLog('AutoPositionedPopup with keyboard: would go behind status bar, showing BELOW instead');
553
- // Show BELOW the input field
554
- // Since y + height represents the trigger's "reference bottom" (accounting for measurement offset),
555
- // we need to add another height to position popup BELOW the actual trigger
556
- popupY = y + height + height;
557
- debugLog('AutoPositionedPopup with keyboard: BELOW position calculated:', {
558
- formula: 'y + 2*height',
559
- y,
560
- height,
561
- popupY
576
+ hasShownRootView.current = true;
577
+ return;
578
+ }
579
+ // Determine which ref is actually being used (for logging)
580
+ const usingTextInputRef = measureTarget === textInputRef.current;
581
+ debugLog('AutoPositionedPopup useTextInput: using measureTarget=', usingTextInputRef ? 'textInputRef' : 'refAutoPositionedPopup');
582
+ // V19f: Position popup above trigger
583
+ // Parent KeyboardAwareScrollView is responsible for scrolling trigger into view
584
+ // This component only handles popup positioning relative to trigger's FINAL position
585
+ const screenHeight = Dimensions.get('window').height;
586
+ const screenWidth = Dimensions.get('window').width;
587
+ const currentKeyboardHeight = keyboardHeightRef.current;
588
+ const popupHeight = listLayout.height; // 200px
589
+ positionDebugLog(`V19f_SCREEN: height=${screenHeight} width=${screenWidth} keyboardH=${currentKeyboardHeight} statusBarH=${statusBarHeight}`);
590
+ measureTarget.measureInWindow((x, y, width, height) => {
591
+ positionDebugLog(`V19f_MEASURE: triggerX=${x} triggerY=${y} triggerW=${width} triggerH=${height}`);
592
+ // Handle undefined values
593
+ if (x === undefined || y === undefined || width === undefined || height === undefined) {
594
+ positionDebugLog('V19f: undefined values, using center fallback');
595
+ const fallbackY = (screenHeight - currentKeyboardHeight - popupHeight) / 2;
596
+ updateRootView(tag, {
597
+ style: { top: fallbackY, left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left, width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width, height: popupHeight, opacity: 1 }
562
598
  });
563
- // 3. Also check if showing below would go off the bottom
564
- const maxY = screenHeight - listLayout.height;
599
+ hasShownRootView.current = true;
600
+ return;
601
+ }
602
+ const triggerTop = y;
603
+ const triggerHeight = height;
604
+ const triggerBottom = y + height;
605
+ const keyboardTop = screenHeight - currentKeyboardHeight;
606
+ positionDebugLog(`V19f_ANALYSIS: triggerTop=${triggerTop} triggerBottom=${triggerBottom} keyboardTop=${keyboardTop}`);
607
+ // V19f: Position popup DIRECTLY above trigger
608
+ // ADD statusBarHeight to close the gap (coordinates adjustment)
609
+ let popupY = triggerTop - popupHeight + statusBarHeight;
610
+ let position = 'ABOVE';
611
+ positionDebugLog(`V19f_CALC: base=${triggerTop - popupHeight} + statusBarH=${statusBarHeight} = popupY=${popupY}`);
612
+ // Safety check: ensure popup doesn't go above screen top
613
+ if (popupY < 0) {
614
+ // If popup would go off screen top, position it BELOW trigger instead
615
+ popupY = triggerBottom + statusBarHeight;
616
+ position = 'BELOW';
617
+ // Clamp to stay above keyboard
618
+ const maxY = keyboardTop - popupHeight;
565
619
  if (popupY > maxY) {
566
- // If both positions are problematic, clamp to visible area
567
- debugLog('AutoPositionedPopup with keyboard: both positions problematic, clamping to visible area');
568
- popupY = Math.min(Math.max(statusBarHeight, y - listLayout.height), maxY);
620
+ popupY = maxY;
569
621
  }
622
+ positionDebugLog(`V19f_BELOW: popupY=${popupY} (clamped to stay above keyboard)`);
570
623
  }
571
- else {
572
- debugLog('AutoPositionedPopup with keyboard: showing ABOVE input field (preferred position)');
573
- }
574
- ref_listPos.current = { x: x, y: popupY, width: width };
575
- debugLog('AutoPositionedPopup useTextInput final position=', ref_listPos.current);
576
- setRootViewNativeStyle(tag, {
577
- top: (_a = ref_listPos.current) === null || _a === void 0 ? void 0 : _a.y,
578
- left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left,
579
- width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width,
580
- height: listLayout.height,
581
- opacity: 1,
624
+ // V19f: Verification
625
+ const popupBottom = popupY + popupHeight;
626
+ const gapPixels = triggerTop - popupBottom;
627
+ positionDebugLog(`V19f_RESULT: position=${position} popupY=${popupY} popupBottom=${popupBottom}`);
628
+ positionDebugLog(`V19f_GAP: trigger_top=${triggerTop} - popup_bottom=${popupBottom} = gap=${gapPixels}px`);
629
+ ref_listPos.current = { x, y: popupY, width };
630
+ updateRootView(tag, {
631
+ style: { top: popupY, left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left, width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width, height: listLayout.height, opacity: 1 }
582
632
  });
583
633
  hasShownRootView.current = true;
584
634
  });
585
635
  });
586
- }, 300); // 300ms is sufficient for keyboard animation, as proven by user testing (even 3000ms didn't fix wrong logic)
636
+ }, KEYBOARD_STABILIZATION_DELAY); // 1000ms to wait for keyboard + scroll stabilization (user request 2025-01-04)
587
637
  }
588
638
  else if (!isKeyboardFullyShown && ref_isFocus.current && keyboardStateChanged) {
589
639
  // Only execute close logic when keyboard state actually changes from true to false
@@ -598,173 +648,52 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
598
648
  }
599
649
  }
600
650
  else {
651
+ // V17 SIMPLIFICATION: When useTextInput=false, ALWAYS show popup in CENTER of screen
652
+ // User request: "只要传入的 useTextInput 是 false, 弹框都显示在屏幕中间"
653
+ // This avoids all complex positioning calculations that kept failing
601
654
  if (state.isFocus) {
602
655
  if (isKeyboardFullyShown) {
603
656
  Keyboard.dismiss();
604
657
  return;
605
658
  }
606
- // CRITICAL FIX: Use triggerBtnRef (the actual TouchableOpacity) for measurement
607
- // instead of refAutoPositionedPopup (the outer View with flex:1/height:100%)
608
- // This ensures accurate position when component is inside complex layouts like KeyboardAwareScrollView
609
- const measureTarget = triggerBtnRef.current || refAutoPositionedPopup.current;
610
- measureTarget === null || measureTarget === void 0 ? void 0 : measureTarget.measureInWindow((x, y, width, height) => {
611
- debugLog('AutoPositionedPopup !useTextInput measureInWindow=', { x, y, width, height, usingTriggerRef: !!triggerBtnRef.current });
612
- // CRITICAL FIX: Handle undefined values from measureInWindow
613
- // This can happen during navigation transitions or when view is not yet mounted
614
- if (x === undefined || y === undefined || width === undefined || height === undefined) {
615
- console.warn('AutoPositionedPopup: measureInWindow returned undefined values, using fallback position');
616
- // Use screen center as fallback position
617
- const screenHeight = Dimensions.get('window').height;
618
- const screenWidth = Dimensions.get('window').width;
619
- const fallbackY = (screenHeight - listLayout.height) / 2;
620
- const fallbackX = screenWidth * 0.05; // 5% from left
621
- const fallbackWidth = screenWidth * 0.9; // 90% width
622
- ref_listPos.current = { x: fallbackX, y: fallbackY, width: fallbackWidth };
623
- debugLog('AutoPositionedPopup !useTextInput using fallback position=', ref_listPos.current);
624
- // Proceed with fallback values
625
- x = fallbackX;
626
- y = fallbackY;
627
- width = fallbackWidth;
628
- height = 50; // Default height for the trigger element
629
- }
630
- // CORRECT POSITIONING LOGIC (as per user requirement)
631
- // Default: show popup ABOVE the input field
632
- // Only if that goes off the top of screen (considering status bar), show BELOW instead
633
- const calculateOptimalPosition = (componentY, componentHeight, popupHeight) => {
634
- debugLog('AutoPositionedPopup calculateOptimalPosition executing');
635
- // Use window height (visible area) instead of screen height
636
- const screenHeight = Dimensions.get('window').height;
637
- debugLog('AutoPositionedPopup positioning data:', {
638
- screenHeight,
639
- componentY,
640
- componentHeight,
641
- popupHeight,
642
- statusBarHeight,
643
- platform: Platform.OS
644
- });
645
- // FIXED POSITIONING LOGIC:
646
- // The popup uses position: 'absolute' relative to the RootViewProvider container
647
- // measureInWindow returns coordinates relative to the window (screen)
648
- // So we should NOT add statusBarHeight to the position calculation
649
- //
650
- // 1. Default: show popup ABOVE the trigger element
651
- // FIX: Use (componentY + componentHeight) as the trigger's bottom edge reference point
652
- // This compensates for measurement inaccuracies when trigger is inside complex layouts (FlatList, ScrollView)
653
- // The popup's bottom should be at the trigger's top with minimal gap (≤5px)
654
- // Formula: popup_top = trigger_bottom - componentHeight - popupHeight
655
- // popup_bottom = trigger_bottom - componentHeight = trigger_top
656
- let popupY = componentY + componentHeight - popupHeight;
657
- debugLog('AutoPositionedPopup: initial calculation for ABOVE position:', {
658
- componentY,
659
- componentHeight,
660
- popupHeight,
661
- popupY,
662
- triggerBottom: componentY + componentHeight,
663
- statusBarHeight
664
- });
665
- // 2. Check if showing above would go off the top of screen (behind status bar)
666
- if (popupY < statusBarHeight) {
667
- debugLog('AutoPositionedPopup: would go behind status bar, showing BELOW instead');
668
- // Show BELOW the trigger element
669
- // Since componentY + componentHeight represents the trigger's "reference bottom" (accounting for measurement offset),
670
- // we need to add another componentHeight to position popup BELOW the actual trigger
671
- // Formula: popup top = componentY + (2 * componentHeight)
672
- // - (componentY + componentHeight) = trigger's actual top (compensated)
673
- // - + componentHeight = skip past trigger height to get to trigger's actual bottom
674
- popupY = componentY + componentHeight + componentHeight;
675
- debugLog('AutoPositionedPopup: BELOW position calculated:', {
676
- formula: 'componentY + 2*componentHeight',
677
- componentY,
678
- componentHeight,
679
- popupY
680
- });
681
- // 3. Also check if showing below would go off the bottom of screen
682
- const maxY = screenHeight - popupHeight;
683
- if (popupY > maxY) {
684
- // If both positions are problematic, clamp to visible area
685
- // Prioritize showing as close to trigger as possible
686
- debugLog('AutoPositionedPopup: both positions problematic, clamping to visible area');
687
- popupY = Math.min(Math.max(statusBarHeight, componentY - popupHeight), maxY);
688
- }
689
- }
690
- else {
691
- debugLog('AutoPositionedPopup: showing ABOVE input field (preferred position)');
692
- }
693
- debugLog('AutoPositionedPopup final position:', {
694
- popupY,
695
- 'showing above': popupY < componentY,
696
- 'below status bar': popupY >= statusBarHeight
697
- });
698
- return { finalY: popupY, showAbove: popupY < componentY };
699
- };
700
- // Calculate position ONCE based on actual popup height
701
- const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
702
- ? CustomPopViewStyle.height
703
- : listLayout.height;
704
- debugLog('AutoPositionedPopup 🔥 Using actualPopupHeight for calculation:', { actualPopupHeight, CustomPopView: !!CustomPopView });
705
- const positionResult = calculateOptimalPosition(y, height, actualPopupHeight);
706
- debugLog('AutoPositionedPopup FINAL position result:', positionResult);
707
- ref_listPos.current = { x: x, y: positionResult.finalY, width: width };
708
- debugLog('AutoPositionedPopup !useTextInput ref_listPos.current=', ref_listPos.current);
709
- if (CustomPopView && CustomPopViewStyle) {
710
- // Position already calculated correctly above, no need to recalculate
711
- const PopViewComponent = CustomPopView();
712
- debugLog('AutoPositionedPopup !useTextInput addRootView=', { CustomPopViewStyle, PopViewComponent, 'state.selectedItem': state.selectedItem });
713
- addRootView({
714
- id: tag,
715
- style: !centerDisplay
716
- ? Object.assign({ top: ref_listPos.current.y, left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left, width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width, height: listLayout.height, opacity: 1 }, CustomPopViewStyle) : Object.assign({ width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width, height: listLayout.height }, CustomPopViewStyle),
717
- component: <PopViewComponent selectedItem={state.selectedItem}></PopViewComponent>,
718
- useModal: true,
719
- onModalClose: () => {
720
- debugLog('AutoPositionedPopup onModalClose');
721
- removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
722
- setState((prevState) => {
723
- return Object.assign(Object.assign({}, prevState), { isFocus: false });
724
- });
725
- hasAddedRootView.current = false;
726
- hasShownRootView.current = false;
727
- hasTriggeredFocus.current = false;
728
- setSearchQuery('');
729
- },
730
- centerDisplay,
731
- });
732
- }
733
- else {
734
- debugLog('AutoPositionedPopup !useTextInput addRootView tag=', tag);
735
- addRootView({
736
- id: tag,
737
- style: {
738
- top: ref_listPos.current.y,
739
- left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left,
740
- width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width,
741
- height: listLayout.height,
742
- opacity: 1,
743
- },
744
- component: (<AutoPositionedPopupList tag={tag} updateState={updateState} fetchData={fetchData} pageSize={pageSize} renderItem={renderItem} selectedItem={state.selectedItem} localSearch={localSearch} showListEmptyComponent={showListEmptyComponent} emptyText={emptyText} themeMode={themeMode}/>),
745
- useModal: true,
746
- onModalClose: () => {
747
- debugLog('AutoPositionedPopup onModalClose tag=', tag);
748
- removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
749
- setState((prevState) => {
750
- return Object.assign({}, prevState);
751
- });
752
- setSearchQuery('');
753
- },
754
- });
755
- }
756
- });
757
- }
758
- }
759
- if (isKeyboardFullyShown) {
760
- ref_isFocus.current = (_a = state.isFocus) !== null && _a !== void 0 ? _a : false;
761
- if (isKeyboardFullyShown !== keyboardVisibleRef.current) {
762
- keyboardVisibleRef.current = isKeyboardFullyShown;
763
- if (isKeyboardFullyShown && textInputRef.current) {
764
- if (ref_searchQuery.current) {
765
- textInputRef.current.setNativeProps({ text: ref_searchQuery.current });
766
- }
659
+ debugLog('🟢🟢🟢 POPUP_V17 useTextInput=false, showing popup in CENTER of screen');
660
+ const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
661
+ ? CustomPopViewStyle.height
662
+ : listLayout.height;
663
+ if (CustomPopView && CustomPopViewStyle) {
664
+ const PopViewComponent = CustomPopView();
665
+ debugLog('🔵🔵🔵 POPUP_V17 CustomPopView centerDisplay=true');
666
+ addRootView({
667
+ id: tag,
668
+ style: Object.assign({ width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width }, CustomPopViewStyle),
669
+ component: <PopViewComponent selectedItem={state.selectedItem}></PopViewComponent>,
670
+ useModal: true,
671
+ centerDisplay: true, // V17: Force center display for useTextInput=false
672
+ onModalClose: () => {
673
+ debugLog('AutoPositionedPopup V17 onModalClose tag=', tag);
674
+ removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
675
+ setState((prevState) => (Object.assign({}, prevState)));
676
+ setSearchQuery('');
677
+ },
678
+ });
679
+ }
680
+ else {
681
+ debugLog('🔵🔵🔵 POPUP_V17 List centerDisplay=true, height=', listLayout.height);
682
+ addRootView({
683
+ id: tag,
684
+ style: { width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width, height: listLayout.height, opacity: 1 },
685
+ component: (<AutoPositionedPopupList tag={tag} updateState={updateState} fetchData={fetchData} pageSize={pageSize} renderItem={renderItem} selectedItem={state.selectedItem} localSearch={localSearch} showListEmptyComponent={showListEmptyComponent} emptyText={emptyText} themeMode={themeMode}/>),
686
+ useModal: true,
687
+ centerDisplay: true, // V17: Force center display for useTextInput=false
688
+ onModalClose: () => {
689
+ debugLog('AutoPositionedPopup V17 onModalClose tag=', tag);
690
+ removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
691
+ setState((prevState) => (Object.assign({}, prevState)));
692
+ setSearchQuery('');
693
+ },
694
+ });
767
695
  }
696
+ return; // V17: Early return after handling !useTextInput case
768
697
  }
769
698
  }
770
699
  }, [isKeyboardFullyShown,
@@ -776,6 +705,10 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
776
705
  tag, TextInputProps,
777
706
  state.selectedItem, showListEmptyComponent, themeMode
778
707
  ]);
708
+ // V18: All positioning logic is now in the useEffect above
709
+ // V18 FIX (2025-01-04): Wait 1000ms after keyboard appears before measuring position
710
+ // This ensures trigger position is stable after KeyboardAwareScrollView scrolls
711
+ // Formula: top = componentY - popupHeight (popup bottom touches trigger top exactly)
779
712
  // Imperative handle for parent component access
780
713
  useImperativeHandle(parentRef, () => ({
781
714
  clearSelectedItem: () => {
@@ -950,7 +883,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
950
883
  }} key={`textinput-${tag}`} style={[
951
884
  styles.inputStyle,
952
885
  stableInputStyle,
953
- (themeMode === 'dark' && { color: '#fff' })
886
+ (themeMode === 'dark' && { color: '#fff' }),
954
887
  ]} textAlign={stableTextInputProps && stableTextInputProps['textAlign'] || 'left'} multiline={stableTextInputProps && stableTextInputProps['multiline'] || false} numberOfLines={stableTextInputProps && stableTextInputProps['numberOfLines'] || 1} onChangeText={(searchQuery) => {
955
888
  ref_searchQuery.current = searchQuery;
956
889
  debugLog('AutoPositionedPopup onChangeText rootViews=', rootViews);
@@ -1007,10 +940,13 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
1007
940
  });
1008
941
  // Capture trigger button position BEFORE switching to TextInput
1009
942
  // This is critical because triggerBtnRef will become null after isFocus=true
1010
- if (triggerBtnRef.current && (parentScrollViewRef === null || parentScrollViewRef === void 0 ? void 0 : parentScrollViewRef.current)) {
943
+ // IMPORTANT: Always capture position regardless of parentScrollViewRef
944
+ if (triggerBtnRef.current) {
1011
945
  triggerBtnRef.current.measureInWindow((x, y, width, height) => {
1012
946
  debugLog('AutoPositionedPopup onPress: captured trigger position=', { tag, x, y, width, height });
1013
- triggerPositionRef.current = { x, y, width, height };
947
+ if (x !== undefined && y !== undefined && width !== undefined && height !== undefined) {
948
+ triggerPositionRef.current = { x, y, width, height };
949
+ }
1014
950
  });
1015
951
  }
1016
952
  if (useTextInput) {
@@ -1050,6 +986,9 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
1050
986
  }
1051
987
  }
1052
988
  else {
989
+ // V17 SIMPLIFICATION: For useTextInput=false, popup will be centered
990
+ // No need for complex position measurement - just trigger focus
991
+ debugLog('🔵🔵🔵 POPUP_V17 onPress useTextInput=false, will show centered popup');
1053
992
  setState((prevState) => {
1054
993
  return Object.assign(Object.assign({}, prevState), { isFocus: true });
1055
994
  });
@@ -1068,29 +1007,29 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
1068
1007
  </CustomRow>);
1069
1008
  }, [
1070
1009
  tag,
1071
- // CRITICAL FIX: Remove all props that may change frequently or are inline functions
1010
+ // CRITICAL FIX: Remove all props that may change frequently or are inline functions
1072
1011
  // Changes to these props should not cause the entire component tree to recreate, especially TextInput
1073
- // fetchData, // ❌ Removed: inline function
1074
- // renderItem, // ❌ Removed: possibly inline function
1075
- // onItemSelected, // ❌ Removed: possibly inline function
1076
- // onSubmitEditing, // ❌ Removed: possibly inline function
1012
+ // fetchData, // ❌Removed: inline function
1013
+ // renderItem, // ❌Removed: possibly inline function
1014
+ // onItemSelected, // ❌Removed: possibly inline function
1015
+ // onSubmitEditing, // ❌Removed: possibly inline function
1077
1016
  localSearch,
1078
- // placeholder, // ❌ Removed: may change
1079
- // textAlign, // ❌ Removed: may change
1017
+ // placeholder, // ❌Removed: may change
1018
+ // textAlign, // ❌Removed: may change
1080
1019
  pageSize,
1081
1020
  selectedItem,
1082
- // CustomRow, // ❌ Removed: inline function, new reference each time
1021
+ // CustomRow, // ❌Removed: inline function, new reference each time
1083
1022
  useTextInput,
1084
- // btwChildren, // ❌ Removed: inline function
1085
- // keyExtractor, // ❌ Removed: possibly inline function
1086
- // AutoPositionedPopupBtnStyle, // ❌ Removed: possibly inline object
1087
- // CustomPopView, // ❌ Removed: may change
1088
- // CustomPopViewStyle, // ❌ Removed: may change
1023
+ // btwChildren, // ❌Removed: inline function
1024
+ // keyExtractor, // ❌Removed: possibly inline function
1025
+ // AutoPositionedPopupBtnStyle, // ❌Removed: possibly inline object
1026
+ // CustomPopView, // ❌Removed: may change
1027
+ // CustomPopViewStyle, // ❌Removed: may change
1089
1028
  forceRemoveAllRootViewOnItemSelected,
1090
1029
  state.isFocus,
1091
1030
  showListEmptyComponent,
1092
1031
  emptyText,
1093
- // Removed most dependencies that may cause re-rendering, keeping only core dependencies that truly affect component structure
1032
+ // Removed most dependencies that may cause re-rendering, keeping only core dependencies that truly affect component structure
1094
1033
  // This prevents TextInput recreation due to inline functions/objects during parent component redraws
1095
1034
  ]);
1096
1035
  }));