react-native-auto-positioned-popup 1.2.16 → 1.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/AutoPositionedPopup.d.ts.map +1 -1
- package/lib/AutoPositionedPopup.js +227 -288
- package/lib/AutoPositionedPopup.js.map +1 -1
- package/lib/AutoPositionedPopup.style.d.ts.map +1 -1
- package/lib/AutoPositionedPopup.style.js +2 -0
- package/lib/AutoPositionedPopup.style.js.map +1 -1
- package/lib/KeyboardManager.d.ts +6 -1
- package/lib/KeyboardManager.d.ts.map +1 -1
- package/lib/KeyboardManager.js +19 -2
- package/lib/KeyboardManager.js.map +1 -1
- package/lib/RootViewContext.d.ts.map +1 -1
- package/lib/RootViewContext.js +76 -44
- package/lib/RootViewContext.js.map +1 -1
- package/package.json +1 -1
- package/src/AutoPositionedPopup.style.ts +2 -0
- package/src/AutoPositionedPopup.tsx +321 -381
- package/src/KeyboardManager.tsx +35 -10
- package/src/RootViewContext.tsx +63 -24
package/src/KeyboardManager.tsx
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import React, { useEffect, useState, useRef } from 'react';
|
|
2
|
-
import { Keyboard, EmitterSubscription, Platform } from 'react-native';
|
|
2
|
+
import { Keyboard, EmitterSubscription, Platform, KeyboardEvent } from 'react-native';
|
|
3
|
+
|
|
4
|
+
// DEBUG FLAG: Set to false to disable all console logs for better performance
|
|
5
|
+
const KEYBOARD_DEBUG = false;
|
|
6
|
+
const debugLog = (...args: any[]) => {
|
|
7
|
+
if (KEYBOARD_DEBUG) {
|
|
8
|
+
console.log(...args);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
3
11
|
|
|
4
12
|
// Debounce function
|
|
5
13
|
const debounce = (func: Function, delay: number) => {
|
|
@@ -10,8 +18,16 @@ const debounce = (func: Function, delay: number) => {
|
|
|
10
18
|
};
|
|
11
19
|
};
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
// V19: Return type for keyboard status hook - includes height for accurate positioning
|
|
22
|
+
interface KeyboardStatus {
|
|
23
|
+
isShown: boolean;
|
|
24
|
+
height: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const useKeyboardStatus = (): KeyboardStatus => {
|
|
14
28
|
const [isKeyboardFullyShown, setIsKeyboardFullyShown] = useState(false);
|
|
29
|
+
// V19: Track keyboard height for popup positioning
|
|
30
|
+
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
|
15
31
|
|
|
16
32
|
// Add state cache to avoid repeatedly setting the same state
|
|
17
33
|
const currentKeyboardStatusRef = useRef<boolean>(false);
|
|
@@ -25,13 +41,13 @@ export const useKeyboardStatus = () => {
|
|
|
25
41
|
|
|
26
42
|
// ✅ FIX: Check state before debounce
|
|
27
43
|
if (currentKeyboardStatusRef.current === value) {
|
|
28
|
-
|
|
44
|
+
debugLog('KeyboardManager: Skip - Keyboard state unchanged (before debounce)', { value, timeSinceLastUpdate });
|
|
29
45
|
return;
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
// ✅ FIX: Skip if the same value is already pending
|
|
33
49
|
if (pendingValueRef.current === value) {
|
|
34
|
-
|
|
50
|
+
debugLog('KeyboardManager: Skip - Same value already in processing queue', { value });
|
|
35
51
|
return;
|
|
36
52
|
}
|
|
37
53
|
|
|
@@ -47,12 +63,12 @@ export const useKeyboardStatus = () => {
|
|
|
47
63
|
debounce((value: boolean, currentTime: number, timeSinceLastUpdate: number) => {
|
|
48
64
|
// ✅ FIX: Check state again (in case state was updated during debounce)
|
|
49
65
|
if (currentKeyboardStatusRef.current === value) {
|
|
50
|
-
|
|
66
|
+
debugLog('KeyboardManager: Skip - Keyboard state unchanged (after debounce)', { value, timeSinceLastUpdate });
|
|
51
67
|
pendingValueRef.current = null;
|
|
52
68
|
return;
|
|
53
69
|
}
|
|
54
70
|
|
|
55
|
-
|
|
71
|
+
debugLog('KeyboardManager: Setting keyboard status to', value, {
|
|
56
72
|
previousValue: currentKeyboardStatusRef.current,
|
|
57
73
|
timeSinceLastUpdate
|
|
58
74
|
});
|
|
@@ -75,10 +91,15 @@ export const useKeyboardStatus = () => {
|
|
|
75
91
|
// Remove Will event listeners to avoid duplicate triggers and state race conditions
|
|
76
92
|
keyboardDidShowListener = Keyboard.addListener(
|
|
77
93
|
'keyboardDidShow',
|
|
78
|
-
() => {
|
|
94
|
+
(e: KeyboardEvent) => {
|
|
95
|
+
// V19: Capture keyboard height from event for accurate popup positioning
|
|
96
|
+
const height = e.endCoordinates?.height || 0;
|
|
97
|
+
debugLog('KeyboardManager: keyboardDidShow event', { height });
|
|
98
|
+
setKeyboardHeight(height);
|
|
99
|
+
|
|
79
100
|
// ✅ FIX: Add protection at event listener level - skip if keyboard is already open
|
|
80
101
|
if (currentKeyboardStatusRef.current === true) {
|
|
81
|
-
|
|
102
|
+
debugLog('KeyboardManager: Skip keyboardDidShow event - Keyboard is already open');
|
|
82
103
|
return;
|
|
83
104
|
}
|
|
84
105
|
debouncedSetKeyboardShown(true);
|
|
@@ -87,9 +108,12 @@ export const useKeyboardStatus = () => {
|
|
|
87
108
|
keyboardDidHideListener = Keyboard.addListener(
|
|
88
109
|
'keyboardDidHide',
|
|
89
110
|
() => {
|
|
111
|
+
// V19: Reset keyboard height when keyboard hides
|
|
112
|
+
setKeyboardHeight(0);
|
|
113
|
+
|
|
90
114
|
// ✅ FIX: Add protection at event listener level - skip if keyboard is already closed
|
|
91
115
|
if (currentKeyboardStatusRef.current === false) {
|
|
92
|
-
|
|
116
|
+
debugLog('KeyboardManager: Skip keyboardDidHide event - Keyboard is already closed');
|
|
93
117
|
return;
|
|
94
118
|
}
|
|
95
119
|
debouncedSetKeyboardShown(false);
|
|
@@ -102,5 +126,6 @@ export const useKeyboardStatus = () => {
|
|
|
102
126
|
};
|
|
103
127
|
}, []);
|
|
104
128
|
|
|
105
|
-
|
|
129
|
+
// V19: Return both keyboard visibility status and height
|
|
130
|
+
return { isShown: isKeyboardFullyShown, height: keyboardHeight };
|
|
106
131
|
};
|
package/src/RootViewContext.tsx
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import React, {ReactNode, createContext, useContext, useEffect, useMemo, useRef, useState} from 'react';
|
|
2
|
-
import {Pressable, View, ViewStyle, Keyboard} from 'react-native';
|
|
2
|
+
import {Pressable, View, ViewStyle, Keyboard, StyleSheet} from 'react-native';
|
|
3
|
+
|
|
4
|
+
// DEBUG FLAG: Set to false to disable all console logs for better performance
|
|
5
|
+
const ROOTVIEW_DEBUG = false;
|
|
6
|
+
const debugLog = (...args: any[]) => {
|
|
7
|
+
if (ROOTVIEW_DEBUG) {
|
|
8
|
+
console.log(...args);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
3
11
|
|
|
4
12
|
interface DynamicViewBase {
|
|
5
13
|
id: string;
|
|
@@ -35,13 +43,13 @@ export const RootViewProvider: React.FC<RootViewProviderProps> = ({children}) =>
|
|
|
35
43
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
|
36
44
|
const viewRefs = useRef<Record<string, View>>({});
|
|
37
45
|
useEffect(() => {
|
|
38
|
-
|
|
46
|
+
debugLog('react-native-auto-positioned-popup RootViewProvider rootViews changed:', rootViews);
|
|
39
47
|
}, [rootViews]);
|
|
40
48
|
const addRootView = (view: DynamicViewBase): void => {
|
|
41
49
|
// const id = `dynamic-view-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
42
50
|
const newView: DynamicViewBase = {...view};
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
debugLog('react-native-auto-positioned-popup RootViewProvider addRootView rootViews=', rootViews);
|
|
52
|
+
debugLog('react-native-auto-positioned-popup RootViewProvider addRootView newView=', newView);
|
|
45
53
|
setRootViews((prev) => [...prev, newView]);
|
|
46
54
|
};
|
|
47
55
|
|
|
@@ -60,7 +68,7 @@ export const RootViewProvider: React.FC<RootViewProviderProps> = ({children}) =>
|
|
|
60
68
|
* @param _rootViews
|
|
61
69
|
*/
|
|
62
70
|
const removeRootView = (id?: string, force?: boolean, _rootViews?: DynamicViewBase[]): void => {
|
|
63
|
-
|
|
71
|
+
debugLog('react-native-auto-positioned-popup RootViewProvider removeRootView=', {id, force, rootViews, _rootViews});
|
|
64
72
|
// Ensure keyboard is dismissed when force removing all root views
|
|
65
73
|
if (force) {
|
|
66
74
|
// Dismiss keyboard first
|
|
@@ -70,14 +78,14 @@ export const RootViewProvider: React.FC<RootViewProviderProps> = ({children}) =>
|
|
|
70
78
|
// 100ms gives touch event system enough time to process pending events before views are removed
|
|
71
79
|
setTimeout(() => {
|
|
72
80
|
setRootViews((prev) => []);
|
|
73
|
-
|
|
81
|
+
debugLog('react-native-auto-positioned-popup RootViewProvider removeRootView setRootViews(prev => []) force=true');
|
|
74
82
|
}, 100);
|
|
75
83
|
return;
|
|
76
84
|
}
|
|
77
85
|
if (rootViews.length > 0 && id) {
|
|
78
86
|
setRootViews((prev) => prev.filter((view) => view.id !== id));
|
|
79
87
|
// else {
|
|
80
|
-
//
|
|
88
|
+
// debugLog('RootViewProvider removeRootView setRootViews(prev => [])')
|
|
81
89
|
// setRootViews(prev => [])
|
|
82
90
|
// }
|
|
83
91
|
} else if (_rootViews && _rootViews.length > 0 && id) {
|
|
@@ -86,10 +94,13 @@ export const RootViewProvider: React.FC<RootViewProviderProps> = ({children}) =>
|
|
|
86
94
|
};
|
|
87
95
|
|
|
88
96
|
const setRootViewNativeStyle = (id: string, style: ViewStyle): void => {
|
|
97
|
+
debugLog('RootViewContext setRootViewNativeStyle called=', {id, style});
|
|
89
98
|
const target = viewRefs.current[id];
|
|
99
|
+
debugLog('RootViewContext setRootViewNativeStyle target exists=', !!target);
|
|
90
100
|
if (target) {
|
|
91
101
|
// @ts-ignore - React Native setNativeProps
|
|
92
102
|
target.setNativeProps({style});
|
|
103
|
+
debugLog('RootViewContext setRootViewNativeStyle applied style=', style);
|
|
93
104
|
}
|
|
94
105
|
};
|
|
95
106
|
|
|
@@ -108,22 +119,33 @@ export const RootViewProvider: React.FC<RootViewProviderProps> = ({children}) =>
|
|
|
108
119
|
|
|
109
120
|
return (
|
|
110
121
|
<RootViewContext.Provider value={contextValue}>
|
|
111
|
-
|
|
122
|
+
{/* CRITICAL FIX: Use View with flex:1 instead of Fragment */}
|
|
123
|
+
{/* Fragment doesn't create actual View, causing absolute positioning to use wrong ancestor */}
|
|
124
|
+
{/* View with flex:1 ensures popup container positions relative to this full-screen View */}
|
|
125
|
+
<View style={rootViewStyles.container}>
|
|
112
126
|
{children}
|
|
113
|
-
{
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
{/* CRITICAL: Full-screen container ensures absolute positioning is relative to screen */}
|
|
128
|
+
{/* Without this, popup position may be offset by parent containers */}
|
|
129
|
+
{rootViews.length > 0 && (
|
|
130
|
+
<View
|
|
131
|
+
pointerEvents="box-none"
|
|
132
|
+
style={rootViewStyles.popupContainer}
|
|
133
|
+
>
|
|
134
|
+
{rootViews.map(
|
|
135
|
+
({id, style, component, useModal, onModalClose, centerDisplay}: DynamicViewBase): React.JSX.Element => {
|
|
136
|
+
// POSITION DEBUG: Log the actual top value being applied (simple format for cleaner logs)
|
|
137
|
+
console.log(`📍 RENDER_STYLE id=${id} top=${style?.top} left=${style?.left} w=${style?.width} h=${style?.height} op=${style?.opacity}`);
|
|
138
|
+
return !useModal ? (
|
|
139
|
+
<View
|
|
140
|
+
key={id}
|
|
141
|
+
ref={(r) => {
|
|
142
|
+
if (r) viewRefs.current[id] = r;
|
|
143
|
+
}}
|
|
144
|
+
style={[style, {position: 'absolute'}]}
|
|
145
|
+
>
|
|
146
|
+
{component}
|
|
147
|
+
</View>
|
|
148
|
+
) : (
|
|
127
149
|
<Pressable
|
|
128
150
|
key={id}
|
|
129
151
|
style={[
|
|
@@ -141,7 +163,7 @@ export const RootViewProvider: React.FC<RootViewProviderProps> = ({children}) =>
|
|
|
141
163
|
centerDisplay && {justifyContent: 'center', alignItems: 'center'},
|
|
142
164
|
]}
|
|
143
165
|
onPress={() => {
|
|
144
|
-
|
|
166
|
+
debugLog('react-native-auto-positioned-popup RootViewProvider Pressable onPress rootViews=', rootViews);
|
|
145
167
|
removeRootView(id, true);
|
|
146
168
|
onModalClose && onModalClose();
|
|
147
169
|
}}
|
|
@@ -169,11 +191,28 @@ export const RootViewProvider: React.FC<RootViewProviderProps> = ({children}) =>
|
|
|
169
191
|
// >
|
|
170
192
|
// </Modal>)
|
|
171
193
|
}
|
|
194
|
+
)}
|
|
195
|
+
</View>
|
|
172
196
|
)}
|
|
173
|
-
|
|
197
|
+
</View>
|
|
174
198
|
</RootViewContext.Provider>
|
|
175
199
|
);
|
|
176
200
|
};
|
|
201
|
+
|
|
202
|
+
// Styles for RootView container - extracted for better performance
|
|
203
|
+
const rootViewStyles = StyleSheet.create({
|
|
204
|
+
container: {
|
|
205
|
+
flex: 1,
|
|
206
|
+
},
|
|
207
|
+
popupContainer: {
|
|
208
|
+
position: 'absolute',
|
|
209
|
+
top: 0,
|
|
210
|
+
left: 0,
|
|
211
|
+
right: 0,
|
|
212
|
+
bottom: 0,
|
|
213
|
+
zIndex: 99999,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
177
216
|
/*
|
|
178
217
|
const { addRootView, updateRootView, removeRootView ,searchQuery } = useRootView();
|
|
179
218
|
*/
|