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.
- package/lib/AutoPositionedPopup.d.ts.map +1 -1
- package/lib/AutoPositionedPopup.js +227 -288
- package/lib/AutoPositionedPopup.js.map +1 -1
- package/lib/AutoPositionedPopup.style.d.ts.map +1 -1
- package/lib/AutoPositionedPopup.style.js +2 -0
- package/lib/AutoPositionedPopup.style.js.map +1 -1
- package/lib/KeyboardManager.d.ts +6 -1
- package/lib/KeyboardManager.d.ts.map +1 -1
- package/lib/KeyboardManager.js +19 -2
- package/lib/KeyboardManager.js.map +1 -1
- package/lib/RootViewContext.d.ts.map +1 -1
- package/lib/RootViewContext.js +76 -44
- package/lib/RootViewContext.js.map +1 -1
- package/package.json +1 -1
- package/src/AutoPositionedPopup.style.ts +2 -0
- package/src/AutoPositionedPopup.tsx +321 -381
- package/src/KeyboardManager.tsx +35 -10
- package/src/RootViewContext.tsx +63 -24
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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:
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
if (parentScrollViewRef === null || parentScrollViewRef === void 0 ? void 0 : parentScrollViewRef.current) {
|
|
472
|
-
debugLog('AutoPositionedPopup: Keyboard appeared,
|
|
473
|
-
// Use
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
|
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: ~
|
|
485
|
-
// 3. Layout tree update: ~
|
|
486
|
-
// Total: ~
|
|
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(
|
|
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:
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
564
|
-
|
|
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
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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, // ❌
|
|
1074
|
-
// renderItem, // ❌
|
|
1075
|
-
// onItemSelected, // ❌
|
|
1076
|
-
// onSubmitEditing, // ❌
|
|
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, // ❌
|
|
1079
|
-
// textAlign, // ❌
|
|
1017
|
+
// placeholder, // ❌Removed: may change
|
|
1018
|
+
// textAlign, // ❌Removed: may change
|
|
1080
1019
|
pageSize,
|
|
1081
1020
|
selectedItem,
|
|
1082
|
-
// CustomRow, // ❌
|
|
1021
|
+
// CustomRow, // ❌Removed: inline function, new reference each time
|
|
1083
1022
|
useTextInput,
|
|
1084
|
-
// btwChildren, // ❌
|
|
1085
|
-
// keyExtractor, // ❌
|
|
1086
|
-
// AutoPositionedPopupBtnStyle, // ❌
|
|
1087
|
-
// CustomPopView, // ❌
|
|
1088
|
-
// CustomPopViewStyle, // ❌
|
|
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
|
-
//
|
|
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
|
}));
|