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,3 +1,15 @@
|
|
|
1
|
+
// Module load marker - unique ID for tracking code version
|
|
2
|
+
// DEBUG FLAG: Set to false to disable all console logs for better performance
|
|
3
|
+
const POPUP_DEBUG = false;
|
|
4
|
+
const debugLog = (...args: any[]) => {
|
|
5
|
+
if (POPUP_DEBUG) {
|
|
6
|
+
console.log(...args);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Only log module load in debug mode
|
|
11
|
+
debugLog('POPUP_MODULE_V17_LOADED at ' + new Date().toISOString());
|
|
12
|
+
|
|
1
13
|
import React, {
|
|
2
14
|
ForwardedRef,
|
|
3
15
|
forwardRef,
|
|
@@ -35,7 +47,7 @@ import {useKeyboardStatus} from './KeyboardManager';
|
|
|
35
47
|
type QueryListener = (query: string) => void;
|
|
36
48
|
const queryChangeListeners: QueryListener[] = [];
|
|
37
49
|
const emitQueryChange = (query: string) => {
|
|
38
|
-
|
|
50
|
+
debugLog('AutoPositionedPopup.tsx emitQueryChange query=', query, ' listeners=', queryChangeListeners.length);
|
|
39
51
|
queryChangeListeners.forEach((l) => l(query));
|
|
40
52
|
};
|
|
41
53
|
const subscribeQueryChange = (listener: QueryListener) => {
|
|
@@ -92,7 +104,7 @@ const ListItem: React.FC<{
|
|
|
92
104
|
rootViewsRef.current = rootViews;
|
|
93
105
|
}, [rootViews]);
|
|
94
106
|
return useMemo(() => {
|
|
95
|
-
//
|
|
107
|
+
// debugLog('AutoPositionedPopup.tsx ListItem=', {index, item, selectedItem});
|
|
96
108
|
const isSelected = item.id === selectedItem?.id || item.title == selectedItem?.title;
|
|
97
109
|
return (
|
|
98
110
|
<TouchableOpacity
|
|
@@ -102,8 +114,8 @@ const ListItem: React.FC<{
|
|
|
102
114
|
{backgroundColor: isSelected ? (themeMode === 'light' ? 'rgba(116, 116, 128, 0.08)' : 'rgba(120, 120, 128, 0.36)') : 'transparent'},
|
|
103
115
|
]}
|
|
104
116
|
onPress={() => {
|
|
105
|
-
//
|
|
106
|
-
//
|
|
117
|
+
// debugLog('AutoPositionedPopup.tsx ListItem onPress item=', item); // Commented to prevent spam
|
|
118
|
+
// debugLog('AutoPositionedPopup.tsx ListItem onPress rootViews=', rootViewsRef.current); // Commented to prevent spam
|
|
107
119
|
updateState('selectedItem', item);
|
|
108
120
|
}}
|
|
109
121
|
>
|
|
@@ -171,16 +183,16 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
171
183
|
useEffect(() => {
|
|
172
184
|
(async () => {
|
|
173
185
|
})();
|
|
174
|
-
|
|
186
|
+
debugLog(`AutoPositionedPopupList componentDidMount`);
|
|
175
187
|
//componentWillUnmount
|
|
176
188
|
return () => {
|
|
177
|
-
|
|
189
|
+
debugLog(`AutoPositionedPopupList componentWillUnmount`);
|
|
178
190
|
setSearchQuery('');
|
|
179
191
|
};
|
|
180
192
|
}, []);
|
|
181
193
|
useEffect(() => {
|
|
182
194
|
const unsubscribe = subscribeQueryChange((newQuery: string) => {
|
|
183
|
-
|
|
195
|
+
debugLog('AutoPositionedPopupList useEffect subscribeQueryChange newQuery=', newQuery);
|
|
184
196
|
ref_searchQuery.current = newQuery;
|
|
185
197
|
if (ref_list.current) {
|
|
186
198
|
ref_list.current.scrollToTop();
|
|
@@ -190,24 +202,24 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
190
202
|
return unsubscribe;
|
|
191
203
|
}, []);
|
|
192
204
|
const _updateState = (key: string, value: SelectedItem) => {
|
|
193
|
-
|
|
205
|
+
debugLog('AutoPositionedPopupList _updateState key=', key, ' value=', value);
|
|
194
206
|
setState((prevState) => ({
|
|
195
207
|
...prevState,
|
|
196
208
|
[key]: value,
|
|
197
209
|
}));
|
|
198
|
-
|
|
210
|
+
debugLog('AutoPositionedPopupList _updateState rootViews=', rootViewsRef.current);
|
|
199
211
|
updateState(key, value);
|
|
200
212
|
};
|
|
201
213
|
const _fetchData = async ({
|
|
202
214
|
pageIndex,
|
|
203
215
|
pageSize: currentPageSize,
|
|
204
216
|
}: FetchDataParams): Promise<ListData | null> => {
|
|
205
|
-
|
|
217
|
+
debugLog('AutoPositionedPopupList _fetchData=', {pageIndex, pageSize: currentPageSize, 'state.localData': state.localData, 'ref_searchQuery.current': ref_searchQuery.current, localSearch});
|
|
206
218
|
if (localSearch && state.localData.length > 0) {
|
|
207
219
|
const result: SelectedItem[] = state.localData.filter((item: SelectedItem) => {
|
|
208
220
|
return `${item.title}`?.toLowerCase().includes(ref_searchQuery.current.toLowerCase());
|
|
209
221
|
});
|
|
210
|
-
|
|
222
|
+
debugLog('AutoPositionedPopupList _fetchData localSearch result=', result);
|
|
211
223
|
return Promise.resolve({
|
|
212
224
|
items: result,
|
|
213
225
|
pageIndex: 0,
|
|
@@ -220,7 +232,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
220
232
|
pageSize: pageSize || 10,
|
|
221
233
|
searchQuery: ref_searchQuery.current,
|
|
222
234
|
});
|
|
223
|
-
|
|
235
|
+
debugLog('AutoPositionedPopupList _fetchData res=', res);
|
|
224
236
|
if (res?.items && localSearch) {
|
|
225
237
|
setState((prevState) => {
|
|
226
238
|
return {
|
|
@@ -239,9 +251,9 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
239
251
|
}
|
|
240
252
|
return null;
|
|
241
253
|
} catch (e) {
|
|
242
|
-
|
|
254
|
+
debugLog('Error in fetchData:', e);
|
|
243
255
|
}
|
|
244
|
-
|
|
256
|
+
debugLog('AutoPositionedPopupList _fetchData res=', null);
|
|
245
257
|
return null;
|
|
246
258
|
};
|
|
247
259
|
const _renderItem = useCallback(
|
|
@@ -251,7 +263,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
251
263
|
[state.selectedItem, themeMode]
|
|
252
264
|
);
|
|
253
265
|
return useMemo(() => {
|
|
254
|
-
|
|
266
|
+
debugLog('AutoPositionedPopupList (global as any)?.$fake=', (global as any)?.$fake);
|
|
255
267
|
// Babel configuration handles the path redirection based on global.$fake
|
|
256
268
|
// No need for conditional import here
|
|
257
269
|
return (
|
|
@@ -298,7 +310,7 @@ const listLayout = {
|
|
|
298
310
|
const AutoPositionedPopup = memo(
|
|
299
311
|
forwardRef<unknown, AutoPositionedPopupProps>(
|
|
300
312
|
(props: AutoPositionedPopupProps, parentRef: ForwardedRef<unknown>): React.JSX.Element => {
|
|
301
|
-
|
|
313
|
+
debugLog('AutoPositionedPopup props=', props);
|
|
302
314
|
const {
|
|
303
315
|
tag,
|
|
304
316
|
style,
|
|
@@ -325,11 +337,11 @@ const AutoPositionedPopup = memo(
|
|
|
325
337
|
};
|
|
326
338
|
try {
|
|
327
339
|
// const res1: any[] = await $api.xxx(pageSize)
|
|
328
|
-
//
|
|
340
|
+
// debugLog('${NAME} xxx res=', res)
|
|
329
341
|
// res.items = res1
|
|
330
342
|
// res.needLoadMore = res1.length === pageSize
|
|
331
343
|
} catch (e) {
|
|
332
|
-
|
|
344
|
+
debugLog('Error in fetch operation:', e);
|
|
333
345
|
}
|
|
334
346
|
return res;
|
|
335
347
|
},
|
|
@@ -356,7 +368,7 @@ const AutoPositionedPopup = memo(
|
|
|
356
368
|
selectedItem: selectedItem,
|
|
357
369
|
});
|
|
358
370
|
// Use RootView context
|
|
359
|
-
const {addRootView, setRootViewNativeStyle, removeRootView, rootViews, setSearchQuery} = useRootView();
|
|
371
|
+
const {addRootView, setRootViewNativeStyle, updateRootView, removeRootView, rootViews, setSearchQuery} = useRootView();
|
|
360
372
|
const rootViewsRef = useRef(rootViews);
|
|
361
373
|
// Track TextInput focus and RootView states like project implementation
|
|
362
374
|
const hasTriggeredFocus = useRef(false);
|
|
@@ -418,7 +430,7 @@ const AutoPositionedPopup = memo(
|
|
|
418
430
|
*/
|
|
419
431
|
const scrollParentToTrigger = useCallback(() => {
|
|
420
432
|
if (!parentScrollViewRef?.current || !triggerBtnRef.current) {
|
|
421
|
-
|
|
433
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: No parentScrollViewRef or triggerBtnRef available');
|
|
422
434
|
return;
|
|
423
435
|
}
|
|
424
436
|
|
|
@@ -428,18 +440,18 @@ const AutoPositionedPopup = memo(
|
|
|
428
440
|
const nodeHandle = findNodeHandle(triggerBtnRef.current);
|
|
429
441
|
|
|
430
442
|
if (nodeHandle && scrollView) {
|
|
431
|
-
|
|
443
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: Scrolling to trigger button with extraHeight=', scrollExtraHeight);
|
|
432
444
|
|
|
433
445
|
// KeyboardAwareScrollView has a scrollToFocusedInput method that handles this
|
|
434
446
|
// However, it requires a ReactNode. We'll use scrollToPosition as an alternative.
|
|
435
447
|
// First, measure the trigger button position relative to the ScrollView
|
|
436
448
|
triggerBtnRef.current.measureInWindow((x, y, width, height) => {
|
|
437
449
|
if (y === undefined || height === undefined) {
|
|
438
|
-
|
|
450
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: measureInWindow returned undefined');
|
|
439
451
|
return;
|
|
440
452
|
}
|
|
441
453
|
|
|
442
|
-
|
|
454
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: trigger position=', { x, y, width, height });
|
|
443
455
|
|
|
444
456
|
// Get keyboard height from Keyboard API
|
|
445
457
|
// On keyboard show, scroll to position that keeps trigger above keyboard
|
|
@@ -451,7 +463,7 @@ const AutoPositionedPopup = memo(
|
|
|
451
463
|
const triggerBottom = y + height;
|
|
452
464
|
const visibleAreaBottom = screenHeight - keyboardHeight;
|
|
453
465
|
|
|
454
|
-
|
|
466
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: keyboard data=', {
|
|
455
467
|
keyboardHeight,
|
|
456
468
|
screenHeight,
|
|
457
469
|
triggerBottom,
|
|
@@ -462,7 +474,7 @@ const AutoPositionedPopup = memo(
|
|
|
462
474
|
if (triggerBottom > visibleAreaBottom) {
|
|
463
475
|
// Calculate how much to scroll
|
|
464
476
|
const scrollAmount = triggerBottom - visibleAreaBottom + scrollExtraHeight;
|
|
465
|
-
|
|
477
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: scrolling by', scrollAmount);
|
|
466
478
|
|
|
467
479
|
// Use scrollForExtraHeightOnAndroid or scrollToPosition
|
|
468
480
|
if (typeof scrollView.scrollToPosition === 'function') {
|
|
@@ -470,7 +482,7 @@ const AutoPositionedPopup = memo(
|
|
|
470
482
|
scrollView.scrollToPosition(0, scrollAmount, true);
|
|
471
483
|
} else if (typeof scrollView.scrollToEnd === 'function') {
|
|
472
484
|
// Fallback: scroll to end might help in some cases
|
|
473
|
-
|
|
485
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: using scrollToEnd fallback');
|
|
474
486
|
}
|
|
475
487
|
}
|
|
476
488
|
});
|
|
@@ -483,21 +495,21 @@ const AutoPositionedPopup = memo(
|
|
|
483
495
|
* Uses stored trigger position (captured before TextInput replaces the trigger button)
|
|
484
496
|
*/
|
|
485
497
|
const scrollToTriggerWithMeasure = useCallback(() => {
|
|
486
|
-
|
|
498
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure called, tag=', tag, {
|
|
487
499
|
hasParentScrollViewRef: !!parentScrollViewRef?.current,
|
|
488
500
|
hasTriggerPosition: !!triggerPositionRef.current,
|
|
489
501
|
triggerPosition: triggerPositionRef.current
|
|
490
502
|
});
|
|
491
503
|
|
|
492
504
|
if (!parentScrollViewRef?.current) {
|
|
493
|
-
|
|
505
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: parentScrollViewRef not available, tag=', tag);
|
|
494
506
|
return;
|
|
495
507
|
}
|
|
496
508
|
|
|
497
509
|
// Use stored trigger position (captured when trigger was clicked)
|
|
498
510
|
const storedPosition = triggerPositionRef.current;
|
|
499
511
|
if (!storedPosition) {
|
|
500
|
-
|
|
512
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: no stored trigger position, tag=', tag);
|
|
501
513
|
return;
|
|
502
514
|
}
|
|
503
515
|
|
|
@@ -513,7 +525,7 @@ const AutoPositionedPopup = memo(
|
|
|
513
525
|
const visibleAreaBottom = screenHeight - keyboardApproxHeight;
|
|
514
526
|
const triggerBottom = triggerY + triggerHeight;
|
|
515
527
|
|
|
516
|
-
|
|
528
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: calculations=', {
|
|
517
529
|
tag,
|
|
518
530
|
triggerY,
|
|
519
531
|
triggerHeight,
|
|
@@ -527,7 +539,7 @@ const AutoPositionedPopup = memo(
|
|
|
527
539
|
if (triggerBottom > visibleAreaBottom) {
|
|
528
540
|
// Calculate scroll amount to bring trigger above keyboard
|
|
529
541
|
const scrollAmount = triggerBottom - visibleAreaBottom + scrollExtraHeight;
|
|
530
|
-
|
|
542
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: scrolling, amount=', scrollAmount, 'tag=', tag);
|
|
531
543
|
|
|
532
544
|
// Use scrollForExtraHeightOnAndroid for KeyboardAwareScrollView
|
|
533
545
|
if (typeof scrollView.scrollForExtraHeightOnAndroid === 'function') {
|
|
@@ -538,10 +550,10 @@ const AutoPositionedPopup = memo(
|
|
|
538
550
|
// Fallback to standard ScrollView method
|
|
539
551
|
(scrollView as any).scrollTo({ y: scrollAmount, animated: true });
|
|
540
552
|
} else {
|
|
541
|
-
|
|
553
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: no scroll method available on scrollView');
|
|
542
554
|
}
|
|
543
555
|
} else {
|
|
544
|
-
|
|
556
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: trigger already visible, no scroll needed, tag=', tag);
|
|
545
557
|
}
|
|
546
558
|
}, [parentScrollViewRef, scrollExtraHeight, tag]);
|
|
547
559
|
|
|
@@ -551,10 +563,10 @@ const AutoPositionedPopup = memo(
|
|
|
551
563
|
useEffect(() => {
|
|
552
564
|
(async () => {
|
|
553
565
|
})();
|
|
554
|
-
|
|
566
|
+
debugLog(`AutoPositionedPopup componentDidMount=`, {tag, CustomPopView});
|
|
555
567
|
//componentWillUnmount
|
|
556
568
|
return () => {
|
|
557
|
-
|
|
569
|
+
debugLog(`AutoPositionedPopup componentWillUnmount tag=`, tag);
|
|
558
570
|
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
559
571
|
setSearchQuery('');
|
|
560
572
|
if (textInputRef.current) {
|
|
@@ -567,7 +579,7 @@ const AutoPositionedPopup = memo(
|
|
|
567
579
|
};
|
|
568
580
|
}, []);
|
|
569
581
|
useEffect(() => {
|
|
570
|
-
|
|
582
|
+
debugLog('AutoPositionedPopup rootViews=', {tag, rootViews});
|
|
571
583
|
rootViewsRef.current = rootViews;
|
|
572
584
|
if (rootViews.length === 0) {
|
|
573
585
|
hasAddedRootView.current = false;
|
|
@@ -584,10 +596,10 @@ const AutoPositionedPopup = memo(
|
|
|
584
596
|
}
|
|
585
597
|
}, [rootViews]);
|
|
586
598
|
useEffect(() => {
|
|
587
|
-
|
|
588
|
-
|
|
599
|
+
debugLog('AutoPositionedPopup useEffect [selectedItem, state.selectedItem, tag]=', {tag, selectedItem, 'state.selectedItem': state.selectedItem});
|
|
600
|
+
debugLog('AutoPositionedPopup useEffect state.selectedItem=', state.selectedItem);
|
|
589
601
|
if (state.selectedItem?.id !== selectedItem?.id || state.selectedItem?.title != selectedItem?.title) {
|
|
590
|
-
|
|
602
|
+
debugLog('AutoPositionedPopup useEffect selectedItem!=state.selectedItem');
|
|
591
603
|
setState((prevState) => {
|
|
592
604
|
return {
|
|
593
605
|
...prevState,
|
|
@@ -603,7 +615,7 @@ const AutoPositionedPopup = memo(
|
|
|
603
615
|
prevPropsRef.current.CustomPopView !== CustomPopView ||
|
|
604
616
|
prevPropsRef.current.CustomPopViewStyle !== CustomPopViewStyle ||
|
|
605
617
|
(prevPropsRef.current.TextInputProps !== TextInputProps && useTextInput);
|
|
606
|
-
|
|
618
|
+
debugLog('AutoPositionedPopup useEffect [isKeyboardFullyShown,\n' +
|
|
607
619
|
' state.isFocus,\n' +
|
|
608
620
|
' useTextInput,\n' +
|
|
609
621
|
' CustomPopView,\n' +
|
|
@@ -634,13 +646,19 @@ const AutoPositionedPopup = memo(
|
|
|
634
646
|
TextInputProps
|
|
635
647
|
};
|
|
636
648
|
// Only execute logic when keyboard state actually changes or user actively operates
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
649
|
+
// CRITICAL FIX: Also allow execution when popup needs initial positioning
|
|
650
|
+
// hasAddedRootView.current = true means popup container exists
|
|
651
|
+
// hasShownRootView.current = false means positioning not done yet
|
|
652
|
+
// We MUST allow execution when popup needs positioning, even if keyboard state unchanged
|
|
653
|
+
if (!keyboardStateChanged && hasAddedRootView.current && hasShownRootView.current) {
|
|
654
|
+
debugLog('AutoPositionedPopup: Skip execution - already positioned and keyboard state unchanged');
|
|
642
655
|
return;
|
|
643
656
|
}
|
|
657
|
+
|
|
658
|
+
// Log when we're allowing execution for initial positioning
|
|
659
|
+
if (!keyboardStateChanged && hasAddedRootView.current && !hasShownRootView.current) {
|
|
660
|
+
debugLog('AutoPositionedPopup: ALLOWING execution for initial positioning (popup added but not positioned yet)');
|
|
661
|
+
}
|
|
644
662
|
const getStatusBarHeight = (): number => {
|
|
645
663
|
if (Platform.OS === 'android') {
|
|
646
664
|
// Android: Use StatusBar.currentHeight API
|
|
@@ -660,7 +678,7 @@ const AutoPositionedPopup = memo(
|
|
|
660
678
|
// When keyboard appears, the trigger button may be covered. If parentScrollViewRef
|
|
661
679
|
// is provided, scroll the parent to keep the trigger visible above the keyboard.
|
|
662
680
|
if (parentScrollViewRef?.current) {
|
|
663
|
-
|
|
681
|
+
debugLog('AutoPositionedPopup: Keyboard appeared, scrolling parent to keep trigger visible');
|
|
664
682
|
// Use a slight delay to ensure keyboard animation has started
|
|
665
683
|
setTimeout(() => {
|
|
666
684
|
scrollToTriggerWithMeasure();
|
|
@@ -681,105 +699,164 @@ const AutoPositionedPopup = memo(
|
|
|
681
699
|
// then requestAnimationFrame ensures measurement happens after next render frame
|
|
682
700
|
setTimeout(() => {
|
|
683
701
|
requestAnimationFrame(() => {
|
|
684
|
-
// CRITICAL FIX:
|
|
685
|
-
//
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
702
|
+
// CRITICAL FIX: Measure CURRENT position AFTER keyboard animation completes
|
|
703
|
+
// DO NOT use stored triggerPositionRef because keyboard may have shifted the view up
|
|
704
|
+
// Instead, measure the outer wrapper (refAutoPositionedPopup)
|
|
705
|
+
// which reflects the ACTUAL current position after keyboard shift
|
|
706
|
+
|
|
707
|
+
// DEBUG: Log both refs to compare their positions
|
|
708
|
+
debugLog('AutoPositionedPopup DEBUG: refs status=', {
|
|
709
|
+
hasTextInputRef: !!textInputRef.current,
|
|
710
|
+
hasRefAutoPositionedPopup: !!refAutoPositionedPopup.current,
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// Measure BOTH refs for comparison
|
|
714
|
+
if (textInputRef.current && refAutoPositionedPopup.current) {
|
|
715
|
+
textInputRef.current.measureInWindow((tx: number | undefined, ty: number | undefined, tw: number | undefined, th: number | undefined) => {
|
|
716
|
+
debugLog('AutoPositionedPopup DEBUG: textInputRef position=', {x: tx, y: ty, width: tw, height: th});
|
|
717
|
+
});
|
|
718
|
+
refAutoPositionedPopup.current.measureInWindow((rx: number | undefined, ry: number | undefined, rw: number | undefined, rh: number | undefined) => {
|
|
719
|
+
debugLog('AutoPositionedPopup DEBUG: refAutoPositionedPopup position=', {x: rx, y: ry, width: rw, height: rh});
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// CRITICAL FIX: Use textInputRef as primary measurement target
|
|
724
|
+
// refAutoPositionedPopup.measureInWindow() returns undefined values
|
|
725
|
+
// because the outer wrapper View uses flex:1/height:100% which makes it unmeasurable
|
|
726
|
+
// textInputRef reliably returns the actual position of the input field
|
|
727
|
+
const measureTarget = textInputRef.current || refAutoPositionedPopup.current;
|
|
728
|
+
|
|
729
|
+
if (!measureTarget) {
|
|
730
|
+
debugLog('AutoPositionedPopup useTextInput: no measureTarget available, using fallback');
|
|
731
|
+
const screenHeightFallback = Dimensions.get('window').height;
|
|
732
|
+
const screenWidthFallback = Dimensions.get('window').width;
|
|
733
|
+
const fallbackY = (screenHeightFallback - listLayout.height) / 2;
|
|
734
|
+
ref_listPos.current = {x: screenWidthFallback * 0.05, y: fallbackY, width: screenWidthFallback * 0.9};
|
|
735
|
+
updateRootView(tag, {
|
|
736
|
+
style: {
|
|
737
|
+
top: ref_listPos.current?.y,
|
|
738
|
+
left: popUpViewStyle?.left,
|
|
739
|
+
width: popUpViewStyle?.width,
|
|
740
|
+
height: listLayout.height,
|
|
741
|
+
opacity: 1,
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
hasShownRootView.current = true;
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Determine which ref is actually being used (for logging)
|
|
749
|
+
const usingTextInputRef = measureTarget === textInputRef.current;
|
|
750
|
+
debugLog('AutoPositionedPopup useTextInput: using measureTarget=', usingTextInputRef ? 'textInputRef' : 'refAutoPositionedPopup');
|
|
751
|
+
|
|
752
|
+
// Measure CURRENT position (after keyboard shifted the view)
|
|
753
|
+
measureTarget.measureInWindow((x: number | undefined, y: number | undefined, width: number | undefined, height: number | undefined) => {
|
|
754
|
+
debugLog('AutoPositionedPopup useTextInput: measured position for positioning=', {
|
|
755
|
+
x, y, width, height,
|
|
756
|
+
measureTarget: usingTextInputRef ? 'textInputRef' : 'refAutoPositionedPopup'
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// Handle undefined values (can happen during navigation transitions)
|
|
691
760
|
if (x === undefined || y === undefined || width === undefined || height === undefined) {
|
|
692
|
-
|
|
761
|
+
debugLog('AutoPositionedPopup useTextInput: measureInWindow returned undefined, using fallback');
|
|
693
762
|
const screenHeightFallback = Dimensions.get('window').height;
|
|
694
763
|
const screenWidthFallback = Dimensions.get('window').width;
|
|
695
764
|
const fallbackY = (screenHeightFallback - listLayout.height) / 2;
|
|
696
|
-
|
|
697
|
-
const fallbackWidth = screenWidthFallback * 0.9;
|
|
698
|
-
x = fallbackX;
|
|
765
|
+
x = screenWidthFallback * 0.05;
|
|
699
766
|
y = fallbackY;
|
|
700
|
-
width =
|
|
767
|
+
width = screenWidthFallback * 0.9;
|
|
701
768
|
height = 50;
|
|
702
769
|
}
|
|
703
|
-
|
|
704
|
-
//
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
// When keyboard appears:
|
|
708
|
-
// 1. measureInWindow returns y relative to window (e.g., y=400 after shifting)
|
|
709
|
-
// 2. But popup's absolute positioning is relative to App container
|
|
710
|
-
// 3. If App container shifted up by 200px, setting top=200 will display at window.y=0 (wrong!)
|
|
711
|
-
//
|
|
712
|
-
// Solution: Since popup is rendered at root level and uses absolute positioning,
|
|
713
|
-
// we should directly use measureInWindow's y value without additional calculations
|
|
714
|
-
// The popup container is at the same level as the page content
|
|
715
|
-
const screenHeight = Dimensions.get('window').height; // Use window height, not screen
|
|
716
|
-
console.log('AutoPositionedPopup useTextInput positioning data=', {
|
|
770
|
+
|
|
771
|
+
// Calculate screen height and popup position
|
|
772
|
+
const screenHeight = Dimensions.get('window').height;
|
|
773
|
+
debugLog('AutoPositionedPopup useTextInput positioning data=', {
|
|
717
774
|
screenHeight,
|
|
718
775
|
componentY: y,
|
|
719
776
|
componentHeight: height,
|
|
720
777
|
listHeight: listLayout.height
|
|
721
778
|
});
|
|
722
|
-
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
console.log('AutoPositionedPopup with keyboard: initial calculation for ABOVE position:', {
|
|
734
|
-
componentY: y,
|
|
735
|
-
componentHeight: height,
|
|
779
|
+
|
|
780
|
+
// POSITIONING LOGIC (with keyboard):
|
|
781
|
+
// Simple rule: popup must TOUCH the trigger with NO GAP
|
|
782
|
+
// 1. Default: show ABOVE trigger (popup bottom touches trigger top)
|
|
783
|
+
// 2. If ABOVE would overlap status bar: show BELOW (popup top touches trigger bottom)
|
|
784
|
+
|
|
785
|
+
debugLog('AutoPositionedPopup POSITIONING:', {
|
|
786
|
+
triggerY: y,
|
|
787
|
+
triggerHeight: height,
|
|
788
|
+
triggerBottom: y + height,
|
|
736
789
|
popupHeight: listLayout.height,
|
|
790
|
+
screenHeight,
|
|
791
|
+
statusBarHeight
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// 1. Default: show popup ABOVE the trigger
|
|
795
|
+
// Popup has internal padding (12px from autoPositionedPopupList style)
|
|
796
|
+
// To make popup CONTENT touch trigger (not container), add padding offset
|
|
797
|
+
// Container bottom at y + POPUP_PADDING, content bottom at y (no gap)
|
|
798
|
+
const POPUP_PADDING = 12;
|
|
799
|
+
let popupY = y - listLayout.height + POPUP_PADDING;
|
|
800
|
+
let position = 'ABOVE';
|
|
801
|
+
|
|
802
|
+
debugLog('AutoPositionedPopup: trying ABOVE position:', {
|
|
737
803
|
popupY,
|
|
738
804
|
popupBottom: popupY + listLayout.height,
|
|
805
|
+
contentBottom: popupY + listLayout.height - POPUP_PADDING,
|
|
739
806
|
triggerTop: y,
|
|
740
|
-
|
|
807
|
+
paddingOffset: POPUP_PADDING,
|
|
808
|
+
wouldOverlapStatusBar: popupY < statusBarHeight
|
|
741
809
|
});
|
|
742
810
|
|
|
743
|
-
// 2.
|
|
811
|
+
// 2. If showing ABOVE would go behind status bar, show BELOW instead
|
|
744
812
|
if (popupY < statusBarHeight) {
|
|
745
|
-
|
|
746
|
-
//
|
|
747
|
-
//
|
|
748
|
-
|
|
749
|
-
popupY = y + height +
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
813
|
+
// Show BELOW: popup top at trigger bottom
|
|
814
|
+
// Use trigger's measured height as buffer to account for row padding
|
|
815
|
+
// The TextInput is only part of the trigger row - row height scales with trigger height
|
|
816
|
+
const BELOW_BUFFER = height;
|
|
817
|
+
popupY = y + height + BELOW_BUFFER;
|
|
818
|
+
position = 'BELOW';
|
|
819
|
+
|
|
820
|
+
debugLog('AutoPositionedPopup: using BELOW position (ABOVE overlaps status bar):', {
|
|
821
|
+
popupY,
|
|
822
|
+
triggerBottom: y + height,
|
|
823
|
+
buffer: BELOW_BUFFER,
|
|
824
|
+
actualGap: BELOW_BUFFER
|
|
755
825
|
});
|
|
756
826
|
|
|
757
|
-
// 3.
|
|
827
|
+
// 3. Safety check: if BELOW would go off screen bottom, clamp it
|
|
758
828
|
const maxY = screenHeight - listLayout.height;
|
|
759
829
|
if (popupY > maxY) {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
popupY = Math.min(Math.max(statusBarHeight, y - listLayout.height), maxY);
|
|
830
|
+
popupY = maxY;
|
|
831
|
+
debugLog('AutoPositionedPopup: clamped to screen bottom:', { popupY, maxY });
|
|
763
832
|
}
|
|
764
833
|
} else {
|
|
765
|
-
|
|
834
|
+
debugLog('AutoPositionedPopup: using ABOVE position (preferred)');
|
|
766
835
|
}
|
|
836
|
+
|
|
837
|
+
debugLog('AutoPositionedPopup FINAL POSITION:', { position, popupY, touchesTrigger: true });
|
|
838
|
+
|
|
767
839
|
ref_listPos.current = {x: x, y: popupY, width: width};
|
|
768
|
-
|
|
769
|
-
|
|
840
|
+
debugLog('AutoPositionedPopup useTextInput final position=', ref_listPos.current);
|
|
841
|
+
|
|
842
|
+
// Use updateRootView instead of setRootViewNativeStyle for more reliable style updates
|
|
843
|
+
// setNativeProps may not work correctly when initial style is in an array
|
|
844
|
+
const newStyle = {
|
|
770
845
|
top: ref_listPos.current?.y,
|
|
771
846
|
left: popUpViewStyle?.left,
|
|
772
847
|
width: popUpViewStyle?.width,
|
|
773
848
|
height: listLayout.height,
|
|
774
849
|
opacity: 1,
|
|
775
|
-
}
|
|
850
|
+
};
|
|
851
|
+
debugLog('AutoPositionedPopup useTextInput: applying new style via updateRootView=', newStyle);
|
|
852
|
+
updateRootView(tag, {style: newStyle});
|
|
776
853
|
hasShownRootView.current = true;
|
|
777
854
|
});
|
|
778
855
|
});
|
|
779
856
|
}, 300) // 300ms is sufficient for keyboard animation, as proven by user testing (even 3000ms didn't fix wrong logic)
|
|
780
857
|
} else if (!isKeyboardFullyShown && ref_isFocus.current && keyboardStateChanged) {
|
|
781
858
|
// Only execute close logic when keyboard state actually changes from true to false
|
|
782
|
-
|
|
859
|
+
debugLog(
|
|
783
860
|
'AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView (keyboard state changed)=',
|
|
784
861
|
{tag, forceRemoveAllRootViewOnItemSelected, keyboardStateChanged}
|
|
785
862
|
);
|
|
@@ -795,217 +872,84 @@ const AutoPositionedPopup = memo(
|
|
|
795
872
|
hasShownRootView.current = false;
|
|
796
873
|
}
|
|
797
874
|
} else {
|
|
875
|
+
// V17 SIMPLIFICATION: When useTextInput=false, ALWAYS show popup in CENTER of screen
|
|
876
|
+
// User request: "只要传入的 useTextInput 是 false, 弹框都显示在屏幕中间"
|
|
877
|
+
// This avoids all complex positioning calculations that kept failing
|
|
798
878
|
if (state.isFocus) {
|
|
799
879
|
if (isKeyboardFullyShown) {
|
|
800
880
|
Keyboard.dismiss();
|
|
801
881
|
return;
|
|
802
882
|
}
|
|
803
|
-
// CRITICAL FIX: Use triggerBtnRef (the actual TouchableOpacity) for measurement
|
|
804
|
-
// instead of refAutoPositionedPopup (the outer View with flex:1/height:100%)
|
|
805
|
-
// This ensures accurate position when component is inside complex layouts like KeyboardAwareScrollView
|
|
806
|
-
const measureTarget = triggerBtnRef.current || refAutoPositionedPopup.current;
|
|
807
|
-
measureTarget?.measureInWindow((x: number | undefined, y: number | undefined, width: number | undefined, height: number | undefined) => {
|
|
808
|
-
console.log('AutoPositionedPopup !useTextInput measureInWindow=', {x, y, width, height, usingTriggerRef: !!triggerBtnRef.current});
|
|
809
|
-
// CRITICAL FIX: Handle undefined values from measureInWindow
|
|
810
|
-
// This can happen during navigation transitions or when view is not yet mounted
|
|
811
|
-
if (x === undefined || y === undefined || width === undefined || height === undefined) {
|
|
812
|
-
console.warn('AutoPositionedPopup: measureInWindow returned undefined values, using fallback position');
|
|
813
|
-
// Use screen center as fallback position
|
|
814
|
-
const screenHeight = Dimensions.get('window').height;
|
|
815
|
-
const screenWidth = Dimensions.get('window').width;
|
|
816
|
-
const fallbackY = (screenHeight - listLayout.height) / 2;
|
|
817
|
-
const fallbackX = screenWidth * 0.05; // 5% from left
|
|
818
|
-
const fallbackWidth = screenWidth * 0.9; // 90% width
|
|
819
|
-
ref_listPos.current = { x: fallbackX, y: fallbackY, width: fallbackWidth };
|
|
820
|
-
console.log('AutoPositionedPopup !useTextInput using fallback position=', ref_listPos.current);
|
|
821
|
-
// Proceed with fallback values
|
|
822
|
-
x = fallbackX;
|
|
823
|
-
y = fallbackY;
|
|
824
|
-
width = fallbackWidth;
|
|
825
|
-
height = 50; // Default height for the trigger element
|
|
826
|
-
}
|
|
827
|
-
// CORRECT POSITIONING LOGIC (as per user requirement)
|
|
828
|
-
// Default: show popup ABOVE the input field
|
|
829
|
-
// Only if that goes off the top of screen (considering status bar), show BELOW instead
|
|
830
|
-
const calculateOptimalPosition = (componentY: number, componentHeight: number, popupHeight: number) => {
|
|
831
|
-
console.log('AutoPositionedPopup calculateOptimalPosition executing');
|
|
832
|
-
// Use window height (visible area) instead of screen height
|
|
833
|
-
const screenHeight = Dimensions.get('window').height;
|
|
834
|
-
console.log('AutoPositionedPopup positioning data:', {
|
|
835
|
-
screenHeight,
|
|
836
|
-
componentY,
|
|
837
|
-
componentHeight,
|
|
838
|
-
popupHeight,
|
|
839
|
-
statusBarHeight,
|
|
840
|
-
platform: Platform.OS
|
|
841
|
-
});
|
|
842
|
-
// FIXED POSITIONING LOGIC:
|
|
843
|
-
// The popup uses position: 'absolute' relative to the RootViewProvider container
|
|
844
|
-
// measureInWindow returns coordinates relative to the window (screen)
|
|
845
|
-
// So we should NOT add statusBarHeight to the position calculation
|
|
846
|
-
//
|
|
847
|
-
// 1. Default: show popup ABOVE the trigger element
|
|
848
|
-
// FIX: Use (componentY + componentHeight) as the trigger's bottom edge reference point
|
|
849
|
-
// This compensates for measurement inaccuracies when trigger is inside complex layouts (FlatList, ScrollView)
|
|
850
|
-
// The popup's bottom should be at the trigger's top with minimal gap (≤5px)
|
|
851
|
-
// Formula: popup_top = trigger_bottom - componentHeight - popupHeight
|
|
852
|
-
// popup_bottom = trigger_bottom - componentHeight = trigger_top
|
|
853
|
-
let popupY = componentY + componentHeight - popupHeight;
|
|
854
|
-
|
|
855
|
-
console.log('AutoPositionedPopup: initial calculation for ABOVE position:', {
|
|
856
|
-
componentY,
|
|
857
|
-
componentHeight,
|
|
858
|
-
popupHeight,
|
|
859
|
-
popupY,
|
|
860
|
-
triggerBottom: componentY + componentHeight,
|
|
861
|
-
statusBarHeight
|
|
862
|
-
});
|
|
863
883
|
|
|
864
|
-
|
|
865
|
-
if (popupY < statusBarHeight) {
|
|
866
|
-
console.log('AutoPositionedPopup: would go behind status bar, showing BELOW instead');
|
|
867
|
-
// Show BELOW the trigger element
|
|
868
|
-
// Since componentY + componentHeight represents the trigger's "reference bottom" (accounting for measurement offset),
|
|
869
|
-
// we need to add another componentHeight to position popup BELOW the actual trigger
|
|
870
|
-
// Formula: popup top = componentY + (2 * componentHeight)
|
|
871
|
-
// - (componentY + componentHeight) = trigger's actual top (compensated)
|
|
872
|
-
// - + componentHeight = skip past trigger height to get to trigger's actual bottom
|
|
873
|
-
popupY = componentY + componentHeight + componentHeight;
|
|
874
|
-
console.log('AutoPositionedPopup: BELOW position calculated:', {
|
|
875
|
-
formula: 'componentY + 2*componentHeight',
|
|
876
|
-
componentY,
|
|
877
|
-
componentHeight,
|
|
878
|
-
popupY
|
|
879
|
-
});
|
|
884
|
+
debugLog('🟢🟢🟢 POPUP_V17 useTextInput=false, showing popup in CENTER of screen');
|
|
880
885
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
'
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
}
|
|
923
|
-
: {width: popUpViewStyle?.width, height: listLayout.height, ...CustomPopViewStyle},
|
|
924
|
-
component: <PopViewComponent selectedItem={state.selectedItem}></PopViewComponent>,
|
|
925
|
-
useModal: true,
|
|
926
|
-
onModalClose: () => {
|
|
927
|
-
console.log('AutoPositionedPopup onModalClose');
|
|
928
|
-
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
929
|
-
setState((prevState) => {
|
|
930
|
-
return {
|
|
931
|
-
...prevState,
|
|
932
|
-
isFocus: false,
|
|
933
|
-
};
|
|
934
|
-
});
|
|
935
|
-
hasAddedRootView.current = false;
|
|
936
|
-
hasShownRootView.current = false;
|
|
937
|
-
hasTriggeredFocus.current = false;
|
|
938
|
-
setSearchQuery('');
|
|
939
|
-
},
|
|
940
|
-
centerDisplay,
|
|
941
|
-
});
|
|
942
|
-
} else {
|
|
943
|
-
console.log('AutoPositionedPopup !useTextInput addRootView tag=', tag);
|
|
944
|
-
addRootView({
|
|
945
|
-
id: tag,
|
|
946
|
-
style: {
|
|
947
|
-
top: ref_listPos.current.y,
|
|
948
|
-
left: popUpViewStyle?.left,
|
|
949
|
-
width: popUpViewStyle?.width,
|
|
950
|
-
height: listLayout.height,
|
|
951
|
-
opacity: 1,
|
|
952
|
-
},
|
|
953
|
-
component: (
|
|
954
|
-
<AutoPositionedPopupList
|
|
955
|
-
tag={tag}
|
|
956
|
-
updateState={updateState}
|
|
957
|
-
fetchData={fetchData}
|
|
958
|
-
pageSize={pageSize}
|
|
959
|
-
renderItem={renderItem}
|
|
960
|
-
selectedItem={state.selectedItem}
|
|
961
|
-
localSearch={localSearch}
|
|
962
|
-
showListEmptyComponent={showListEmptyComponent}
|
|
963
|
-
emptyText={emptyText}
|
|
964
|
-
themeMode={themeMode}
|
|
965
|
-
/>
|
|
966
|
-
),
|
|
967
|
-
useModal: true,
|
|
968
|
-
onModalClose: () => {
|
|
969
|
-
console.log('AutoPositionedPopup onModalClose tag=', tag);
|
|
970
|
-
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
971
|
-
setState((prevState) => {
|
|
972
|
-
return {
|
|
973
|
-
...prevState,
|
|
974
|
-
};
|
|
975
|
-
});
|
|
976
|
-
setSearchQuery('');
|
|
977
|
-
},
|
|
978
|
-
});
|
|
979
|
-
}
|
|
980
|
-
});
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
if (isKeyboardFullyShown) {
|
|
984
|
-
ref_isFocus.current = state.isFocus ?? false;
|
|
985
|
-
if (isKeyboardFullyShown !== keyboardVisibleRef.current) {
|
|
986
|
-
keyboardVisibleRef.current = isKeyboardFullyShown;
|
|
987
|
-
if (isKeyboardFullyShown && textInputRef.current) {
|
|
988
|
-
if (ref_searchQuery.current) {
|
|
989
|
-
textInputRef.current.setNativeProps({text: ref_searchQuery.current});
|
|
990
|
-
}
|
|
886
|
+
const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
|
|
887
|
+
? CustomPopViewStyle.height
|
|
888
|
+
: listLayout.height;
|
|
889
|
+
|
|
890
|
+
if (CustomPopView && CustomPopViewStyle) {
|
|
891
|
+
const PopViewComponent = CustomPopView();
|
|
892
|
+
debugLog('🔵🔵🔵 POPUP_V17 CustomPopView centerDisplay=true');
|
|
893
|
+
addRootView({
|
|
894
|
+
id: tag,
|
|
895
|
+
style: { width: popUpViewStyle?.width, ...CustomPopViewStyle },
|
|
896
|
+
component: <PopViewComponent selectedItem={state.selectedItem}></PopViewComponent>,
|
|
897
|
+
useModal: true,
|
|
898
|
+
centerDisplay: true, // V17: Force center display for useTextInput=false
|
|
899
|
+
onModalClose: () => {
|
|
900
|
+
debugLog('AutoPositionedPopup V17 onModalClose tag=', tag);
|
|
901
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
902
|
+
setState((prevState) => ({ ...prevState }));
|
|
903
|
+
setSearchQuery('');
|
|
904
|
+
},
|
|
905
|
+
});
|
|
906
|
+
} else {
|
|
907
|
+
debugLog('🔵🔵🔵 POPUP_V17 List centerDisplay=true, height=', listLayout.height);
|
|
908
|
+
addRootView({
|
|
909
|
+
id: tag,
|
|
910
|
+
style: { width: popUpViewStyle?.width, height: listLayout.height, opacity: 1 },
|
|
911
|
+
component: (
|
|
912
|
+
<AutoPositionedPopupList
|
|
913
|
+
tag={tag} updateState={updateState} fetchData={fetchData} pageSize={pageSize}
|
|
914
|
+
renderItem={renderItem} selectedItem={state.selectedItem} localSearch={localSearch}
|
|
915
|
+
showListEmptyComponent={showListEmptyComponent} emptyText={emptyText} themeMode={themeMode}
|
|
916
|
+
/>
|
|
917
|
+
),
|
|
918
|
+
useModal: true,
|
|
919
|
+
centerDisplay: true, // V17: Force center display for useTextInput=false
|
|
920
|
+
onModalClose: () => {
|
|
921
|
+
debugLog('AutoPositionedPopup V17 onModalClose tag=', tag);
|
|
922
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
923
|
+
setState((prevState) => ({ ...prevState }));
|
|
924
|
+
setSearchQuery('');
|
|
925
|
+
},
|
|
926
|
+
});
|
|
991
927
|
}
|
|
928
|
+
return; // V17: Early return after handling !useTextInput case
|
|
992
929
|
}
|
|
993
930
|
}
|
|
994
931
|
}, [isKeyboardFullyShown,
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
932
|
+
state.isFocus,
|
|
933
|
+
useTextInput,
|
|
934
|
+
CustomPopView,
|
|
935
|
+
CustomPopViewStyle,
|
|
936
|
+
forceRemoveAllRootViewOnItemSelected,
|
|
937
|
+
tag, TextInputProps,
|
|
938
|
+
state.selectedItem, showListEmptyComponent, themeMode
|
|
939
|
+
]);
|
|
940
|
+
|
|
941
|
+
// V16: All positioning logic is now in the useEffect above (calculateOptimalPosition + processPosition)
|
|
942
|
+
// V16 FIX: Capture position in onPress callback BEFORE setState is called
|
|
943
|
+
// This ensures triggerPositionRef.current is set when useEffect runs
|
|
944
|
+
// Formula: top = componentY - popupHeight (popup bottom touches trigger top exactly)
|
|
945
|
+
debugLog('🟢 POPUP_MODULE_V16_LOADED - capturing position in onPress callback before setState');
|
|
946
|
+
|
|
947
|
+
// Imperative handle for parent component access
|
|
948
|
+
useImperativeHandle(
|
|
1005
949
|
parentRef,
|
|
1006
950
|
() => ({
|
|
1007
951
|
clearSelectedItem: () => {
|
|
1008
|
-
|
|
952
|
+
debugLog('AutoPositionedPopup clearSelectedItem tag=', tag);
|
|
1009
953
|
setState((prevState) => {
|
|
1010
954
|
return {
|
|
1011
955
|
...prevState,
|
|
@@ -1027,14 +971,14 @@ const AutoPositionedPopup = memo(
|
|
|
1027
971
|
[]
|
|
1028
972
|
);
|
|
1029
973
|
const updateState = (key: string, value: SelectedItem) => {
|
|
1030
|
-
|
|
974
|
+
debugLog('AutoPositionedPopup updateState=', {key, value});
|
|
1031
975
|
setState((prevState) => ({
|
|
1032
976
|
...prevState,
|
|
1033
977
|
[key]: value,
|
|
1034
978
|
}));
|
|
1035
979
|
if (key === 'selectedItem' && onItemSelected) {
|
|
1036
980
|
onItemSelected(value);
|
|
1037
|
-
|
|
981
|
+
debugLog('AutoPositionedPopup updateState onItemSelected rootViewsRef.current=', rootViewsRef.current);
|
|
1038
982
|
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
1039
983
|
hasAddedRootView.current = false;
|
|
1040
984
|
hasShownRootView.current = false;
|
|
@@ -1069,7 +1013,7 @@ const AutoPositionedPopup = memo(
|
|
|
1069
1013
|
// Only update when deep comparison detects real changes to avoid TextInput recreation due to reference changes during parent component redraws
|
|
1070
1014
|
const stableInputStyle = useMemo(() => {
|
|
1071
1015
|
if (!shallowEqual(stableInputStyleRef.current, inputStyle)) {
|
|
1072
|
-
|
|
1016
|
+
debugLog(`AutoPositionedPopup stableInputStyle: `, {tag, inputStyle, themeMode});
|
|
1073
1017
|
stableInputStyleRef.current = inputStyle;
|
|
1074
1018
|
}
|
|
1075
1019
|
return stableInputStyleRef.current;
|
|
@@ -1077,10 +1021,10 @@ const AutoPositionedPopup = memo(
|
|
|
1077
1021
|
|
|
1078
1022
|
const stableTextInputProps = useMemo(() => {
|
|
1079
1023
|
if (!shallowEqual(stableTextInputPropsRef.current, TextInputProps)) {
|
|
1080
|
-
|
|
1024
|
+
debugLog(`AutoPositionedPopup TextInputProps deep change detected, updating stable reference - tag: ${tag}`);
|
|
1081
1025
|
stableTextInputPropsRef.current = TextInputProps;
|
|
1082
1026
|
}
|
|
1083
|
-
|
|
1027
|
+
debugLog('AutoPositionedPopup stableTextInputProps=', {tag, TextInputProps, 'stableTextInputPropsRef.current': stableTextInputPropsRef.current})
|
|
1084
1028
|
return stableTextInputPropsRef.current;
|
|
1085
1029
|
}, [TextInputProps, tag]);
|
|
1086
1030
|
|
|
@@ -1093,7 +1037,7 @@ const AutoPositionedPopup = memo(
|
|
|
1093
1037
|
const handleTextInputFocus = useCallback(() => {
|
|
1094
1038
|
const currentTime = Date.now();
|
|
1095
1039
|
const timeSinceLastFocus = currentTime - lastFocusTimeRef.current;
|
|
1096
|
-
|
|
1040
|
+
debugLog(
|
|
1097
1041
|
'AutoPositionedPopup onFocus=',
|
|
1098
1042
|
{
|
|
1099
1043
|
tag,
|
|
@@ -1108,17 +1052,17 @@ const AutoPositionedPopup = memo(
|
|
|
1108
1052
|
);
|
|
1109
1053
|
// Prevent rapid repeated triggers (repeated events within 300ms are ignored)
|
|
1110
1054
|
if (timeSinceLastFocus < 300) {
|
|
1111
|
-
|
|
1055
|
+
debugLog('AutoPositionedPopup onFocus: Skip - event triggered too quickly (< 300ms)');
|
|
1112
1056
|
return;
|
|
1113
1057
|
}
|
|
1114
1058
|
// Skip if keyboard is already open and focus has been handled
|
|
1115
1059
|
if (isKeyboardFullyShown && hasTriggeredFocus.current) {
|
|
1116
|
-
|
|
1060
|
+
debugLog('AutoPositionedPopup onFocus: Skip - keyboard already open and focus handled');
|
|
1117
1061
|
return;
|
|
1118
1062
|
}
|
|
1119
1063
|
// Prevent concurrent processing
|
|
1120
1064
|
if (isFocusEventProcessingRef.current) {
|
|
1121
|
-
|
|
1065
|
+
debugLog('AutoPositionedPopup onFocus: Skip - processing another focus event');
|
|
1122
1066
|
return;
|
|
1123
1067
|
}
|
|
1124
1068
|
isFocusEventProcessingRef.current = true;
|
|
@@ -1142,7 +1086,7 @@ const AutoPositionedPopup = memo(
|
|
|
1142
1086
|
}, [tag, isKeyboardFullyShown]); // Remove state.selectedItem, use stateRef instead
|
|
1143
1087
|
|
|
1144
1088
|
const handleTextInputBlur = useCallback(() => {
|
|
1145
|
-
|
|
1089
|
+
debugLog(
|
|
1146
1090
|
'AutoPositionedPopup onBlur=',
|
|
1147
1091
|
{
|
|
1148
1092
|
tag,
|
|
@@ -1153,7 +1097,7 @@ const AutoPositionedPopup = memo(
|
|
|
1153
1097
|
);
|
|
1154
1098
|
// If keyboard is still open, this is a false trigger caused by parent component re-render, should not reset
|
|
1155
1099
|
if (isKeyboardFullyShown && hasTriggeredFocus.current) {
|
|
1156
|
-
|
|
1100
|
+
debugLog('AutoPositionedPopup onBlur: Skip - keyboard still open, possibly caused by parent component re-render');
|
|
1157
1101
|
return;
|
|
1158
1102
|
}
|
|
1159
1103
|
|
|
@@ -1182,7 +1126,7 @@ const AutoPositionedPopup = memo(
|
|
|
1182
1126
|
// Wrap TextInput independently in useMemo to recreate only when key props change
|
|
1183
1127
|
// This avoids repeated ref callback triggers due to other props changes during parent component redraws
|
|
1184
1128
|
const memoizedTextInput = useMemo(() => {
|
|
1185
|
-
|
|
1129
|
+
debugLog('AutoPositionedPopup memoizedTextInput=', {tag, useTextInput, 'state.isFocus': state.isFocus, stableTextInputProps});
|
|
1186
1130
|
if (!useTextInput || !state.isFocus) {
|
|
1187
1131
|
return null;
|
|
1188
1132
|
}
|
|
@@ -1191,11 +1135,11 @@ const AutoPositionedPopup = memo(
|
|
|
1191
1135
|
ref={(ref) => {
|
|
1192
1136
|
// Monitor TextInput mounting and unmounting
|
|
1193
1137
|
if (ref && !textInputRef.current) {
|
|
1194
|
-
|
|
1138
|
+
debugLog(`AutoPositionedPopup TextInput created/mounted - tag: ${tag}, ref:`, ref);
|
|
1195
1139
|
} else if (!ref && textInputRef.current) {
|
|
1196
|
-
|
|
1140
|
+
debugLog(`AutoPositionedPopup TextInput unmounted - tag: ${tag}`);
|
|
1197
1141
|
} else if (ref && textInputRef.current && ref !== textInputRef.current) {
|
|
1198
|
-
|
|
1142
|
+
debugLog(`AutoPositionedPopup TextInput replaced - tag: ${tag}, oldRef:`, textInputRef.current, 'newRef:', ref);
|
|
1199
1143
|
}
|
|
1200
1144
|
textInputRef.current = ref;
|
|
1201
1145
|
}}
|
|
@@ -1203,14 +1147,14 @@ const AutoPositionedPopup = memo(
|
|
|
1203
1147
|
style={[
|
|
1204
1148
|
styles.inputStyle,
|
|
1205
1149
|
stableInputStyle,
|
|
1206
|
-
(themeMode==='dark' && {color:'#fff'})
|
|
1150
|
+
(themeMode==='dark' && {color:'#fff'}),
|
|
1207
1151
|
]}
|
|
1208
1152
|
textAlign={stableTextInputProps && stableTextInputProps['textAlign'] || 'left'}
|
|
1209
1153
|
multiline={stableTextInputProps && stableTextInputProps['multiline'] || false}
|
|
1210
1154
|
numberOfLines={stableTextInputProps && stableTextInputProps['numberOfLines'] || 1}
|
|
1211
1155
|
onChangeText={(searchQuery) => {
|
|
1212
1156
|
ref_searchQuery.current = searchQuery;
|
|
1213
|
-
|
|
1157
|
+
debugLog('AutoPositionedPopup onChangeText rootViews=', rootViews);
|
|
1214
1158
|
if (!localSearch) {
|
|
1215
1159
|
if (debounceTimerRef.current) {
|
|
1216
1160
|
clearTimeout(debounceTimerRef.current);
|
|
@@ -1249,7 +1193,7 @@ const AutoPositionedPopup = memo(
|
|
|
1249
1193
|
onBlur={handleTextInputBlur}
|
|
1250
1194
|
selectTextOnFocus={stableTextInputProps && stableTextInputProps['selectTextOnFocus'] || false}
|
|
1251
1195
|
onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
|
|
1252
|
-
|
|
1196
|
+
debugLog(
|
|
1253
1197
|
'AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=',
|
|
1254
1198
|
e.nativeEvent.text
|
|
1255
1199
|
);
|
|
@@ -1273,7 +1217,7 @@ const AutoPositionedPopup = memo(
|
|
|
1273
1217
|
|
|
1274
1218
|
// Render the component following project implementation
|
|
1275
1219
|
return useMemo(() => {
|
|
1276
|
-
|
|
1220
|
+
debugLog('AutoPositionedPopup render tag=', tag); // Now safe - circular dependency fixed
|
|
1277
1221
|
return (
|
|
1278
1222
|
<CustomRow>
|
|
1279
1223
|
<View style={[styles.contain, style]} ref={refAutoPositionedPopup}>
|
|
@@ -1283,7 +1227,7 @@ const AutoPositionedPopup = memo(
|
|
|
1283
1227
|
style={[styles.AutoPositionedPopupBtn, AutoPositionedPopupBtnStyle]}
|
|
1284
1228
|
disabled={AutoPositionedPopupBtnDisabled}
|
|
1285
1229
|
onPress={() => {
|
|
1286
|
-
|
|
1230
|
+
debugLog('AutoPositionedPopup onPress=', {
|
|
1287
1231
|
tag,
|
|
1288
1232
|
'state.isFocus': state.isFocus,
|
|
1289
1233
|
useTextInput,
|
|
@@ -1296,10 +1240,13 @@ const AutoPositionedPopup = memo(
|
|
|
1296
1240
|
|
|
1297
1241
|
// Capture trigger button position BEFORE switching to TextInput
|
|
1298
1242
|
// This is critical because triggerBtnRef will become null after isFocus=true
|
|
1299
|
-
|
|
1243
|
+
// IMPORTANT: Always capture position regardless of parentScrollViewRef
|
|
1244
|
+
if (triggerBtnRef.current) {
|
|
1300
1245
|
triggerBtnRef.current.measureInWindow((x, y, width, height) => {
|
|
1301
|
-
|
|
1302
|
-
|
|
1246
|
+
debugLog('AutoPositionedPopup onPress: captured trigger position=', {tag, x, y, width, height});
|
|
1247
|
+
if (x !== undefined && y !== undefined && width !== undefined && height !== undefined) {
|
|
1248
|
+
triggerPositionRef.current = {x, y, width, height};
|
|
1249
|
+
}
|
|
1303
1250
|
});
|
|
1304
1251
|
}
|
|
1305
1252
|
|
|
@@ -1357,6 +1304,9 @@ const AutoPositionedPopup = memo(
|
|
|
1357
1304
|
_addRootView()
|
|
1358
1305
|
}
|
|
1359
1306
|
} else {
|
|
1307
|
+
// V17 SIMPLIFICATION: For useTextInput=false, popup will be centered
|
|
1308
|
+
// No need for complex position measurement - just trigger focus
|
|
1309
|
+
debugLog('🔵🔵🔵 POPUP_V17 onPress useTextInput=false, will show centered popup');
|
|
1360
1310
|
setState((prevState) => {
|
|
1361
1311
|
return {
|
|
1362
1312
|
...prevState,
|
|
@@ -1364,7 +1314,7 @@ const AutoPositionedPopup = memo(
|
|
|
1364
1314
|
};
|
|
1365
1315
|
});
|
|
1366
1316
|
}
|
|
1367
|
-
|
|
1317
|
+
debugLog('AutoPositionedPopup onPress done')
|
|
1368
1318
|
}}
|
|
1369
1319
|
>
|
|
1370
1320
|
{!btwChildren ? (
|
|
@@ -1391,29 +1341,29 @@ const AutoPositionedPopup = memo(
|
|
|
1391
1341
|
);
|
|
1392
1342
|
}, [
|
|
1393
1343
|
tag,
|
|
1394
|
-
//
|
|
1344
|
+
// �?CRITICAL FIX: Remove all props that may change frequently or are inline functions
|
|
1395
1345
|
// Changes to these props should not cause the entire component tree to recreate, especially TextInput
|
|
1396
|
-
// fetchData, //
|
|
1397
|
-
// renderItem, //
|
|
1398
|
-
// onItemSelected, //
|
|
1399
|
-
// onSubmitEditing, //
|
|
1346
|
+
// fetchData, // �?Removed: inline function
|
|
1347
|
+
// renderItem, // �?Removed: possibly inline function
|
|
1348
|
+
// onItemSelected, // �?Removed: possibly inline function
|
|
1349
|
+
// onSubmitEditing, // �?Removed: possibly inline function
|
|
1400
1350
|
localSearch,
|
|
1401
|
-
// placeholder, //
|
|
1402
|
-
// textAlign, //
|
|
1351
|
+
// placeholder, // �?Removed: may change
|
|
1352
|
+
// textAlign, // �?Removed: may change
|
|
1403
1353
|
pageSize,
|
|
1404
1354
|
selectedItem,
|
|
1405
|
-
// CustomRow, //
|
|
1355
|
+
// CustomRow, // �?Removed: inline function, new reference each time
|
|
1406
1356
|
useTextInput,
|
|
1407
|
-
// btwChildren, //
|
|
1408
|
-
// keyExtractor, //
|
|
1409
|
-
// AutoPositionedPopupBtnStyle, //
|
|
1410
|
-
// CustomPopView, //
|
|
1411
|
-
// CustomPopViewStyle, //
|
|
1357
|
+
// btwChildren, // �?Removed: inline function
|
|
1358
|
+
// keyExtractor, // �?Removed: possibly inline function
|
|
1359
|
+
// AutoPositionedPopupBtnStyle, // �?Removed: possibly inline object
|
|
1360
|
+
// CustomPopView, // �?Removed: may change
|
|
1361
|
+
// CustomPopViewStyle, // �?Removed: may change
|
|
1412
1362
|
forceRemoveAllRootViewOnItemSelected,
|
|
1413
1363
|
state.isFocus,
|
|
1414
1364
|
showListEmptyComponent,
|
|
1415
1365
|
emptyText,
|
|
1416
|
-
//
|
|
1366
|
+
// �?Removed most dependencies that may cause re-rendering, keeping only core dependencies that truly affect component structure
|
|
1417
1367
|
// This prevents TextInput recreation due to inline functions/objects during parent component redraws
|
|
1418
1368
|
]);
|
|
1419
1369
|
}
|