react-native-auto-positioned-popup 1.2.14 → 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 +257 -307
- 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 +14 -6
- package/lib/KeyboardManager.js.map +1 -1
- package/lib/RootViewContext.d.ts.map +1 -1
- package/lib/RootViewContext.js +19 -8
- package/lib/RootViewContext.js.map +1 -1
- package/lib/constants.js +13 -0
- package/package.json +4 -4
- 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,14 @@
|
|
|
1
|
+
import { debugLog } from './constants';
|
|
2
|
+
// Module load marker - unique ID for tracking code version
|
|
3
|
+
// DEBUG FLAG: Set to false to disable all console logs for better performance
|
|
4
|
+
const POPUP_DEBUG = false;
|
|
5
|
+
const debugLog = (...args) => {
|
|
6
|
+
if (POPUP_DEBUG) {
|
|
7
|
+
debugLog(...args);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
// Only log module load in debug mode
|
|
11
|
+
debugLog('POPUP_MODULE_V17_LOADED at ' + new Date().toISOString());
|
|
1
12
|
import React, { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
2
13
|
import { Dimensions, findNodeHandle, Keyboard, Platform, StatusBar, Text, TextInput as RNTextInput, TouchableOpacity, View, } from 'react-native';
|
|
3
14
|
import { AdvancedFlatList } from 'react-native-advanced-flatlist';
|
|
@@ -6,7 +17,7 @@ import { useRootView } from './RootViewContext';
|
|
|
6
17
|
import { useKeyboardStatus } from './KeyboardManager';
|
|
7
18
|
const queryChangeListeners = [];
|
|
8
19
|
const emitQueryChange = (query) => {
|
|
9
|
-
|
|
20
|
+
debugLog('AutoPositionedPopup.tsx emitQueryChange query=', query, ' listeners=', queryChangeListeners.length);
|
|
10
21
|
queryChangeListeners.forEach((l) => l(query));
|
|
11
22
|
};
|
|
12
23
|
const subscribeQueryChange = (listener) => {
|
|
@@ -34,14 +45,14 @@ const ListItem = memo(({ updateState, item, index, selectedItem, themeMode }) =>
|
|
|
34
45
|
rootViewsRef.current = rootViews;
|
|
35
46
|
}, [rootViews]);
|
|
36
47
|
return useMemo(() => {
|
|
37
|
-
//
|
|
48
|
+
// debugLog('AutoPositionedPopup.tsx ListItem=', {index, item, selectedItem});
|
|
38
49
|
const isSelected = item.id === (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) || item.title == (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.title);
|
|
39
50
|
return (<TouchableOpacity key={item.id} style={[
|
|
40
51
|
styles.commonModalRow,
|
|
41
52
|
{ backgroundColor: isSelected ? (themeMode === 'light' ? 'rgba(116, 116, 128, 0.08)' : 'rgba(120, 120, 128, 0.36)') : 'transparent' },
|
|
42
53
|
]} onPress={() => {
|
|
43
|
-
//
|
|
44
|
-
//
|
|
54
|
+
// debugLog('AutoPositionedPopup.tsx ListItem onPress item=', item); // Commented to prevent spam
|
|
55
|
+
// debugLog('AutoPositionedPopup.tsx ListItem onPress rootViews=', rootViewsRef.current); // Commented to prevent spam
|
|
45
56
|
updateState('selectedItem', item);
|
|
46
57
|
}}>
|
|
47
58
|
<Text style={(themeMode === 'light' ? styles.ListItemCode : Object.assign(Object.assign({}, styles.ListItemCode), { color: '#fff' }))} numberOfLines={1} ellipsizeMode="tail">
|
|
@@ -69,16 +80,16 @@ const AutoPositionedPopupList = memo(({ tag, updateState, fetchData, keyExtracto
|
|
|
69
80
|
useEffect(() => {
|
|
70
81
|
(async () => {
|
|
71
82
|
})();
|
|
72
|
-
|
|
83
|
+
debugLog(`AutoPositionedPopupList componentDidMount`);
|
|
73
84
|
//componentWillUnmount
|
|
74
85
|
return () => {
|
|
75
|
-
|
|
86
|
+
debugLog(`AutoPositionedPopupList componentWillUnmount`);
|
|
76
87
|
setSearchQuery('');
|
|
77
88
|
};
|
|
78
89
|
}, []);
|
|
79
90
|
useEffect(() => {
|
|
80
91
|
const unsubscribe = subscribeQueryChange((newQuery) => {
|
|
81
|
-
|
|
92
|
+
debugLog('AutoPositionedPopupList useEffect subscribeQueryChange newQuery=', newQuery);
|
|
82
93
|
ref_searchQuery.current = newQuery;
|
|
83
94
|
if (ref_list.current) {
|
|
84
95
|
ref_list.current.scrollToTop();
|
|
@@ -88,19 +99,19 @@ const AutoPositionedPopupList = memo(({ tag, updateState, fetchData, keyExtracto
|
|
|
88
99
|
return unsubscribe;
|
|
89
100
|
}, []);
|
|
90
101
|
const _updateState = (key, value) => {
|
|
91
|
-
|
|
102
|
+
debugLog('AutoPositionedPopupList _updateState key=', key, ' value=', value);
|
|
92
103
|
setState((prevState) => (Object.assign(Object.assign({}, prevState), { [key]: value })));
|
|
93
|
-
|
|
104
|
+
debugLog('AutoPositionedPopupList _updateState rootViews=', rootViewsRef.current);
|
|
94
105
|
updateState(key, value);
|
|
95
106
|
};
|
|
96
107
|
const _fetchData = async ({ pageIndex, pageSize: currentPageSize, }) => {
|
|
97
|
-
|
|
108
|
+
debugLog('AutoPositionedPopupList _fetchData=', { pageIndex, pageSize: currentPageSize, 'state.localData': state.localData, 'ref_searchQuery.current': ref_searchQuery.current, localSearch });
|
|
98
109
|
if (localSearch && state.localData.length > 0) {
|
|
99
110
|
const result = state.localData.filter((item) => {
|
|
100
111
|
var _a;
|
|
101
112
|
return (_a = `${item.title}`) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes(ref_searchQuery.current.toLowerCase());
|
|
102
113
|
});
|
|
103
|
-
|
|
114
|
+
debugLog('AutoPositionedPopupList _fetchData localSearch result=', result);
|
|
104
115
|
return Promise.resolve({
|
|
105
116
|
items: result,
|
|
106
117
|
pageIndex: 0,
|
|
@@ -113,7 +124,7 @@ const AutoPositionedPopupList = memo(({ tag, updateState, fetchData, keyExtracto
|
|
|
113
124
|
pageSize: pageSize || 10,
|
|
114
125
|
searchQuery: ref_searchQuery.current,
|
|
115
126
|
});
|
|
116
|
-
|
|
127
|
+
debugLog('AutoPositionedPopupList _fetchData res=', res);
|
|
117
128
|
if ((res === null || res === void 0 ? void 0 : res.items) && localSearch) {
|
|
118
129
|
setState((prevState) => {
|
|
119
130
|
return Object.assign(Object.assign({}, prevState), { localData: res.items });
|
|
@@ -130,16 +141,16 @@ const AutoPositionedPopupList = memo(({ tag, updateState, fetchData, keyExtracto
|
|
|
130
141
|
return null;
|
|
131
142
|
}
|
|
132
143
|
catch (e) {
|
|
133
|
-
|
|
144
|
+
debugLog('Error in fetchData:', e);
|
|
134
145
|
}
|
|
135
|
-
|
|
146
|
+
debugLog('AutoPositionedPopupList _fetchData res=', null);
|
|
136
147
|
return null;
|
|
137
148
|
};
|
|
138
149
|
const _renderItem = useCallback(({ item, index }) => {
|
|
139
150
|
return <ListItem item={item} index={index} updateState={_updateState} selectedItem={state.selectedItem} themeMode={themeMode}/>;
|
|
140
151
|
}, [state.selectedItem, themeMode]);
|
|
141
152
|
return useMemo(() => {
|
|
142
|
-
|
|
153
|
+
debugLog('AutoPositionedPopupList (global as any)?.$fake=', global === null || global === void 0 ? void 0 : global.$fake);
|
|
143
154
|
// Babel configuration handles the path redirection based on global.$fake
|
|
144
155
|
// No need for conditional import here
|
|
145
156
|
return (<View style={[styles.baseModalView, styles.autoPositionedPopupList, { backgroundColor: themeMode === 'light' ? '#fff' : 'rgba(44, 44, 46, 1)', }]}>
|
|
@@ -164,7 +175,7 @@ const listLayout = {
|
|
|
164
175
|
};
|
|
165
176
|
// Main AutoPositionedPopup component
|
|
166
177
|
const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
167
|
-
|
|
178
|
+
debugLog('AutoPositionedPopup props=', props);
|
|
168
179
|
const { tag, style, AutoPositionedPopupBtnStyle, placeholder = 'Please Select', onSubmitEditing, TextInputProps, //= {autoFocus: true},
|
|
169
180
|
inputStyle, labelStyle, popUpViewStyle = { left: '5%', width: '90%' }, fetchData = async ({ pageIndex, pageSize, searchQuery, }) => {
|
|
170
181
|
const res = {
|
|
@@ -174,12 +185,12 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
174
185
|
};
|
|
175
186
|
try {
|
|
176
187
|
// const res1: any[] = await $api.xxx(pageSize)
|
|
177
|
-
//
|
|
188
|
+
// debugLog('${NAME} xxx res=', res)
|
|
178
189
|
// res.items = res1
|
|
179
190
|
// res.needLoadMore = res1.length === pageSize
|
|
180
191
|
}
|
|
181
192
|
catch (e) {
|
|
182
|
-
|
|
193
|
+
debugLog('Error in fetch operation:', e);
|
|
183
194
|
}
|
|
184
195
|
return res;
|
|
185
196
|
}, renderItem, onItemSelected, localSearch = false, pageSize = 20, selectedItem, useTextInput = false, btwChildren, CustomRow = ({ children }) => <View>{children}</View>, keyExtractor = (item) => String((item === null || item === void 0 ? void 0 : item.id) || ''), AutoPositionedPopupBtnDisabled = false, forceRemoveAllRootViewOnItemSelected = false, centerDisplay = false, selectedItemBackgroundColor = 'rgba(116, 116, 128, 0.08)',
|
|
@@ -191,7 +202,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
191
202
|
selectedItem: selectedItem,
|
|
192
203
|
});
|
|
193
204
|
// Use RootView context
|
|
194
|
-
const { addRootView, setRootViewNativeStyle, removeRootView, rootViews, setSearchQuery } = useRootView();
|
|
205
|
+
const { addRootView, setRootViewNativeStyle, updateRootView, removeRootView, rootViews, setSearchQuery } = useRootView();
|
|
195
206
|
const rootViewsRef = useRef(rootViews);
|
|
196
207
|
// Track TextInput focus and RootView states like project implementation
|
|
197
208
|
const hasTriggeredFocus = useRef(false);
|
|
@@ -244,7 +255,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
244
255
|
*/
|
|
245
256
|
const scrollParentToTrigger = useCallback(() => {
|
|
246
257
|
if (!(parentScrollViewRef === null || parentScrollViewRef === void 0 ? void 0 : parentScrollViewRef.current) || !triggerBtnRef.current) {
|
|
247
|
-
|
|
258
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: No parentScrollViewRef or triggerBtnRef available');
|
|
248
259
|
return;
|
|
249
260
|
}
|
|
250
261
|
// Use scrollToFocusedInput method from KeyboardAwareScrollView
|
|
@@ -252,16 +263,16 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
252
263
|
const scrollView = parentScrollViewRef.current;
|
|
253
264
|
const nodeHandle = findNodeHandle(triggerBtnRef.current);
|
|
254
265
|
if (nodeHandle && scrollView) {
|
|
255
|
-
|
|
266
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: Scrolling to trigger button with extraHeight=', scrollExtraHeight);
|
|
256
267
|
// KeyboardAwareScrollView has a scrollToFocusedInput method that handles this
|
|
257
268
|
// However, it requires a ReactNode. We'll use scrollToPosition as an alternative.
|
|
258
269
|
// First, measure the trigger button position relative to the ScrollView
|
|
259
270
|
triggerBtnRef.current.measureInWindow((x, y, width, height) => {
|
|
260
271
|
if (y === undefined || height === undefined) {
|
|
261
|
-
|
|
272
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: measureInWindow returned undefined');
|
|
262
273
|
return;
|
|
263
274
|
}
|
|
264
|
-
|
|
275
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: trigger position=', { x, y, width, height });
|
|
265
276
|
// Get keyboard height from Keyboard API
|
|
266
277
|
// On keyboard show, scroll to position that keeps trigger above keyboard
|
|
267
278
|
Keyboard.addListener('keyboardDidShow', (event) => {
|
|
@@ -270,7 +281,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
270
281
|
// Calculate if trigger is below keyboard
|
|
271
282
|
const triggerBottom = y + height;
|
|
272
283
|
const visibleAreaBottom = screenHeight - keyboardHeight;
|
|
273
|
-
|
|
284
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: keyboard data=', {
|
|
274
285
|
keyboardHeight,
|
|
275
286
|
screenHeight,
|
|
276
287
|
triggerBottom,
|
|
@@ -280,7 +291,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
280
291
|
if (triggerBottom > visibleAreaBottom) {
|
|
281
292
|
// Calculate how much to scroll
|
|
282
293
|
const scrollAmount = triggerBottom - visibleAreaBottom + scrollExtraHeight;
|
|
283
|
-
|
|
294
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: scrolling by', scrollAmount);
|
|
284
295
|
// Use scrollForExtraHeightOnAndroid or scrollToPosition
|
|
285
296
|
if (typeof scrollView.scrollToPosition === 'function') {
|
|
286
297
|
// scrollToPosition(x, y, animated)
|
|
@@ -288,7 +299,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
288
299
|
}
|
|
289
300
|
else if (typeof scrollView.scrollToEnd === 'function') {
|
|
290
301
|
// Fallback: scroll to end might help in some cases
|
|
291
|
-
|
|
302
|
+
debugLog('AutoPositionedPopup scrollParentToTrigger: using scrollToEnd fallback');
|
|
292
303
|
}
|
|
293
304
|
}
|
|
294
305
|
});
|
|
@@ -300,19 +311,19 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
300
311
|
* Uses stored trigger position (captured before TextInput replaces the trigger button)
|
|
301
312
|
*/
|
|
302
313
|
const scrollToTriggerWithMeasure = useCallback(() => {
|
|
303
|
-
|
|
314
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure called, tag=', tag, {
|
|
304
315
|
hasParentScrollViewRef: !!(parentScrollViewRef === null || parentScrollViewRef === void 0 ? void 0 : parentScrollViewRef.current),
|
|
305
316
|
hasTriggerPosition: !!triggerPositionRef.current,
|
|
306
317
|
triggerPosition: triggerPositionRef.current
|
|
307
318
|
});
|
|
308
319
|
if (!(parentScrollViewRef === null || parentScrollViewRef === void 0 ? void 0 : parentScrollViewRef.current)) {
|
|
309
|
-
|
|
320
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: parentScrollViewRef not available, tag=', tag);
|
|
310
321
|
return;
|
|
311
322
|
}
|
|
312
323
|
// Use stored trigger position (captured when trigger was clicked)
|
|
313
324
|
const storedPosition = triggerPositionRef.current;
|
|
314
325
|
if (!storedPosition) {
|
|
315
|
-
|
|
326
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: no stored trigger position, tag=', tag);
|
|
316
327
|
return;
|
|
317
328
|
}
|
|
318
329
|
const scrollView = parentScrollViewRef.current;
|
|
@@ -324,7 +335,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
324
335
|
const keyboardApproxHeight = screenHeight * 0.4; // Conservative estimate
|
|
325
336
|
const visibleAreaBottom = screenHeight - keyboardApproxHeight;
|
|
326
337
|
const triggerBottom = triggerY + triggerHeight;
|
|
327
|
-
|
|
338
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: calculations=', {
|
|
328
339
|
tag,
|
|
329
340
|
triggerY,
|
|
330
341
|
triggerHeight,
|
|
@@ -337,7 +348,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
337
348
|
if (triggerBottom > visibleAreaBottom) {
|
|
338
349
|
// Calculate scroll amount to bring trigger above keyboard
|
|
339
350
|
const scrollAmount = triggerBottom - visibleAreaBottom + scrollExtraHeight;
|
|
340
|
-
|
|
351
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: scrolling, amount=', scrollAmount, 'tag=', tag);
|
|
341
352
|
// Use scrollForExtraHeightOnAndroid for KeyboardAwareScrollView
|
|
342
353
|
if (typeof scrollView.scrollForExtraHeightOnAndroid === 'function') {
|
|
343
354
|
scrollView.scrollForExtraHeightOnAndroid(scrollAmount);
|
|
@@ -350,11 +361,11 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
350
361
|
scrollView.scrollTo({ y: scrollAmount, animated: true });
|
|
351
362
|
}
|
|
352
363
|
else {
|
|
353
|
-
|
|
364
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: no scroll method available on scrollView');
|
|
354
365
|
}
|
|
355
366
|
}
|
|
356
367
|
else {
|
|
357
|
-
|
|
368
|
+
debugLog('AutoPositionedPopup scrollToTriggerWithMeasure: trigger already visible, no scroll needed, tag=', tag);
|
|
358
369
|
}
|
|
359
370
|
}, [parentScrollViewRef, scrollExtraHeight, tag]);
|
|
360
371
|
/**
|
|
@@ -363,10 +374,10 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
363
374
|
useEffect(() => {
|
|
364
375
|
(async () => {
|
|
365
376
|
})();
|
|
366
|
-
|
|
377
|
+
debugLog(`AutoPositionedPopup componentDidMount=`, { tag, CustomPopView });
|
|
367
378
|
//componentWillUnmount
|
|
368
379
|
return () => {
|
|
369
|
-
|
|
380
|
+
debugLog(`AutoPositionedPopup componentWillUnmount tag=`, tag);
|
|
370
381
|
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
371
382
|
setSearchQuery('');
|
|
372
383
|
if (textInputRef.current) {
|
|
@@ -379,7 +390,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
379
390
|
};
|
|
380
391
|
}, []);
|
|
381
392
|
useEffect(() => {
|
|
382
|
-
|
|
393
|
+
debugLog('AutoPositionedPopup rootViews=', { tag, rootViews });
|
|
383
394
|
rootViewsRef.current = rootViews;
|
|
384
395
|
if (rootViews.length === 0) {
|
|
385
396
|
hasAddedRootView.current = false;
|
|
@@ -394,23 +405,22 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
394
405
|
}, [rootViews]);
|
|
395
406
|
useEffect(() => {
|
|
396
407
|
var _a, _b;
|
|
397
|
-
|
|
398
|
-
|
|
408
|
+
debugLog('AutoPositionedPopup useEffect [selectedItem, state.selectedItem, tag]=', { tag, selectedItem, 'state.selectedItem': state.selectedItem });
|
|
409
|
+
debugLog('AutoPositionedPopup useEffect state.selectedItem=', state.selectedItem);
|
|
399
410
|
if (((_a = state.selectedItem) === null || _a === void 0 ? void 0 : _a.id) !== (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) || ((_b = state.selectedItem) === null || _b === void 0 ? void 0 : _b.title) != (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.title)) {
|
|
400
|
-
|
|
411
|
+
debugLog('AutoPositionedPopup useEffect selectedItem!=state.selectedItem');
|
|
401
412
|
setState((prevState) => {
|
|
402
413
|
return Object.assign(Object.assign({}, prevState), { selectedItem: selectedItem });
|
|
403
414
|
});
|
|
404
415
|
}
|
|
405
416
|
}, [selectedItem, state.selectedItem, tag]);
|
|
406
417
|
useEffect(() => {
|
|
407
|
-
var _a;
|
|
408
418
|
// Detect if keyboard state has actually changed to avoid false triggers during parent component re-renders
|
|
409
419
|
const keyboardStateChanged = prevIsKeyboardFullyShownRef.current !== isKeyboardFullyShown;
|
|
410
420
|
const propsChanged = prevPropsRef.current.CustomPopView !== CustomPopView ||
|
|
411
421
|
prevPropsRef.current.CustomPopViewStyle !== CustomPopViewStyle ||
|
|
412
422
|
(prevPropsRef.current.TextInputProps !== TextInputProps && useTextInput);
|
|
413
|
-
|
|
423
|
+
debugLog('AutoPositionedPopup useEffect [isKeyboardFullyShown,\n' +
|
|
414
424
|
' state.isFocus,\n' +
|
|
415
425
|
' useTextInput,\n' +
|
|
416
426
|
' CustomPopView,\n' +
|
|
@@ -441,13 +451,18 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
441
451
|
TextInputProps
|
|
442
452
|
};
|
|
443
453
|
// Only execute logic when keyboard state actually changes or user actively operates
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
454
|
+
// CRITICAL FIX: Also allow execution when popup needs initial positioning
|
|
455
|
+
// hasAddedRootView.current = true means popup container exists
|
|
456
|
+
// hasShownRootView.current = false means positioning not done yet
|
|
457
|
+
// We MUST allow execution when popup needs positioning, even if keyboard state unchanged
|
|
458
|
+
if (!keyboardStateChanged && hasAddedRootView.current && hasShownRootView.current) {
|
|
459
|
+
debugLog('AutoPositionedPopup: Skip execution - already positioned and keyboard state unchanged');
|
|
449
460
|
return;
|
|
450
461
|
}
|
|
462
|
+
// Log when we're allowing execution for initial positioning
|
|
463
|
+
if (!keyboardStateChanged && hasAddedRootView.current && !hasShownRootView.current) {
|
|
464
|
+
debugLog('AutoPositionedPopup: ALLOWING execution for initial positioning (popup added but not positioned yet)');
|
|
465
|
+
}
|
|
451
466
|
const getStatusBarHeight = () => {
|
|
452
467
|
if (Platform.OS === 'android') {
|
|
453
468
|
// Android: Use StatusBar.currentHeight API
|
|
@@ -468,7 +483,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
468
483
|
// When keyboard appears, the trigger button may be covered. If parentScrollViewRef
|
|
469
484
|
// is provided, scroll the parent to keep the trigger visible above the keyboard.
|
|
470
485
|
if (parentScrollViewRef === null || parentScrollViewRef === void 0 ? void 0 : parentScrollViewRef.current) {
|
|
471
|
-
|
|
486
|
+
debugLog('AutoPositionedPopup: Keyboard appeared, scrolling parent to keep trigger visible');
|
|
472
487
|
// Use a slight delay to ensure keyboard animation has started
|
|
473
488
|
setTimeout(() => {
|
|
474
489
|
scrollToTriggerWithMeasure();
|
|
@@ -488,97 +503,142 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
488
503
|
// then requestAnimationFrame ensures measurement happens after next render frame
|
|
489
504
|
setTimeout(() => {
|
|
490
505
|
requestAnimationFrame(() => {
|
|
491
|
-
// CRITICAL FIX:
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
|
|
506
|
+
// CRITICAL FIX: Measure CURRENT position AFTER keyboard animation completes
|
|
507
|
+
// DO NOT use stored triggerPositionRef because keyboard may have shifted the view up
|
|
508
|
+
// Instead, measure the outer wrapper (refAutoPositionedPopup)
|
|
509
|
+
// which reflects the ACTUAL current position after keyboard shift
|
|
510
|
+
var _a;
|
|
511
|
+
// DEBUG: Log both refs to compare their positions
|
|
512
|
+
debugLog('AutoPositionedPopup DEBUG: refs status=', {
|
|
513
|
+
hasTextInputRef: !!textInputRef.current,
|
|
514
|
+
hasRefAutoPositionedPopup: !!refAutoPositionedPopup.current,
|
|
515
|
+
});
|
|
516
|
+
// Measure BOTH refs for comparison
|
|
517
|
+
if (textInputRef.current && refAutoPositionedPopup.current) {
|
|
518
|
+
textInputRef.current.measureInWindow((tx, ty, tw, th) => {
|
|
519
|
+
debugLog('AutoPositionedPopup DEBUG: textInputRef position=', { x: tx, y: ty, width: tw, height: th });
|
|
520
|
+
});
|
|
521
|
+
refAutoPositionedPopup.current.measureInWindow((rx, ry, rw, rh) => {
|
|
522
|
+
debugLog('AutoPositionedPopup DEBUG: refAutoPositionedPopup position=', { x: rx, y: ry, width: rw, height: rh });
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
// CRITICAL FIX: Use textInputRef as primary measurement target
|
|
526
|
+
// refAutoPositionedPopup.measureInWindow() returns undefined values
|
|
527
|
+
// because the outer wrapper View uses flex:1/height:100% which makes it unmeasurable
|
|
528
|
+
// textInputRef reliably returns the actual position of the input field
|
|
529
|
+
const measureTarget = textInputRef.current || refAutoPositionedPopup.current;
|
|
530
|
+
if (!measureTarget) {
|
|
531
|
+
debugLog('AutoPositionedPopup useTextInput: no measureTarget available, using fallback');
|
|
532
|
+
const screenHeightFallback = Dimensions.get('window').height;
|
|
533
|
+
const screenWidthFallback = Dimensions.get('window').width;
|
|
534
|
+
const fallbackY = (screenHeightFallback - listLayout.height) / 2;
|
|
535
|
+
ref_listPos.current = { x: screenWidthFallback * 0.05, y: fallbackY, width: screenWidthFallback * 0.9 };
|
|
536
|
+
updateRootView(tag, {
|
|
537
|
+
style: {
|
|
538
|
+
top: (_a = ref_listPos.current) === null || _a === void 0 ? void 0 : _a.y,
|
|
539
|
+
left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left,
|
|
540
|
+
width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width,
|
|
541
|
+
height: listLayout.height,
|
|
542
|
+
opacity: 1,
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
hasShownRootView.current = true;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
// Determine which ref is actually being used (for logging)
|
|
549
|
+
const usingTextInputRef = measureTarget === textInputRef.current;
|
|
550
|
+
debugLog('AutoPositionedPopup useTextInput: using measureTarget=', usingTextInputRef ? 'textInputRef' : 'refAutoPositionedPopup');
|
|
551
|
+
// Measure CURRENT position (after keyboard shifted the view)
|
|
552
|
+
measureTarget.measureInWindow((x, y, width, height) => {
|
|
495
553
|
var _a;
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
554
|
+
debugLog('AutoPositionedPopup useTextInput: measured position for positioning=', {
|
|
555
|
+
x, y, width, height,
|
|
556
|
+
measureTarget: usingTextInputRef ? 'textInputRef' : 'refAutoPositionedPopup'
|
|
557
|
+
});
|
|
558
|
+
// Handle undefined values (can happen during navigation transitions)
|
|
499
559
|
if (x === undefined || y === undefined || width === undefined || height === undefined) {
|
|
500
|
-
|
|
560
|
+
debugLog('AutoPositionedPopup useTextInput: measureInWindow returned undefined, using fallback');
|
|
501
561
|
const screenHeightFallback = Dimensions.get('window').height;
|
|
502
562
|
const screenWidthFallback = Dimensions.get('window').width;
|
|
503
563
|
const fallbackY = (screenHeightFallback - listLayout.height) / 2;
|
|
504
|
-
|
|
505
|
-
const fallbackWidth = screenWidthFallback * 0.9;
|
|
506
|
-
x = fallbackX;
|
|
564
|
+
x = screenWidthFallback * 0.05;
|
|
507
565
|
y = fallbackY;
|
|
508
|
-
width =
|
|
566
|
+
width = screenWidthFallback * 0.9;
|
|
509
567
|
height = 50;
|
|
510
568
|
}
|
|
511
|
-
//
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
//
|
|
515
|
-
// When keyboard appears:
|
|
516
|
-
// 1. measureInWindow returns y relative to window (e.g., y=400 after shifting)
|
|
517
|
-
// 2. But popup's absolute positioning is relative to App container
|
|
518
|
-
// 3. If App container shifted up by 200px, setting top=200 will display at window.y=0 (wrong!)
|
|
519
|
-
//
|
|
520
|
-
// Solution: Since popup is rendered at root level and uses absolute positioning,
|
|
521
|
-
// we should directly use measureInWindow's y value without additional calculations
|
|
522
|
-
// The popup container is at the same level as the page content
|
|
523
|
-
const screenHeight = Dimensions.get('window').height; // Use window height, not screen
|
|
524
|
-
console.log('AutoPositionedPopup useTextInput positioning data=', {
|
|
569
|
+
// Calculate screen height and popup position
|
|
570
|
+
const screenHeight = Dimensions.get('window').height;
|
|
571
|
+
debugLog('AutoPositionedPopup useTextInput positioning data=', {
|
|
525
572
|
screenHeight,
|
|
526
573
|
componentY: y,
|
|
527
574
|
componentHeight: height,
|
|
528
575
|
listHeight: listLayout.height
|
|
529
576
|
});
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
// while still leaving trigger visible (30% of trigger height exposed)
|
|
539
|
-
let popupY = y + (height * 0.7) - listLayout.height;
|
|
540
|
-
console.log('AutoPositionedPopup with keyboard: initial calculation for ABOVE position:', {
|
|
541
|
-
componentY: y,
|
|
542
|
-
componentHeight: height,
|
|
577
|
+
// POSITIONING LOGIC (with keyboard):
|
|
578
|
+
// Simple rule: popup must TOUCH the trigger with NO GAP
|
|
579
|
+
// 1. Default: show ABOVE trigger (popup bottom touches trigger top)
|
|
580
|
+
// 2. If ABOVE would overlap status bar: show BELOW (popup top touches trigger bottom)
|
|
581
|
+
debugLog('AutoPositionedPopup POSITIONING:', {
|
|
582
|
+
triggerY: y,
|
|
583
|
+
triggerHeight: height,
|
|
584
|
+
triggerBottom: y + height,
|
|
543
585
|
popupHeight: listLayout.height,
|
|
586
|
+
screenHeight,
|
|
587
|
+
statusBarHeight
|
|
588
|
+
});
|
|
589
|
+
// 1. Default: show popup ABOVE the trigger
|
|
590
|
+
// Popup has internal padding (12px from autoPositionedPopupList style)
|
|
591
|
+
// To make popup CONTENT touch trigger (not container), add padding offset
|
|
592
|
+
// Container bottom at y + POPUP_PADDING, content bottom at y (no gap)
|
|
593
|
+
const POPUP_PADDING = 12;
|
|
594
|
+
let popupY = y - listLayout.height + POPUP_PADDING;
|
|
595
|
+
let position = 'ABOVE';
|
|
596
|
+
debugLog('AutoPositionedPopup: trying ABOVE position:', {
|
|
544
597
|
popupY,
|
|
545
598
|
popupBottom: popupY + listLayout.height,
|
|
599
|
+
contentBottom: popupY + listLayout.height - POPUP_PADDING,
|
|
546
600
|
triggerTop: y,
|
|
547
|
-
|
|
601
|
+
paddingOffset: POPUP_PADDING,
|
|
602
|
+
wouldOverlapStatusBar: popupY < statusBarHeight
|
|
548
603
|
});
|
|
549
|
-
// 2.
|
|
604
|
+
// 2. If showing ABOVE would go behind status bar, show BELOW instead
|
|
550
605
|
if (popupY < statusBarHeight) {
|
|
551
|
-
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
popupY = y + height +
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
height,
|
|
560
|
-
|
|
606
|
+
// Show BELOW: popup top at trigger bottom
|
|
607
|
+
// Use trigger's measured height as buffer to account for row padding
|
|
608
|
+
// The TextInput is only part of the trigger row - row height scales with trigger height
|
|
609
|
+
const BELOW_BUFFER = height;
|
|
610
|
+
popupY = y + height + BELOW_BUFFER;
|
|
611
|
+
position = 'BELOW';
|
|
612
|
+
debugLog('AutoPositionedPopup: using BELOW position (ABOVE overlaps status bar):', {
|
|
613
|
+
popupY,
|
|
614
|
+
triggerBottom: y + height,
|
|
615
|
+
buffer: BELOW_BUFFER,
|
|
616
|
+
actualGap: BELOW_BUFFER
|
|
561
617
|
});
|
|
562
|
-
// 3.
|
|
618
|
+
// 3. Safety check: if BELOW would go off screen bottom, clamp it
|
|
563
619
|
const maxY = screenHeight - listLayout.height;
|
|
564
620
|
if (popupY > maxY) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
popupY = Math.min(Math.max(statusBarHeight, y - listLayout.height), maxY);
|
|
621
|
+
popupY = maxY;
|
|
622
|
+
debugLog('AutoPositionedPopup: clamped to screen bottom:', { popupY, maxY });
|
|
568
623
|
}
|
|
569
624
|
}
|
|
570
625
|
else {
|
|
571
|
-
|
|
626
|
+
debugLog('AutoPositionedPopup: using ABOVE position (preferred)');
|
|
572
627
|
}
|
|
628
|
+
debugLog('AutoPositionedPopup FINAL POSITION:', { position, popupY, touchesTrigger: true });
|
|
573
629
|
ref_listPos.current = { x: x, y: popupY, width: width };
|
|
574
|
-
|
|
575
|
-
setRootViewNativeStyle
|
|
630
|
+
debugLog('AutoPositionedPopup useTextInput final position=', ref_listPos.current);
|
|
631
|
+
// Use updateRootView instead of setRootViewNativeStyle for more reliable style updates
|
|
632
|
+
// setNativeProps may not work correctly when initial style is in an array
|
|
633
|
+
const newStyle = {
|
|
576
634
|
top: (_a = ref_listPos.current) === null || _a === void 0 ? void 0 : _a.y,
|
|
577
635
|
left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left,
|
|
578
636
|
width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width,
|
|
579
637
|
height: listLayout.height,
|
|
580
638
|
opacity: 1,
|
|
581
|
-
}
|
|
639
|
+
};
|
|
640
|
+
debugLog('AutoPositionedPopup useTextInput: applying new style via updateRootView=', newStyle);
|
|
641
|
+
updateRootView(tag, { style: newStyle });
|
|
582
642
|
hasShownRootView.current = true;
|
|
583
643
|
});
|
|
584
644
|
});
|
|
@@ -586,7 +646,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
586
646
|
}
|
|
587
647
|
else if (!isKeyboardFullyShown && ref_isFocus.current && keyboardStateChanged) {
|
|
588
648
|
// Only execute close logic when keyboard state actually changes from true to false
|
|
589
|
-
|
|
649
|
+
debugLog('AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView (keyboard state changed)=', { tag, forceRemoveAllRootViewOnItemSelected, keyboardStateChanged });
|
|
590
650
|
removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
|
|
591
651
|
setState((prevState) => {
|
|
592
652
|
return Object.assign(Object.assign({}, prevState), { isFocus: false });
|
|
@@ -597,173 +657,52 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
597
657
|
}
|
|
598
658
|
}
|
|
599
659
|
else {
|
|
660
|
+
// V17 SIMPLIFICATION: When useTextInput=false, ALWAYS show popup in CENTER of screen
|
|
661
|
+
// User request: "只要传入的 useTextInput 是 false, 弹框都显示在屏幕中间"
|
|
662
|
+
// This avoids all complex positioning calculations that kept failing
|
|
600
663
|
if (state.isFocus) {
|
|
601
664
|
if (isKeyboardFullyShown) {
|
|
602
665
|
Keyboard.dismiss();
|
|
603
666
|
return;
|
|
604
667
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
statusBarHeight,
|
|
642
|
-
platform: Platform.OS
|
|
643
|
-
});
|
|
644
|
-
// FIXED POSITIONING LOGIC:
|
|
645
|
-
// The popup uses position: 'absolute' relative to the RootViewProvider container
|
|
646
|
-
// measureInWindow returns coordinates relative to the window (screen)
|
|
647
|
-
// So we should NOT add statusBarHeight to the position calculation
|
|
648
|
-
//
|
|
649
|
-
// 1. Default: show popup ABOVE the trigger element
|
|
650
|
-
// FIX: Use (componentY + componentHeight) as the trigger's bottom edge reference point
|
|
651
|
-
// This compensates for measurement inaccuracies when trigger is inside complex layouts (FlatList, ScrollView)
|
|
652
|
-
// The popup's bottom should be at the trigger's top with minimal gap (≤5px)
|
|
653
|
-
// Formula: popup_top = trigger_bottom - componentHeight - popupHeight
|
|
654
|
-
// popup_bottom = trigger_bottom - componentHeight = trigger_top
|
|
655
|
-
let popupY = componentY + componentHeight - popupHeight;
|
|
656
|
-
console.log('AutoPositionedPopup: initial calculation for ABOVE position:', {
|
|
657
|
-
componentY,
|
|
658
|
-
componentHeight,
|
|
659
|
-
popupHeight,
|
|
660
|
-
popupY,
|
|
661
|
-
triggerBottom: componentY + componentHeight,
|
|
662
|
-
statusBarHeight
|
|
663
|
-
});
|
|
664
|
-
// 2. Check if showing above would go off the top of screen (behind status bar)
|
|
665
|
-
if (popupY < statusBarHeight) {
|
|
666
|
-
console.log('AutoPositionedPopup: would go behind status bar, showing BELOW instead');
|
|
667
|
-
// Show BELOW the trigger element
|
|
668
|
-
// Since componentY + componentHeight represents the trigger's "reference bottom" (accounting for measurement offset),
|
|
669
|
-
// we need to add another componentHeight to position popup BELOW the actual trigger
|
|
670
|
-
// Formula: popup top = componentY + (2 * componentHeight)
|
|
671
|
-
// - (componentY + componentHeight) = trigger's actual top (compensated)
|
|
672
|
-
// - + componentHeight = skip past trigger height to get to trigger's actual bottom
|
|
673
|
-
popupY = componentY + componentHeight + componentHeight;
|
|
674
|
-
console.log('AutoPositionedPopup: BELOW position calculated:', {
|
|
675
|
-
formula: 'componentY + 2*componentHeight',
|
|
676
|
-
componentY,
|
|
677
|
-
componentHeight,
|
|
678
|
-
popupY
|
|
679
|
-
});
|
|
680
|
-
// 3. Also check if showing below would go off the bottom of screen
|
|
681
|
-
const maxY = screenHeight - popupHeight;
|
|
682
|
-
if (popupY > maxY) {
|
|
683
|
-
// If both positions are problematic, clamp to visible area
|
|
684
|
-
// Prioritize showing as close to trigger as possible
|
|
685
|
-
console.log('AutoPositionedPopup: both positions problematic, clamping to visible area');
|
|
686
|
-
popupY = Math.min(Math.max(statusBarHeight, componentY - popupHeight), maxY);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
else {
|
|
690
|
-
console.log('AutoPositionedPopup: showing ABOVE input field (preferred position)');
|
|
691
|
-
}
|
|
692
|
-
console.log('AutoPositionedPopup final position:', {
|
|
693
|
-
popupY,
|
|
694
|
-
'showing above': popupY < componentY,
|
|
695
|
-
'below status bar': popupY >= statusBarHeight
|
|
696
|
-
});
|
|
697
|
-
return { finalY: popupY, showAbove: popupY < componentY };
|
|
698
|
-
};
|
|
699
|
-
// Calculate position ONCE based on actual popup height
|
|
700
|
-
const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
|
|
701
|
-
? CustomPopViewStyle.height
|
|
702
|
-
: listLayout.height;
|
|
703
|
-
console.log('AutoPositionedPopup 🔥 Using actualPopupHeight for calculation:', { actualPopupHeight, CustomPopView: !!CustomPopView });
|
|
704
|
-
const positionResult = calculateOptimalPosition(y, height, actualPopupHeight);
|
|
705
|
-
console.log('AutoPositionedPopup FINAL position result:', positionResult);
|
|
706
|
-
ref_listPos.current = { x: x, y: positionResult.finalY, width: width };
|
|
707
|
-
console.log('AutoPositionedPopup !useTextInput ref_listPos.current=', ref_listPos.current);
|
|
708
|
-
if (CustomPopView && CustomPopViewStyle) {
|
|
709
|
-
// Position already calculated correctly above, no need to recalculate
|
|
710
|
-
const PopViewComponent = CustomPopView();
|
|
711
|
-
console.log('AutoPositionedPopup !useTextInput addRootView=', { CustomPopViewStyle, PopViewComponent, 'state.selectedItem': state.selectedItem });
|
|
712
|
-
addRootView({
|
|
713
|
-
id: tag,
|
|
714
|
-
style: !centerDisplay
|
|
715
|
-
? Object.assign({ top: ref_listPos.current.y, left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left, width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width, height: listLayout.height, opacity: 1 }, CustomPopViewStyle) : Object.assign({ width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width, height: listLayout.height }, CustomPopViewStyle),
|
|
716
|
-
component: <PopViewComponent selectedItem={state.selectedItem}></PopViewComponent>,
|
|
717
|
-
useModal: true,
|
|
718
|
-
onModalClose: () => {
|
|
719
|
-
console.log('AutoPositionedPopup onModalClose');
|
|
720
|
-
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
721
|
-
setState((prevState) => {
|
|
722
|
-
return Object.assign(Object.assign({}, prevState), { isFocus: false });
|
|
723
|
-
});
|
|
724
|
-
hasAddedRootView.current = false;
|
|
725
|
-
hasShownRootView.current = false;
|
|
726
|
-
hasTriggeredFocus.current = false;
|
|
727
|
-
setSearchQuery('');
|
|
728
|
-
},
|
|
729
|
-
centerDisplay,
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
else {
|
|
733
|
-
console.log('AutoPositionedPopup !useTextInput addRootView tag=', tag);
|
|
734
|
-
addRootView({
|
|
735
|
-
id: tag,
|
|
736
|
-
style: {
|
|
737
|
-
top: ref_listPos.current.y,
|
|
738
|
-
left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left,
|
|
739
|
-
width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width,
|
|
740
|
-
height: listLayout.height,
|
|
741
|
-
opacity: 1,
|
|
742
|
-
},
|
|
743
|
-
component: (<AutoPositionedPopupList tag={tag} updateState={updateState} fetchData={fetchData} pageSize={pageSize} renderItem={renderItem} selectedItem={state.selectedItem} localSearch={localSearch} showListEmptyComponent={showListEmptyComponent} emptyText={emptyText} themeMode={themeMode}/>),
|
|
744
|
-
useModal: true,
|
|
745
|
-
onModalClose: () => {
|
|
746
|
-
console.log('AutoPositionedPopup onModalClose tag=', tag);
|
|
747
|
-
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
748
|
-
setState((prevState) => {
|
|
749
|
-
return Object.assign({}, prevState);
|
|
750
|
-
});
|
|
751
|
-
setSearchQuery('');
|
|
752
|
-
},
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
if (isKeyboardFullyShown) {
|
|
759
|
-
ref_isFocus.current = (_a = state.isFocus) !== null && _a !== void 0 ? _a : false;
|
|
760
|
-
if (isKeyboardFullyShown !== keyboardVisibleRef.current) {
|
|
761
|
-
keyboardVisibleRef.current = isKeyboardFullyShown;
|
|
762
|
-
if (isKeyboardFullyShown && textInputRef.current) {
|
|
763
|
-
if (ref_searchQuery.current) {
|
|
764
|
-
textInputRef.current.setNativeProps({ text: ref_searchQuery.current });
|
|
765
|
-
}
|
|
668
|
+
debugLog('🟢🟢🟢 POPUP_V17 useTextInput=false, showing popup in CENTER of screen');
|
|
669
|
+
const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
|
|
670
|
+
? CustomPopViewStyle.height
|
|
671
|
+
: listLayout.height;
|
|
672
|
+
if (CustomPopView && CustomPopViewStyle) {
|
|
673
|
+
const PopViewComponent = CustomPopView();
|
|
674
|
+
debugLog('🔵🔵🔵 POPUP_V17 CustomPopView centerDisplay=true');
|
|
675
|
+
addRootView({
|
|
676
|
+
id: tag,
|
|
677
|
+
style: Object.assign({ width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width }, CustomPopViewStyle),
|
|
678
|
+
component: <PopViewComponent selectedItem={state.selectedItem}></PopViewComponent>,
|
|
679
|
+
useModal: true,
|
|
680
|
+
centerDisplay: true, // V17: Force center display for useTextInput=false
|
|
681
|
+
onModalClose: () => {
|
|
682
|
+
debugLog('AutoPositionedPopup V17 onModalClose tag=', tag);
|
|
683
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
684
|
+
setState((prevState) => (Object.assign({}, prevState)));
|
|
685
|
+
setSearchQuery('');
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
debugLog('🔵🔵🔵 POPUP_V17 List centerDisplay=true, height=', listLayout.height);
|
|
691
|
+
addRootView({
|
|
692
|
+
id: tag,
|
|
693
|
+
style: { width: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.width, height: listLayout.height, opacity: 1 },
|
|
694
|
+
component: (<AutoPositionedPopupList tag={tag} updateState={updateState} fetchData={fetchData} pageSize={pageSize} renderItem={renderItem} selectedItem={state.selectedItem} localSearch={localSearch} showListEmptyComponent={showListEmptyComponent} emptyText={emptyText} themeMode={themeMode}/>),
|
|
695
|
+
useModal: true,
|
|
696
|
+
centerDisplay: true, // V17: Force center display for useTextInput=false
|
|
697
|
+
onModalClose: () => {
|
|
698
|
+
debugLog('AutoPositionedPopup V17 onModalClose tag=', tag);
|
|
699
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
700
|
+
setState((prevState) => (Object.assign({}, prevState)));
|
|
701
|
+
setSearchQuery('');
|
|
702
|
+
},
|
|
703
|
+
});
|
|
766
704
|
}
|
|
705
|
+
return; // V17: Early return after handling !useTextInput case
|
|
767
706
|
}
|
|
768
707
|
}
|
|
769
708
|
}, [isKeyboardFullyShown,
|
|
@@ -775,10 +714,15 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
775
714
|
tag, TextInputProps,
|
|
776
715
|
state.selectedItem, showListEmptyComponent, themeMode
|
|
777
716
|
]);
|
|
717
|
+
// V16: All positioning logic is now in the useEffect above (calculateOptimalPosition + processPosition)
|
|
718
|
+
// V16 FIX: Capture position in onPress callback BEFORE setState is called
|
|
719
|
+
// This ensures triggerPositionRef.current is set when useEffect runs
|
|
720
|
+
// Formula: top = componentY - popupHeight (popup bottom touches trigger top exactly)
|
|
721
|
+
debugLog('🟢 POPUP_MODULE_V16_LOADED - capturing position in onPress callback before setState');
|
|
778
722
|
// Imperative handle for parent component access
|
|
779
723
|
useImperativeHandle(parentRef, () => ({
|
|
780
724
|
clearSelectedItem: () => {
|
|
781
|
-
|
|
725
|
+
debugLog('AutoPositionedPopup clearSelectedItem tag=', tag);
|
|
782
726
|
setState((prevState) => {
|
|
783
727
|
return Object.assign(Object.assign({}, prevState), { selectedItem: undefined, isFocus: false });
|
|
784
728
|
});
|
|
@@ -795,11 +739,11 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
795
739
|
},
|
|
796
740
|
}), []);
|
|
797
741
|
const updateState = (key, value) => {
|
|
798
|
-
|
|
742
|
+
debugLog('AutoPositionedPopup updateState=', { key, value });
|
|
799
743
|
setState((prevState) => (Object.assign(Object.assign({}, prevState), { [key]: value })));
|
|
800
744
|
if (key === 'selectedItem' && onItemSelected) {
|
|
801
745
|
onItemSelected(value);
|
|
802
|
-
|
|
746
|
+
debugLog('AutoPositionedPopup updateState onItemSelected rootViewsRef.current=', rootViewsRef.current);
|
|
803
747
|
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
804
748
|
hasAddedRootView.current = false;
|
|
805
749
|
hasShownRootView.current = false;
|
|
@@ -832,17 +776,17 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
832
776
|
// Only update when deep comparison detects real changes to avoid TextInput recreation due to reference changes during parent component redraws
|
|
833
777
|
const stableInputStyle = useMemo(() => {
|
|
834
778
|
if (!shallowEqual(stableInputStyleRef.current, inputStyle)) {
|
|
835
|
-
|
|
779
|
+
debugLog(`AutoPositionedPopup stableInputStyle: `, { tag, inputStyle, themeMode });
|
|
836
780
|
stableInputStyleRef.current = inputStyle;
|
|
837
781
|
}
|
|
838
782
|
return stableInputStyleRef.current;
|
|
839
783
|
}, [inputStyle, tag, themeMode]);
|
|
840
784
|
const stableTextInputProps = useMemo(() => {
|
|
841
785
|
if (!shallowEqual(stableTextInputPropsRef.current, TextInputProps)) {
|
|
842
|
-
|
|
786
|
+
debugLog(`AutoPositionedPopup TextInputProps deep change detected, updating stable reference - tag: ${tag}`);
|
|
843
787
|
stableTextInputPropsRef.current = TextInputProps;
|
|
844
788
|
}
|
|
845
|
-
|
|
789
|
+
debugLog('AutoPositionedPopup stableTextInputProps=', { tag, TextInputProps, 'stableTextInputPropsRef.current': stableTextInputPropsRef.current });
|
|
846
790
|
return stableTextInputPropsRef.current;
|
|
847
791
|
}, [TextInputProps, tag]);
|
|
848
792
|
// Use useCallback to stabilize onFocus and onBlur callback references
|
|
@@ -853,7 +797,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
853
797
|
const handleTextInputFocus = useCallback(() => {
|
|
854
798
|
const currentTime = Date.now();
|
|
855
799
|
const timeSinceLastFocus = currentTime - lastFocusTimeRef.current;
|
|
856
|
-
|
|
800
|
+
debugLog('AutoPositionedPopup onFocus=', {
|
|
857
801
|
tag,
|
|
858
802
|
'state.selectedItem': stateRef.current.selectedItem,
|
|
859
803
|
'hasTriggeredFocus.current=': hasTriggeredFocus.current,
|
|
@@ -865,17 +809,17 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
865
809
|
});
|
|
866
810
|
// Prevent rapid repeated triggers (repeated events within 300ms are ignored)
|
|
867
811
|
if (timeSinceLastFocus < 300) {
|
|
868
|
-
|
|
812
|
+
debugLog('AutoPositionedPopup onFocus: Skip - event triggered too quickly (< 300ms)');
|
|
869
813
|
return;
|
|
870
814
|
}
|
|
871
815
|
// Skip if keyboard is already open and focus has been handled
|
|
872
816
|
if (isKeyboardFullyShown && hasTriggeredFocus.current) {
|
|
873
|
-
|
|
817
|
+
debugLog('AutoPositionedPopup onFocus: Skip - keyboard already open and focus handled');
|
|
874
818
|
return;
|
|
875
819
|
}
|
|
876
820
|
// Prevent concurrent processing
|
|
877
821
|
if (isFocusEventProcessingRef.current) {
|
|
878
|
-
|
|
822
|
+
debugLog('AutoPositionedPopup onFocus: Skip - processing another focus event');
|
|
879
823
|
return;
|
|
880
824
|
}
|
|
881
825
|
isFocusEventProcessingRef.current = true;
|
|
@@ -898,7 +842,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
898
842
|
}, 100);
|
|
899
843
|
}, [tag, isKeyboardFullyShown]); // Remove state.selectedItem, use stateRef instead
|
|
900
844
|
const handleTextInputBlur = useCallback(() => {
|
|
901
|
-
|
|
845
|
+
debugLog('AutoPositionedPopup onBlur=', {
|
|
902
846
|
tag,
|
|
903
847
|
'textInputRef.current': textInputRef.current,
|
|
904
848
|
'isKeyboardFullyShown': isKeyboardFullyShown,
|
|
@@ -906,7 +850,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
906
850
|
});
|
|
907
851
|
// If keyboard is still open, this is a false trigger caused by parent component re-render, should not reset
|
|
908
852
|
if (isKeyboardFullyShown && hasTriggeredFocus.current) {
|
|
909
|
-
|
|
853
|
+
debugLog('AutoPositionedPopup onBlur: Skip - keyboard still open, possibly caused by parent component re-render');
|
|
910
854
|
return;
|
|
911
855
|
}
|
|
912
856
|
// Only reset internal state, do not actively close keyboard
|
|
@@ -930,29 +874,29 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
930
874
|
// Wrap TextInput independently in useMemo to recreate only when key props change
|
|
931
875
|
// This avoids repeated ref callback triggers due to other props changes during parent component redraws
|
|
932
876
|
const memoizedTextInput = useMemo(() => {
|
|
933
|
-
|
|
877
|
+
debugLog('AutoPositionedPopup memoizedTextInput=', { tag, useTextInput, 'state.isFocus': state.isFocus, stableTextInputProps });
|
|
934
878
|
if (!useTextInput || !state.isFocus) {
|
|
935
879
|
return null;
|
|
936
880
|
}
|
|
937
881
|
return (<RNTextInput ref={(ref) => {
|
|
938
882
|
// Monitor TextInput mounting and unmounting
|
|
939
883
|
if (ref && !textInputRef.current) {
|
|
940
|
-
|
|
884
|
+
debugLog(`AutoPositionedPopup TextInput created/mounted - tag: ${tag}, ref:`, ref);
|
|
941
885
|
}
|
|
942
886
|
else if (!ref && textInputRef.current) {
|
|
943
|
-
|
|
887
|
+
debugLog(`AutoPositionedPopup TextInput unmounted - tag: ${tag}`);
|
|
944
888
|
}
|
|
945
889
|
else if (ref && textInputRef.current && ref !== textInputRef.current) {
|
|
946
|
-
|
|
890
|
+
debugLog(`AutoPositionedPopup TextInput replaced - tag: ${tag}, oldRef:`, textInputRef.current, 'newRef:', ref);
|
|
947
891
|
}
|
|
948
892
|
textInputRef.current = ref;
|
|
949
893
|
}} key={`textinput-${tag}`} style={[
|
|
950
894
|
styles.inputStyle,
|
|
951
895
|
stableInputStyle,
|
|
952
|
-
(themeMode === 'dark' && { color: '#fff' })
|
|
896
|
+
(themeMode === 'dark' && { color: '#fff' }),
|
|
953
897
|
]} textAlign={stableTextInputProps && stableTextInputProps['textAlign'] || 'left'} multiline={stableTextInputProps && stableTextInputProps['multiline'] || false} numberOfLines={stableTextInputProps && stableTextInputProps['numberOfLines'] || 1} onChangeText={(searchQuery) => {
|
|
954
898
|
ref_searchQuery.current = searchQuery;
|
|
955
|
-
|
|
899
|
+
debugLog('AutoPositionedPopup onChangeText rootViews=', rootViews);
|
|
956
900
|
if (!localSearch) {
|
|
957
901
|
if (debounceTimerRef.current) {
|
|
958
902
|
clearTimeout(debounceTimerRef.current);
|
|
@@ -971,7 +915,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
971
915
|
Keyboard.dismiss();
|
|
972
916
|
}
|
|
973
917
|
}} keyboardType={stableTextInputProps && stableTextInputProps['keyboardType'] || 'default'} clearButtonMode="while-editing" returnKeyType={stableTextInputProps && stableTextInputProps['returnKeyType'] || 'done'} maxLength={stableTextInputProps && stableTextInputProps['maxLength'] || 100} accessibilityLabel="selectInput" accessible={true} autoFocus={stableTextInputProps && stableTextInputProps['autoFocus'] || true} autoCorrect={false} underlineColorAndroid="transparent" editable={stableTextInputProps && stableTextInputProps['editable'] || true} secureTextEntry={stableTextInputProps && stableTextInputProps['secureTextEntry'] || false} defaultValue="" caretHidden={false} enablesReturnKeyAutomatically onFocus={handleTextInputFocus} onBlur={handleTextInputBlur} selectTextOnFocus={stableTextInputProps && stableTextInputProps['selectTextOnFocus'] || false} onSubmitEditing={(e) => {
|
|
974
|
-
|
|
918
|
+
debugLog('AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=', e.nativeEvent.text);
|
|
975
919
|
onSubmitEditing && onSubmitEditing(e);
|
|
976
920
|
}}/>);
|
|
977
921
|
}, [
|
|
@@ -990,11 +934,11 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
990
934
|
// Render the component following project implementation
|
|
991
935
|
return useMemo(() => {
|
|
992
936
|
var _a;
|
|
993
|
-
|
|
937
|
+
debugLog('AutoPositionedPopup render tag=', tag); // Now safe - circular dependency fixed
|
|
994
938
|
return (<CustomRow>
|
|
995
939
|
<View style={[styles.contain, style]} ref={refAutoPositionedPopup}>
|
|
996
940
|
{!state.isFocus || !useTextInput ? (<TouchableOpacity ref={triggerBtnRef} style={[styles.AutoPositionedPopupBtn, AutoPositionedPopupBtnStyle]} disabled={AutoPositionedPopupBtnDisabled} onPress={() => {
|
|
997
|
-
|
|
941
|
+
debugLog('AutoPositionedPopup onPress=', {
|
|
998
942
|
tag,
|
|
999
943
|
'state.isFocus': state.isFocus,
|
|
1000
944
|
useTextInput,
|
|
@@ -1006,10 +950,13 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
1006
950
|
});
|
|
1007
951
|
// Capture trigger button position BEFORE switching to TextInput
|
|
1008
952
|
// This is critical because triggerBtnRef will become null after isFocus=true
|
|
1009
|
-
|
|
953
|
+
// IMPORTANT: Always capture position regardless of parentScrollViewRef
|
|
954
|
+
if (triggerBtnRef.current) {
|
|
1010
955
|
triggerBtnRef.current.measureInWindow((x, y, width, height) => {
|
|
1011
|
-
|
|
1012
|
-
|
|
956
|
+
debugLog('AutoPositionedPopup onPress: captured trigger position=', { tag, x, y, width, height });
|
|
957
|
+
if (x !== undefined && y !== undefined && width !== undefined && height !== undefined) {
|
|
958
|
+
triggerPositionRef.current = { x, y, width, height };
|
|
959
|
+
}
|
|
1013
960
|
});
|
|
1014
961
|
}
|
|
1015
962
|
if (useTextInput) {
|
|
@@ -1049,11 +996,14 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
1049
996
|
}
|
|
1050
997
|
}
|
|
1051
998
|
else {
|
|
999
|
+
// V17 SIMPLIFICATION: For useTextInput=false, popup will be centered
|
|
1000
|
+
// No need for complex position measurement - just trigger focus
|
|
1001
|
+
debugLog('🔵🔵🔵 POPUP_V17 onPress useTextInput=false, will show centered popup');
|
|
1052
1002
|
setState((prevState) => {
|
|
1053
1003
|
return Object.assign(Object.assign({}, prevState), { isFocus: true });
|
|
1054
1004
|
});
|
|
1055
1005
|
}
|
|
1056
|
-
|
|
1006
|
+
debugLog('AutoPositionedPopup onPress done');
|
|
1057
1007
|
}}>
|
|
1058
1008
|
{!btwChildren ? (<Text style={[
|
|
1059
1009
|
styles.searchQueryTxt,
|
|
@@ -1067,29 +1017,29 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
1067
1017
|
</CustomRow>);
|
|
1068
1018
|
}, [
|
|
1069
1019
|
tag,
|
|
1070
|
-
//
|
|
1020
|
+
// �?CRITICAL FIX: Remove all props that may change frequently or are inline functions
|
|
1071
1021
|
// Changes to these props should not cause the entire component tree to recreate, especially TextInput
|
|
1072
|
-
// fetchData, //
|
|
1073
|
-
// renderItem, //
|
|
1074
|
-
// onItemSelected, //
|
|
1075
|
-
// onSubmitEditing, //
|
|
1022
|
+
// fetchData, // �?Removed: inline function
|
|
1023
|
+
// renderItem, // �?Removed: possibly inline function
|
|
1024
|
+
// onItemSelected, // �?Removed: possibly inline function
|
|
1025
|
+
// onSubmitEditing, // �?Removed: possibly inline function
|
|
1076
1026
|
localSearch,
|
|
1077
|
-
// placeholder, //
|
|
1078
|
-
// textAlign, //
|
|
1027
|
+
// placeholder, // �?Removed: may change
|
|
1028
|
+
// textAlign, // �?Removed: may change
|
|
1079
1029
|
pageSize,
|
|
1080
1030
|
selectedItem,
|
|
1081
|
-
// CustomRow, //
|
|
1031
|
+
// CustomRow, // �?Removed: inline function, new reference each time
|
|
1082
1032
|
useTextInput,
|
|
1083
|
-
// btwChildren, //
|
|
1084
|
-
// keyExtractor, //
|
|
1085
|
-
// AutoPositionedPopupBtnStyle, //
|
|
1086
|
-
// CustomPopView, //
|
|
1087
|
-
// CustomPopViewStyle, //
|
|
1033
|
+
// btwChildren, // �?Removed: inline function
|
|
1034
|
+
// keyExtractor, // �?Removed: possibly inline function
|
|
1035
|
+
// AutoPositionedPopupBtnStyle, // �?Removed: possibly inline object
|
|
1036
|
+
// CustomPopView, // �?Removed: may change
|
|
1037
|
+
// CustomPopViewStyle, // �?Removed: may change
|
|
1088
1038
|
forceRemoveAllRootViewOnItemSelected,
|
|
1089
1039
|
state.isFocus,
|
|
1090
1040
|
showListEmptyComponent,
|
|
1091
1041
|
emptyText,
|
|
1092
|
-
//
|
|
1042
|
+
// �?Removed most dependencies that may cause re-rendering, keeping only core dependencies that truly affect component structure
|
|
1093
1043
|
// This prevents TextInput recreation due to inline functions/objects during parent component redraws
|
|
1094
1044
|
]);
|
|
1095
1045
|
}));
|