react-native-auto-positioned-popup 1.0.12 → 1.0.13
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 +287 -128
- package/lib/AutoPositionedPopup.js.map +1 -1
- package/lib/AutoPositionedPopupProps.d.ts +6 -0
- package/lib/AutoPositionedPopupProps.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/AutoPositionedPopup.tsx +356 -217
- package/src/AutoPositionedPopupProps.ts +13 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AutoPositionedPopup.d.ts","sourceRoot":"","sources":["../src/AutoPositionedPopup.tsx"],"names":[],"mappings":"AAAA,OAAO,KAYN,MAAM,OAAO,CAAC;AAcf,OAAO,EAAC,wBAAwB,EAAqB,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"AutoPositionedPopup.d.ts","sourceRoot":"","sources":["../src/AutoPositionedPopup.tsx"],"names":[],"mappings":"AAAA,OAAO,KAYN,MAAM,OAAO,CAAC;AAcf,OAAO,EAAC,wBAAwB,EAAqB,MAAM,4BAA4B,CAAC;AAuRxF,QAAA,MAAM,mBAAmB,qFA+4BxB,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
|
|
@@ -34,9 +34,7 @@ const ListItem = memo(({ updateState, item, index, selectedItem, }) => {
|
|
|
34
34
|
rootViewsRef.current = rootViews;
|
|
35
35
|
}, [rootViews]);
|
|
36
36
|
return useMemo(() => {
|
|
37
|
-
// console.log('AutoPositionedPopup.tsx ListItem
|
|
38
|
-
console.log('AutoPositionedPopup.tsx ListItem item=', item);
|
|
39
|
-
console.log('AutoPositionedPopup.tsx ListItem selectedItem=', selectedItem);
|
|
37
|
+
// console.log('AutoPositionedPopup.tsx ListItem=', {index, item, selectedItem});
|
|
40
38
|
const isSelected = item.id === (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id);
|
|
41
39
|
return (<TouchableOpacity key={item.id} style={[
|
|
42
40
|
styles.commonModalRow,
|
|
@@ -79,8 +77,8 @@ const AutoPositionedPopupList = memo(({ tag, updateState, fetchData, keyExtracto
|
|
|
79
77
|
};
|
|
80
78
|
}, []);
|
|
81
79
|
// useEffect(() => {
|
|
82
|
-
// //
|
|
83
|
-
// //
|
|
80
|
+
// // Listen to TextInput events, refresh list when received, not dependent on global searchQuery
|
|
81
|
+
// // Sync the latest searchQuery to list-specific ref for _fetchData to use
|
|
84
82
|
// ref_searchQuery.current = searchQuery;
|
|
85
83
|
// console.log('AutoPositionedPopupList useEffect searchQuery=', searchQuery);
|
|
86
84
|
// console.log('AutoPositionedPopupList useEffect state.localData=', state.localData);
|
|
@@ -109,10 +107,7 @@ const AutoPositionedPopupList = memo(({ tag, updateState, fetchData, keyExtracto
|
|
|
109
107
|
updateState(key, value);
|
|
110
108
|
};
|
|
111
109
|
const _fetchData = async ({ pageIndex, pageSize: currentPageSize, }) => {
|
|
112
|
-
console.log('AutoPositionedPopupList _fetchData
|
|
113
|
-
console.log('AutoPositionedPopupList _fetchData state.localData=', state.localData);
|
|
114
|
-
console.log('AutoPositionedPopupList _fetchData ref_searchQuery.current=', ref_searchQuery.current);
|
|
115
|
-
console.log('AutoPositionedPopupList _fetchData localSearch=', localSearch);
|
|
110
|
+
console.log('AutoPositionedPopupList _fetchData=', { pageIndex, pageSize: currentPageSize, 'state.localData': state.localData, 'ref_searchQuery.current': ref_searchQuery.current, localSearch });
|
|
116
111
|
if (localSearch && state.localData.length > 0) {
|
|
117
112
|
const result = state.localData.filter((item) => {
|
|
118
113
|
var _a;
|
|
@@ -183,7 +178,7 @@ const listLayout = {
|
|
|
183
178
|
// Main AutoPositionedPopup component
|
|
184
179
|
const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
185
180
|
console.log('AutoPositionedPopup props=', props);
|
|
186
|
-
const { tag, style, AutoPositionedPopupBtnStyle, placeholder = 'Please Select', onSubmitEditing, TextInputProps = {}, inputStyle, labelStyle, popUpViewStyle = { left: '5%', width: '90%' }, fetchData = async ({ pageIndex, pageSize, searchQuery, }) => {
|
|
181
|
+
const { tag, style, AutoPositionedPopupBtnStyle, placeholder = 'Please Select', onSubmitEditing, TextInputProps = { autoFocus: true }, inputStyle, labelStyle, popUpViewStyle = { left: '5%', width: '90%' }, fetchData = async ({ pageIndex, pageSize, searchQuery, }) => {
|
|
187
182
|
const res = {
|
|
188
183
|
items: [],
|
|
189
184
|
pageIndex,
|
|
@@ -219,6 +214,16 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
219
214
|
const keyboardVisibleRef = useRef(false);
|
|
220
215
|
const refAutoPositionedPopup = useRef(null);
|
|
221
216
|
const ref_searchQuery = useRef('');
|
|
217
|
+
// Add ref to track previous keyboard state to avoid false triggers during parent component re-renders
|
|
218
|
+
const prevIsKeyboardFullyShownRef = useRef(false);
|
|
219
|
+
const prevPropsRef = useRef({});
|
|
220
|
+
// Add ref to prevent onFocus/onBlur loop triggers during parent component re-renders
|
|
221
|
+
const lastFocusTimeRef = useRef(0);
|
|
222
|
+
const isFocusEventProcessingRef = useRef(false);
|
|
223
|
+
// Add ref to stabilize TextInput props reference
|
|
224
|
+
// Only update when deep comparison detects real changes to avoid TextInput recreation due to reference changes during parent component redraws
|
|
225
|
+
const stableInputStyleRef = useRef(inputStyle);
|
|
226
|
+
const stableTextInputPropsRef = useRef(TextInputProps);
|
|
222
227
|
// Simple keyboard status tracking (alternative to useKeyboardStatus hook)
|
|
223
228
|
// Legacy state for compatibility
|
|
224
229
|
const [isVisible, setIsVisible] = useState(false);
|
|
@@ -240,8 +245,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
240
245
|
useEffect(() => {
|
|
241
246
|
(async () => {
|
|
242
247
|
})();
|
|
243
|
-
console.log(`AutoPositionedPopup componentDidMount tag
|
|
244
|
-
console.log('AutoPositionedPopup componentDidMount CustomPopView=', CustomPopView);
|
|
248
|
+
console.log(`AutoPositionedPopup componentDidMount=`, { tag, CustomPopView });
|
|
245
249
|
//componentWillUnmount
|
|
246
250
|
return () => {
|
|
247
251
|
console.log(`AutoPositionedPopup componentWillUnmount tag=`, tag);
|
|
@@ -272,8 +276,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
272
276
|
}, [rootViews]);
|
|
273
277
|
useEffect(() => {
|
|
274
278
|
var _a, _b;
|
|
275
|
-
console.log('AutoPositionedPopup useEffect tag=', tag);
|
|
276
|
-
console.log('AutoPositionedPopup useEffect selectedItem=', selectedItem);
|
|
279
|
+
console.log('AutoPositionedPopup useEffect [selectedItem, state.selectedItem, tag]=', { tag, selectedItem, 'state.selectedItem': state.selectedItem });
|
|
277
280
|
console.log('AutoPositionedPopup useEffect state.selectedItem=', state.selectedItem);
|
|
278
281
|
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)) {
|
|
279
282
|
console.log('AutoPositionedPopup useEffect selectedItem!=state.selectedItem');
|
|
@@ -284,24 +287,55 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
284
287
|
}, [selectedItem, state.selectedItem, tag]);
|
|
285
288
|
useEffect(() => {
|
|
286
289
|
var _a, _b, _c;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
console.log('AutoPositionedPopup useEffect
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
290
|
+
// Detect if keyboard state has actually changed to avoid false triggers during parent component re-renders
|
|
291
|
+
const keyboardStateChanged = prevIsKeyboardFullyShownRef.current !== isKeyboardFullyShown;
|
|
292
|
+
const propsChanged = prevPropsRef.current.CustomPopView !== CustomPopView ||
|
|
293
|
+
prevPropsRef.current.CustomPopViewStyle !== CustomPopViewStyle ||
|
|
294
|
+
prevPropsRef.current.TextInputProps !== TextInputProps;
|
|
295
|
+
console.log('AutoPositionedPopup useEffect [isKeyboardFullyShown,\n' +
|
|
296
|
+
' state.isFocus,\n' +
|
|
297
|
+
' useTextInput,\n' +
|
|
298
|
+
' CustomPopView,\n' +
|
|
299
|
+
' CustomPopViewStyle,\n' +
|
|
300
|
+
' forceRemoveAllRootViewOnItemSelected,\n' +
|
|
301
|
+
' tag, TextInputProps,\n' +
|
|
302
|
+
' state.selectedItem, showListEmptyComponent\n' +
|
|
303
|
+
' ]=', {
|
|
304
|
+
tag,
|
|
305
|
+
'state.isFocus': state.isFocus,
|
|
306
|
+
isKeyboardFullyShown,
|
|
307
|
+
'ref_isFocus.current': ref_isFocus.current,
|
|
308
|
+
'ref_isKeyboardFullyShown.current': ref_isKeyboardFullyShown.current,
|
|
309
|
+
useTextInput, TextInputProps,
|
|
310
|
+
'hasAddedRootView.current': hasAddedRootView.current,
|
|
311
|
+
'hasShownRootView.current': hasShownRootView.current,
|
|
312
|
+
'keyboardStateChanged': keyboardStateChanged,
|
|
313
|
+
'propsChanged': propsChanged
|
|
314
|
+
});
|
|
315
|
+
// Update ref to record current state
|
|
316
|
+
prevIsKeyboardFullyShownRef.current = isKeyboardFullyShown;
|
|
317
|
+
prevPropsRef.current = {
|
|
318
|
+
CustomPopView,
|
|
319
|
+
CustomPopViewStyle,
|
|
320
|
+
TextInputProps
|
|
321
|
+
};
|
|
322
|
+
// Only execute logic when keyboard state actually changes or user actively operates
|
|
323
|
+
if (!keyboardStateChanged && hasAddedRootView.current) {
|
|
324
|
+
console.log('AutoPositionedPopup: Skip execution - parent component re-rendered but keyboard state unchanged');
|
|
325
|
+
// if (!ref_isFocus.current) {
|
|
326
|
+
// textInputRef.current?.focus()
|
|
327
|
+
// }
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
296
330
|
if (useTextInput) {
|
|
297
331
|
if (isKeyboardFullyShown && hasAddedRootView.current && !hasShownRootView.current && state.isFocus) {
|
|
298
332
|
(_a = refAutoPositionedPopup.current) === null || _a === void 0 ? void 0 : _a.measureInWindow((x, y, width, height) => {
|
|
299
333
|
var _a;
|
|
300
|
-
console.log('AutoPositionedPopup measureInWindow
|
|
334
|
+
console.log('AutoPositionedPopup useTextInput measureInWindow=', { x, y, width, height });
|
|
301
335
|
// SIMPLE CENTER-BASED POSITIONING STRATEGY
|
|
302
336
|
const screenHeight = Dimensions.get('screen').height;
|
|
303
337
|
const screenCenter = screenHeight / 2;
|
|
304
|
-
console.log('AutoPositionedPopup
|
|
338
|
+
console.log('AutoPositionedPopup useTextInput measureInWindow =', { screenHeight, screenCenter, componentY: y });
|
|
305
339
|
// Simple rule: if component Y > screen center, show popup above; otherwise show below
|
|
306
340
|
if (y > screenCenter) {
|
|
307
341
|
console.log('AutoPositionedPopup with keyboard: showing above (Y > center)');
|
|
@@ -311,7 +345,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
311
345
|
console.log('AutoPositionedPopup with keyboard: showing below (Y <= center)');
|
|
312
346
|
ref_listPos.current = { x: x, y: y + height, width: width };
|
|
313
347
|
}
|
|
314
|
-
console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
|
|
348
|
+
console.log('AutoPositionedPopup useTextInput ref_listPos.current=', ref_listPos.current);
|
|
315
349
|
setRootViewNativeStyle(tag, {
|
|
316
350
|
top: (_a = ref_listPos.current) === null || _a === void 0 ? void 0 : _a.y,
|
|
317
351
|
left: popUpViewStyle === null || popUpViewStyle === void 0 ? void 0 : popUpViewStyle.left,
|
|
@@ -322,8 +356,9 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
322
356
|
hasShownRootView.current = true;
|
|
323
357
|
});
|
|
324
358
|
}
|
|
325
|
-
else if (!isKeyboardFullyShown && ref_isFocus.current) {
|
|
326
|
-
|
|
359
|
+
else if (!isKeyboardFullyShown && ref_isFocus.current && keyboardStateChanged) {
|
|
360
|
+
// Only execute close logic when keyboard state actually changes from true to false
|
|
361
|
+
console.log('AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView (keyboard state changed)=', { tag, forceRemoveAllRootViewOnItemSelected, keyboardStateChanged });
|
|
327
362
|
removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
|
|
328
363
|
setState((prevState) => {
|
|
329
364
|
return Object.assign(Object.assign({}, prevState), { isFocus: false });
|
|
@@ -336,10 +371,10 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
336
371
|
else {
|
|
337
372
|
if (state.isFocus) {
|
|
338
373
|
(_b = refAutoPositionedPopup.current) === null || _b === void 0 ? void 0 : _b.measureInWindow((x, y, width, height) => {
|
|
339
|
-
console.log('AutoPositionedPopup measureInWindow
|
|
374
|
+
console.log('AutoPositionedPopup !useTextInput measureInWindow=', { x, y, width, height });
|
|
340
375
|
// INTELLIGENT POSITION CALCULATION - MODIFIED VERSION WITH STATUS BAR SAFETY
|
|
341
376
|
const calculateOptimalPosition = (componentY, componentHeight, popupHeight) => {
|
|
342
|
-
console.log('🔥🔥🔥 NEW CALCULATE OPTIMAL POSITION FUNCTION EXECUTING 🔥🔥🔥');
|
|
377
|
+
console.log('AutoPositionedPopup 🔥🔥🔥 NEW CALCULATE OPTIMAL POSITION FUNCTION EXECUTING 🔥🔥🔥');
|
|
343
378
|
// Use window height (visible area) instead of screen height (includes status bar)
|
|
344
379
|
const windowHeight = Dimensions.get('window').height;
|
|
345
380
|
const visibleAreaCenter = windowHeight / 2;
|
|
@@ -356,7 +391,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
356
391
|
}
|
|
357
392
|
};
|
|
358
393
|
const statusBarHeight = getStatusBarHeight();
|
|
359
|
-
console.log('🔥 Cross-platform StatusBar height:', statusBarHeight, 'Platform:', Platform.OS);
|
|
394
|
+
console.log('AutoPositionedPopup 🔥 Cross-platform StatusBar height:', statusBarHeight, 'Platform:', Platform.OS);
|
|
360
395
|
// Calculate component center point as requested
|
|
361
396
|
const componentCenterY = componentY + componentHeight / 2;
|
|
362
397
|
console.log('AutoPositionedPopup positioning data:', {
|
|
@@ -404,7 +439,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
404
439
|
// Strong platform adjustment - much smaller for Android bottom components
|
|
405
440
|
const platformMultiplier = Platform.OS === 'ios' ? 1.0 : (isInBottomHalf ? 0.5 : 0.9);
|
|
406
441
|
const finalSpacing = Math.max(baseSpacing, relativeSpacing) * edgeProximityFactor * platformMultiplier;
|
|
407
|
-
console.log('🔥 Advanced spacing calculation:', {
|
|
442
|
+
console.log('AutoPositionedPopup 🔥 Advanced spacing calculation:', {
|
|
408
443
|
componentCenter,
|
|
409
444
|
screenCenter,
|
|
410
445
|
distanceFromCenter,
|
|
@@ -440,7 +475,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
440
475
|
// Component in bottom half + enough space above = FORCE ABOVE
|
|
441
476
|
showAbove = true;
|
|
442
477
|
finalY = componentY - popupHeight + componentHeight / 2;
|
|
443
|
-
console.log('🔥 AutoPositionedPopup: FORCE ABOVE - bottom half component with enough space, finalY=', finalY);
|
|
478
|
+
console.log('AutoPositionedPopup 🔥 AutoPositionedPopup: FORCE ABOVE - bottom half component with enough space, finalY=', finalY);
|
|
444
479
|
}
|
|
445
480
|
else if (!isInBottomHalf && spaceBelow >= popupHeight) {
|
|
446
481
|
// Component in top half + enough space below = show below
|
|
@@ -474,7 +509,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
474
509
|
}
|
|
475
510
|
}
|
|
476
511
|
// Enhanced boundary check with detailed logging
|
|
477
|
-
console.log('🔥 Pre-boundary check:', {
|
|
512
|
+
console.log('AutoPositionedPopup 🔥 Pre-boundary check:', {
|
|
478
513
|
originalFinalY: finalY,
|
|
479
514
|
showAbove,
|
|
480
515
|
statusBarHeight,
|
|
@@ -486,21 +521,21 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
486
521
|
if (showAbove && finalY < statusBarHeight) {
|
|
487
522
|
const oldFinalY = finalY;
|
|
488
523
|
finalY = statusBarHeight;
|
|
489
|
-
console.log('🔥 BOUNDARY
|
|
524
|
+
console.log('AutoPositionedPopup 🔥 BOUNDARY : Above display adjusted for status bar:', oldFinalY, '->', finalY);
|
|
490
525
|
}
|
|
491
526
|
if (!showAbove && finalY + popupHeight > windowHeight) {
|
|
492
527
|
const oldFinalY = finalY;
|
|
493
528
|
finalY = windowHeight - popupHeight;
|
|
494
|
-
console.log('🔥 BOUNDARY
|
|
529
|
+
console.log('AutoPositionedPopup 🔥 BOUNDARY : Below display adjusted to fit window:', oldFinalY, '->', finalY);
|
|
495
530
|
}
|
|
496
531
|
// CRITICAL CHECK: Detect if boundary check is changing display direction
|
|
497
532
|
if (showAbove && finalY + popupHeight > componentY) {
|
|
498
|
-
console.log('🚨 WARNING: Above positioning may overlap with component!');
|
|
533
|
+
console.log('AutoPositionedPopup 🚨 WARNING: Above positioning may overlap with component!');
|
|
499
534
|
}
|
|
500
535
|
if (!showAbove && finalY < componentY + componentHeight) {
|
|
501
|
-
console.log('🚨 WARNING: Below positioning may overlap with component!');
|
|
536
|
+
console.log('AutoPositionedPopup 🚨 WARNING: Below positioning may overlap with component!');
|
|
502
537
|
}
|
|
503
|
-
console.log('🔥 Post-boundary check final result:', {
|
|
538
|
+
console.log('AutoPositionedPopup 🔥 Post-boundary check final result:', {
|
|
504
539
|
finalY,
|
|
505
540
|
showAbove,
|
|
506
541
|
'popupTop': finalY,
|
|
@@ -514,17 +549,15 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
514
549
|
const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
|
|
515
550
|
? CustomPopViewStyle.height
|
|
516
551
|
: listLayout.height;
|
|
517
|
-
console.log('🔥 Using actualPopupHeight for calculation:', actualPopupHeight,
|
|
552
|
+
console.log('AutoPositionedPopup 🔥 Using actualPopupHeight for calculation:', { actualPopupHeight, CustomPopView: !!CustomPopView });
|
|
518
553
|
const positionResult = calculateOptimalPosition(y, height, actualPopupHeight);
|
|
519
554
|
console.log('AutoPositionedPopup FINAL position result:', positionResult);
|
|
520
555
|
ref_listPos.current = { x: x, y: positionResult.finalY, width: width };
|
|
521
|
-
console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
|
|
556
|
+
console.log('AutoPositionedPopup !useTextInput ref_listPos.current=', ref_listPos.current);
|
|
522
557
|
if (CustomPopView && CustomPopViewStyle) {
|
|
523
|
-
console.log('AutoPositionedPopup CustomPopViewStyle=', CustomPopViewStyle);
|
|
524
558
|
// Position already calculated correctly above, no need to recalculate
|
|
525
559
|
const PopViewComponent = CustomPopView();
|
|
526
|
-
console.log('AutoPositionedPopup addRootView
|
|
527
|
-
console.log('AutoPositionedPopup addRootView state.selectedItem=', state.selectedItem);
|
|
560
|
+
console.log('AutoPositionedPopup !useTextInput addRootView=', { CustomPopViewStyle, PopViewComponent, 'state.selectedItem': state.selectedItem });
|
|
528
561
|
addRootView({
|
|
529
562
|
id: tag,
|
|
530
563
|
style: !centerDisplay
|
|
@@ -546,7 +579,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
546
579
|
});
|
|
547
580
|
}
|
|
548
581
|
else {
|
|
549
|
-
console.log('AutoPositionedPopup addRootView tag=', tag);
|
|
582
|
+
console.log('AutoPositionedPopup !useTextInput addRootView tag=', tag);
|
|
550
583
|
addRootView({
|
|
551
584
|
id: tag,
|
|
552
585
|
style: {
|
|
@@ -588,7 +621,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
588
621
|
CustomPopView,
|
|
589
622
|
CustomPopViewStyle,
|
|
590
623
|
forceRemoveAllRootViewOnItemSelected,
|
|
591
|
-
tag,
|
|
624
|
+
tag, TextInputProps,
|
|
592
625
|
state.selectedItem, showListEmptyComponent
|
|
593
626
|
]);
|
|
594
627
|
// Imperative handle for parent component access
|
|
@@ -601,7 +634,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
601
634
|
},
|
|
602
635
|
}), []);
|
|
603
636
|
const updateState = (key, value) => {
|
|
604
|
-
console.log('AutoPositionedPopup updateState
|
|
637
|
+
console.log('AutoPositionedPopup updateState=', { key, value });
|
|
605
638
|
setState((prevState) => (Object.assign(Object.assign({}, prevState), { [key]: value })));
|
|
606
639
|
if (key === 'selectedItem' && onItemSelected) {
|
|
607
640
|
onItemSelected(value);
|
|
@@ -616,6 +649,179 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
616
649
|
setSearchQuery('');
|
|
617
650
|
}
|
|
618
651
|
};
|
|
652
|
+
// Simple deep comparison function (for style objects only)
|
|
653
|
+
const shallowEqual = (obj1, obj2) => {
|
|
654
|
+
if (obj1 === obj2)
|
|
655
|
+
return true;
|
|
656
|
+
if (!obj1 || !obj2)
|
|
657
|
+
return false;
|
|
658
|
+
if (typeof obj1 !== 'object' || typeof obj2 !== 'object')
|
|
659
|
+
return false;
|
|
660
|
+
const keys1 = Object.keys(obj1);
|
|
661
|
+
const keys2 = Object.keys(obj2);
|
|
662
|
+
if (keys1.length !== keys2.length)
|
|
663
|
+
return false;
|
|
664
|
+
for (const key of keys1) {
|
|
665
|
+
if (obj1[key] !== obj2[key])
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
return true;
|
|
669
|
+
};
|
|
670
|
+
// Use useMemo to create stable props reference
|
|
671
|
+
// Only update when deep comparison detects real changes to avoid TextInput recreation due to reference changes during parent component redraws
|
|
672
|
+
const stableInputStyle = useMemo(() => {
|
|
673
|
+
if (!shallowEqual(stableInputStyleRef.current, inputStyle)) {
|
|
674
|
+
console.log(`AutoPositionedPopup inputStyle deep change detected, updating stable reference - tag: ${tag}`);
|
|
675
|
+
stableInputStyleRef.current = inputStyle;
|
|
676
|
+
}
|
|
677
|
+
return stableInputStyleRef.current;
|
|
678
|
+
}, [inputStyle, tag]);
|
|
679
|
+
const stableTextInputProps = useMemo(() => {
|
|
680
|
+
if (!shallowEqual(stableTextInputPropsRef.current, TextInputProps)) {
|
|
681
|
+
console.log(`AutoPositionedPopup TextInputProps deep change detected, updating stable reference - tag: ${tag}`);
|
|
682
|
+
stableTextInputPropsRef.current = TextInputProps;
|
|
683
|
+
}
|
|
684
|
+
return stableTextInputPropsRef.current;
|
|
685
|
+
}, [TextInputProps, tag]);
|
|
686
|
+
// Use useCallback to stabilize onFocus and onBlur callback references
|
|
687
|
+
// Prevent creating new callback functions during parent component redraws to avoid TextInput re-triggering focus
|
|
688
|
+
// Use ref to store latest state values to avoid adding frequently changing values to dependencies
|
|
689
|
+
const stateRef = useRef(state);
|
|
690
|
+
stateRef.current = state;
|
|
691
|
+
const handleTextInputFocus = useCallback(() => {
|
|
692
|
+
const currentTime = Date.now();
|
|
693
|
+
const timeSinceLastFocus = currentTime - lastFocusTimeRef.current;
|
|
694
|
+
console.log('AutoPositionedPopup onFocus=', {
|
|
695
|
+
tag,
|
|
696
|
+
'state.selectedItem': stateRef.current.selectedItem,
|
|
697
|
+
'hasTriggeredFocus.current=': hasTriggeredFocus.current,
|
|
698
|
+
'textInputRef.current=': textInputRef.current,
|
|
699
|
+
'ref_searchQuery.current=': ref_searchQuery.current,
|
|
700
|
+
'timeSinceLastFocus': timeSinceLastFocus,
|
|
701
|
+
'isKeyboardFullyShown': isKeyboardFullyShown,
|
|
702
|
+
'isFocusEventProcessing': isFocusEventProcessingRef.current
|
|
703
|
+
});
|
|
704
|
+
// Prevent rapid repeated triggers (repeated events within 300ms are ignored)
|
|
705
|
+
if (timeSinceLastFocus < 300) {
|
|
706
|
+
console.log('AutoPositionedPopup onFocus: Skip - event triggered too quickly (< 300ms)');
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
// Skip if keyboard is already open and focus has been handled
|
|
710
|
+
if (isKeyboardFullyShown && hasTriggeredFocus.current) {
|
|
711
|
+
console.log('AutoPositionedPopup onFocus: Skip - keyboard already open and focus handled');
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
// Prevent concurrent processing
|
|
715
|
+
if (isFocusEventProcessingRef.current) {
|
|
716
|
+
console.log('AutoPositionedPopup onFocus: Skip - processing another focus event');
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
isFocusEventProcessingRef.current = true;
|
|
720
|
+
lastFocusTimeRef.current = currentTime;
|
|
721
|
+
if (!hasTriggeredFocus.current) {
|
|
722
|
+
hasTriggeredFocus.current = true;
|
|
723
|
+
ref_isFocus.current = true;
|
|
724
|
+
if (stateRef.current.selectedItem) {
|
|
725
|
+
ref_searchQuery.current = stateRef.current.selectedItem.title;
|
|
726
|
+
}
|
|
727
|
+
if (textInputRef.current && ref_searchQuery.current) {
|
|
728
|
+
textInputRef.current.setNativeProps({
|
|
729
|
+
text: ref_searchQuery.current,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// Delay resetting processing flag to avoid blocking subsequent legitimate focus events
|
|
734
|
+
setTimeout(() => {
|
|
735
|
+
isFocusEventProcessingRef.current = false;
|
|
736
|
+
}, 100);
|
|
737
|
+
}, [tag, isKeyboardFullyShown]); // Remove state.selectedItem, use stateRef instead
|
|
738
|
+
const handleTextInputBlur = useCallback(() => {
|
|
739
|
+
console.log('AutoPositionedPopup onBlur=', {
|
|
740
|
+
tag,
|
|
741
|
+
'textInputRef.current': textInputRef.current,
|
|
742
|
+
'isKeyboardFullyShown': isKeyboardFullyShown,
|
|
743
|
+
'hasTriggeredFocus.current': hasTriggeredFocus.current
|
|
744
|
+
});
|
|
745
|
+
// If keyboard is still open, this is a false trigger caused by parent component re-render, should not reset
|
|
746
|
+
if (isKeyboardFullyShown && hasTriggeredFocus.current) {
|
|
747
|
+
console.log('AutoPositionedPopup onBlur: Skip - keyboard still open, possibly caused by parent component re-render');
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
// Only reset internal state, do not actively close keyboard
|
|
751
|
+
// Keyboard will close naturally when TextInput loses focus, no need to manually call Keyboard.dismiss()
|
|
752
|
+
hasTriggeredFocus.current = false;
|
|
753
|
+
hasAddedRootView.current = false;
|
|
754
|
+
hasShownRootView.current = false;
|
|
755
|
+
ref_isFocus.current = false;
|
|
756
|
+
setState((prevState) => {
|
|
757
|
+
return Object.assign(Object.assign({}, prevState), { isFocus: false });
|
|
758
|
+
});
|
|
759
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
|
|
760
|
+
setSearchQuery('');
|
|
761
|
+
if (textInputRef.current) {
|
|
762
|
+
textInputRef.current.setNativeProps({ text: '' });
|
|
763
|
+
ref_searchQuery.current = '';
|
|
764
|
+
// Remove textInputRef.current.blur() - avoid forcing blur causing keyboard to close
|
|
765
|
+
}
|
|
766
|
+
// Remove Keyboard.dismiss() - let keyboard close naturally to avoid triggering keyboardDidHide event
|
|
767
|
+
}, [tag, isKeyboardFullyShown, forceRemoveAllRootViewOnItemSelected]);
|
|
768
|
+
// Wrap TextInput independently in useMemo to recreate only when key props change
|
|
769
|
+
// This avoids repeated ref callback triggers due to other props changes during parent component redraws
|
|
770
|
+
const memoizedTextInput = useMemo(() => {
|
|
771
|
+
console.log(`AutoPositionedPopup useMemo creating TextInput - tag: ${tag}, isFocus: ${state.isFocus}`);
|
|
772
|
+
if (!useTextInput || !state.isFocus) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
return (<RNTextInput ref={(ref) => {
|
|
776
|
+
// Monitor TextInput mounting and unmounting
|
|
777
|
+
if (ref && !textInputRef.current) {
|
|
778
|
+
console.log(`AutoPositionedPopup TextInput created/mounted - tag: ${tag}, ref:`, ref);
|
|
779
|
+
}
|
|
780
|
+
else if (!ref && textInputRef.current) {
|
|
781
|
+
console.log(`AutoPositionedPopup TextInput unmounted - tag: ${tag}`);
|
|
782
|
+
}
|
|
783
|
+
else if (ref && textInputRef.current && ref !== textInputRef.current) {
|
|
784
|
+
console.log(`AutoPositionedPopup TextInput replaced - tag: ${tag}, oldRef:`, textInputRef.current, 'newRef:', ref);
|
|
785
|
+
}
|
|
786
|
+
textInputRef.current = ref;
|
|
787
|
+
}} key={`textinput-${tag}`} style={[
|
|
788
|
+
styles.inputStyle,
|
|
789
|
+
stableInputStyle,
|
|
790
|
+
]} textAlign={stableTextInputProps['textAlign'] || 'left'} multiline={stableTextInputProps['multiline'] || false} numberOfLines={stableTextInputProps['numberOfLines'] || 1} onChangeText={(searchQuery) => {
|
|
791
|
+
ref_searchQuery.current = searchQuery;
|
|
792
|
+
console.log('AutoPositionedPopup onChangeText rootViews=', rootViews);
|
|
793
|
+
if (!localSearch) {
|
|
794
|
+
if (debounceTimerRef.current) {
|
|
795
|
+
clearTimeout(debounceTimerRef.current);
|
|
796
|
+
}
|
|
797
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
798
|
+
emitQueryChange(ref_searchQuery.current);
|
|
799
|
+
}, 500);
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
emitQueryChange(ref_searchQuery.current);
|
|
803
|
+
}
|
|
804
|
+
}} placeholderTextColor={theme.colors.placeholderText} placeholder={placeholder} onKeyPress={(e) => {
|
|
805
|
+
if (e.nativeEvent.key === 'Enter') {
|
|
806
|
+
Keyboard.dismiss();
|
|
807
|
+
}
|
|
808
|
+
}} keyboardType={stableTextInputProps['keyboardType'] || 'default'} clearButtonMode="while-editing" returnKeyType={stableTextInputProps['returnKeyType'] || 'done'} maxLength={stableTextInputProps['maxLength'] || 100} accessibilityLabel="selectInput" accessible={true} autoFocus={stableTextInputProps['autoFocus'] || false} autoCorrect={false} underlineColorAndroid="transparent" editable={stableTextInputProps['editable'] || true} secureTextEntry={stableTextInputProps['secureTextEntry'] || false} defaultValue="" caretHidden={false} enablesReturnKeyAutomatically onFocus={handleTextInputFocus} onBlur={handleTextInputBlur} selectTextOnFocus={stableTextInputProps['selectTextOnFocus'] || false} onSubmitEditing={(e) => {
|
|
809
|
+
console.log('AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=', e.nativeEvent.text);
|
|
810
|
+
onSubmitEditing && onSubmitEditing(e);
|
|
811
|
+
}}/>);
|
|
812
|
+
}, [
|
|
813
|
+
tag, // tag 是稳定的
|
|
814
|
+
useTextInput, // useTextInput 是稳定的
|
|
815
|
+
state.isFocus, // isFocus 控制显示/隐藏
|
|
816
|
+
handleTextInputFocus, // useCallback wrapped, reference stable
|
|
817
|
+
handleTextInputBlur, // useCallback wrapped, reference stable
|
|
818
|
+
stableInputStyle, // Use stable inputStyle reference (after deep comparison)
|
|
819
|
+
stableTextInputProps, // Use stable TextInputProps reference (after deep comparison)
|
|
820
|
+
placeholder, // placeholder usually stable
|
|
821
|
+
onSubmitEditing, // onSubmitEditing usually stable
|
|
822
|
+
// No longer use original inputStyle and TextInputProps, use stable references instead
|
|
823
|
+
// Stable references only update when deep comparison detects actual content changes, avoiding frequent TextInput recreation during parent component redraws
|
|
824
|
+
]);
|
|
619
825
|
// Render the component following project implementation
|
|
620
826
|
return useMemo(() => {
|
|
621
827
|
var _a;
|
|
@@ -623,17 +829,20 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
623
829
|
return (<CustomRow>
|
|
624
830
|
<View style={[styles.contain, style]} ref={refAutoPositionedPopup}>
|
|
625
831
|
{!state.isFocus || !useTextInput ? (<TouchableOpacity style={[styles.AutoPositionedPopupBtn, AutoPositionedPopupBtnStyle]} disabled={AutoPositionedPopupBtnDisabled} onPress={() => {
|
|
626
|
-
console.log('AutoPositionedPopup onPress
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
832
|
+
console.log('AutoPositionedPopup onPress=', {
|
|
833
|
+
tag,
|
|
834
|
+
'state.isFocus': state.isFocus,
|
|
835
|
+
useTextInput,
|
|
836
|
+
'hasAddedRootView.current': hasAddedRootView.current,
|
|
837
|
+
'hasShownRootView.current': hasShownRootView.current,
|
|
838
|
+
'hasTriggeredFocus.current': hasTriggeredFocus.current,
|
|
839
|
+
'state.selectedItem': state.selectedItem
|
|
840
|
+
});
|
|
633
841
|
setState((prevState) => {
|
|
634
842
|
return Object.assign(Object.assign({}, prevState), { isFocus: true });
|
|
635
843
|
});
|
|
636
844
|
if (!hasAddedRootView.current && useTextInput) {
|
|
845
|
+
// TextInput version: hide first, show after keyboard is fully displayed
|
|
637
846
|
hasAddedRootView.current = true;
|
|
638
847
|
hasShownRootView.current = false;
|
|
639
848
|
addRootView({
|
|
@@ -649,6 +858,7 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
649
858
|
useModal: false,
|
|
650
859
|
});
|
|
651
860
|
}
|
|
861
|
+
console.log('AutoPositionedPopup onPress done');
|
|
652
862
|
}}>
|
|
653
863
|
{!btwChildren ? (<Text style={[
|
|
654
864
|
styles.searchQueryTxt,
|
|
@@ -657,87 +867,36 @@ const AutoPositionedPopup = memo(forwardRef((props, parentRef) => {
|
|
|
657
867
|
]} numberOfLines={1} ellipsizeMode={'tail'}>
|
|
658
868
|
{((_a = state.selectedItem) === null || _a === void 0 ? void 0 : _a.title) || placeholder}
|
|
659
869
|
</Text>) : (btwChildren())}
|
|
660
|
-
</TouchableOpacity>) : (
|
|
661
|
-
state.isFocus && (<RNTextInput ref={textInputRef} key="fixed-textinput-key" style={[
|
|
662
|
-
styles.inputStyle,
|
|
663
|
-
inputStyle,
|
|
664
|
-
]} textAlign={TextInputProps['textAlign'] || 'left'} multiline={TextInputProps['multiline'] || false} numberOfLines={TextInputProps['numberOfLines'] || 1} onChangeText={(searchQuery) => {
|
|
665
|
-
ref_searchQuery.current = searchQuery;
|
|
666
|
-
console.log('AutoPositionedPopup onChangeText rootViews=', rootViews);
|
|
667
|
-
if (!localSearch) {
|
|
668
|
-
if (debounceTimerRef.current) {
|
|
669
|
-
clearTimeout(debounceTimerRef.current);
|
|
670
|
-
}
|
|
671
|
-
debounceTimerRef.current = setTimeout(() => {
|
|
672
|
-
emitQueryChange(ref_searchQuery.current);
|
|
673
|
-
}, 500);
|
|
674
|
-
}
|
|
675
|
-
else {
|
|
676
|
-
emitQueryChange(ref_searchQuery.current);
|
|
677
|
-
}
|
|
678
|
-
}} placeholderTextColor={theme.colors.placeholderText} placeholder={placeholder} onKeyPress={(e) => {
|
|
679
|
-
if (e.nativeEvent.key === 'Enter') {
|
|
680
|
-
Keyboard.dismiss();
|
|
681
|
-
}
|
|
682
|
-
}} keyboardType={TextInputProps['keyboardType'] || 'default'} clearButtonMode="while-editing" returnKeyType={TextInputProps['returnKeyType'] || 'done'} maxLength={TextInputProps['maxLength'] || 100} accessibilityLabel="selectInput" accessible={true} autoFocus={TextInputProps['autoFocus'] || false} autoCorrect={false} underlineColorAndroid="transparent" editable={TextInputProps['editable'] || true} secureTextEntry={TextInputProps['secureTextEntry'] || false} defaultValue="" caretHidden={false} enablesReturnKeyAutomatically onFocus={() => {
|
|
683
|
-
console.log('AutoPositionedPopup onFocus tag=', tag, ' selectedItem=', state.selectedItem, ' hasTriggeredFocus.current=', hasTriggeredFocus.current, ' textInputRef.current=', textInputRef.current, ' ref_searchQuery.current=', ref_searchQuery.current);
|
|
684
|
-
if (!hasTriggeredFocus.current) {
|
|
685
|
-
hasTriggeredFocus.current = true;
|
|
686
|
-
ref_isFocus.current = true;
|
|
687
|
-
if (state.selectedItem) {
|
|
688
|
-
ref_searchQuery.current = state.selectedItem.title;
|
|
689
|
-
}
|
|
690
|
-
if (textInputRef.current && ref_searchQuery.current) {
|
|
691
|
-
textInputRef.current.setNativeProps({
|
|
692
|
-
text: ref_searchQuery.current,
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}} onBlur={() => {
|
|
697
|
-
console.log('AutoPositionedPopup onBlur tag=', tag, 'textInputRef.current=', textInputRef.current);
|
|
698
|
-
hasTriggeredFocus.current = false;
|
|
699
|
-
hasAddedRootView.current = false; // 重置 RootView 狀態
|
|
700
|
-
hasShownRootView.current = false;
|
|
701
|
-
ref_isFocus.current = false;
|
|
702
|
-
setState((prevState) => {
|
|
703
|
-
return Object.assign(Object.assign({}, prevState), { isFocus: false });
|
|
704
|
-
});
|
|
705
|
-
removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
|
|
706
|
-
setSearchQuery('');
|
|
707
|
-
if (textInputRef.current) {
|
|
708
|
-
textInputRef.current.setNativeProps({ text: '' });
|
|
709
|
-
ref_searchQuery.current = '';
|
|
710
|
-
textInputRef.current.blur();
|
|
711
|
-
}
|
|
712
|
-
Keyboard.dismiss();
|
|
713
|
-
}} selectTextOnFocus={TextInputProps['selectTextOnFocus'] || false} onSubmitEditing={(e) => {
|
|
714
|
-
console.log('AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=', e.nativeEvent.text);
|
|
715
|
-
onSubmitEditing && onSubmitEditing(e);
|
|
716
|
-
}}/>))}
|
|
870
|
+
</TouchableOpacity>) : (memoizedTextInput)}
|
|
717
871
|
</View>
|
|
718
872
|
</CustomRow>);
|
|
719
|
-
}, [
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
873
|
+
}, [
|
|
874
|
+
tag,
|
|
875
|
+
// ✅ CRITICAL FIX: Remove all props that may change frequently or are inline functions
|
|
876
|
+
// Changes to these props should not cause the entire component tree to recreate, especially TextInput
|
|
877
|
+
// fetchData, // ❌ Removed: inline function
|
|
878
|
+
// renderItem, // ❌ Removed: possibly inline function
|
|
879
|
+
// onItemSelected, // ❌ Removed: possibly inline function
|
|
880
|
+
// onSubmitEditing, // ❌ Removed: possibly inline function
|
|
724
881
|
localSearch,
|
|
725
|
-
placeholder,
|
|
726
|
-
textAlign,
|
|
882
|
+
// placeholder, // ❌ Removed: may change
|
|
883
|
+
// textAlign, // ❌ Removed: may change
|
|
727
884
|
pageSize,
|
|
728
885
|
selectedItem,
|
|
729
|
-
CustomRow,
|
|
886
|
+
// CustomRow, // ❌ Removed: inline function, new reference each time
|
|
730
887
|
useTextInput,
|
|
731
|
-
btwChildren,
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
CustomPopViewStyle,
|
|
888
|
+
// btwChildren, // ❌ Removed: inline function
|
|
889
|
+
// keyExtractor, // ❌ Removed: possibly inline function
|
|
890
|
+
// AutoPositionedPopupBtnStyle, // ❌ Removed: possibly inline object
|
|
891
|
+
// CustomPopView, // ❌ Removed: may change
|
|
892
|
+
// CustomPopViewStyle, // ❌ Removed: may change
|
|
737
893
|
forceRemoveAllRootViewOnItemSelected,
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
894
|
+
state.isFocus,
|
|
895
|
+
showListEmptyComponent,
|
|
896
|
+
emptyText,
|
|
897
|
+
// ✅ Removed most dependencies that may cause re-rendering, keeping only core dependencies that truly affect component structure
|
|
898
|
+
// This prevents TextInput recreation due to inline functions/objects during parent component redraws
|
|
899
|
+
]);
|
|
741
900
|
}));
|
|
742
901
|
export default AutoPositionedPopup;
|
|
743
902
|
//# sourceMappingURL=AutoPositionedPopup.js.map
|