react-native-auto-positioned-popup 1.2.16 → 1.2.17
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 +193 -244
- 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.map +1 -1
- package/lib/KeyboardManager.js +7 -0
- package/lib/KeyboardManager.js.map +1 -1
- package/lib/RootViewContext.d.ts.map +1 -1
- package/lib/RootViewContext.js +10 -0
- package/lib/RootViewContext.js.map +1 -1
- package/package.json +1 -1
- package/src/AutoPositionedPopup.style.ts +2 -0
- package/src/AutoPositionedPopup.tsx +292 -342
- package/src/KeyboardManager.tsx +14 -6
- package/src/RootViewContext.tsx +19 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AutoPositionedPopup.d.ts","sourceRoot":"","sources":["../src/AutoPositionedPopup.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AutoPositionedPopup.d.ts","sourceRoot":"","sources":["../src/AutoPositionedPopup.tsx"],"names":[],"mappings":"AAYA,OAAO,KAYN,MAAM,OAAO,CAAC;AAgBf,OAAO,EAAC,wBAAwB,EAAqB,MAAM,4BAA4B,CAAC;AA6QxF,QAAA,MAAM,mBAAmB,qFAqiCxB,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { debugLog } from './constants';
|
|
2
|
+
// Module load marker - unique ID for tracking code version
|
|
3
|
+
// DEBUG FLAG: Set to false to disable all console logs for better performance
|
|
4
|
+
const POPUP_DEBUG = false;
|
|
5
|
+
const debugLog = (...args) => {
|
|
6
|
+
if (POPUP_DEBUG) {
|
|
7
|
+
debugLog(...args);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
// Only log module load in debug mode
|
|
11
|
+
debugLog('POPUP_MODULE_V17_LOADED at ' + new Date().toISOString());
|
|
2
12
|
import React, { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
3
13
|
import { Dimensions, findNodeHandle, Keyboard, Platform, StatusBar, Text, TextInput as RNTextInput, TouchableOpacity, View, } from 'react-native';
|
|
4
14
|
import { AdvancedFlatList } from 'react-native-advanced-flatlist';
|
|
@@ -131,7 +141,7 @@ const AutoPositionedPopupList = memo(({ tag, updateState, fetchData, keyExtracto
|
|
|
131
141
|
return null;
|
|
132
142
|
}
|
|
133
143
|
catch (e) {
|
|
134
|
-
|
|
144
|
+
debugLog('Error in fetchData:', e);
|
|
135
145
|
}
|
|
136
146
|
debugLog('AutoPositionedPopupList _fetchData res=', null);
|
|
137
147
|
return null;
|
|
@@ -180,7 +190,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
180
190
|
// res.needLoadMore = res1.length === pageSize
|
|
181
191
|
}
|
|
182
192
|
catch (e) {
|
|
183
|
-
|
|
193
|
+
debugLog('Error in fetch operation:', e);
|
|
184
194
|
}
|
|
185
195
|
return res;
|
|
186
196
|
}, 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 +202,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
192
202
|
selectedItem: selectedItem,
|
|
193
203
|
});
|
|
194
204
|
// Use RootView context
|
|
195
|
-
const { addRootView, setRootViewNativeStyle, removeRootView, rootViews, setSearchQuery } = useRootView();
|
|
205
|
+
const { addRootView, setRootViewNativeStyle, updateRootView, removeRootView, rootViews, setSearchQuery } = useRootView();
|
|
196
206
|
const rootViewsRef = useRef(rootViews);
|
|
197
207
|
// Track TextInput focus and RootView states like project implementation
|
|
198
208
|
const hasTriggeredFocus = useRef(false);
|
|
@@ -405,7 +415,6 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
405
415
|
}
|
|
406
416
|
}, [selectedItem, state.selectedItem, tag]);
|
|
407
417
|
useEffect(() => {
|
|
408
|
-
var _a;
|
|
409
418
|
// Detect if keyboard state has actually changed to avoid false triggers during parent component re-renders
|
|
410
419
|
const keyboardStateChanged = prevIsKeyboardFullyShownRef.current !== isKeyboardFullyShown;
|
|
411
420
|
const propsChanged = prevPropsRef.current.CustomPopView !== CustomPopView ||
|
|
@@ -442,13 +451,18 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
442
451
|
TextInputProps
|
|
443
452
|
};
|
|
444
453
|
// Only execute logic when keyboard state actually changes or user actively operates
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
454
|
+
// CRITICAL FIX: Also allow execution when popup needs initial positioning
|
|
455
|
+
// hasAddedRootView.current = true means popup container exists
|
|
456
|
+
// hasShownRootView.current = false means positioning not done yet
|
|
457
|
+
// We MUST allow execution when popup needs positioning, even if keyboard state unchanged
|
|
458
|
+
if (!keyboardStateChanged && hasAddedRootView.current && hasShownRootView.current) {
|
|
459
|
+
debugLog('AutoPositionedPopup: Skip execution - already positioned and keyboard state unchanged');
|
|
450
460
|
return;
|
|
451
461
|
}
|
|
462
|
+
// Log when we're allowing execution for initial positioning
|
|
463
|
+
if (!keyboardStateChanged && hasAddedRootView.current && !hasShownRootView.current) {
|
|
464
|
+
debugLog('AutoPositionedPopup: ALLOWING execution for initial positioning (popup added but not positioned yet)');
|
|
465
|
+
}
|
|
452
466
|
const getStatusBarHeight = () => {
|
|
453
467
|
if (Platform.OS === 'android') {
|
|
454
468
|
// Android: Use StatusBar.currentHeight API
|
|
@@ -489,97 +503,142 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
489
503
|
// then requestAnimationFrame ensures measurement happens after next render frame
|
|
490
504
|
setTimeout(() => {
|
|
491
505
|
requestAnimationFrame(() => {
|
|
492
|
-
// CRITICAL FIX:
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
506
|
+
// CRITICAL FIX: Measure CURRENT position AFTER keyboard animation completes
|
|
507
|
+
// DO NOT use stored triggerPositionRef because keyboard may have shifted the view up
|
|
508
|
+
// Instead, measure the outer wrapper (refAutoPositionedPopup)
|
|
509
|
+
// which reflects the ACTUAL current position after keyboard shift
|
|
510
|
+
var _a;
|
|
511
|
+
// DEBUG: Log both refs to compare their positions
|
|
512
|
+
debugLog('AutoPositionedPopup DEBUG: refs status=', {
|
|
513
|
+
hasTextInputRef: !!textInputRef.current,
|
|
514
|
+
hasRefAutoPositionedPopup: !!refAutoPositionedPopup.current,
|
|
515
|
+
});
|
|
516
|
+
// Measure BOTH refs for comparison
|
|
517
|
+
if (textInputRef.current && refAutoPositionedPopup.current) {
|
|
518
|
+
textInputRef.current.measureInWindow((tx, ty, tw, th) => {
|
|
519
|
+
debugLog('AutoPositionedPopup DEBUG: textInputRef position=', { x: tx, y: ty, width: tw, height: th });
|
|
520
|
+
});
|
|
521
|
+
refAutoPositionedPopup.current.measureInWindow((rx, ry, rw, rh) => {
|
|
522
|
+
debugLog('AutoPositionedPopup DEBUG: refAutoPositionedPopup position=', { x: rx, y: ry, width: rw, height: rh });
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
// CRITICAL FIX: Use textInputRef as primary measurement target
|
|
526
|
+
// refAutoPositionedPopup.measureInWindow() returns undefined values
|
|
527
|
+
// because the outer wrapper View uses flex:1/height:100% which makes it unmeasurable
|
|
528
|
+
// textInputRef reliably returns the actual position of the input field
|
|
529
|
+
const measureTarget = textInputRef.current || refAutoPositionedPopup.current;
|
|
530
|
+
if (!measureTarget) {
|
|
531
|
+
debugLog('AutoPositionedPopup useTextInput: no measureTarget available, using fallback');
|
|
532
|
+
const screenHeightFallback = Dimensions.get('window').height;
|
|
533
|
+
const screenWidthFallback = Dimensions.get('window').width;
|
|
534
|
+
const fallbackY = (screenHeightFallback - listLayout.height) / 2;
|
|
535
|
+
ref_listPos.current = { x: screenWidthFallback * 0.05, y: fallbackY, width: screenWidthFallback * 0.9 };
|
|
536
|
+
updateRootView(tag, {
|
|
537
|
+
style: {
|
|
538
|
+
top: (_a = ref_listPos.current) === null || _a === void 0 ? void 0 : _a.y,
|
|
539
|
+
left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left,
|
|
540
|
+
width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width,
|
|
541
|
+
height: listLayout.height,
|
|
542
|
+
opacity: 1,
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
hasShownRootView.current = true;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
// Determine which ref is actually being used (for logging)
|
|
549
|
+
const usingTextInputRef = measureTarget === textInputRef.current;
|
|
550
|
+
debugLog('AutoPositionedPopup useTextInput: using measureTarget=', usingTextInputRef ? 'textInputRef' : 'refAutoPositionedPopup');
|
|
551
|
+
// Measure CURRENT position (after keyboard shifted the view)
|
|
552
|
+
measureTarget.measureInWindow((x, y, width, height) => {
|
|
496
553
|
var _a;
|
|
497
|
-
debugLog('AutoPositionedPopup useTextInput
|
|
498
|
-
|
|
499
|
-
|
|
554
|
+
debugLog('AutoPositionedPopup useTextInput: measured position for positioning=', {
|
|
555
|
+
x, y, width, height,
|
|
556
|
+
measureTarget: usingTextInputRef ? 'textInputRef' : 'refAutoPositionedPopup'
|
|
557
|
+
});
|
|
558
|
+
// Handle undefined values (can happen during navigation transitions)
|
|
500
559
|
if (x === undefined || y === undefined || width === undefined || height === undefined) {
|
|
501
|
-
|
|
560
|
+
debugLog('AutoPositionedPopup useTextInput: measureInWindow returned undefined, using fallback');
|
|
502
561
|
const screenHeightFallback = Dimensions.get('window').height;
|
|
503
562
|
const screenWidthFallback = Dimensions.get('window').width;
|
|
504
563
|
const fallbackY = (screenHeightFallback - listLayout.height) / 2;
|
|
505
|
-
|
|
506
|
-
const fallbackWidth = screenWidthFallback * 0.9;
|
|
507
|
-
x = fallbackX;
|
|
564
|
+
x = screenWidthFallback * 0.05;
|
|
508
565
|
y = fallbackY;
|
|
509
|
-
width =
|
|
566
|
+
width = screenWidthFallback * 0.9;
|
|
510
567
|
height = 50;
|
|
511
568
|
}
|
|
512
|
-
//
|
|
513
|
-
|
|
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
|
|
569
|
+
// Calculate screen height and popup position
|
|
570
|
+
const screenHeight = Dimensions.get('window').height;
|
|
525
571
|
debugLog('AutoPositionedPopup useTextInput positioning data=', {
|
|
526
572
|
screenHeight,
|
|
527
573
|
componentY: y,
|
|
528
574
|
componentHeight: height,
|
|
529
575
|
listHeight: listLayout.height
|
|
530
576
|
});
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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,
|
|
577
|
+
// POSITIONING LOGIC (with keyboard):
|
|
578
|
+
// Simple rule: popup must TOUCH the trigger with NO GAP
|
|
579
|
+
// 1. Default: show ABOVE trigger (popup bottom touches trigger top)
|
|
580
|
+
// 2. If ABOVE would overlap status bar: show BELOW (popup top touches trigger bottom)
|
|
581
|
+
debugLog('AutoPositionedPopup POSITIONING:', {
|
|
582
|
+
triggerY: y,
|
|
583
|
+
triggerHeight: height,
|
|
584
|
+
triggerBottom: y + height,
|
|
544
585
|
popupHeight: listLayout.height,
|
|
586
|
+
screenHeight,
|
|
587
|
+
statusBarHeight
|
|
588
|
+
});
|
|
589
|
+
// 1. Default: show popup ABOVE the trigger
|
|
590
|
+
// Popup has internal padding (12px from autoPositionedPopupList style)
|
|
591
|
+
// To make popup CONTENT touch trigger (not container), add padding offset
|
|
592
|
+
// Container bottom at y + POPUP_PADDING, content bottom at y (no gap)
|
|
593
|
+
const POPUP_PADDING = 12;
|
|
594
|
+
let popupY = y - listLayout.height + POPUP_PADDING;
|
|
595
|
+
let position = 'ABOVE';
|
|
596
|
+
debugLog('AutoPositionedPopup: trying ABOVE position:', {
|
|
545
597
|
popupY,
|
|
546
598
|
popupBottom: popupY + listLayout.height,
|
|
599
|
+
contentBottom: popupY + listLayout.height - POPUP_PADDING,
|
|
547
600
|
triggerTop: y,
|
|
548
|
-
|
|
601
|
+
paddingOffset: POPUP_PADDING,
|
|
602
|
+
wouldOverlapStatusBar: popupY < statusBarHeight
|
|
549
603
|
});
|
|
550
|
-
// 2.
|
|
604
|
+
// 2. If showing ABOVE would go behind status bar, show BELOW instead
|
|
551
605
|
if (popupY < statusBarHeight) {
|
|
552
|
-
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
popupY = y + height +
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
height,
|
|
561
|
-
|
|
606
|
+
// Show BELOW: popup top at trigger bottom
|
|
607
|
+
// Use trigger's measured height as buffer to account for row padding
|
|
608
|
+
// The TextInput is only part of the trigger row - row height scales with trigger height
|
|
609
|
+
const BELOW_BUFFER = height;
|
|
610
|
+
popupY = y + height + BELOW_BUFFER;
|
|
611
|
+
position = 'BELOW';
|
|
612
|
+
debugLog('AutoPositionedPopup: using BELOW position (ABOVE overlaps status bar):', {
|
|
613
|
+
popupY,
|
|
614
|
+
triggerBottom: y + height,
|
|
615
|
+
buffer: BELOW_BUFFER,
|
|
616
|
+
actualGap: BELOW_BUFFER
|
|
562
617
|
});
|
|
563
|
-
// 3.
|
|
618
|
+
// 3. Safety check: if BELOW would go off screen bottom, clamp it
|
|
564
619
|
const maxY = screenHeight - listLayout.height;
|
|
565
620
|
if (popupY > maxY) {
|
|
566
|
-
|
|
567
|
-
debugLog('AutoPositionedPopup
|
|
568
|
-
popupY = Math.min(Math.max(statusBarHeight, y - listLayout.height), maxY);
|
|
621
|
+
popupY = maxY;
|
|
622
|
+
debugLog('AutoPositionedPopup: clamped to screen bottom:', { popupY, maxY });
|
|
569
623
|
}
|
|
570
624
|
}
|
|
571
625
|
else {
|
|
572
|
-
debugLog('AutoPositionedPopup
|
|
626
|
+
debugLog('AutoPositionedPopup: using ABOVE position (preferred)');
|
|
573
627
|
}
|
|
628
|
+
debugLog('AutoPositionedPopup FINAL POSITION:', { position, popupY, touchesTrigger: true });
|
|
574
629
|
ref_listPos.current = { x: x, y: popupY, width: width };
|
|
575
630
|
debugLog('AutoPositionedPopup useTextInput final position=', ref_listPos.current);
|
|
576
|
-
setRootViewNativeStyle
|
|
631
|
+
// Use updateRootView instead of setRootViewNativeStyle for more reliable style updates
|
|
632
|
+
// setNativeProps may not work correctly when initial style is in an array
|
|
633
|
+
const newStyle = {
|
|
577
634
|
top: (_a = ref_listPos.current) === null || _a === void 0 ? void 0 : _a.y,
|
|
578
635
|
left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left,
|
|
579
636
|
width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width,
|
|
580
637
|
height: listLayout.height,
|
|
581
638
|
opacity: 1,
|
|
582
|
-
}
|
|
639
|
+
};
|
|
640
|
+
debugLog('AutoPositionedPopup useTextInput: applying new style via updateRootView=', newStyle);
|
|
641
|
+
updateRootView(tag, { style: newStyle });
|
|
583
642
|
hasShownRootView.current = true;
|
|
584
643
|
});
|
|
585
644
|
});
|
|
@@ -598,173 +657,52 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
598
657
|
}
|
|
599
658
|
}
|
|
600
659
|
else {
|
|
660
|
+
// V17 SIMPLIFICATION: When useTextInput=false, ALWAYS show popup in CENTER of screen
|
|
661
|
+
// User request: "只要传入的 useTextInput 是 false, 弹框都显示在屏幕中间"
|
|
662
|
+
// This avoids all complex positioning calculations that kept failing
|
|
601
663
|
if (state.isFocus) {
|
|
602
664
|
if (isKeyboardFullyShown) {
|
|
603
665
|
Keyboard.dismiss();
|
|
604
666
|
return;
|
|
605
667
|
}
|
|
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
|
-
}
|
|
668
|
+
debugLog('🟢🟢🟢 POPUP_V17 useTextInput=false, showing popup in CENTER of screen');
|
|
669
|
+
const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
|
|
670
|
+
? CustomPopViewStyle.height
|
|
671
|
+
: listLayout.height;
|
|
672
|
+
if (CustomPopView && CustomPopViewStyle) {
|
|
673
|
+
const PopViewComponent = CustomPopView();
|
|
674
|
+
debugLog('🔵🔵🔵 POPUP_V17 CustomPopView centerDisplay=true');
|
|
675
|
+
addRootView({
|
|
676
|
+
id: tag,
|
|
677
|
+
style: Object.assign({ width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width }, CustomPopViewStyle),
|
|
678
|
+
component: <PopViewComponent selectedItem={state.selectedItem}></PopViewComponent>,
|
|
679
|
+
useModal: true,
|
|
680
|
+
centerDisplay: true, // V17: Force center display for useTextInput=false
|
|
681
|
+
onModalClose: () => {
|
|
682
|
+
debugLog('AutoPositionedPopup V17 onModalClose tag=', tag);
|
|
683
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
684
|
+
setState((prevState) => (Object.assign({}, prevState)));
|
|
685
|
+
setSearchQuery('');
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
debugLog('🔵🔵🔵 POPUP_V17 List centerDisplay=true, height=', listLayout.height);
|
|
691
|
+
addRootView({
|
|
692
|
+
id: tag,
|
|
693
|
+
style: { width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width, height: listLayout.height, opacity: 1 },
|
|
694
|
+
component: (<AutoPositionedPopupList tag={tag} updateState={updateState} fetchData={fetchData} pageSize={pageSize} renderItem={renderItem} selectedItem={state.selectedItem} localSearch={localSearch} showListEmptyComponent={showListEmptyComponent} emptyText={emptyText} themeMode={themeMode}/>),
|
|
695
|
+
useModal: true,
|
|
696
|
+
centerDisplay: true, // V17: Force center display for useTextInput=false
|
|
697
|
+
onModalClose: () => {
|
|
698
|
+
debugLog('AutoPositionedPopup V17 onModalClose tag=', tag);
|
|
699
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
700
|
+
setState((prevState) => (Object.assign({}, prevState)));
|
|
701
|
+
setSearchQuery('');
|
|
702
|
+
},
|
|
703
|
+
});
|
|
767
704
|
}
|
|
705
|
+
return; // V17: Early return after handling !useTextInput case
|
|
768
706
|
}
|
|
769
707
|
}
|
|
770
708
|
}, [isKeyboardFullyShown,
|
|
@@ -776,6 +714,11 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
776
714
|
tag, TextInputProps,
|
|
777
715
|
state.selectedItem, showListEmptyComponent, themeMode
|
|
778
716
|
]);
|
|
717
|
+
// V16: All positioning logic is now in the useEffect above (calculateOptimalPosition + processPosition)
|
|
718
|
+
// V16 FIX: Capture position in onPress callback BEFORE setState is called
|
|
719
|
+
// This ensures triggerPositionRef.current is set when useEffect runs
|
|
720
|
+
// Formula: top = componentY - popupHeight (popup bottom touches trigger top exactly)
|
|
721
|
+
debugLog('🟢 POPUP_MODULE_V16_LOADED - capturing position in onPress callback before setState');
|
|
779
722
|
// Imperative handle for parent component access
|
|
780
723
|
useImperativeHandle(parentRef, () => ({
|
|
781
724
|
clearSelectedItem: () => {
|
|
@@ -950,7 +893,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
950
893
|
}} key={`textinput-${tag}`} style={[
|
|
951
894
|
styles.inputStyle,
|
|
952
895
|
stableInputStyle,
|
|
953
|
-
(themeMode === 'dark' && { color: '#fff' })
|
|
896
|
+
(themeMode === 'dark' && { color: '#fff' }),
|
|
954
897
|
]} textAlign={stableTextInputProps && stableTextInputProps['textAlign'] || 'left'} multiline={stableTextInputProps && stableTextInputProps['multiline'] || false} numberOfLines={stableTextInputProps && stableTextInputProps['numberOfLines'] || 1} onChangeText={(searchQuery) => {
|
|
955
898
|
ref_searchQuery.current = searchQuery;
|
|
956
899
|
debugLog('AutoPositionedPopup onChangeText rootViews=', rootViews);
|
|
@@ -1007,10 +950,13 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
1007
950
|
});
|
|
1008
951
|
// Capture trigger button position BEFORE switching to TextInput
|
|
1009
952
|
// This is critical because triggerBtnRef will become null after isFocus=true
|
|
1010
|
-
|
|
953
|
+
// IMPORTANT: Always capture position regardless of parentScrollViewRef
|
|
954
|
+
if (triggerBtnRef.current) {
|
|
1011
955
|
triggerBtnRef.current.measureInWindow((x, y, width, height) => {
|
|
1012
956
|
debugLog('AutoPositionedPopup onPress: captured trigger position=', { tag, x, y, width, height });
|
|
1013
|
-
|
|
957
|
+
if (x !== undefined && y !== undefined && width !== undefined && height !== undefined) {
|
|
958
|
+
triggerPositionRef.current = { x, y, width, height };
|
|
959
|
+
}
|
|
1014
960
|
});
|
|
1015
961
|
}
|
|
1016
962
|
if (useTextInput) {
|
|
@@ -1050,6 +996,9 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
1050
996
|
}
|
|
1051
997
|
}
|
|
1052
998
|
else {
|
|
999
|
+
// V17 SIMPLIFICATION: For useTextInput=false, popup will be centered
|
|
1000
|
+
// No need for complex position measurement - just trigger focus
|
|
1001
|
+
debugLog('🔵🔵🔵 POPUP_V17 onPress useTextInput=false, will show centered popup');
|
|
1053
1002
|
setState((prevState) => {
|
|
1054
1003
|
return Object.assign(Object.assign({}, prevState), { isFocus: true });
|
|
1055
1004
|
});
|
|
@@ -1068,29 +1017,29 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
1068
1017
|
</CustomRow>);
|
|
1069
1018
|
}, [
|
|
1070
1019
|
tag,
|
|
1071
|
-
//
|
|
1020
|
+
// �?CRITICAL FIX: Remove all props that may change frequently or are inline functions
|
|
1072
1021
|
// Changes to these props should not cause the entire component tree to recreate, especially TextInput
|
|
1073
|
-
// fetchData, //
|
|
1074
|
-
// renderItem, //
|
|
1075
|
-
// onItemSelected, //
|
|
1076
|
-
// onSubmitEditing, //
|
|
1022
|
+
// fetchData, // �?Removed: inline function
|
|
1023
|
+
// renderItem, // �?Removed: possibly inline function
|
|
1024
|
+
// onItemSelected, // �?Removed: possibly inline function
|
|
1025
|
+
// onSubmitEditing, // �?Removed: possibly inline function
|
|
1077
1026
|
localSearch,
|
|
1078
|
-
// placeholder, //
|
|
1079
|
-
// textAlign, //
|
|
1027
|
+
// placeholder, // �?Removed: may change
|
|
1028
|
+
// textAlign, // �?Removed: may change
|
|
1080
1029
|
pageSize,
|
|
1081
1030
|
selectedItem,
|
|
1082
|
-
// CustomRow, //
|
|
1031
|
+
// CustomRow, // �?Removed: inline function, new reference each time
|
|
1083
1032
|
useTextInput,
|
|
1084
|
-
// btwChildren, //
|
|
1085
|
-
// keyExtractor, //
|
|
1086
|
-
// AutoPositionedPopupBtnStyle, //
|
|
1087
|
-
// CustomPopView, //
|
|
1088
|
-
// CustomPopViewStyle, //
|
|
1033
|
+
// btwChildren, // �?Removed: inline function
|
|
1034
|
+
// keyExtractor, // �?Removed: possibly inline function
|
|
1035
|
+
// AutoPositionedPopupBtnStyle, // �?Removed: possibly inline object
|
|
1036
|
+
// CustomPopView, // �?Removed: may change
|
|
1037
|
+
// CustomPopViewStyle, // �?Removed: may change
|
|
1089
1038
|
forceRemoveAllRootViewOnItemSelected,
|
|
1090
1039
|
state.isFocus,
|
|
1091
1040
|
showListEmptyComponent,
|
|
1092
1041
|
emptyText,
|
|
1093
|
-
//
|
|
1042
|
+
// �?Removed most dependencies that may cause re-rendering, keeping only core dependencies that truly affect component structure
|
|
1094
1043
|
// This prevents TextInput recreation due to inline functions/objects during parent component redraws
|
|
1095
1044
|
]);
|
|
1096
1045
|
}));
|