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.
@@ -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
- export const useKeyboardStatus = () => {
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
- console.log('KeyboardManager: Skip - Keyboard state unchanged (before debounce)', { value, timeSinceLastUpdate });
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
- console.log('KeyboardManager: Skip - Same value already in processing queue', { value });
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
- console.log('KeyboardManager: Skip - Keyboard state unchanged (after debounce)', { value, timeSinceLastUpdate });
66
+ debugLog('KeyboardManager: Skip - Keyboard state unchanged (after debounce)', { value, timeSinceLastUpdate });
51
67
  pendingValueRef.current = null;
52
68
  return;
53
69
  }
54
70
 
55
- console.log('KeyboardManager: Setting keyboard status to', value, {
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
- console.log('KeyboardManager: Skip keyboardDidShow event - Keyboard is already open');
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
- console.log('KeyboardManager: Skip keyboardDidHide event - Keyboard is already closed');
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
- return isKeyboardFullyShown;
129
+ // V19: Return both keyboard visibility status and height
130
+ return { isShown: isKeyboardFullyShown, height: keyboardHeight };
106
131
  };
@@ -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
- console.log('react-native-auto-positioned-popup RootViewProvider rootViews changed:', rootViews);
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
- console.log('react-native-auto-positioned-popup RootViewProvider addRootView rootViews=', rootViews);
44
- console.log('react-native-auto-positioned-popup RootViewProvider addRootView newView=', newView);
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
- console.log('react-native-auto-positioned-popup RootViewProvider removeRootView=', {id, force, rootViews, _rootViews});
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
- console.log('react-native-auto-positioned-popup RootViewProvider removeRootView setRootViews(prev => []) force=true');
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
- // console.log('RootViewProvider removeRootView setRootViews(prev => [])')
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
- {rootViews.map(
114
- ({id, style, component, useModal, onModalClose, centerDisplay}: DynamicViewBase): React.JSX.Element => {
115
- console.log('react-native-auto-positioned-popup RootViewProvider rootViews.map=', {id, style, component, useModal, centerDisplay});
116
- return !useModal ? (
117
- <View
118
- key={id}
119
- ref={(r) => {
120
- if (r) viewRefs.current[id] = r;
121
- }}
122
- style={[style, {position: 'absolute'}]}
123
- >
124
- {component}
125
- </View>
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
- console.log('react-native-auto-positioned-popup RootViewProvider Pressable onPress rootViews=', rootViews);
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
  */