react-native-auto-positioned-popup 1.0.12 → 1.0.14
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 +356 -153
- package/lib/AutoPositionedPopup.js.map +1 -1
- package/lib/AutoPositionedPopupProps.d.ts +6 -0
- package/lib/AutoPositionedPopupProps.d.ts.map +1 -1
- package/lib/KeyboardManager.d.ts.map +1 -1
- package/lib/KeyboardManager.js +60 -34
- package/lib/KeyboardManager.js.map +1 -1
- package/lib/RootViewContext.d.ts.map +1 -1
- package/lib/RootViewContext.js +21 -16
- package/lib/RootViewContext.js.map +1 -1
- package/package.json +1 -1
- package/src/AutoPositionedPopup.tsx +419 -238
- package/src/AutoPositionedPopupProps.ts +13 -7
- package/src/KeyboardManager.tsx +73 -49
- package/src/RootViewContext.tsx +31 -28
|
@@ -23,10 +23,10 @@ export interface AutoPositionedPopupProps {
|
|
|
23
23
|
tag: string;
|
|
24
24
|
tagStyle?: ViewStyle;
|
|
25
25
|
fetchData?: ({
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
pageIndex,
|
|
27
|
+
pageSize,
|
|
28
|
+
searchQuery,
|
|
29
|
+
}: {
|
|
30
30
|
pageIndex: number;
|
|
31
31
|
pageSize: number;
|
|
32
32
|
searchQuery?: string;
|
|
@@ -45,12 +45,18 @@ export interface AutoPositionedPopupProps {
|
|
|
45
45
|
keyExtractor?: (item: SelectedItem) => string;
|
|
46
46
|
CustomPopView?: () => React.ComponentType<
|
|
47
47
|
ViewStyle & {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
children?: React.ReactNode;
|
|
49
|
+
selectedItem?: SelectedItem | any;
|
|
50
|
+
}
|
|
51
51
|
>;
|
|
52
52
|
CustomPopViewStyle?: ViewStyle;
|
|
53
53
|
forceRemoveAllRootViewOnItemSelected?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* inputStyle={stableTransparentInputStyle}
|
|
56
|
+
* const stableTransparentInputStyle = useMemo(() => {
|
|
57
|
+
* return mode === 'light' ? {backgroundColor: 'transparent'} : false;
|
|
58
|
+
* }, [mode]);
|
|
59
|
+
*/
|
|
54
60
|
inputStyle?: StyleProp<TextStyle>;
|
|
55
61
|
TextInputProps?: TextInputProps;
|
|
56
62
|
popUpViewStyle?: ViewStyle;
|
package/src/KeyboardManager.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useState, useRef } from 'react';
|
|
2
2
|
import { Keyboard, EmitterSubscription, Platform } from 'react-native';
|
|
3
3
|
|
|
4
|
+
// Debounce function
|
|
4
5
|
const debounce = (func: Function, delay: number) => {
|
|
5
6
|
let timer: NodeJS.Timeout;
|
|
6
7
|
return (...args: any[]) => {
|
|
@@ -12,68 +13,91 @@ const debounce = (func: Function, delay: number) => {
|
|
|
12
13
|
export const useKeyboardStatus = () => {
|
|
13
14
|
const [isKeyboardFullyShown, setIsKeyboardFullyShown] = useState(false);
|
|
14
15
|
|
|
15
|
-
//
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
// Add state cache to avoid repeatedly setting the same state
|
|
17
|
+
const currentKeyboardStatusRef = useRef<boolean>(false);
|
|
18
|
+
const lastUpdateTimeRef = useRef<number>(0);
|
|
19
|
+
const pendingValueRef = useRef<boolean | null>(null);
|
|
20
|
+
|
|
21
|
+
// Wrapper function: check state before debounce
|
|
22
|
+
const safeSetKeyboardShown = useRef((value: boolean) => {
|
|
23
|
+
const currentTime = Date.now();
|
|
24
|
+
const timeSinceLastUpdate = currentTime - lastUpdateTimeRef.current;
|
|
25
|
+
|
|
26
|
+
// ✅ FIX: Check state before debounce
|
|
27
|
+
if (currentKeyboardStatusRef.current === value) {
|
|
28
|
+
console.log('KeyboardManager: Skip - Keyboard state unchanged (before debounce)', { value, timeSinceLastUpdate });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ✅ FIX: Skip if the same value is already pending
|
|
33
|
+
if (pendingValueRef.current === value) {
|
|
34
|
+
console.log('KeyboardManager: Skip - Same value already in processing queue', { value });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ✅ FIX: Mark the value being processed
|
|
39
|
+
pendingValueRef.current = value;
|
|
40
|
+
|
|
41
|
+
// Call the actual update function
|
|
42
|
+
debouncedSetKeyboardShownInternal(value, currentTime, timeSinceLastUpdate);
|
|
43
|
+
}).current;
|
|
44
|
+
|
|
45
|
+
// Internal debounce function
|
|
46
|
+
const debouncedSetKeyboardShownInternal = useRef(
|
|
47
|
+
debounce((value: boolean, currentTime: number, timeSinceLastUpdate: number) => {
|
|
48
|
+
// ✅ FIX: Check state again (in case state was updated during debounce)
|
|
49
|
+
if (currentKeyboardStatusRef.current === value) {
|
|
50
|
+
console.log('KeyboardManager: Skip - Keyboard state unchanged (after debounce)', { value, timeSinceLastUpdate });
|
|
51
|
+
pendingValueRef.current = null;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log('KeyboardManager: Setting keyboard status to', value, {
|
|
56
|
+
previousValue: currentKeyboardStatusRef.current,
|
|
57
|
+
timeSinceLastUpdate
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
currentKeyboardStatusRef.current = value;
|
|
61
|
+
lastUpdateTimeRef.current = currentTime;
|
|
62
|
+
pendingValueRef.current = null;
|
|
19
63
|
setIsKeyboardFullyShown(value);
|
|
20
64
|
}, 300)
|
|
21
65
|
).current;
|
|
22
66
|
|
|
67
|
+
// Use the wrapped function
|
|
68
|
+
const debouncedSetKeyboardShown = safeSetKeyboardShown;
|
|
69
|
+
|
|
23
70
|
useEffect(() => {
|
|
24
|
-
let keyboardWillShowListener: EmitterSubscription;
|
|
25
71
|
let keyboardDidShowListener: EmitterSubscription;
|
|
26
|
-
let keyboardWillHideListener: EmitterSubscription;
|
|
27
72
|
let keyboardDidHideListener: EmitterSubscription;
|
|
28
73
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
() => {
|
|
39
|
-
debouncedSetKeyboardShown(true);
|
|
40
|
-
}
|
|
41
|
-
);
|
|
42
|
-
keyboardWillHideListener = Keyboard.addListener(
|
|
43
|
-
'keyboardWillHide',
|
|
44
|
-
() => {
|
|
45
|
-
debouncedSetKeyboardShown(false);
|
|
46
|
-
}
|
|
47
|
-
);
|
|
48
|
-
keyboardDidHideListener = Keyboard.addListener(
|
|
49
|
-
'keyboardDidHide',
|
|
50
|
-
() => {
|
|
51
|
-
debouncedSetKeyboardShown(false);
|
|
52
|
-
}
|
|
53
|
-
);
|
|
54
|
-
} else {
|
|
55
|
-
keyboardDidShowListener = Keyboard.addListener(
|
|
56
|
-
'keyboardDidShow',
|
|
57
|
-
() => {
|
|
58
|
-
debouncedSetKeyboardShown(true);
|
|
74
|
+
// ✅ FIX: Use the same logic for iOS and Android - only listen to Did events
|
|
75
|
+
// Remove Will event listeners to avoid duplicate triggers and state race conditions
|
|
76
|
+
keyboardDidShowListener = Keyboard.addListener(
|
|
77
|
+
'keyboardDidShow',
|
|
78
|
+
() => {
|
|
79
|
+
// ✅ FIX: Add protection at event listener level - skip if keyboard is already open
|
|
80
|
+
if (currentKeyboardStatusRef.current === true) {
|
|
81
|
+
console.log('KeyboardManager: Skip keyboardDidShow event - Keyboard is already open');
|
|
82
|
+
return;
|
|
59
83
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
84
|
+
debouncedSetKeyboardShown(true);
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
keyboardDidHideListener = Keyboard.addListener(
|
|
88
|
+
'keyboardDidHide',
|
|
89
|
+
() => {
|
|
90
|
+
// ✅ FIX: Add protection at event listener level - skip if keyboard is already closed
|
|
91
|
+
if (currentKeyboardStatusRef.current === false) {
|
|
92
|
+
console.log('KeyboardManager: Skip keyboardDidHide event - Keyboard is already closed');
|
|
93
|
+
return;
|
|
65
94
|
}
|
|
66
|
-
|
|
67
|
-
|
|
95
|
+
debouncedSetKeyboardShown(false);
|
|
96
|
+
}
|
|
97
|
+
);
|
|
68
98
|
|
|
69
99
|
return () => {
|
|
70
|
-
if (Platform.OS === 'ios') {
|
|
71
|
-
keyboardWillShowListener?.remove();
|
|
72
|
-
}
|
|
73
100
|
keyboardDidShowListener?.remove();
|
|
74
|
-
if (Platform.OS === 'ios') {
|
|
75
|
-
keyboardWillHideListener?.remove();
|
|
76
|
-
}
|
|
77
101
|
keyboardDidHideListener?.remove();
|
|
78
102
|
};
|
|
79
103
|
}, []);
|
package/src/RootViewContext.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
1
|
+
import React, {ReactNode, createContext, useContext, useEffect, useMemo, useRef, useState} from 'react';
|
|
2
|
+
import {Pressable, View, ViewStyle, Keyboard} from 'react-native';
|
|
3
3
|
|
|
4
4
|
interface DynamicViewBase {
|
|
5
5
|
id: string;
|
|
@@ -30,41 +30,48 @@ const RootViewContext = createContext<RootViewContextType | undefined>(undefined
|
|
|
30
30
|
* @param children
|
|
31
31
|
* @constructor
|
|
32
32
|
*/
|
|
33
|
-
export const RootViewProvider: React.FC<RootViewProviderProps> = ({
|
|
33
|
+
export const RootViewProvider: React.FC<RootViewProviderProps> = ({children}) => {
|
|
34
34
|
const [rootViews, setRootViews] = useState<DynamicViewBase[]>([]);
|
|
35
35
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
|
36
36
|
const viewRefs = useRef<Record<string, View>>({});
|
|
37
37
|
useEffect(() => {
|
|
38
|
-
console.log('RootViewProvider rootViews changed:', rootViews);
|
|
38
|
+
console.log('react-native-auto-positioned-popup RootViewProvider rootViews changed:', rootViews);
|
|
39
39
|
}, [rootViews]);
|
|
40
40
|
const addRootView = (view: DynamicViewBase): void => {
|
|
41
41
|
// const id = `dynamic-view-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
42
|
-
const newView: DynamicViewBase = {
|
|
43
|
-
console.log('RootViewProvider addRootView rootViews=', rootViews);
|
|
44
|
-
console.log('RootViewProvider addRootView newView=', newView);
|
|
42
|
+
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);
|
|
45
45
|
setRootViews((prev) => [...prev, newView]);
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
const updateRootView = (id: string, update: Partial<DynamicViewBase>): void => {
|
|
49
|
-
setRootViews((prev) => prev.map((view) => (view.id === id ? {
|
|
49
|
+
setRootViews((prev) => prev.map((view) => (view.id === id ? {...view, ...update} : view)));
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* onScrollBeginDrag={() => {
|
|
54
|
+
* requestAnimationFrame(() => {
|
|
55
|
+
* removeRootView(undefined, true)
|
|
56
|
+
* })
|
|
57
|
+
* }}
|
|
58
|
+
* @param id
|
|
59
|
+
* @param force
|
|
60
|
+
* @param _rootViews
|
|
61
|
+
*/
|
|
52
62
|
const removeRootView = (id?: string, force?: boolean, _rootViews?: DynamicViewBase[]): void => {
|
|
53
|
-
console.log('RootViewProvider removeRootView
|
|
54
|
-
console.log('RootViewProvider removeRootView force=', force);
|
|
55
|
-
console.log('RootViewProvider removeRootView rootViews=', rootViews);
|
|
56
|
-
console.log('RootViewProvider removeRootView _rootViews=', _rootViews);
|
|
57
|
-
|
|
63
|
+
console.log('react-native-auto-positioned-popup RootViewProvider removeRootView=', {id, force, rootViews, _rootViews});
|
|
58
64
|
// Ensure keyboard is dismissed when force removing all root views
|
|
59
65
|
if (force) {
|
|
60
66
|
// Dismiss keyboard first
|
|
61
67
|
Keyboard.dismiss();
|
|
62
|
-
|
|
63
|
-
//
|
|
68
|
+
// IMPORTANT: Increased delay from 50ms to 100ms to prevent touch event loss
|
|
69
|
+
// Keyboard dismiss animation takes ~250-300ms, but we don't need to wait for it to fully complete
|
|
70
|
+
// 100ms gives touch event system enough time to process pending events before views are removed
|
|
64
71
|
setTimeout(() => {
|
|
65
72
|
setRootViews((prev) => []);
|
|
66
|
-
console.log('RootViewProvider removeRootView setRootViews(prev => []) force=true');
|
|
67
|
-
},
|
|
73
|
+
console.log('react-native-auto-positioned-popup RootViewProvider removeRootView setRootViews(prev => []) force=true');
|
|
74
|
+
}, 100);
|
|
68
75
|
return;
|
|
69
76
|
}
|
|
70
77
|
if (rootViews.length > 0 && id) {
|
|
@@ -82,7 +89,7 @@ export const RootViewProvider: React.FC<RootViewProviderProps> = ({ children })
|
|
|
82
89
|
const target = viewRefs.current[id];
|
|
83
90
|
if (target) {
|
|
84
91
|
// @ts-ignore - React Native setNativeProps
|
|
85
|
-
target.setNativeProps({
|
|
92
|
+
target.setNativeProps({style});
|
|
86
93
|
}
|
|
87
94
|
};
|
|
88
95
|
|
|
@@ -104,19 +111,15 @@ export const RootViewProvider: React.FC<RootViewProviderProps> = ({ children })
|
|
|
104
111
|
<>
|
|
105
112
|
{children}
|
|
106
113
|
{rootViews.map(
|
|
107
|
-
({
|
|
108
|
-
console.log('RootViewProvider rootViews.map
|
|
109
|
-
console.log('RootViewProvider rootViews.map style=', style);
|
|
110
|
-
console.log('RootViewProvider rootViews.map component=', component);
|
|
111
|
-
console.log('RootViewProvider rootViews.map useModal=', useModal);
|
|
112
|
-
console.log('RootViewProvider rootViews.map centerDisplay=', centerDisplay);
|
|
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});
|
|
113
116
|
return !useModal ? (
|
|
114
117
|
<View
|
|
115
118
|
key={id}
|
|
116
119
|
ref={(r) => {
|
|
117
120
|
if (r) viewRefs.current[id] = r;
|
|
118
121
|
}}
|
|
119
|
-
style={[style, {
|
|
122
|
+
style={[style, {position: 'absolute'}]}
|
|
120
123
|
>
|
|
121
124
|
{component}
|
|
122
125
|
</View>
|
|
@@ -135,10 +138,10 @@ export const RootViewProvider: React.FC<RootViewProviderProps> = ({ children })
|
|
|
135
138
|
zIndex: 99999999999,
|
|
136
139
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
137
140
|
},
|
|
138
|
-
centerDisplay && {
|
|
141
|
+
centerDisplay && {justifyContent: 'center', alignItems: 'center'},
|
|
139
142
|
]}
|
|
140
143
|
onPress={() => {
|
|
141
|
-
console.log('RootViewProvider Pressable onPress rootViews=', rootViews);
|
|
144
|
+
console.log('react-native-auto-positioned-popup RootViewProvider Pressable onPress rootViews=', rootViews);
|
|
142
145
|
removeRootView(id, true);
|
|
143
146
|
onModalClose && onModalClose();
|
|
144
147
|
}}
|
|
@@ -147,7 +150,7 @@ export const RootViewProvider: React.FC<RootViewProviderProps> = ({ children })
|
|
|
147
150
|
ref={(r) => {
|
|
148
151
|
if (r) viewRefs.current[id] = r;
|
|
149
152
|
}}
|
|
150
|
-
style={[{
|
|
153
|
+
style={[{position: 'absolute'}, style]}
|
|
151
154
|
>
|
|
152
155
|
{component}
|
|
153
156
|
</View>
|