react-native-auto-positioned-popup 1.0.12 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/AutoPositionedPopup.d.ts.map +1 -1
- package/lib/AutoPositionedPopup.js +356 -153
- package/lib/AutoPositionedPopup.js.map +1 -1
- package/lib/AutoPositionedPopupProps.d.ts +6 -0
- package/lib/AutoPositionedPopupProps.d.ts.map +1 -1
- package/lib/KeyboardManager.d.ts.map +1 -1
- package/lib/KeyboardManager.js +60 -34
- package/lib/KeyboardManager.js.map +1 -1
- package/lib/RootViewContext.d.ts.map +1 -1
- package/lib/RootViewContext.js +21 -16
- package/lib/RootViewContext.js.map +1 -1
- package/package.json +1 -1
- package/src/AutoPositionedPopup.tsx +419 -238
- package/src/AutoPositionedPopupProps.ts +13 -7
- package/src/KeyboardManager.tsx +73 -49
- package/src/RootViewContext.tsx +31 -28
|
@@ -88,9 +88,7 @@ const ListItem: React.FC<{
|
|
|
88
88
|
rootViewsRef.current = rootViews;
|
|
89
89
|
}, [rootViews]);
|
|
90
90
|
return useMemo(() => {
|
|
91
|
-
// console.log('AutoPositionedPopup.tsx ListItem
|
|
92
|
-
console.log('AutoPositionedPopup.tsx ListItem item=', item);
|
|
93
|
-
console.log('AutoPositionedPopup.tsx ListItem selectedItem=', selectedItem);
|
|
91
|
+
// console.log('AutoPositionedPopup.tsx ListItem=', {index, item, selectedItem});
|
|
94
92
|
const isSelected = item.id === selectedItem?.id;
|
|
95
93
|
return (
|
|
96
94
|
<TouchableOpacity
|
|
@@ -132,7 +130,7 @@ interface AutoPositionedPopupListProps {
|
|
|
132
130
|
selectedItem?: SelectedItem;
|
|
133
131
|
localSearch?: boolean;
|
|
134
132
|
pageSize?: number;
|
|
135
|
-
showListEmptyComponent?:boolean;
|
|
133
|
+
showListEmptyComponent?: boolean;
|
|
136
134
|
emptyText?: string;
|
|
137
135
|
}
|
|
138
136
|
|
|
@@ -145,7 +143,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
145
143
|
renderItem,
|
|
146
144
|
selectedItem,
|
|
147
145
|
localSearch,
|
|
148
|
-
pageSize,showListEmptyComponent,emptyText
|
|
146
|
+
pageSize, showListEmptyComponent, emptyText
|
|
149
147
|
}: AutoPositionedPopupListProps): React.JSX.Element => {
|
|
150
148
|
const [state, setState] = useState<{
|
|
151
149
|
selectedItem?: SelectedItem;
|
|
@@ -176,8 +174,8 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
176
174
|
};
|
|
177
175
|
}, []);
|
|
178
176
|
// useEffect(() => {
|
|
179
|
-
// //
|
|
180
|
-
// //
|
|
177
|
+
// // Listen to TextInput events, refresh list when received, not dependent on global searchQuery
|
|
178
|
+
// // Sync the latest searchQuery to list-specific ref for _fetchData to use
|
|
181
179
|
// ref_searchQuery.current = searchQuery;
|
|
182
180
|
// console.log('AutoPositionedPopupList useEffect searchQuery=', searchQuery);
|
|
183
181
|
// console.log('AutoPositionedPopupList useEffect state.localData=', state.localData);
|
|
@@ -212,10 +210,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
212
210
|
pageIndex,
|
|
213
211
|
pageSize: currentPageSize,
|
|
214
212
|
}: FetchDataParams): Promise<ListData | null> => {
|
|
215
|
-
console.log('AutoPositionedPopupList _fetchData
|
|
216
|
-
console.log('AutoPositionedPopupList _fetchData state.localData=', state.localData);
|
|
217
|
-
console.log('AutoPositionedPopupList _fetchData ref_searchQuery.current=', ref_searchQuery.current);
|
|
218
|
-
console.log('AutoPositionedPopupList _fetchData localSearch=', localSearch);
|
|
213
|
+
console.log('AutoPositionedPopupList _fetchData=', {pageIndex, pageSize: currentPageSize, 'state.localData': state.localData, 'ref_searchQuery.current': ref_searchQuery.current, localSearch});
|
|
219
214
|
if (localSearch && state.localData.length > 0) {
|
|
220
215
|
const result: SelectedItem[] = state.localData.filter((item: SelectedItem) => {
|
|
221
216
|
return item.title?.toLowerCase().includes(ref_searchQuery.current.toLowerCase());
|
|
@@ -291,7 +286,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
291
286
|
searchQuery,
|
|
292
287
|
localSearch,
|
|
293
288
|
pageSize,
|
|
294
|
-
rootViewsRef,showListEmptyComponent,emptyText
|
|
289
|
+
rootViewsRef, showListEmptyComponent, emptyText
|
|
295
290
|
]);
|
|
296
291
|
}
|
|
297
292
|
);
|
|
@@ -318,7 +313,7 @@ const AutoPositionedPopup = memo(
|
|
|
318
313
|
AutoPositionedPopupBtnStyle,
|
|
319
314
|
placeholder = 'Please Select',
|
|
320
315
|
onSubmitEditing,
|
|
321
|
-
TextInputProps = {},
|
|
316
|
+
TextInputProps = {autoFocus: true},
|
|
322
317
|
inputStyle,
|
|
323
318
|
labelStyle,
|
|
324
319
|
popUpViewStyle = {left: '5%', width: '90%'},
|
|
@@ -360,7 +355,7 @@ const AutoPositionedPopup = memo(
|
|
|
360
355
|
centerDisplay = false,
|
|
361
356
|
selectedItemBackgroundColor = 'rgba(116, 116, 128, 0.08)',
|
|
362
357
|
textAlign = 'right',
|
|
363
|
-
CustomPopView = undefined, CustomPopViewStyle,showListEmptyComponent=true,emptyText=''
|
|
358
|
+
CustomPopView = undefined, CustomPopViewStyle, showListEmptyComponent = true, emptyText = ''
|
|
364
359
|
} = props;
|
|
365
360
|
// State management similar to project implementation
|
|
366
361
|
const [state, setState] = useState<StateProps>({
|
|
@@ -381,6 +376,20 @@ const AutoPositionedPopup = memo(
|
|
|
381
376
|
const keyboardVisibleRef = useRef(false);
|
|
382
377
|
const refAutoPositionedPopup = useRef<View>(null);
|
|
383
378
|
const ref_searchQuery = useRef<string>('');
|
|
379
|
+
// Add ref to track previous keyboard state to avoid false triggers during parent component re-renders
|
|
380
|
+
const prevIsKeyboardFullyShownRef = useRef<boolean>(false);
|
|
381
|
+
const prevPropsRef = useRef<{
|
|
382
|
+
CustomPopView?: any;
|
|
383
|
+
CustomPopViewStyle?: any;
|
|
384
|
+
TextInputProps?: any;
|
|
385
|
+
}>({});
|
|
386
|
+
// Add ref to prevent onFocus/onBlur loop triggers during parent component re-renders
|
|
387
|
+
const lastFocusTimeRef = useRef<number>(0);
|
|
388
|
+
const isFocusEventProcessingRef = useRef<boolean>(false);
|
|
389
|
+
// Add ref to stabilize TextInput props reference
|
|
390
|
+
// Only update when deep comparison detects real changes to avoid TextInput recreation due to reference changes during parent component redraws
|
|
391
|
+
const stableInputStyleRef = useRef<any>(inputStyle);
|
|
392
|
+
const stableTextInputPropsRef = useRef<any>(TextInputProps);
|
|
384
393
|
// Simple keyboard status tracking (alternative to useKeyboardStatus hook)
|
|
385
394
|
// Legacy state for compatibility
|
|
386
395
|
const [isVisible, setIsVisible] = useState(false);
|
|
@@ -406,8 +415,7 @@ const AutoPositionedPopup = memo(
|
|
|
406
415
|
useEffect(() => {
|
|
407
416
|
(async () => {
|
|
408
417
|
})();
|
|
409
|
-
console.log(`AutoPositionedPopup componentDidMount tag
|
|
410
|
-
console.log('AutoPositionedPopup componentDidMount CustomPopView=', CustomPopView);
|
|
418
|
+
console.log(`AutoPositionedPopup componentDidMount=`, {tag, CustomPopView});
|
|
411
419
|
//componentWillUnmount
|
|
412
420
|
return () => {
|
|
413
421
|
console.log(`AutoPositionedPopup componentWillUnmount tag=`, tag);
|
|
@@ -440,8 +448,7 @@ const AutoPositionedPopup = memo(
|
|
|
440
448
|
}
|
|
441
449
|
}, [rootViews]);
|
|
442
450
|
useEffect(() => {
|
|
443
|
-
console.log('AutoPositionedPopup useEffect tag=', tag);
|
|
444
|
-
console.log('AutoPositionedPopup useEffect selectedItem=', selectedItem);
|
|
451
|
+
console.log('AutoPositionedPopup useEffect [selectedItem, state.selectedItem, tag]=', {tag, selectedItem, 'state.selectedItem': state.selectedItem});
|
|
445
452
|
console.log('AutoPositionedPopup useEffect state.selectedItem=', state.selectedItem);
|
|
446
453
|
if (state.selectedItem?.id !== selectedItem?.id || state.selectedItem?.title !== selectedItem?.title) {
|
|
447
454
|
console.log('AutoPositionedPopup useEffect selectedItem!=state.selectedItem');
|
|
@@ -454,51 +461,121 @@ const AutoPositionedPopup = memo(
|
|
|
454
461
|
}
|
|
455
462
|
}, [selectedItem, state.selectedItem, tag]);
|
|
456
463
|
useEffect(() => {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
464
|
+
// Detect if keyboard state has actually changed to avoid false triggers during parent component re-renders
|
|
465
|
+
const keyboardStateChanged = prevIsKeyboardFullyShownRef.current !== isKeyboardFullyShown;
|
|
466
|
+
const propsChanged =
|
|
467
|
+
prevPropsRef.current.CustomPopView !== CustomPopView ||
|
|
468
|
+
prevPropsRef.current.CustomPopViewStyle !== CustomPopViewStyle ||
|
|
469
|
+
prevPropsRef.current.TextInputProps !== TextInputProps;
|
|
470
|
+
console.log('AutoPositionedPopup useEffect [isKeyboardFullyShown,\n' +
|
|
471
|
+
' state.isFocus,\n' +
|
|
472
|
+
' useTextInput,\n' +
|
|
473
|
+
' CustomPopView,\n' +
|
|
474
|
+
' CustomPopViewStyle,\n' +
|
|
475
|
+
' forceRemoveAllRootViewOnItemSelected,\n' +
|
|
476
|
+
' tag, TextInputProps,\n' +
|
|
477
|
+
' state.selectedItem, showListEmptyComponent\n' +
|
|
478
|
+
' ]=', {
|
|
479
|
+
tag,
|
|
480
|
+
'state.isFocus': state.isFocus,
|
|
481
|
+
isKeyboardFullyShown,
|
|
482
|
+
'ref_isFocus.current': ref_isFocus.current,
|
|
483
|
+
'ref_isKeyboardFullyShown.current': ref_isKeyboardFullyShown.current,
|
|
484
|
+
useTextInput, TextInputProps,
|
|
485
|
+
'hasAddedRootView.current': hasAddedRootView.current,
|
|
486
|
+
'hasShownRootView.current': hasShownRootView.current,
|
|
487
|
+
'keyboardStateChanged': keyboardStateChanged,
|
|
488
|
+
'propsChanged': propsChanged
|
|
489
|
+
});
|
|
490
|
+
// Update ref to record current state
|
|
491
|
+
prevIsKeyboardFullyShownRef.current = isKeyboardFullyShown;
|
|
492
|
+
prevPropsRef.current = {
|
|
493
|
+
CustomPopView,
|
|
494
|
+
CustomPopViewStyle,
|
|
495
|
+
TextInputProps
|
|
496
|
+
};
|
|
497
|
+
// Only execute logic when keyboard state actually changes or user actively operates
|
|
498
|
+
if (!keyboardStateChanged && hasAddedRootView.current) {
|
|
499
|
+
console.log('AutoPositionedPopup: Skip execution - parent component re-rendered but keyboard state unchanged textInputRef.current=',textInputRef.current);
|
|
500
|
+
// if (!ref_isFocus.current) {
|
|
501
|
+
// textInputRef.current?.focus()
|
|
502
|
+
// }
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
469
505
|
if (useTextInput) {
|
|
470
506
|
if (isKeyboardFullyShown && hasAddedRootView.current && !hasShownRootView.current && state.isFocus) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
507
|
+
// CRITICAL FIX FOR KEYBOARD POSITION CALCULATION
|
|
508
|
+
// Problem: When keyboard appears, the page shifts up but measureInWindow executes too early
|
|
509
|
+
// Solution: Wait for keyboard animation (300ms) + use requestAnimationFrame for next render frame
|
|
510
|
+
//
|
|
511
|
+
// Timing breakdown:
|
|
512
|
+
// 1. Keyboard animation: ~250-300ms (iOS/Android)
|
|
513
|
+
// 2. Page shift animation: ~200-300ms (KeyboardAvoidingView)
|
|
514
|
+
// 3. Layout tree update: ~50-100ms (React Native)
|
|
515
|
+
// Total: ~500-700ms needed for stable layout
|
|
516
|
+
//
|
|
517
|
+
// Strategy: setTimeout(300ms) waits for most animations to complete,
|
|
518
|
+
// then requestAnimationFrame ensures measurement happens after next render frame
|
|
519
|
+
setTimeout(() => {
|
|
520
|
+
requestAnimationFrame(() => {
|
|
521
|
+
refAutoPositionedPopup.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
|
|
522
|
+
console.log('AutoPositionedPopup useTextInput measureInWindow (after 300ms + RAF, layout stable)=', {x, y, width, height});
|
|
523
|
+
// CRITICAL FIX: Coordinate system mismatch issue
|
|
524
|
+
// Problem: measureInWindow returns coordinates relative to window (fixed reference),
|
|
525
|
+
// but popup uses absolute positioning relative to App container (which shifts when keyboard appears)
|
|
526
|
+
//
|
|
527
|
+
// When keyboard appears:
|
|
528
|
+
// 1. measureInWindow returns y relative to window (e.g., y=400 after shifting)
|
|
529
|
+
// 2. But popup's absolute positioning is relative to App container
|
|
530
|
+
// 3. If App container shifted up by 200px, setting top=200 will display at window.y=0 (wrong!)
|
|
531
|
+
//
|
|
532
|
+
// Solution: Since popup is rendered at root level and uses absolute positioning,
|
|
533
|
+
// we should directly use measureInWindow's y value without additional calculations
|
|
534
|
+
// The popup container is at the same level as the page content
|
|
535
|
+
const screenHeight = Dimensions.get('window').height; // Use window height, not screen
|
|
536
|
+
console.log('AutoPositionedPopup useTextInput positioning data=', {
|
|
537
|
+
screenHeight,
|
|
538
|
+
componentY: y,
|
|
539
|
+
componentHeight: height,
|
|
540
|
+
listHeight: listLayout.height
|
|
541
|
+
});
|
|
542
|
+
// CORRECT POSITIONING LOGIC (as per user requirement):
|
|
543
|
+
// 1. ALWAYS try to show popup ABOVE the input field first
|
|
544
|
+
// 2. Only if that goes off the top of screen, show BELOW instead
|
|
545
|
+
// 3. Don't cover/overlap the input field
|
|
546
|
+
let popupY = y - listLayout.height; // Default: above input field
|
|
547
|
+
// Check if showing above would go off the top of screen
|
|
548
|
+
if (popupY < 0) {
|
|
549
|
+
console.log('AutoPositionedPopup with keyboard: would go off screen top, showing BELOW instead');
|
|
550
|
+
popupY = y + height; // Show below input field
|
|
551
|
+
// Also check if showing below would go off the bottom
|
|
552
|
+
const maxY = screenHeight - listLayout.height;
|
|
553
|
+
if (popupY > maxY) {
|
|
554
|
+
// If both positions are problematic, clamp to visible area
|
|
555
|
+
console.log('AutoPositionedPopup with keyboard: both positions problematic, clamping to visible area');
|
|
556
|
+
popupY = Math.min(Math.max(0, y - listLayout.height), maxY);
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
console.log('AutoPositionedPopup with keyboard: showing ABOVE input field (preferred position)');
|
|
560
|
+
}
|
|
561
|
+
ref_listPos.current = {x: x, y: popupY, width: width};
|
|
562
|
+
console.log('AutoPositionedPopup useTextInput final position=', ref_listPos.current);
|
|
563
|
+
setRootViewNativeStyle(tag, {
|
|
564
|
+
top: ref_listPos.current?.y,
|
|
565
|
+
left: popUpViewStyle?.left,
|
|
566
|
+
width: popUpViewStyle?.width,
|
|
567
|
+
height: listLayout.height,
|
|
568
|
+
opacity: 1,
|
|
569
|
+
});
|
|
570
|
+
hasShownRootView.current = true;
|
|
571
|
+
});
|
|
493
572
|
});
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
573
|
+
}, 300) // 300ms is sufficient for keyboard animation, as proven by user testing (even 3000ms didn't fix wrong logic)
|
|
574
|
+
} else if (!isKeyboardFullyShown && ref_isFocus.current && keyboardStateChanged) {
|
|
575
|
+
// Only execute close logic when keyboard state actually changes from true to false
|
|
497
576
|
console.log(
|
|
498
|
-
'AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView
|
|
499
|
-
tag,
|
|
500
|
-
' forceRemoveAllRootViewOnItemSelected=',
|
|
501
|
-
forceRemoveAllRootViewOnItemSelected
|
|
577
|
+
'AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView (keyboard state changed)=',
|
|
578
|
+
{tag, forceRemoveAllRootViewOnItemSelected, keyboardStateChanged}
|
|
502
579
|
);
|
|
503
580
|
removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
|
|
504
581
|
setState((prevState) => {
|
|
@@ -514,11 +591,10 @@ const AutoPositionedPopup = memo(
|
|
|
514
591
|
} else {
|
|
515
592
|
if (state.isFocus) {
|
|
516
593
|
refAutoPositionedPopup.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
|
|
517
|
-
console.log('AutoPositionedPopup measureInWindow
|
|
518
|
-
|
|
594
|
+
console.log('AutoPositionedPopup !useTextInput measureInWindow=', {x, y, width, height});
|
|
519
595
|
// INTELLIGENT POSITION CALCULATION - MODIFIED VERSION WITH STATUS BAR SAFETY
|
|
520
596
|
const calculateOptimalPosition = (componentY: number, componentHeight: number, popupHeight: number) => {
|
|
521
|
-
console.log('🔥🔥🔥 NEW CALCULATE OPTIMAL POSITION FUNCTION EXECUTING 🔥🔥🔥');
|
|
597
|
+
console.log('AutoPositionedPopup 🔥🔥🔥 NEW CALCULATE OPTIMAL POSITION FUNCTION EXECUTING 🔥🔥🔥');
|
|
522
598
|
|
|
523
599
|
// Use window height (visible area) instead of screen height (includes status bar)
|
|
524
600
|
const windowHeight = Dimensions.get('window').height;
|
|
@@ -536,7 +612,7 @@ const AutoPositionedPopup = memo(
|
|
|
536
612
|
}
|
|
537
613
|
};
|
|
538
614
|
const statusBarHeight = getStatusBarHeight();
|
|
539
|
-
console.log('🔥 Cross-platform StatusBar height:', statusBarHeight, 'Platform:', Platform.OS);
|
|
615
|
+
console.log('AutoPositionedPopup 🔥 Cross-platform StatusBar height:', statusBarHeight, 'Platform:', Platform.OS);
|
|
540
616
|
|
|
541
617
|
// Calculate component center point as requested
|
|
542
618
|
const componentCenterY = componentY + componentHeight / 2;
|
|
@@ -597,7 +673,7 @@ const AutoPositionedPopup = memo(
|
|
|
597
673
|
|
|
598
674
|
const finalSpacing = Math.max(baseSpacing, relativeSpacing) * edgeProximityFactor * platformMultiplier;
|
|
599
675
|
|
|
600
|
-
console.log('🔥 Advanced spacing calculation:', {
|
|
676
|
+
console.log('AutoPositionedPopup 🔥 Advanced spacing calculation:', {
|
|
601
677
|
componentCenter,
|
|
602
678
|
screenCenter,
|
|
603
679
|
distanceFromCenter,
|
|
@@ -634,41 +710,41 @@ const AutoPositionedPopup = memo(
|
|
|
634
710
|
// 'usableSpaceAbove >= needed': usableSpaceAbove >= popupHeight + POPUP_SPACING
|
|
635
711
|
// });
|
|
636
712
|
|
|
637
|
-
if (isInBottomHalf && usableSpaceAbove >= popupHeight
|
|
713
|
+
if (isInBottomHalf && usableSpaceAbove >= popupHeight) {
|
|
638
714
|
// Component in bottom half + enough space above = FORCE ABOVE
|
|
639
715
|
showAbove = true;
|
|
640
|
-
finalY = componentY - popupHeight +componentHeight/2;
|
|
641
|
-
console.log('🔥 AutoPositionedPopup: FORCE ABOVE - bottom half component with enough space, finalY=', finalY);
|
|
642
|
-
} else if (!isInBottomHalf && spaceBelow >= popupHeight
|
|
716
|
+
finalY = componentY - popupHeight + componentHeight / 2;
|
|
717
|
+
console.log('AutoPositionedPopup 🔥 AutoPositionedPopup: FORCE ABOVE - bottom half component with enough space, finalY=', finalY);
|
|
718
|
+
} else if (!isInBottomHalf && spaceBelow >= popupHeight) {
|
|
643
719
|
// Component in top half + enough space below = show below
|
|
644
720
|
showAbove = false;
|
|
645
|
-
finalY = componentY + componentHeight*2;
|
|
721
|
+
finalY = componentY + componentHeight * 2;
|
|
646
722
|
console.log('🔥 AutoPositionedPopup: Showing below - top half component with enough space, finalY=', finalY);
|
|
647
|
-
} else if (usableSpaceAbove >= popupHeight
|
|
723
|
+
} else if (usableSpaceAbove >= popupHeight) {
|
|
648
724
|
// Fallback: enough space above
|
|
649
725
|
showAbove = true;
|
|
650
|
-
finalY = componentY - popupHeight
|
|
726
|
+
finalY = componentY - popupHeight;
|
|
651
727
|
console.log('🔥 AutoPositionedPopup: Showing above - enough space available (fallback), finalY=', finalY);
|
|
652
|
-
} else if (spaceBelow >= popupHeight
|
|
728
|
+
} else if (spaceBelow >= popupHeight) {
|
|
653
729
|
// Fallback: enough space below
|
|
654
730
|
showAbove = false;
|
|
655
|
-
finalY = componentY + componentHeight
|
|
731
|
+
finalY = componentY + componentHeight;
|
|
656
732
|
console.log('🔥 AutoPositionedPopup: Showing below - enough space available (fallback), finalY=', finalY);
|
|
657
733
|
} else {
|
|
658
734
|
// Emergency fallback: choose larger space
|
|
659
735
|
if (usableSpaceAbove >= spaceBelow) {
|
|
660
736
|
showAbove = true;
|
|
661
|
-
finalY = Math.max(statusBarHeight, componentY - popupHeight
|
|
737
|
+
finalY = Math.max(statusBarHeight, componentY - popupHeight);
|
|
662
738
|
console.log('🔥 AutoPositionedPopup: Emergency above - larger space, finalY=', finalY);
|
|
663
739
|
} else {
|
|
664
740
|
showAbove = false;
|
|
665
|
-
finalY = componentY + componentHeight
|
|
741
|
+
finalY = componentY + componentHeight;
|
|
666
742
|
console.log('🔥 AutoPositionedPopup: Emergency below - larger space, finalY=', finalY);
|
|
667
743
|
}
|
|
668
744
|
}
|
|
669
745
|
|
|
670
746
|
// Enhanced boundary check with detailed logging
|
|
671
|
-
console.log('🔥 Pre-boundary check:', {
|
|
747
|
+
console.log('AutoPositionedPopup 🔥 Pre-boundary check:', {
|
|
672
748
|
originalFinalY: finalY,
|
|
673
749
|
showAbove,
|
|
674
750
|
statusBarHeight,
|
|
@@ -681,25 +757,25 @@ const AutoPositionedPopup = memo(
|
|
|
681
757
|
if (showAbove && finalY < statusBarHeight) {
|
|
682
758
|
const oldFinalY = finalY;
|
|
683
759
|
finalY = statusBarHeight;
|
|
684
|
-
console.log('🔥 BOUNDARY
|
|
760
|
+
console.log('AutoPositionedPopup 🔥 BOUNDARY : Above display adjusted for status bar:', oldFinalY, '->', finalY);
|
|
685
761
|
}
|
|
686
762
|
|
|
687
763
|
if (!showAbove && finalY + popupHeight > windowHeight) {
|
|
688
764
|
const oldFinalY = finalY;
|
|
689
765
|
finalY = windowHeight - popupHeight;
|
|
690
|
-
console.log('🔥 BOUNDARY
|
|
766
|
+
console.log('AutoPositionedPopup 🔥 BOUNDARY : Below display adjusted to fit window:', oldFinalY, '->', finalY);
|
|
691
767
|
}
|
|
692
768
|
|
|
693
769
|
// CRITICAL CHECK: Detect if boundary check is changing display direction
|
|
694
|
-
if (showAbove && finalY + popupHeight > componentY
|
|
695
|
-
console.log('🚨 WARNING: Above positioning may overlap with component!');
|
|
770
|
+
if (showAbove && finalY + popupHeight > componentY) {
|
|
771
|
+
console.log('AutoPositionedPopup 🚨 WARNING: Above positioning may overlap with component!');
|
|
696
772
|
}
|
|
697
773
|
|
|
698
|
-
if (!showAbove && finalY < componentY + componentHeight
|
|
699
|
-
console.log('🚨 WARNING: Below positioning may overlap with component!');
|
|
774
|
+
if (!showAbove && finalY < componentY + componentHeight) {
|
|
775
|
+
console.log('AutoPositionedPopup 🚨 WARNING: Below positioning may overlap with component!');
|
|
700
776
|
}
|
|
701
777
|
|
|
702
|
-
console.log('🔥 Post-boundary check final result:', {
|
|
778
|
+
console.log('AutoPositionedPopup 🔥 Post-boundary check final result:', {
|
|
703
779
|
finalY,
|
|
704
780
|
showAbove,
|
|
705
781
|
'popupTop': finalY,
|
|
@@ -715,21 +791,15 @@ const AutoPositionedPopup = memo(
|
|
|
715
791
|
const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
|
|
716
792
|
? CustomPopViewStyle.height
|
|
717
793
|
: listLayout.height;
|
|
718
|
-
|
|
719
|
-
console.log('🔥 Using actualPopupHeight for calculation:', actualPopupHeight, 'CustomPopView:', !!CustomPopView);
|
|
720
|
-
|
|
794
|
+
console.log('AutoPositionedPopup 🔥 Using actualPopupHeight for calculation:', {actualPopupHeight, CustomPopView: !!CustomPopView});
|
|
721
795
|
const positionResult = calculateOptimalPosition(y, height, actualPopupHeight);
|
|
722
796
|
console.log('AutoPositionedPopup FINAL position result:', positionResult);
|
|
723
|
-
|
|
724
797
|
ref_listPos.current = {x: x, y: positionResult.finalY, width: width};
|
|
725
|
-
console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
|
|
726
|
-
|
|
798
|
+
console.log('AutoPositionedPopup !useTextInput ref_listPos.current=', ref_listPos.current);
|
|
727
799
|
if (CustomPopView && CustomPopViewStyle) {
|
|
728
|
-
console.log('AutoPositionedPopup CustomPopViewStyle=', CustomPopViewStyle);
|
|
729
800
|
// Position already calculated correctly above, no need to recalculate
|
|
730
801
|
const PopViewComponent = CustomPopView();
|
|
731
|
-
console.log('AutoPositionedPopup addRootView
|
|
732
|
-
console.log('AutoPositionedPopup addRootView state.selectedItem=', state.selectedItem);
|
|
802
|
+
console.log('AutoPositionedPopup !useTextInput addRootView=', {CustomPopViewStyle, PopViewComponent, 'state.selectedItem': state.selectedItem});
|
|
733
803
|
addRootView({
|
|
734
804
|
id: tag,
|
|
735
805
|
style: !centerDisplay
|
|
@@ -761,7 +831,7 @@ const AutoPositionedPopup = memo(
|
|
|
761
831
|
centerDisplay,
|
|
762
832
|
});
|
|
763
833
|
} else {
|
|
764
|
-
console.log('AutoPositionedPopup addRootView tag=', tag);
|
|
834
|
+
console.log('AutoPositionedPopup !useTextInput addRootView tag=', tag);
|
|
765
835
|
addRootView({
|
|
766
836
|
id: tag,
|
|
767
837
|
style: {
|
|
@@ -817,8 +887,8 @@ const AutoPositionedPopup = memo(
|
|
|
817
887
|
CustomPopView,
|
|
818
888
|
CustomPopViewStyle,
|
|
819
889
|
forceRemoveAllRootViewOnItemSelected,
|
|
820
|
-
tag,
|
|
821
|
-
state.selectedItem,showListEmptyComponent
|
|
890
|
+
tag, TextInputProps,
|
|
891
|
+
state.selectedItem, showListEmptyComponent
|
|
822
892
|
]);
|
|
823
893
|
// Imperative handle for parent component access
|
|
824
894
|
useImperativeHandle(
|
|
@@ -837,7 +907,7 @@ const AutoPositionedPopup = memo(
|
|
|
837
907
|
[]
|
|
838
908
|
);
|
|
839
909
|
const updateState = (key: string, value: SelectedItem) => {
|
|
840
|
-
console.log('AutoPositionedPopup updateState
|
|
910
|
+
console.log('AutoPositionedPopup updateState=', {key, value});
|
|
841
911
|
setState((prevState) => ({
|
|
842
912
|
...prevState,
|
|
843
913
|
[key]: value,
|
|
@@ -858,6 +928,225 @@ const AutoPositionedPopup = memo(
|
|
|
858
928
|
setSearchQuery('');
|
|
859
929
|
}
|
|
860
930
|
};
|
|
931
|
+
|
|
932
|
+
// Simple deep comparison function (for style objects only)
|
|
933
|
+
const shallowEqual = (obj1: any, obj2: any): boolean => {
|
|
934
|
+
if (obj1 === obj2) return true;
|
|
935
|
+
if (!obj1 || !obj2) return false;
|
|
936
|
+
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false;
|
|
937
|
+
|
|
938
|
+
const keys1 = Object.keys(obj1);
|
|
939
|
+
const keys2 = Object.keys(obj2);
|
|
940
|
+
if (keys1.length !== keys2.length) return false;
|
|
941
|
+
|
|
942
|
+
for (const key of keys1) {
|
|
943
|
+
if (obj1[key] !== obj2[key]) return false;
|
|
944
|
+
}
|
|
945
|
+
return true;
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
// Use useMemo to create stable props reference
|
|
949
|
+
// Only update when deep comparison detects real changes to avoid TextInput recreation due to reference changes during parent component redraws
|
|
950
|
+
const stableInputStyle = useMemo(() => {
|
|
951
|
+
if (!shallowEqual(stableInputStyleRef.current, inputStyle)) {
|
|
952
|
+
console.log(`AutoPositionedPopup inputStyle deep change detected, updating stable reference - tag: ${tag}`);
|
|
953
|
+
stableInputStyleRef.current = inputStyle;
|
|
954
|
+
}
|
|
955
|
+
return stableInputStyleRef.current;
|
|
956
|
+
}, [inputStyle, tag]);
|
|
957
|
+
|
|
958
|
+
const stableTextInputProps = useMemo(() => {
|
|
959
|
+
if (!shallowEqual(stableTextInputPropsRef.current, TextInputProps)) {
|
|
960
|
+
console.log(`AutoPositionedPopup TextInputProps deep change detected, updating stable reference - tag: ${tag}`);
|
|
961
|
+
stableTextInputPropsRef.current = TextInputProps;
|
|
962
|
+
}
|
|
963
|
+
return stableTextInputPropsRef.current;
|
|
964
|
+
}, [TextInputProps, tag]);
|
|
965
|
+
|
|
966
|
+
// Use useCallback to stabilize onFocus and onBlur callback references
|
|
967
|
+
// Prevent creating new callback functions during parent component redraws to avoid TextInput re-triggering focus
|
|
968
|
+
// Use ref to store latest state values to avoid adding frequently changing values to dependencies
|
|
969
|
+
const stateRef = useRef(state);
|
|
970
|
+
stateRef.current = state;
|
|
971
|
+
|
|
972
|
+
const handleTextInputFocus = useCallback(() => {
|
|
973
|
+
const currentTime = Date.now();
|
|
974
|
+
const timeSinceLastFocus = currentTime - lastFocusTimeRef.current;
|
|
975
|
+
console.log(
|
|
976
|
+
'AutoPositionedPopup onFocus=',
|
|
977
|
+
{
|
|
978
|
+
tag,
|
|
979
|
+
'state.selectedItem': stateRef.current.selectedItem,
|
|
980
|
+
'hasTriggeredFocus.current=': hasTriggeredFocus.current,
|
|
981
|
+
'textInputRef.current=': textInputRef.current,
|
|
982
|
+
'ref_searchQuery.current=': ref_searchQuery.current,
|
|
983
|
+
'timeSinceLastFocus': timeSinceLastFocus,
|
|
984
|
+
'isKeyboardFullyShown': isKeyboardFullyShown,
|
|
985
|
+
'isFocusEventProcessing': isFocusEventProcessingRef.current
|
|
986
|
+
}
|
|
987
|
+
);
|
|
988
|
+
// Prevent rapid repeated triggers (repeated events within 300ms are ignored)
|
|
989
|
+
if (timeSinceLastFocus < 300) {
|
|
990
|
+
console.log('AutoPositionedPopup onFocus: Skip - event triggered too quickly (< 300ms)');
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
// Skip if keyboard is already open and focus has been handled
|
|
994
|
+
if (isKeyboardFullyShown && hasTriggeredFocus.current) {
|
|
995
|
+
console.log('AutoPositionedPopup onFocus: Skip - keyboard already open and focus handled');
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
// Prevent concurrent processing
|
|
999
|
+
if (isFocusEventProcessingRef.current) {
|
|
1000
|
+
console.log('AutoPositionedPopup onFocus: Skip - processing another focus event');
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
isFocusEventProcessingRef.current = true;
|
|
1004
|
+
lastFocusTimeRef.current = currentTime;
|
|
1005
|
+
if (!hasTriggeredFocus.current) {
|
|
1006
|
+
hasTriggeredFocus.current = true;
|
|
1007
|
+
ref_isFocus.current = true;
|
|
1008
|
+
if (stateRef.current.selectedItem) {
|
|
1009
|
+
ref_searchQuery.current = stateRef.current.selectedItem.title;
|
|
1010
|
+
}
|
|
1011
|
+
if (textInputRef.current && ref_searchQuery.current) {
|
|
1012
|
+
textInputRef.current.setNativeProps({
|
|
1013
|
+
text: ref_searchQuery.current,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
// Delay resetting processing flag to avoid blocking subsequent legitimate focus events
|
|
1018
|
+
setTimeout(() => {
|
|
1019
|
+
isFocusEventProcessingRef.current = false;
|
|
1020
|
+
}, 100);
|
|
1021
|
+
}, [tag, isKeyboardFullyShown]); // Remove state.selectedItem, use stateRef instead
|
|
1022
|
+
|
|
1023
|
+
const handleTextInputBlur = useCallback(() => {
|
|
1024
|
+
console.log(
|
|
1025
|
+
'AutoPositionedPopup onBlur=',
|
|
1026
|
+
{
|
|
1027
|
+
tag,
|
|
1028
|
+
'textInputRef.current': textInputRef.current,
|
|
1029
|
+
'isKeyboardFullyShown': isKeyboardFullyShown,
|
|
1030
|
+
'hasTriggeredFocus.current': hasTriggeredFocus.current
|
|
1031
|
+
}
|
|
1032
|
+
);
|
|
1033
|
+
// If keyboard is still open, this is a false trigger caused by parent component re-render, should not reset
|
|
1034
|
+
if (isKeyboardFullyShown && hasTriggeredFocus.current) {
|
|
1035
|
+
console.log('AutoPositionedPopup onBlur: Skip - keyboard still open, possibly caused by parent component re-render');
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Only reset internal state, do not actively close keyboard
|
|
1040
|
+
// Keyboard will close naturally when TextInput loses focus, no need to manually call Keyboard.dismiss()
|
|
1041
|
+
hasTriggeredFocus.current = false;
|
|
1042
|
+
hasAddedRootView.current = false;
|
|
1043
|
+
hasShownRootView.current = false;
|
|
1044
|
+
ref_isFocus.current = false;
|
|
1045
|
+
setState((prevState) => {
|
|
1046
|
+
return {
|
|
1047
|
+
...prevState,
|
|
1048
|
+
isFocus: false,
|
|
1049
|
+
};
|
|
1050
|
+
});
|
|
1051
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
|
|
1052
|
+
setSearchQuery('');
|
|
1053
|
+
if (textInputRef.current) {
|
|
1054
|
+
textInputRef.current.setNativeProps({text: ''});
|
|
1055
|
+
ref_searchQuery.current = '';
|
|
1056
|
+
// Remove textInputRef.current.blur() - avoid forcing blur causing keyboard to close
|
|
1057
|
+
}
|
|
1058
|
+
// Remove Keyboard.dismiss() - let keyboard close naturally to avoid triggering keyboardDidHide event
|
|
1059
|
+
}, [tag, isKeyboardFullyShown, forceRemoveAllRootViewOnItemSelected]);
|
|
1060
|
+
|
|
1061
|
+
// Wrap TextInput independently in useMemo to recreate only when key props change
|
|
1062
|
+
// This avoids repeated ref callback triggers due to other props changes during parent component redraws
|
|
1063
|
+
const memoizedTextInput = useMemo(() => {
|
|
1064
|
+
console.log(`AutoPositionedPopup useMemo creating TextInput - tag: ${tag}, isFocus: ${state.isFocus}`);
|
|
1065
|
+
if (!useTextInput || !state.isFocus) {
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
1068
|
+
return (
|
|
1069
|
+
<RNTextInput
|
|
1070
|
+
ref={(ref) => {
|
|
1071
|
+
// Monitor TextInput mounting and unmounting
|
|
1072
|
+
if (ref && !textInputRef.current) {
|
|
1073
|
+
console.log(`AutoPositionedPopup TextInput created/mounted - tag: ${tag}, ref:`, ref);
|
|
1074
|
+
} else if (!ref && textInputRef.current) {
|
|
1075
|
+
console.log(`AutoPositionedPopup TextInput unmounted - tag: ${tag}`);
|
|
1076
|
+
} else if (ref && textInputRef.current && ref !== textInputRef.current) {
|
|
1077
|
+
console.log(`AutoPositionedPopup TextInput replaced - tag: ${tag}, oldRef:`, textInputRef.current, 'newRef:', ref);
|
|
1078
|
+
}
|
|
1079
|
+
textInputRef.current = ref;
|
|
1080
|
+
}}
|
|
1081
|
+
key={`textinput-${tag}`}
|
|
1082
|
+
style={[
|
|
1083
|
+
styles.inputStyle,
|
|
1084
|
+
stableInputStyle,
|
|
1085
|
+
]}
|
|
1086
|
+
textAlign={stableTextInputProps['textAlign'] || 'left'}
|
|
1087
|
+
multiline={stableTextInputProps['multiline'] || false}
|
|
1088
|
+
numberOfLines={stableTextInputProps['numberOfLines'] || 1}
|
|
1089
|
+
onChangeText={(searchQuery) => {
|
|
1090
|
+
ref_searchQuery.current = searchQuery;
|
|
1091
|
+
console.log('AutoPositionedPopup onChangeText rootViews=', rootViews);
|
|
1092
|
+
if (!localSearch) {
|
|
1093
|
+
if (debounceTimerRef.current) {
|
|
1094
|
+
clearTimeout(debounceTimerRef.current);
|
|
1095
|
+
}
|
|
1096
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
1097
|
+
emitQueryChange(ref_searchQuery.current);
|
|
1098
|
+
}, 500);
|
|
1099
|
+
} else {
|
|
1100
|
+
emitQueryChange(ref_searchQuery.current);
|
|
1101
|
+
}
|
|
1102
|
+
}}
|
|
1103
|
+
placeholderTextColor={theme.colors.placeholderText}
|
|
1104
|
+
placeholder={placeholder}
|
|
1105
|
+
onKeyPress={(e) => {
|
|
1106
|
+
if (e.nativeEvent.key === 'Enter') {
|
|
1107
|
+
Keyboard.dismiss();
|
|
1108
|
+
}
|
|
1109
|
+
}}
|
|
1110
|
+
keyboardType={stableTextInputProps['keyboardType'] || 'default'}
|
|
1111
|
+
clearButtonMode="while-editing"
|
|
1112
|
+
returnKeyType={stableTextInputProps['returnKeyType'] || 'done'}
|
|
1113
|
+
maxLength={stableTextInputProps['maxLength'] || 100}
|
|
1114
|
+
accessibilityLabel="selectInput"
|
|
1115
|
+
accessible={true}
|
|
1116
|
+
autoFocus={stableTextInputProps['autoFocus'] || false}
|
|
1117
|
+
autoCorrect={false}
|
|
1118
|
+
underlineColorAndroid="transparent"
|
|
1119
|
+
editable={stableTextInputProps['editable'] || true}
|
|
1120
|
+
secureTextEntry={stableTextInputProps['secureTextEntry'] || false}
|
|
1121
|
+
defaultValue=""
|
|
1122
|
+
caretHidden={false}
|
|
1123
|
+
enablesReturnKeyAutomatically
|
|
1124
|
+
onFocus={handleTextInputFocus}
|
|
1125
|
+
onBlur={handleTextInputBlur}
|
|
1126
|
+
selectTextOnFocus={stableTextInputProps['selectTextOnFocus'] || false}
|
|
1127
|
+
onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
|
|
1128
|
+
console.log(
|
|
1129
|
+
'AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=',
|
|
1130
|
+
e.nativeEvent.text
|
|
1131
|
+
);
|
|
1132
|
+
onSubmitEditing && onSubmitEditing(e);
|
|
1133
|
+
}}
|
|
1134
|
+
/>
|
|
1135
|
+
);
|
|
1136
|
+
}, [
|
|
1137
|
+
tag, // tag 是稳定的
|
|
1138
|
+
useTextInput, // useTextInput 是稳定的
|
|
1139
|
+
state.isFocus, // isFocus 控制显示/隐藏
|
|
1140
|
+
handleTextInputFocus, // useCallback wrapped, reference stable
|
|
1141
|
+
handleTextInputBlur, // useCallback wrapped, reference stable
|
|
1142
|
+
stableInputStyle, // Use stable inputStyle reference (after deep comparison)
|
|
1143
|
+
stableTextInputProps, // Use stable TextInputProps reference (after deep comparison)
|
|
1144
|
+
placeholder, // placeholder usually stable
|
|
1145
|
+
onSubmitEditing, // onSubmitEditing usually stable
|
|
1146
|
+
// No longer use original inputStyle and TextInputProps, use stable references instead
|
|
1147
|
+
// Stable references only update when deep comparison detects actual content changes, avoiding frequent TextInput recreation during parent component redraws
|
|
1148
|
+
]);
|
|
1149
|
+
|
|
861
1150
|
// Render the component following project implementation
|
|
862
1151
|
return useMemo(() => {
|
|
863
1152
|
console.log('AutoPositionedPopup render tag=', tag); // Now safe - circular dependency fixed
|
|
@@ -869,22 +1158,15 @@ const AutoPositionedPopup = memo(
|
|
|
869
1158
|
style={[styles.AutoPositionedPopupBtn, AutoPositionedPopupBtnStyle]}
|
|
870
1159
|
disabled={AutoPositionedPopupBtnDisabled}
|
|
871
1160
|
onPress={() => {
|
|
872
|
-
console.log('AutoPositionedPopup onPress
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
'
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
hasShownRootView.current
|
|
882
|
-
);
|
|
883
|
-
console.log(
|
|
884
|
-
'AutoPositionedPopup onPress hasTriggeredFocus.current=',
|
|
885
|
-
hasTriggeredFocus.current
|
|
886
|
-
);
|
|
887
|
-
console.log('AutoPositionedPopup onPress state.selectedItem=', state.selectedItem);
|
|
1161
|
+
console.log('AutoPositionedPopup onPress=', {
|
|
1162
|
+
tag,
|
|
1163
|
+
'state.isFocus': state.isFocus,
|
|
1164
|
+
useTextInput,
|
|
1165
|
+
'hasAddedRootView.current': hasAddedRootView.current,
|
|
1166
|
+
'hasShownRootView.current': hasShownRootView.current,
|
|
1167
|
+
'hasTriggeredFocus.current': hasTriggeredFocus.current,
|
|
1168
|
+
'state.selectedItem': state.selectedItem
|
|
1169
|
+
});
|
|
888
1170
|
setState((prevState) => {
|
|
889
1171
|
return {
|
|
890
1172
|
...prevState,
|
|
@@ -892,6 +1174,7 @@ const AutoPositionedPopup = memo(
|
|
|
892
1174
|
};
|
|
893
1175
|
});
|
|
894
1176
|
if (!hasAddedRootView.current && useTextInput) {
|
|
1177
|
+
// TextInput version: hide first, show after keyboard is fully displayed
|
|
895
1178
|
hasAddedRootView.current = true;
|
|
896
1179
|
hasShownRootView.current = false;
|
|
897
1180
|
addRootView({
|
|
@@ -919,6 +1202,7 @@ const AutoPositionedPopup = memo(
|
|
|
919
1202
|
useModal: false,
|
|
920
1203
|
});
|
|
921
1204
|
}
|
|
1205
|
+
console.log('AutoPositionedPopup onPress done')
|
|
922
1206
|
}}
|
|
923
1207
|
>
|
|
924
1208
|
{!btwChildren ? (
|
|
@@ -938,141 +1222,38 @@ const AutoPositionedPopup = memo(
|
|
|
938
1222
|
)}
|
|
939
1223
|
</TouchableOpacity>
|
|
940
1224
|
) : (
|
|
941
|
-
|
|
942
|
-
state.isFocus && (
|
|
943
|
-
<RNTextInput
|
|
944
|
-
ref={textInputRef}
|
|
945
|
-
key="fixed-textinput-key"
|
|
946
|
-
style={[
|
|
947
|
-
styles.inputStyle,
|
|
948
|
-
inputStyle,
|
|
949
|
-
]}
|
|
950
|
-
textAlign={TextInputProps['textAlign'] || 'left'}
|
|
951
|
-
multiline={TextInputProps['multiline'] || false}
|
|
952
|
-
numberOfLines={TextInputProps['numberOfLines'] || 1}
|
|
953
|
-
onChangeText={(searchQuery) => {
|
|
954
|
-
ref_searchQuery.current = searchQuery;
|
|
955
|
-
console.log('AutoPositionedPopup onChangeText rootViews=', rootViews);
|
|
956
|
-
if (!localSearch) {
|
|
957
|
-
if (debounceTimerRef.current) {
|
|
958
|
-
clearTimeout(debounceTimerRef.current);
|
|
959
|
-
}
|
|
960
|
-
debounceTimerRef.current = setTimeout(() => {
|
|
961
|
-
emitQueryChange(ref_searchQuery.current);
|
|
962
|
-
}, 500);
|
|
963
|
-
} else {
|
|
964
|
-
emitQueryChange(ref_searchQuery.current);
|
|
965
|
-
}
|
|
966
|
-
}}
|
|
967
|
-
placeholderTextColor={theme.colors.placeholderText}
|
|
968
|
-
placeholder={placeholder}
|
|
969
|
-
onKeyPress={(e) => {
|
|
970
|
-
if (e.nativeEvent.key === 'Enter') {
|
|
971
|
-
Keyboard.dismiss();
|
|
972
|
-
}
|
|
973
|
-
}}
|
|
974
|
-
keyboardType={TextInputProps['keyboardType'] || 'default'}
|
|
975
|
-
clearButtonMode="while-editing"
|
|
976
|
-
returnKeyType={TextInputProps['returnKeyType'] || 'done'}
|
|
977
|
-
maxLength={TextInputProps['maxLength'] || 100}
|
|
978
|
-
accessibilityLabel="selectInput"
|
|
979
|
-
accessible={true}
|
|
980
|
-
autoFocus={TextInputProps['autoFocus'] || false}
|
|
981
|
-
autoCorrect={false}
|
|
982
|
-
underlineColorAndroid="transparent"
|
|
983
|
-
editable={TextInputProps['editable'] || true}
|
|
984
|
-
secureTextEntry={TextInputProps['secureTextEntry'] || false}
|
|
985
|
-
defaultValue=""
|
|
986
|
-
caretHidden={false}
|
|
987
|
-
enablesReturnKeyAutomatically
|
|
988
|
-
onFocus={() => {
|
|
989
|
-
console.log(
|
|
990
|
-
'AutoPositionedPopup onFocus tag=',
|
|
991
|
-
tag,
|
|
992
|
-
' selectedItem=',
|
|
993
|
-
state.selectedItem,
|
|
994
|
-
' hasTriggeredFocus.current=',
|
|
995
|
-
hasTriggeredFocus.current,
|
|
996
|
-
' textInputRef.current=',
|
|
997
|
-
textInputRef.current,
|
|
998
|
-
' ref_searchQuery.current=',
|
|
999
|
-
ref_searchQuery.current
|
|
1000
|
-
);
|
|
1001
|
-
if (!hasTriggeredFocus.current) {
|
|
1002
|
-
hasTriggeredFocus.current = true;
|
|
1003
|
-
ref_isFocus.current = true;
|
|
1004
|
-
if (state.selectedItem) {
|
|
1005
|
-
ref_searchQuery.current = state.selectedItem.title;
|
|
1006
|
-
}
|
|
1007
|
-
if (textInputRef.current && ref_searchQuery.current) {
|
|
1008
|
-
textInputRef.current.setNativeProps({
|
|
1009
|
-
text: ref_searchQuery.current,
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
}}
|
|
1014
|
-
onBlur={() => {
|
|
1015
|
-
console.log(
|
|
1016
|
-
'AutoPositionedPopup onBlur tag=',
|
|
1017
|
-
tag,
|
|
1018
|
-
'textInputRef.current=',
|
|
1019
|
-
textInputRef.current
|
|
1020
|
-
);
|
|
1021
|
-
hasTriggeredFocus.current = false;
|
|
1022
|
-
hasAddedRootView.current = false; // 重置 RootView 狀態
|
|
1023
|
-
hasShownRootView.current = false;
|
|
1024
|
-
ref_isFocus.current = false;
|
|
1025
|
-
setState((prevState) => {
|
|
1026
|
-
return {
|
|
1027
|
-
...prevState,
|
|
1028
|
-
isFocus: false,
|
|
1029
|
-
};
|
|
1030
|
-
});
|
|
1031
|
-
removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
|
|
1032
|
-
setSearchQuery('');
|
|
1033
|
-
if (textInputRef.current) {
|
|
1034
|
-
textInputRef.current.setNativeProps({text: ''});
|
|
1035
|
-
ref_searchQuery.current = '';
|
|
1036
|
-
textInputRef.current.blur();
|
|
1037
|
-
}
|
|
1038
|
-
Keyboard.dismiss();
|
|
1039
|
-
}}
|
|
1040
|
-
selectTextOnFocus={TextInputProps['selectTextOnFocus'] || false}
|
|
1041
|
-
onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
|
|
1042
|
-
console.log(
|
|
1043
|
-
'AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=',
|
|
1044
|
-
e.nativeEvent.text
|
|
1045
|
-
);
|
|
1046
|
-
onSubmitEditing && onSubmitEditing(e);
|
|
1047
|
-
}}
|
|
1048
|
-
/>
|
|
1049
|
-
)
|
|
1225
|
+
memoizedTextInput
|
|
1050
1226
|
)}
|
|
1051
1227
|
</View>
|
|
1052
1228
|
</CustomRow>
|
|
1053
1229
|
);
|
|
1054
|
-
}, [
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1230
|
+
}, [
|
|
1231
|
+
tag,
|
|
1232
|
+
// ✅ CRITICAL FIX: Remove all props that may change frequently or are inline functions
|
|
1233
|
+
// Changes to these props should not cause the entire component tree to recreate, especially TextInput
|
|
1234
|
+
// fetchData, // ❌ Removed: inline function
|
|
1235
|
+
// renderItem, // ❌ Removed: possibly inline function
|
|
1236
|
+
// onItemSelected, // ❌ Removed: possibly inline function
|
|
1237
|
+
// onSubmitEditing, // ❌ Removed: possibly inline function
|
|
1059
1238
|
localSearch,
|
|
1060
|
-
placeholder,
|
|
1061
|
-
textAlign,
|
|
1239
|
+
// placeholder, // ❌ Removed: may change
|
|
1240
|
+
// textAlign, // ❌ Removed: may change
|
|
1062
1241
|
pageSize,
|
|
1063
1242
|
selectedItem,
|
|
1064
|
-
CustomRow,
|
|
1243
|
+
// CustomRow, // ❌ Removed: inline function, new reference each time
|
|
1065
1244
|
useTextInput,
|
|
1066
|
-
btwChildren,
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
CustomPopViewStyle,
|
|
1245
|
+
// btwChildren, // ❌ Removed: inline function
|
|
1246
|
+
// keyExtractor, // ❌ Removed: possibly inline function
|
|
1247
|
+
// AutoPositionedPopupBtnStyle, // ❌ Removed: possibly inline object
|
|
1248
|
+
// CustomPopView, // ❌ Removed: may change
|
|
1249
|
+
// CustomPopViewStyle, // ❌ Removed: may change
|
|
1072
1250
|
forceRemoveAllRootViewOnItemSelected,
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1251
|
+
state.isFocus,
|
|
1252
|
+
showListEmptyComponent,
|
|
1253
|
+
emptyText,
|
|
1254
|
+
// ✅ Removed most dependencies that may cause re-rendering, keeping only core dependencies that truly affect component structure
|
|
1255
|
+
// This prevents TextInput recreation due to inline functions/objects during parent component redraws
|
|
1256
|
+
]);
|
|
1076
1257
|
}
|
|
1077
1258
|
)
|
|
1078
1259
|
);
|