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.
@@ -23,10 +23,10 @@ export interface AutoPositionedPopupProps {
23
23
  tag: string;
24
24
  tagStyle?: ViewStyle;
25
25
  fetchData?: ({
26
- pageIndex,
27
- pageSize,
28
- searchQuery,
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
- children?: React.ReactNode;
49
- selectedItem?: SelectedItem | any;
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;
@@ -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 debouncedSetKeyboardShown = useRef(
17
- debounce((value: boolean) => {
18
- console.log('KeyboardManager: Setting keyboard status to', value);
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
- if (Platform.OS === 'ios') {
30
- keyboardWillShowListener = Keyboard.addListener(
31
- 'keyboardWillShow',
32
- () => {
33
- debouncedSetKeyboardShown(false);
34
- }
35
- );
36
- keyboardDidShowListener = Keyboard.addListener(
37
- 'keyboardDidShow',
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
- keyboardDidHideListener = Keyboard.addListener(
62
- 'keyboardDidHide',
63
- () => {
64
- debouncedSetKeyboardShown(false);
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
  }, []);
@@ -1,5 +1,5 @@
1
- import React, { ReactNode, createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
2
- import { Pressable, View, ViewStyle,Keyboard } from 'react-native';
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> = ({ children }) => {
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 = { ...view };
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 ? { ...view, ...update } : view)));
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 id=', id);
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
- // Small delay to ensure keyboard is dismissed before removing views
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
- }, 50);
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({ style });
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
- ({ id, style, component, useModal, onModalClose, centerDisplay }: DynamicViewBase): React.JSX.Element => {
108
- console.log('RootViewProvider rootViews.map id=', id);
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, { position: 'absolute' }]}
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 && { justifyContent: 'center', alignItems: 'center' },
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={[{ position: 'absolute' }, style]}
153
+ style={[{position: 'absolute'}, style]}
151
154
  >
152
155
  {component}
153
156
  </View>