react-native-auto-positioned-popup 1.2.16 → 1.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/AutoPositionedPopup.d.ts.map +1 -1
- package/lib/AutoPositionedPopup.js +227 -288
- package/lib/AutoPositionedPopup.js.map +1 -1
- package/lib/AutoPositionedPopup.style.d.ts.map +1 -1
- package/lib/AutoPositionedPopup.style.js +2 -0
- package/lib/AutoPositionedPopup.style.js.map +1 -1
- package/lib/KeyboardManager.d.ts +6 -1
- package/lib/KeyboardManager.d.ts.map +1 -1
- package/lib/KeyboardManager.js +19 -2
- package/lib/KeyboardManager.js.map +1 -1
- package/lib/RootViewContext.d.ts.map +1 -1
- package/lib/RootViewContext.js +76 -44
- package/lib/RootViewContext.js.map +1 -1
- package/package.json +1 -1
- package/src/AutoPositionedPopup.style.ts +2 -0
- package/src/AutoPositionedPopup.tsx +321 -381
- package/src/KeyboardManager.tsx +35 -10
- package/src/RootViewContext.tsx +63 -24
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
// Module load marker - unique ID for tracking code version
|
|
2
|
+
// V19f (2025-01-04): CORRECT direction for coordinate adjustment - ADD statusBarHeight to move popup DOWN
|
|
3
|
+
// Wait 1 second for KeyboardAwareScrollView to stabilize, then use measureInWindow to get trigger's FINAL position
|
|
4
|
+
// NOTE: Parent component (KeyboardAwareScrollView) is responsible for scrolling trigger into view
|
|
5
|
+
// DEBUG FLAG: Set to false to disable all console logs for better performance
|
|
6
|
+
const POPUP_DEBUG = false; // DISABLED: Too many logs cause app freeze
|
|
7
|
+
const POPUP_POSITION_DEBUG = true; // Only log positioning calculations
|
|
8
|
+
const debugLog = (...args: any[]) => {
|
|
9
|
+
if (POPUP_DEBUG) {
|
|
10
|
+
console.log(...args);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
// Separate logging function for position-related logs only
|
|
14
|
+
const positionDebugLog = (...args: any[]) => {
|
|
15
|
+
if (POPUP_POSITION_DEBUG) {
|
|
16
|
+
console.log(...args);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Only log module load in debug mode
|
|
21
|
+
positionDebugLog('POPUP_MODULE_V19f_LOADED at ' + new Date().toISOString() + ' (Parent handles scroll)');
|
|
22
|
+
|
|
1
23
|
import React, {
|
|
2
24
|
ForwardedRef,
|
|
3
25
|
forwardRef,
|
|
@@ -35,7 +57,7 @@ import {useKeyboardStatus} from './KeyboardManager';
|
|
|
35
57
|
type QueryListener = (query: string) => void;
|
|
36
58
|
const queryChangeListeners: QueryListener[] = [];
|
|
37
59
|
const emitQueryChange = (query: string) => {
|
|
38
|
-
|
|
60
|
+
debugLog('AutoPositionedPopup.tsx emitQueryChange query=', query, ' listeners=', queryChangeListeners.length);
|
|
39
61
|
queryChangeListeners.forEach((l) => l(query));
|
|
40
62
|
};
|
|
41
63
|
const subscribeQueryChange = (listener: QueryListener) => {
|
|
@@ -92,7 +114,7 @@ const ListItem: React.FC<{
|
|
|
92
114
|
rootViewsRef.current = rootViews;
|
|
93
115
|
}, [rootViews]);
|
|
94
116
|
return useMemo(() => {
|
|
95
|
-
//
|
|
117
|
+
// debugLog('AutoPositionedPopup.tsx ListItem=', {index, item, selectedItem});
|
|
96
118
|
const isSelected = item.id === selectedItem?.id || item.title == selectedItem?.title;
|
|
97
119
|
return (
|
|
98
120
|
<TouchableOpacity
|
|
@@ -102,8 +124,8 @@ const ListItem: React.FC<{
|
|
|
102
124
|
{backgroundColor: isSelected ? (themeMode === 'light' ? 'rgba(116, 116, 128, 0.08)' : 'rgba(120, 120, 128, 0.36)') : 'transparent'},
|
|
103
125
|
]}
|
|
104
126
|
onPress={() => {
|
|
105
|
-
//
|
|
106
|
-
//
|
|
127
|
+
// debugLog('AutoPositionedPopup.tsx ListItem onPress item=', item); // Commented to prevent spam
|
|
128
|
+
// debugLog('AutoPositionedPopup.tsx ListItem onPress rootViews=', rootViewsRef.current); // Commented to prevent spam
|
|
107
129
|
updateState('selectedItem', item);
|
|
108
130
|
}}
|
|
109
131
|
>
|
|
@@ -171,16 +193,16 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
171
193
|
useEffect(() => {
|
|
172
194
|
(async () => {
|
|
173
195
|
})();
|
|
174
|
-
|
|
196
|
+
debugLog(`AutoPositionedPopupList componentDidMount`);
|
|
175
197
|
//componentWillUnmount
|
|
176
198
|
return () => {
|
|
177
|
-
|
|
199
|
+
debugLog(`AutoPositionedPopupList componentWillUnmount`);
|
|
178
200
|
setSearchQuery('');
|
|
179
201
|
};
|
|
180
202
|
}, []);
|
|
181
203
|
useEffect(() => {
|
|
182
204
|
const unsubscribe = subscribeQueryChange((newQuery: string) => {
|
|
183
|
-
|
|
205
|
+
debugLog('AutoPositionedPopupList useEffect subscribeQueryChange newQuery=', newQuery);
|
|
184
206
|
ref_searchQuery.current = newQuery;
|
|
185
207
|
if (ref_list.current) {
|
|
186
208
|
ref_list.current.scrollToTop();
|
|
@@ -190,24 +212,24 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
190
212
|
return unsubscribe;
|
|
191
213
|
}, []);
|
|
192
214
|
const _updateState = (key: string, value: SelectedItem) => {
|
|
193
|
-
|
|
215
|
+
debugLog('AutoPositionedPopupList _updateState key=', key, ' value=', value);
|
|
194
216
|
setState((prevState) => ({
|
|
195
217
|
...prevState,
|
|
196
218
|
[key]: value,
|
|
197
219
|
}));
|
|
198
|
-
|
|
220
|
+
debugLog('AutoPositionedPopupList _updateState rootViews=', rootViewsRef.current);
|
|
199
221
|
updateState(key, value);
|
|
200
222
|
};
|
|
201
223
|
const _fetchData = async ({
|
|
202
224
|
pageIndex,
|
|
203
225
|
pageSize: currentPageSize,
|
|
204
226
|
}: FetchDataParams): Promise<ListData | null> => {
|
|
205
|
-
|
|
227
|
+
debugLog('AutoPositionedPopupList _fetchData=', {pageIndex, pageSize: currentPageSize, 'state.localData': state.localData, 'ref_searchQuery.current': ref_searchQuery.current, localSearch});
|
|
206
228
|
if (localSearch && state.localData.length > 0) {
|
|
207
229
|
const result: SelectedItem[] = state.localData.filter((item: SelectedItem) => {
|
|
208
230
|
return `${item.title}`?.toLowerCase().includes(ref_searchQuery.current.toLowerCase());
|
|
209
231
|
});
|
|
210
|
-
|
|
232
|
+
debugLog('AutoPositionedPopupList _fetchData localSearch result=', result);
|
|
211
233
|
return Promise.resolve({
|
|
212
234
|
items: result,
|
|
213
235
|
pageIndex: 0,
|
|
@@ -220,7 +242,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
220
242
|
pageSize: pageSize || 10,
|
|
221
243
|
searchQuery: ref_searchQuery.current,
|
|
222
244
|
});
|
|
223
|
-
|
|
245
|
+
debugLog('AutoPositionedPopupList _fetchData res=', res);
|
|
224
246
|
if (res?.items && localSearch) {
|
|
225
247
|
setState((prevState) => {
|
|
226
248
|
return {
|
|
@@ -239,9 +261,9 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
239
261
|
}
|
|
240
262
|
return null;
|
|
241
263
|
} catch (e) {
|
|
242
|
-
|
|
264
|
+
debugLog('Error in fetchData:', e);
|
|
243
265
|
}
|
|
244
|
-
|
|
266
|
+
debugLog('AutoPositionedPopupList _fetchData res=', null);
|
|
245
267
|
return null;
|
|
246
268
|
};
|
|
247
269
|
const _renderItem = useCallback(
|
|
@@ -251,7 +273,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
251
273
|
[state.selectedItem, themeMode]
|
|
252
274
|
);
|
|
253
275
|
return useMemo(() => {
|
|
254
|
-
|
|
276
|
+
debugLog('AutoPositionedPopupList (global as any)?.$fake=', (global as any)?.$fake);
|
|
255
277
|
// Babel configuration handles the path redirection based on global.$fake
|
|
256
278
|
// No need for conditional import here
|
|
257
279
|
return (
|
|
@@ -298,7 +320,7 @@ const listLayout = {
|
|
|
298
320
|
const AutoPositionedPopup = memo(
|
|
299
321
|
forwardRef<unknown, AutoPositionedPopupProps>(
|
|
300
322
|
(props: AutoPositionedPopupProps, parentRef: ForwardedRef<unknown>): React.JSX.Element => {
|
|
301
|
-
|
|
323
|
+
debugLog('AutoPositionedPopup props=', props);
|
|
302
324
|
const {
|
|
303
325
|
tag,
|
|
304
326
|
style,
|
|
@@ -325,11 +347,11 @@ const AutoPositionedPopup = memo(
|
|
|
325
347
|
};
|
|
326
348
|
try {
|
|
327
349
|
// const res1: any[] = await $api.xxx(pageSize)
|
|
328
|
-
//
|
|
350
|
+
// debugLog('${NAME} xxx res=', res)
|
|
329
351
|
// res.items = res1
|
|
330
352
|
// res.needLoadMore = res1.length === pageSize
|
|
331
353
|
} catch (e) {
|
|
332
|
-
|
|
354
|
+
debugLog('Error in fetch operation:', e);
|
|
333
355
|
}
|
|
334
356
|
return res;
|
|
335
357
|
},
|
|
@@ -356,7 +378,7 @@ const AutoPositionedPopup = memo(
|
|
|
356
378
|
selectedItem: selectedItem,
|
|
357
379
|
});
|
|
358
380
|
// Use RootView context
|
|
359
|
-
const {addRootView, setRootViewNativeStyle, removeRootView, rootViews, setSearchQuery} = useRootView();
|
|
381
|
+
const {addRootView, setRootViewNativeStyle, updateRootView, removeRootView, rootViews, setSearchQuery} = useRootView();
|
|
360
382
|
const rootViewsRef = useRef(rootViews);
|
|
361
383
|
// Track TextInput focus and RootView states like project implementation
|
|
362
384
|
const hasTriggeredFocus = useRef(false);
|
|
@@ -370,6 +392,8 @@ const AutoPositionedPopup = memo(
|
|
|
370
392
|
const ref_searchQuery = useRef<string>('');
|
|
371
393
|
// Store trigger button position when clicked (before it's replaced by TextInput)
|
|
372
394
|
const triggerPositionRef = useRef<{x: number; y: number; width: number; height: number} | null>(null);
|
|
395
|
+
// V19: Track keyboard height for accurate popup positioning
|
|
396
|
+
const keyboardHeightRef = useRef<number>(0);
|
|
373
397
|
// Add ref to track previous keyboard state to avoid false triggers during parent component re-renders
|
|
374
398
|
const prevIsKeyboardFullyShownRef = useRef<boolean>(false);
|
|
375
399
|
const prevPropsRef = useRef<{
|
|
@@ -404,11 +428,16 @@ const AutoPositionedPopup = memo(
|
|
|
404
428
|
const searchQueryRef = useRef<string>(''); // Use ref instead of state to avoid re-renders
|
|
405
429
|
// Refs to store latest values for useEffect without adding to dependency array
|
|
406
430
|
const dataRef = useRef<SelectedItem[]>(data);
|
|
407
|
-
|
|
431
|
+
// V19: useKeyboardStatus now returns { isShown, height } for accurate positioning
|
|
432
|
+
const keyboardStatus = useKeyboardStatus();
|
|
433
|
+
const isKeyboardFullyShown = keyboardStatus.isShown;
|
|
408
434
|
const ref_isKeyboardFullyShown = useRef<boolean>(isKeyboardFullyShown);
|
|
409
435
|
useEffect(() => {
|
|
410
436
|
ref_isKeyboardFullyShown.current = isKeyboardFullyShown;
|
|
411
|
-
|
|
437
|
+
// V19: Store keyboard height for popup positioning calculations
|
|
438
|
+
keyboardHeightRef.current = keyboardStatus.height;
|
|
439
|
+
positionDebugLog(`KEYBOARD_HEIGHT_UPDATE: height=${keyboardStatus.height} isShown=${isKeyboardFullyShown}`);
|
|
440
|
+
}, [keyboardStatus.isShown, keyboardStatus.height])
|
|
412
441
|
const theme = defaultTheme;
|
|
413
442
|
|
|
414
443
|
/**
|
|
@@ -418,7 +447,7 @@ const AutoPositionedPopup = memo(
|
|
|
418
447
|
*/
|
|
419
448
|
const scrollParentToTrigger = useCallback(() => {
|
|
420
449
|
if (!parentScrollViewRef?.current || !triggerBtnRef.current) {
|
|
421
|
-
|
|
450
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: No parentScrollViewRef or triggerBtnRef available');
|
|
422
451
|
return;
|
|
423
452
|
}
|
|
424
453
|
|
|
@@ -428,18 +457,18 @@ const AutoPositionedPopup = memo(
|
|
|
428
457
|
const nodeHandle = findNodeHandle(triggerBtnRef.current);
|
|
429
458
|
|
|
430
459
|
if (nodeHandle && scrollView) {
|
|
431
|
-
|
|
460
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: Scrolling to trigger button with extraHeight=', scrollExtraHeight);
|
|
432
461
|
|
|
433
462
|
// KeyboardAwareScrollView has a scrollToFocusedInput method that handles this
|
|
434
463
|
// However, it requires a ReactNode. We'll use scrollToPosition as an alternative.
|
|
435
464
|
// First, measure the trigger button position relative to the ScrollView
|
|
436
465
|
triggerBtnRef.current.measureInWindow((x, y, width, height) => {
|
|
437
466
|
if (y === undefined || height === undefined) {
|
|
438
|
-
|
|
467
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: measureInWindow returned undefined');
|
|
439
468
|
return;
|
|
440
469
|
}
|
|
441
470
|
|
|
442
|
-
|
|
471
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: trigger position=', { x, y, width, height });
|
|
443
472
|
|
|
444
473
|
// Get keyboard height from Keyboard API
|
|
445
474
|
// On keyboard show, scroll to position that keeps trigger above keyboard
|
|
@@ -451,7 +480,7 @@ const AutoPositionedPopup = memo(
|
|
|
451
480
|
const triggerBottom = y + height;
|
|
452
481
|
const visibleAreaBottom = screenHeight - keyboardHeight;
|
|
453
482
|
|
|
454
|
-
|
|
483
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: keyboard data=', {
|
|
455
484
|
keyboardHeight,
|
|
456
485
|
screenHeight,
|
|
457
486
|
triggerBottom,
|
|
@@ -462,7 +491,7 @@ const AutoPositionedPopup = memo(
|
|
|
462
491
|
if (triggerBottom > visibleAreaBottom) {
|
|
463
492
|
// Calculate how much to scroll
|
|
464
493
|
const scrollAmount = triggerBottom - visibleAreaBottom + scrollExtraHeight;
|
|
465
|
-
|
|
494
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: scrolling by', scrollAmount);
|
|
466
495
|
|
|
467
496
|
// Use scrollForExtraHeightOnAndroid or scrollToPosition
|
|
468
497
|
if (typeof scrollView.scrollToPosition === 'function') {
|
|
@@ -470,7 +499,7 @@ const AutoPositionedPopup = memo(
|
|
|
470
499
|
scrollView.scrollToPosition(0, scrollAmount, true);
|
|
471
500
|
} else if (typeof scrollView.scrollToEnd === 'function') {
|
|
472
501
|
// Fallback: scroll to end might help in some cases
|
|
473
|
-
|
|
502
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: using scrollToEnd fallback');
|
|
474
503
|
}
|
|
475
504
|
}
|
|
476
505
|
});
|
|
@@ -483,21 +512,21 @@ const AutoPositionedPopup = memo(
|
|
|
483
512
|
* Uses stored trigger position (captured before TextInput replaces the trigger button)
|
|
484
513
|
*/
|
|
485
514
|
const scrollToTriggerWithMeasure = useCallback(() => {
|
|
486
|
-
|
|
515
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure called, tag=', tag, {
|
|
487
516
|
hasParentScrollViewRef: !!parentScrollViewRef?.current,
|
|
488
517
|
hasTriggerPosition: !!triggerPositionRef.current,
|
|
489
518
|
triggerPosition: triggerPositionRef.current
|
|
490
519
|
});
|
|
491
520
|
|
|
492
521
|
if (!parentScrollViewRef?.current) {
|
|
493
|
-
|
|
522
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: parentScrollViewRef not available, tag=', tag);
|
|
494
523
|
return;
|
|
495
524
|
}
|
|
496
525
|
|
|
497
526
|
// Use stored trigger position (captured when trigger was clicked)
|
|
498
527
|
const storedPosition = triggerPositionRef.current;
|
|
499
528
|
if (!storedPosition) {
|
|
500
|
-
|
|
529
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: no stored trigger position, tag=', tag);
|
|
501
530
|
return;
|
|
502
531
|
}
|
|
503
532
|
|
|
@@ -513,7 +542,7 @@ const AutoPositionedPopup = memo(
|
|
|
513
542
|
const visibleAreaBottom = screenHeight - keyboardApproxHeight;
|
|
514
543
|
const triggerBottom = triggerY + triggerHeight;
|
|
515
544
|
|
|
516
|
-
|
|
545
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: calculations=', {
|
|
517
546
|
tag,
|
|
518
547
|
triggerY,
|
|
519
548
|
triggerHeight,
|
|
@@ -527,7 +556,7 @@ const AutoPositionedPopup = memo(
|
|
|
527
556
|
if (triggerBottom > visibleAreaBottom) {
|
|
528
557
|
// Calculate scroll amount to bring trigger above keyboard
|
|
529
558
|
const scrollAmount = triggerBottom - visibleAreaBottom + scrollExtraHeight;
|
|
530
|
-
|
|
559
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: scrolling, amount=', scrollAmount, 'tag=', tag);
|
|
531
560
|
|
|
532
561
|
// Use scrollForExtraHeightOnAndroid for KeyboardAwareScrollView
|
|
533
562
|
if (typeof scrollView.scrollForExtraHeightOnAndroid === 'function') {
|
|
@@ -538,10 +567,10 @@ const AutoPositionedPopup = memo(
|
|
|
538
567
|
// Fallback to standard ScrollView method
|
|
539
568
|
(scrollView as any).scrollTo({ y: scrollAmount, animated: true });
|
|
540
569
|
} else {
|
|
541
|
-
|
|
570
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: no scroll method available on scrollView');
|
|
542
571
|
}
|
|
543
572
|
} else {
|
|
544
|
-
|
|
573
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: trigger already visible, no scroll needed, tag=', tag);
|
|
545
574
|
}
|
|
546
575
|
}, [parentScrollViewRef, scrollExtraHeight, tag]);
|
|
547
576
|
|
|
@@ -551,10 +580,10 @@ const AutoPositionedPopup = memo(
|
|
|
551
580
|
useEffect(() => {
|
|
552
581
|
(async () => {
|
|
553
582
|
})();
|
|
554
|
-
|
|
583
|
+
debugLog(`AutoPositionedPopup componentDidMount=`, {tag, CustomPopView});
|
|
555
584
|
//componentWillUnmount
|
|
556
585
|
return () => {
|
|
557
|
-
|
|
586
|
+
debugLog(`AutoPositionedPopup componentWillUnmount tag=`, tag);
|
|
558
587
|
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
559
588
|
setSearchQuery('');
|
|
560
589
|
if (textInputRef.current) {
|
|
@@ -567,7 +596,7 @@ const AutoPositionedPopup = memo(
|
|
|
567
596
|
};
|
|
568
597
|
}, []);
|
|
569
598
|
useEffect(() => {
|
|
570
|
-
|
|
599
|
+
debugLog('AutoPositionedPopup rootViews=', {tag, rootViews});
|
|
571
600
|
rootViewsRef.current = rootViews;
|
|
572
601
|
if (rootViews.length === 0) {
|
|
573
602
|
hasAddedRootView.current = false;
|
|
@@ -584,10 +613,10 @@ const AutoPositionedPopup = memo(
|
|
|
584
613
|
}
|
|
585
614
|
}, [rootViews]);
|
|
586
615
|
useEffect(() => {
|
|
587
|
-
|
|
588
|
-
|
|
616
|
+
debugLog('AutoPositionedPopup useEffect [selectedItem, state.selectedItem, tag]=', {tag, selectedItem, 'state.selectedItem': state.selectedItem});
|
|
617
|
+
debugLog('AutoPositionedPopup useEffect state.selectedItem=', state.selectedItem);
|
|
589
618
|
if (state.selectedItem?.id !== selectedItem?.id || state.selectedItem?.title != selectedItem?.title) {
|
|
590
|
-
|
|
619
|
+
debugLog('AutoPositionedPopup useEffect selectedItem!=state.selectedItem');
|
|
591
620
|
setState((prevState) => {
|
|
592
621
|
return {
|
|
593
622
|
...prevState,
|
|
@@ -603,7 +632,7 @@ const AutoPositionedPopup = memo(
|
|
|
603
632
|
prevPropsRef.current.CustomPopView !== CustomPopView ||
|
|
604
633
|
prevPropsRef.current.CustomPopViewStyle !== CustomPopViewStyle ||
|
|
605
634
|
(prevPropsRef.current.TextInputProps !== TextInputProps && useTextInput);
|
|
606
|
-
|
|
635
|
+
debugLog('AutoPositionedPopup useEffect [isKeyboardFullyShown,\n' +
|
|
607
636
|
' state.isFocus,\n' +
|
|
608
637
|
' useTextInput,\n' +
|
|
609
638
|
' CustomPopView,\n' +
|
|
@@ -634,13 +663,19 @@ const AutoPositionedPopup = memo(
|
|
|
634
663
|
TextInputProps
|
|
635
664
|
};
|
|
636
665
|
// Only execute logic when keyboard state actually changes or user actively operates
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
666
|
+
// CRITICAL FIX: Also allow execution when popup needs initial positioning
|
|
667
|
+
// hasAddedRootView.current = true means popup container exists
|
|
668
|
+
// hasShownRootView.current = false means positioning not done yet
|
|
669
|
+
// We MUST allow execution when popup needs positioning, even if keyboard state unchanged
|
|
670
|
+
if (!keyboardStateChanged && hasAddedRootView.current && hasShownRootView.current) {
|
|
671
|
+
debugLog('AutoPositionedPopup: Skip execution - already positioned and keyboard state unchanged');
|
|
642
672
|
return;
|
|
643
673
|
}
|
|
674
|
+
|
|
675
|
+
// Log when we're allowing execution for initial positioning
|
|
676
|
+
if (!keyboardStateChanged && hasAddedRootView.current && !hasShownRootView.current) {
|
|
677
|
+
debugLog('AutoPositionedPopup: ALLOWING execution for initial positioning (popup added but not positioned yet)');
|
|
678
|
+
}
|
|
644
679
|
const getStatusBarHeight = (): number => {
|
|
645
680
|
if (Platform.OS === 'android') {
|
|
646
681
|
// Android: Use StatusBar.currentHeight API
|
|
@@ -656,130 +691,163 @@ const AutoPositionedPopup = memo(
|
|
|
656
691
|
const statusBarHeight = getStatusBarHeight();
|
|
657
692
|
if (useTextInput) {
|
|
658
693
|
if (isKeyboardFullyShown && hasAddedRootView.current && !hasShownRootView.current && state.isFocus) {
|
|
659
|
-
// KEYBOARD AVOIDANCE FIX:
|
|
660
|
-
//
|
|
661
|
-
//
|
|
662
|
-
if (parentScrollViewRef?.current) {
|
|
663
|
-
|
|
664
|
-
// Use
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
694
|
+
// KEYBOARD AVOIDANCE FIX: Use KeyboardAwareScrollView's native scrollToFocusedInput method
|
|
695
|
+
// This properly scrolls to the dynamically created TextInput without causing double scrolling.
|
|
696
|
+
// The previous custom scrollToTriggerWithMeasure() caused over-scrolling issues.
|
|
697
|
+
if (parentScrollViewRef?.current && textInputRef.current) {
|
|
698
|
+
debugLog('AutoPositionedPopup: Keyboard appeared, using scrollToFocusedInput to scroll parent');
|
|
699
|
+
// Use KeyboardAwareScrollView's native method to scroll to the focused TextInput
|
|
700
|
+
// This is more reliable than custom scroll calculations
|
|
701
|
+
const scrollView = parentScrollViewRef.current;
|
|
702
|
+
if (typeof scrollView.scrollToFocusedInput === 'function') {
|
|
703
|
+
// findNodeHandle is needed to get the native node reference
|
|
704
|
+
const nodeHandle = findNodeHandle(textInputRef.current);
|
|
705
|
+
if (nodeHandle) {
|
|
706
|
+
// scrollToFocusedInput expects a ReactNode, use the TextInput ref
|
|
707
|
+
scrollView.scrollToFocusedInput(textInputRef.current, scrollExtraHeight);
|
|
708
|
+
debugLog('AutoPositionedPopup: Called scrollToFocusedInput with extraHeight=', scrollExtraHeight);
|
|
709
|
+
}
|
|
710
|
+
} else {
|
|
711
|
+
debugLog('AutoPositionedPopup: scrollToFocusedInput not available, skipping scroll');
|
|
712
|
+
}
|
|
668
713
|
}
|
|
669
714
|
|
|
670
715
|
// CRITICAL FIX FOR KEYBOARD POSITION CALCULATION
|
|
671
716
|
// Problem: When keyboard appears, the page shifts up but measureInWindow executes too early
|
|
672
|
-
// Solution: Wait for keyboard animation
|
|
717
|
+
// Solution: Wait for keyboard animation + page scroll to complete before measuring
|
|
673
718
|
//
|
|
674
719
|
// Timing breakdown:
|
|
675
720
|
// 1. Keyboard animation: ~250-300ms (iOS/Android)
|
|
676
|
-
// 2. Page shift animation: ~
|
|
677
|
-
// 3. Layout tree update: ~
|
|
678
|
-
// Total: ~
|
|
721
|
+
// 2. Page shift animation: ~300-500ms (KeyboardAwareScrollView)
|
|
722
|
+
// 3. Layout tree update: ~100-200ms (React Native)
|
|
723
|
+
// Total: ~700-1000ms needed for stable layout
|
|
724
|
+
//
|
|
725
|
+
// USER REQUEST (2025-01-04): Wait 1 second (1000ms) after keyboard appears
|
|
726
|
+
// to ensure trigger component position has fully stabilized after scroll
|
|
679
727
|
//
|
|
680
|
-
// Strategy: setTimeout(
|
|
728
|
+
// Strategy: setTimeout(1000ms) waits for all animations to complete,
|
|
681
729
|
// then requestAnimationFrame ensures measurement happens after next render frame
|
|
730
|
+
const KEYBOARD_STABILIZATION_DELAY = 500; // 500ms as requested by user
|
|
731
|
+
positionDebugLog(`POPUP_WAIT: Waiting ${KEYBOARD_STABILIZATION_DELAY}ms for keyboard/scroll stabilization, tag=${tag}`);
|
|
682
732
|
setTimeout(() => {
|
|
733
|
+
positionDebugLog(`POPUP_MEASURE_START: ${KEYBOARD_STABILIZATION_DELAY}ms elapsed, now measuring position for tag=${tag}`);
|
|
683
734
|
requestAnimationFrame(() => {
|
|
684
|
-
// CRITICAL FIX:
|
|
685
|
-
//
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
y = fallbackY;
|
|
700
|
-
width = fallbackWidth;
|
|
701
|
-
height = 50;
|
|
702
|
-
}
|
|
703
|
-
// CRITICAL FIX: Coordinate system mismatch issue
|
|
704
|
-
// Problem: measureInWindow returns coordinates relative to window (fixed reference),
|
|
705
|
-
// but popup uses absolute positioning relative to App container (which shifts when keyboard appears)
|
|
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=', {
|
|
717
|
-
screenHeight,
|
|
718
|
-
componentY: y,
|
|
719
|
-
componentHeight: height,
|
|
720
|
-
listHeight: listLayout.height
|
|
735
|
+
// CRITICAL FIX: Measure CURRENT position AFTER keyboard animation completes
|
|
736
|
+
// DO NOT use stored triggerPositionRef because keyboard may have shifted the view up
|
|
737
|
+
// Instead, measure the outer wrapper (refAutoPositionedPopup)
|
|
738
|
+
// which reflects the ACTUAL current position after keyboard shift
|
|
739
|
+
|
|
740
|
+
// DEBUG: Log both refs to compare their positions
|
|
741
|
+
positionDebugLog(`POPUP_REFS: textInputRef=${!!textInputRef.current} refAutoPositionedPopup=${!!refAutoPositionedPopup.current}`);
|
|
742
|
+
|
|
743
|
+
// Measure BOTH refs for comparison
|
|
744
|
+
if (textInputRef.current && refAutoPositionedPopup.current) {
|
|
745
|
+
textInputRef.current.measureInWindow((tx: number | undefined, ty: number | undefined, tw: number | undefined, th: number | undefined) => {
|
|
746
|
+
debugLog('AutoPositionedPopup DEBUG: textInputRef position=', {x: tx, y: ty, width: tw, height: th});
|
|
747
|
+
});
|
|
748
|
+
refAutoPositionedPopup.current.measureInWindow((rx: number | undefined, ry: number | undefined, rw: number | undefined, rh: number | undefined) => {
|
|
749
|
+
debugLog('AutoPositionedPopup DEBUG: refAutoPositionedPopup position=', {x: rx, y: ry, width: rw, height: rh});
|
|
721
750
|
});
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// CRITICAL FIX: Use textInputRef as primary measurement target
|
|
754
|
+
// refAutoPositionedPopup.measureInWindow() returns undefined values
|
|
755
|
+
// because the outer wrapper View uses flex:1/height:100% which makes it unmeasurable
|
|
756
|
+
// textInputRef reliably returns the actual position of the input field
|
|
757
|
+
const measureTarget = textInputRef.current || refAutoPositionedPopup.current;
|
|
758
|
+
|
|
759
|
+
if (!measureTarget) {
|
|
760
|
+
debugLog('AutoPositionedPopup useTextInput: no measureTarget available, using fallback');
|
|
761
|
+
const screenHeightFallback = Dimensions.get('window').height;
|
|
762
|
+
const screenWidthFallback = Dimensions.get('window').width;
|
|
763
|
+
const fallbackY = (screenHeightFallback - listLayout.height) / 2;
|
|
764
|
+
ref_listPos.current = {x: screenWidthFallback * 0.05, y: fallbackY, width: screenWidthFallback * 0.9};
|
|
765
|
+
updateRootView(tag, {
|
|
766
|
+
style: {
|
|
767
|
+
top: ref_listPos.current?.y,
|
|
768
|
+
left: popUpViewStyle?.left,
|
|
769
|
+
width: popUpViewStyle?.width,
|
|
770
|
+
height: listLayout.height,
|
|
771
|
+
opacity: 1,
|
|
772
|
+
}
|
|
741
773
|
});
|
|
774
|
+
hasShownRootView.current = true;
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Determine which ref is actually being used (for logging)
|
|
779
|
+
const usingTextInputRef = measureTarget === textInputRef.current;
|
|
780
|
+
debugLog('AutoPositionedPopup useTextInput: using measureTarget=', usingTextInputRef ? 'textInputRef' : 'refAutoPositionedPopup');
|
|
781
|
+
|
|
782
|
+
// V19f: Position popup above trigger
|
|
783
|
+
// Parent KeyboardAwareScrollView is responsible for scrolling trigger into view
|
|
784
|
+
// This component only handles popup positioning relative to trigger's FINAL position
|
|
785
|
+
const screenHeight = Dimensions.get('window').height;
|
|
786
|
+
const screenWidth = Dimensions.get('window').width;
|
|
787
|
+
const currentKeyboardHeight = keyboardHeightRef.current;
|
|
788
|
+
const popupHeight = listLayout.height; // 200px
|
|
789
|
+
|
|
790
|
+
positionDebugLog(`V19f_SCREEN: height=${screenHeight} width=${screenWidth} keyboardH=${currentKeyboardHeight} statusBarH=${statusBarHeight}`);
|
|
791
|
+
|
|
792
|
+
measureTarget.measureInWindow((x: number | undefined, y: number | undefined, width: number | undefined, height: number | undefined) => {
|
|
793
|
+
positionDebugLog(`V19f_MEASURE: triggerX=${x} triggerY=${y} triggerW=${width} triggerH=${height}`);
|
|
742
794
|
|
|
743
|
-
//
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
popupY = y + height + height;
|
|
750
|
-
console.log('AutoPositionedPopup with keyboard: BELOW position calculated:', {
|
|
751
|
-
formula: 'y + 2*height',
|
|
752
|
-
y,
|
|
753
|
-
height,
|
|
754
|
-
popupY
|
|
795
|
+
// Handle undefined values
|
|
796
|
+
if (x === undefined || y === undefined || width === undefined || height === undefined) {
|
|
797
|
+
positionDebugLog('V19f: undefined values, using center fallback');
|
|
798
|
+
const fallbackY = (screenHeight - currentKeyboardHeight - popupHeight) / 2;
|
|
799
|
+
updateRootView(tag, {
|
|
800
|
+
style: { top: fallbackY, left: popUpViewStyle?.left, width: popUpViewStyle?.width, height: popupHeight, opacity: 1 }
|
|
755
801
|
});
|
|
802
|
+
hasShownRootView.current = true;
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
756
805
|
|
|
757
|
-
|
|
758
|
-
|
|
806
|
+
const triggerTop = y;
|
|
807
|
+
const triggerHeight = height;
|
|
808
|
+
const triggerBottom = y + height;
|
|
809
|
+
const keyboardTop = screenHeight - currentKeyboardHeight;
|
|
810
|
+
|
|
811
|
+
positionDebugLog(`V19f_ANALYSIS: triggerTop=${triggerTop} triggerBottom=${triggerBottom} keyboardTop=${keyboardTop}`);
|
|
812
|
+
|
|
813
|
+
// V19f: Position popup DIRECTLY above trigger
|
|
814
|
+
// ADD statusBarHeight to close the gap (coordinates adjustment)
|
|
815
|
+
let popupY = triggerTop - popupHeight + statusBarHeight;
|
|
816
|
+
let position = 'ABOVE';
|
|
817
|
+
|
|
818
|
+
positionDebugLog(`V19f_CALC: base=${triggerTop - popupHeight} + statusBarH=${statusBarHeight} = popupY=${popupY}`);
|
|
819
|
+
|
|
820
|
+
// Safety check: ensure popup doesn't go above screen top
|
|
821
|
+
if (popupY < 0) {
|
|
822
|
+
// If popup would go off screen top, position it BELOW trigger instead
|
|
823
|
+
popupY = triggerBottom + statusBarHeight;
|
|
824
|
+
position = 'BELOW';
|
|
825
|
+
// Clamp to stay above keyboard
|
|
826
|
+
const maxY = keyboardTop - popupHeight;
|
|
759
827
|
if (popupY > maxY) {
|
|
760
|
-
|
|
761
|
-
console.log('AutoPositionedPopup with keyboard: both positions problematic, clamping to visible area');
|
|
762
|
-
popupY = Math.min(Math.max(statusBarHeight, y - listLayout.height), maxY);
|
|
828
|
+
popupY = maxY;
|
|
763
829
|
}
|
|
764
|
-
|
|
765
|
-
console.log('AutoPositionedPopup with keyboard: showing ABOVE input field (preferred position)');
|
|
830
|
+
positionDebugLog(`V19f_BELOW: popupY=${popupY} (clamped to stay above keyboard)`);
|
|
766
831
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
832
|
+
|
|
833
|
+
// V19f: Verification
|
|
834
|
+
const popupBottom = popupY + popupHeight;
|
|
835
|
+
const gapPixels = triggerTop - popupBottom;
|
|
836
|
+
|
|
837
|
+
positionDebugLog(`V19f_RESULT: position=${position} popupY=${popupY} popupBottom=${popupBottom}`);
|
|
838
|
+
positionDebugLog(`V19f_GAP: trigger_top=${triggerTop} - popup_bottom=${popupBottom} = gap=${gapPixels}px`);
|
|
839
|
+
|
|
840
|
+
ref_listPos.current = {x, y: popupY, width};
|
|
841
|
+
updateRootView(tag, {
|
|
842
|
+
style: { top: popupY, left: popUpViewStyle?.left, width: popUpViewStyle?.width, height: listLayout.height, opacity: 1 }
|
|
775
843
|
});
|
|
776
844
|
hasShownRootView.current = true;
|
|
777
845
|
});
|
|
778
846
|
});
|
|
779
|
-
},
|
|
847
|
+
}, KEYBOARD_STABILIZATION_DELAY) // 1000ms to wait for keyboard + scroll stabilization (user request 2025-01-04)
|
|
780
848
|
} else if (!isKeyboardFullyShown && ref_isFocus.current && keyboardStateChanged) {
|
|
781
849
|
// Only execute close logic when keyboard state actually changes from true to false
|
|
782
|
-
|
|
850
|
+
debugLog(
|
|
783
851
|
'AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView (keyboard state changed)=',
|
|
784
852
|
{tag, forceRemoveAllRootViewOnItemSelected, keyboardStateChanged}
|
|
785
853
|
);
|
|
@@ -795,217 +863,83 @@ const AutoPositionedPopup = memo(
|
|
|
795
863
|
hasShownRootView.current = false;
|
|
796
864
|
}
|
|
797
865
|
} else {
|
|
866
|
+
// V17 SIMPLIFICATION: When useTextInput=false, ALWAYS show popup in CENTER of screen
|
|
867
|
+
// User request: "只要传入的 useTextInput 是 false, 弹框都显示在屏幕中间"
|
|
868
|
+
// This avoids all complex positioning calculations that kept failing
|
|
798
869
|
if (state.isFocus) {
|
|
799
870
|
if (isKeyboardFullyShown) {
|
|
800
871
|
Keyboard.dismiss();
|
|
801
872
|
return;
|
|
802
873
|
}
|
|
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
874
|
|
|
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
|
-
});
|
|
875
|
+
debugLog('🟢🟢🟢 POPUP_V17 useTextInput=false, showing popup in CENTER of screen');
|
|
880
876
|
|
|
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
|
-
}
|
|
877
|
+
const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
|
|
878
|
+
? CustomPopViewStyle.height
|
|
879
|
+
: listLayout.height;
|
|
880
|
+
|
|
881
|
+
if (CustomPopView && CustomPopViewStyle) {
|
|
882
|
+
const PopViewComponent = CustomPopView();
|
|
883
|
+
debugLog('🔵🔵🔵 POPUP_V17 CustomPopView centerDisplay=true');
|
|
884
|
+
addRootView({
|
|
885
|
+
id: tag,
|
|
886
|
+
style: { width: popUpViewStyle?.width, ...CustomPopViewStyle },
|
|
887
|
+
component: <PopViewComponent selectedItem={state.selectedItem}></PopViewComponent>,
|
|
888
|
+
useModal: true,
|
|
889
|
+
centerDisplay: true, // V17: Force center display for useTextInput=false
|
|
890
|
+
onModalClose: () => {
|
|
891
|
+
debugLog('AutoPositionedPopup V17 onModalClose tag=', tag);
|
|
892
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
893
|
+
setState((prevState) => ({ ...prevState }));
|
|
894
|
+
setSearchQuery('');
|
|
895
|
+
},
|
|
896
|
+
});
|
|
897
|
+
} else {
|
|
898
|
+
debugLog('🔵🔵🔵 POPUP_V17 List centerDisplay=true, height=', listLayout.height);
|
|
899
|
+
addRootView({
|
|
900
|
+
id: tag,
|
|
901
|
+
style: { width: popUpViewStyle?.width, height: listLayout.height, opacity: 1 },
|
|
902
|
+
component: (
|
|
903
|
+
<AutoPositionedPopupList
|
|
904
|
+
tag={tag} updateState={updateState} fetchData={fetchData} pageSize={pageSize}
|
|
905
|
+
renderItem={renderItem} selectedItem={state.selectedItem} localSearch={localSearch}
|
|
906
|
+
showListEmptyComponent={showListEmptyComponent} emptyText={emptyText} themeMode={themeMode}
|
|
907
|
+
/>
|
|
908
|
+
),
|
|
909
|
+
useModal: true,
|
|
910
|
+
centerDisplay: true, // V17: Force center display for useTextInput=false
|
|
911
|
+
onModalClose: () => {
|
|
912
|
+
debugLog('AutoPositionedPopup V17 onModalClose tag=', tag);
|
|
913
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
914
|
+
setState((prevState) => ({ ...prevState }));
|
|
915
|
+
setSearchQuery('');
|
|
916
|
+
},
|
|
917
|
+
});
|
|
991
918
|
}
|
|
919
|
+
return; // V17: Early return after handling !useTextInput case
|
|
992
920
|
}
|
|
993
921
|
}
|
|
994
922
|
}, [isKeyboardFullyShown,
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
923
|
+
state.isFocus,
|
|
924
|
+
useTextInput,
|
|
925
|
+
CustomPopView,
|
|
926
|
+
CustomPopViewStyle,
|
|
927
|
+
forceRemoveAllRootViewOnItemSelected,
|
|
928
|
+
tag, TextInputProps,
|
|
929
|
+
state.selectedItem, showListEmptyComponent, themeMode
|
|
930
|
+
]);
|
|
931
|
+
|
|
932
|
+
// V18: All positioning logic is now in the useEffect above
|
|
933
|
+
// V18 FIX (2025-01-04): Wait 1000ms after keyboard appears before measuring position
|
|
934
|
+
// This ensures trigger position is stable after KeyboardAwareScrollView scrolls
|
|
935
|
+
// Formula: top = componentY - popupHeight (popup bottom touches trigger top exactly)
|
|
936
|
+
|
|
937
|
+
// Imperative handle for parent component access
|
|
938
|
+
useImperativeHandle(
|
|
1005
939
|
parentRef,
|
|
1006
940
|
() => ({
|
|
1007
941
|
clearSelectedItem: () => {
|
|
1008
|
-
|
|
942
|
+
debugLog('AutoPositionedPopup clearSelectedItem tag=', tag);
|
|
1009
943
|
setState((prevState) => {
|
|
1010
944
|
return {
|
|
1011
945
|
...prevState,
|
|
@@ -1027,14 +961,14 @@ const AutoPositionedPopup = memo(
|
|
|
1027
961
|
[]
|
|
1028
962
|
);
|
|
1029
963
|
const updateState = (key: string, value: SelectedItem) => {
|
|
1030
|
-
|
|
964
|
+
debugLog('AutoPositionedPopup updateState=', {key, value});
|
|
1031
965
|
setState((prevState) => ({
|
|
1032
966
|
...prevState,
|
|
1033
967
|
[key]: value,
|
|
1034
968
|
}));
|
|
1035
969
|
if (key === 'selectedItem' && onItemSelected) {
|
|
1036
970
|
onItemSelected(value);
|
|
1037
|
-
|
|
971
|
+
debugLog('AutoPositionedPopup updateState onItemSelected rootViewsRef.current=', rootViewsRef.current);
|
|
1038
972
|
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
1039
973
|
hasAddedRootView.current = false;
|
|
1040
974
|
hasShownRootView.current = false;
|
|
@@ -1069,7 +1003,7 @@ const AutoPositionedPopup = memo(
|
|
|
1069
1003
|
// Only update when deep comparison detects real changes to avoid TextInput recreation due to reference changes during parent component redraws
|
|
1070
1004
|
const stableInputStyle = useMemo(() => {
|
|
1071
1005
|
if (!shallowEqual(stableInputStyleRef.current, inputStyle)) {
|
|
1072
|
-
|
|
1006
|
+
debugLog(`AutoPositionedPopup stableInputStyle: `, {tag, inputStyle, themeMode});
|
|
1073
1007
|
stableInputStyleRef.current = inputStyle;
|
|
1074
1008
|
}
|
|
1075
1009
|
return stableInputStyleRef.current;
|
|
@@ -1077,10 +1011,10 @@ const AutoPositionedPopup = memo(
|
|
|
1077
1011
|
|
|
1078
1012
|
const stableTextInputProps = useMemo(() => {
|
|
1079
1013
|
if (!shallowEqual(stableTextInputPropsRef.current, TextInputProps)) {
|
|
1080
|
-
|
|
1014
|
+
debugLog(`AutoPositionedPopup TextInputProps deep change detected, updating stable reference - tag: ${tag}`);
|
|
1081
1015
|
stableTextInputPropsRef.current = TextInputProps;
|
|
1082
1016
|
}
|
|
1083
|
-
|
|
1017
|
+
debugLog('AutoPositionedPopup stableTextInputProps=', {tag, TextInputProps, 'stableTextInputPropsRef.current': stableTextInputPropsRef.current})
|
|
1084
1018
|
return stableTextInputPropsRef.current;
|
|
1085
1019
|
}, [TextInputProps, tag]);
|
|
1086
1020
|
|
|
@@ -1093,7 +1027,7 @@ const AutoPositionedPopup = memo(
|
|
|
1093
1027
|
const handleTextInputFocus = useCallback(() => {
|
|
1094
1028
|
const currentTime = Date.now();
|
|
1095
1029
|
const timeSinceLastFocus = currentTime - lastFocusTimeRef.current;
|
|
1096
|
-
|
|
1030
|
+
debugLog(
|
|
1097
1031
|
'AutoPositionedPopup onFocus=',
|
|
1098
1032
|
{
|
|
1099
1033
|
tag,
|
|
@@ -1108,17 +1042,17 @@ const AutoPositionedPopup = memo(
|
|
|
1108
1042
|
);
|
|
1109
1043
|
// Prevent rapid repeated triggers (repeated events within 300ms are ignored)
|
|
1110
1044
|
if (timeSinceLastFocus < 300) {
|
|
1111
|
-
|
|
1045
|
+
debugLog('AutoPositionedPopup onFocus: Skip - event triggered too quickly (< 300ms)');
|
|
1112
1046
|
return;
|
|
1113
1047
|
}
|
|
1114
1048
|
// Skip if keyboard is already open and focus has been handled
|
|
1115
1049
|
if (isKeyboardFullyShown && hasTriggeredFocus.current) {
|
|
1116
|
-
|
|
1050
|
+
debugLog('AutoPositionedPopup onFocus: Skip - keyboard already open and focus handled');
|
|
1117
1051
|
return;
|
|
1118
1052
|
}
|
|
1119
1053
|
// Prevent concurrent processing
|
|
1120
1054
|
if (isFocusEventProcessingRef.current) {
|
|
1121
|
-
|
|
1055
|
+
debugLog('AutoPositionedPopup onFocus: Skip - processing another focus event');
|
|
1122
1056
|
return;
|
|
1123
1057
|
}
|
|
1124
1058
|
isFocusEventProcessingRef.current = true;
|
|
@@ -1142,7 +1076,7 @@ const AutoPositionedPopup = memo(
|
|
|
1142
1076
|
}, [tag, isKeyboardFullyShown]); // Remove state.selectedItem, use stateRef instead
|
|
1143
1077
|
|
|
1144
1078
|
const handleTextInputBlur = useCallback(() => {
|
|
1145
|
-
|
|
1079
|
+
debugLog(
|
|
1146
1080
|
'AutoPositionedPopup onBlur=',
|
|
1147
1081
|
{
|
|
1148
1082
|
tag,
|
|
@@ -1153,7 +1087,7 @@ const AutoPositionedPopup = memo(
|
|
|
1153
1087
|
);
|
|
1154
1088
|
// If keyboard is still open, this is a false trigger caused by parent component re-render, should not reset
|
|
1155
1089
|
if (isKeyboardFullyShown && hasTriggeredFocus.current) {
|
|
1156
|
-
|
|
1090
|
+
debugLog('AutoPositionedPopup onBlur: Skip - keyboard still open, possibly caused by parent component re-render');
|
|
1157
1091
|
return;
|
|
1158
1092
|
}
|
|
1159
1093
|
|
|
@@ -1182,7 +1116,7 @@ const AutoPositionedPopup = memo(
|
|
|
1182
1116
|
// Wrap TextInput independently in useMemo to recreate only when key props change
|
|
1183
1117
|
// This avoids repeated ref callback triggers due to other props changes during parent component redraws
|
|
1184
1118
|
const memoizedTextInput = useMemo(() => {
|
|
1185
|
-
|
|
1119
|
+
debugLog('AutoPositionedPopup memoizedTextInput=', {tag, useTextInput, 'state.isFocus': state.isFocus, stableTextInputProps});
|
|
1186
1120
|
if (!useTextInput || !state.isFocus) {
|
|
1187
1121
|
return null;
|
|
1188
1122
|
}
|
|
@@ -1191,11 +1125,11 @@ const AutoPositionedPopup = memo(
|
|
|
1191
1125
|
ref={(ref) => {
|
|
1192
1126
|
// Monitor TextInput mounting and unmounting
|
|
1193
1127
|
if (ref && !textInputRef.current) {
|
|
1194
|
-
|
|
1128
|
+
debugLog(`AutoPositionedPopup TextInput created/mounted - tag: ${tag}, ref:`, ref);
|
|
1195
1129
|
} else if (!ref && textInputRef.current) {
|
|
1196
|
-
|
|
1130
|
+
debugLog(`AutoPositionedPopup TextInput unmounted - tag: ${tag}`);
|
|
1197
1131
|
} else if (ref && textInputRef.current && ref !== textInputRef.current) {
|
|
1198
|
-
|
|
1132
|
+
debugLog(`AutoPositionedPopup TextInput replaced - tag: ${tag}, oldRef:`, textInputRef.current, 'newRef:', ref);
|
|
1199
1133
|
}
|
|
1200
1134
|
textInputRef.current = ref;
|
|
1201
1135
|
}}
|
|
@@ -1203,14 +1137,14 @@ const AutoPositionedPopup = memo(
|
|
|
1203
1137
|
style={[
|
|
1204
1138
|
styles.inputStyle,
|
|
1205
1139
|
stableInputStyle,
|
|
1206
|
-
(themeMode==='dark' && {color:'#fff'})
|
|
1140
|
+
(themeMode==='dark' && {color:'#fff'}),
|
|
1207
1141
|
]}
|
|
1208
1142
|
textAlign={stableTextInputProps && stableTextInputProps['textAlign'] || 'left'}
|
|
1209
1143
|
multiline={stableTextInputProps && stableTextInputProps['multiline'] || false}
|
|
1210
1144
|
numberOfLines={stableTextInputProps && stableTextInputProps['numberOfLines'] || 1}
|
|
1211
1145
|
onChangeText={(searchQuery) => {
|
|
1212
1146
|
ref_searchQuery.current = searchQuery;
|
|
1213
|
-
|
|
1147
|
+
debugLog('AutoPositionedPopup onChangeText rootViews=', rootViews);
|
|
1214
1148
|
if (!localSearch) {
|
|
1215
1149
|
if (debounceTimerRef.current) {
|
|
1216
1150
|
clearTimeout(debounceTimerRef.current);
|
|
@@ -1249,7 +1183,7 @@ const AutoPositionedPopup = memo(
|
|
|
1249
1183
|
onBlur={handleTextInputBlur}
|
|
1250
1184
|
selectTextOnFocus={stableTextInputProps && stableTextInputProps['selectTextOnFocus'] || false}
|
|
1251
1185
|
onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
|
|
1252
|
-
|
|
1186
|
+
debugLog(
|
|
1253
1187
|
'AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=',
|
|
1254
1188
|
e.nativeEvent.text
|
|
1255
1189
|
);
|
|
@@ -1273,7 +1207,7 @@ const AutoPositionedPopup = memo(
|
|
|
1273
1207
|
|
|
1274
1208
|
// Render the component following project implementation
|
|
1275
1209
|
return useMemo(() => {
|
|
1276
|
-
|
|
1210
|
+
debugLog('AutoPositionedPopup render tag=', tag); // Now safe - circular dependency fixed
|
|
1277
1211
|
return (
|
|
1278
1212
|
<CustomRow>
|
|
1279
1213
|
<View style={[styles.contain, style]} ref={refAutoPositionedPopup}>
|
|
@@ -1283,7 +1217,7 @@ const AutoPositionedPopup = memo(
|
|
|
1283
1217
|
style={[styles.AutoPositionedPopupBtn, AutoPositionedPopupBtnStyle]}
|
|
1284
1218
|
disabled={AutoPositionedPopupBtnDisabled}
|
|
1285
1219
|
onPress={() => {
|
|
1286
|
-
|
|
1220
|
+
debugLog('AutoPositionedPopup onPress=', {
|
|
1287
1221
|
tag,
|
|
1288
1222
|
'state.isFocus': state.isFocus,
|
|
1289
1223
|
useTextInput,
|
|
@@ -1296,10 +1230,13 @@ const AutoPositionedPopup = memo(
|
|
|
1296
1230
|
|
|
1297
1231
|
// Capture trigger button position BEFORE switching to TextInput
|
|
1298
1232
|
// This is critical because triggerBtnRef will become null after isFocus=true
|
|
1299
|
-
|
|
1233
|
+
// IMPORTANT: Always capture position regardless of parentScrollViewRef
|
|
1234
|
+
if (triggerBtnRef.current) {
|
|
1300
1235
|
triggerBtnRef.current.measureInWindow((x, y, width, height) => {
|
|
1301
|
-
|
|
1302
|
-
|
|
1236
|
+
debugLog('AutoPositionedPopup onPress: captured trigger position=', {tag, x, y, width, height});
|
|
1237
|
+
if (x !== undefined && y !== undefined && width !== undefined && height !== undefined) {
|
|
1238
|
+
triggerPositionRef.current = {x, y, width, height};
|
|
1239
|
+
}
|
|
1303
1240
|
});
|
|
1304
1241
|
}
|
|
1305
1242
|
|
|
@@ -1357,6 +1294,9 @@ const AutoPositionedPopup = memo(
|
|
|
1357
1294
|
_addRootView()
|
|
1358
1295
|
}
|
|
1359
1296
|
} else {
|
|
1297
|
+
// V17 SIMPLIFICATION: For useTextInput=false, popup will be centered
|
|
1298
|
+
// No need for complex position measurement - just trigger focus
|
|
1299
|
+
debugLog('🔵🔵🔵 POPUP_V17 onPress useTextInput=false, will show centered popup');
|
|
1360
1300
|
setState((prevState) => {
|
|
1361
1301
|
return {
|
|
1362
1302
|
...prevState,
|
|
@@ -1364,7 +1304,7 @@ const AutoPositionedPopup = memo(
|
|
|
1364
1304
|
};
|
|
1365
1305
|
});
|
|
1366
1306
|
}
|
|
1367
|
-
|
|
1307
|
+
debugLog('AutoPositionedPopup onPress done')
|
|
1368
1308
|
}}
|
|
1369
1309
|
>
|
|
1370
1310
|
{!btwChildren ? (
|
|
@@ -1391,29 +1331,29 @@ const AutoPositionedPopup = memo(
|
|
|
1391
1331
|
);
|
|
1392
1332
|
}, [
|
|
1393
1333
|
tag,
|
|
1394
|
-
//
|
|
1334
|
+
// ⚠CRITICAL FIX: Remove all props that may change frequently or are inline functions
|
|
1395
1335
|
// Changes to these props should not cause the entire component tree to recreate, especially TextInput
|
|
1396
|
-
// fetchData, // ❌
|
|
1397
|
-
// renderItem, // ❌
|
|
1398
|
-
// onItemSelected, // ❌
|
|
1399
|
-
// onSubmitEditing, // ❌
|
|
1336
|
+
// fetchData, // ❌Removed: inline function
|
|
1337
|
+
// renderItem, // ❌Removed: possibly inline function
|
|
1338
|
+
// onItemSelected, // ❌Removed: possibly inline function
|
|
1339
|
+
// onSubmitEditing, // ❌Removed: possibly inline function
|
|
1400
1340
|
localSearch,
|
|
1401
|
-
// placeholder, // ❌
|
|
1402
|
-
// textAlign, // ❌
|
|
1341
|
+
// placeholder, // ❌Removed: may change
|
|
1342
|
+
// textAlign, // ❌Removed: may change
|
|
1403
1343
|
pageSize,
|
|
1404
1344
|
selectedItem,
|
|
1405
|
-
// CustomRow, // ❌
|
|
1345
|
+
// CustomRow, // ❌Removed: inline function, new reference each time
|
|
1406
1346
|
useTextInput,
|
|
1407
|
-
// btwChildren, // ❌
|
|
1408
|
-
// keyExtractor, // ❌
|
|
1409
|
-
// AutoPositionedPopupBtnStyle, // ❌
|
|
1410
|
-
// CustomPopView, // ❌
|
|
1411
|
-
// CustomPopViewStyle, // ❌
|
|
1347
|
+
// btwChildren, // ❌Removed: inline function
|
|
1348
|
+
// keyExtractor, // ❌Removed: possibly inline function
|
|
1349
|
+
// AutoPositionedPopupBtnStyle, // ❌Removed: possibly inline object
|
|
1350
|
+
// CustomPopView, // ❌Removed: may change
|
|
1351
|
+
// CustomPopViewStyle, // ❌Removed: may change
|
|
1412
1352
|
forceRemoveAllRootViewOnItemSelected,
|
|
1413
1353
|
state.isFocus,
|
|
1414
1354
|
showListEmptyComponent,
|
|
1415
1355
|
emptyText,
|
|
1416
|
-
//
|
|
1356
|
+
// ⚠Removed most dependencies that may cause re-rendering, keeping only core dependencies that truly affect component structure
|
|
1417
1357
|
// This prevents TextInput recreation due to inline functions/objects during parent component redraws
|
|
1418
1358
|
]);
|
|
1419
1359
|
}
|