react-native-auto-positioned-popup 1.2.19 → 1.2.21
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 +129 -12
- package/lib/AutoPositionedPopup.js.map +1 -1
- package/lib/AutoPositionedPopup.style.d.ts +10 -1
- package/lib/AutoPositionedPopup.style.d.ts.map +1 -1
- package/lib/AutoPositionedPopup.style.js +11 -2
- package/lib/AutoPositionedPopup.style.js.map +1 -1
- package/lib/AutoPositionedPopupProps.d.ts +1 -0
- package/lib/AutoPositionedPopupProps.d.ts.map +1 -1
- package/lib/KeyboardManager.js +1 -2
- package/lib/RootViewContext.js +2 -3
- package/package.json +1 -1
- package/src/AutoPositionedPopup.style.ts +12 -3
- package/src/AutoPositionedPopup.tsx +253 -88
- package/src/AutoPositionedPopupProps.ts +16 -14
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Wait 1 second for KeyboardAwareScrollView to stabilize, then use measureInWindow to get trigger's FINAL position
|
|
4
4
|
// NOTE: Parent component (KeyboardAwareScrollView) is responsible for scrolling trigger into view
|
|
5
5
|
// DEBUG FLAG: Set to false to disable all console logs for better performance
|
|
6
|
-
const POPUP_DEBUG =
|
|
6
|
+
const POPUP_DEBUG = global.$fake; // DISABLED: Too many logs cause app freeze
|
|
7
7
|
const POPUP_POSITION_DEBUG = true; // Only log positioning calculations
|
|
8
8
|
const debugLog = (...args: any[]) => {
|
|
9
9
|
if (POPUP_DEBUG) {
|
|
@@ -45,13 +45,13 @@ import {
|
|
|
45
45
|
UIManager,
|
|
46
46
|
View,
|
|
47
47
|
} from 'react-native';
|
|
48
|
-
import {AdvancedFlatList, ListData, FetchDataParams} from 'react-native-advanced-flatlist';
|
|
49
|
-
import {TextInputSubmitEditingEventData} from 'react-native/Libraries/Components/TextInput/TextInput';
|
|
50
|
-
import {LayoutRectangle, NativeSyntheticEvent} from 'react-native/Libraries/Types/CoreEventTypes';
|
|
51
|
-
import {AutoPositionedPopupProps, Data, SelectedItem} from './AutoPositionedPopupProps';
|
|
48
|
+
import { AdvancedFlatList, ListData, FetchDataParams } from 'react-native-advanced-flatlist';
|
|
49
|
+
import { TextInputSubmitEditingEventData } from 'react-native/Libraries/Components/TextInput/TextInput';
|
|
50
|
+
import { LayoutRectangle, NativeSyntheticEvent } from 'react-native/Libraries/Types/CoreEventTypes';
|
|
51
|
+
import { AutoPositionedPopupProps, Data, SelectedItem } from './AutoPositionedPopupProps';
|
|
52
52
|
import styles from './AutoPositionedPopup.style';
|
|
53
|
-
import {useRootView} from './RootViewContext';
|
|
54
|
-
import {useKeyboardStatus} from './KeyboardManager';
|
|
53
|
+
import { useRootView } from './RootViewContext';
|
|
54
|
+
import { useKeyboardStatus } from './KeyboardManager';
|
|
55
55
|
|
|
56
56
|
// Lightweight emitter to decouple TextInput and list without re-rendering context
|
|
57
57
|
type QueryListener = (query: string) => void;
|
|
@@ -97,18 +97,18 @@ const ListItem: React.FC<{
|
|
|
97
97
|
themeMode?: string | null | undefined;
|
|
98
98
|
}> = memo(
|
|
99
99
|
({
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
updateState,
|
|
101
|
+
item,
|
|
102
|
+
index,
|
|
103
|
+
selectedItem, themeMode
|
|
104
|
+
}: {
|
|
105
105
|
updateState: (key: string, value: SelectedItem) => void;
|
|
106
106
|
item: SelectedItem;
|
|
107
107
|
index: number;
|
|
108
108
|
selectedItem?: SelectedItem;
|
|
109
109
|
themeMode?: string | null | undefined;
|
|
110
110
|
}): React.JSX.Element => {
|
|
111
|
-
const {addRootView, setRootViewNativeStyle, removeRootView, rootViews} = useRootView();
|
|
111
|
+
const { addRootView, setRootViewNativeStyle, removeRootView, rootViews } = useRootView();
|
|
112
112
|
const rootViewsRef = useRef(rootViews);
|
|
113
113
|
useEffect(() => {
|
|
114
114
|
rootViewsRef.current = rootViews;
|
|
@@ -121,7 +121,7 @@ const ListItem: React.FC<{
|
|
|
121
121
|
key={item.id}
|
|
122
122
|
style={[
|
|
123
123
|
styles.commonModalRow,
|
|
124
|
-
{backgroundColor: isSelected ? (themeMode === 'light' ? 'rgba(116, 116, 128, 0.08)' : 'rgba(120, 120, 128, 0.36)') : 'transparent'},
|
|
124
|
+
{ backgroundColor: isSelected ? (themeMode === 'light' ? 'rgba(116, 116, 128, 0.08)' : 'rgba(120, 120, 128, 0.36)') : 'transparent' },
|
|
125
125
|
]}
|
|
126
126
|
onPress={() => {
|
|
127
127
|
// debugLog('AutoPositionedPopup.tsx ListItem onPress item=', item); // Commented to prevent spam
|
|
@@ -129,9 +129,11 @@ const ListItem: React.FC<{
|
|
|
129
129
|
updateState('selectedItem', item);
|
|
130
130
|
}}
|
|
131
131
|
>
|
|
132
|
-
<Text style={(themeMode === 'light' ? styles.ListItemCode : {...styles.ListItemCode, color: '#fff'})} numberOfLines={1} ellipsizeMode="tail">
|
|
132
|
+
<Text style={(themeMode === 'light' ? styles.ListItemCode : { ...styles.ListItemCode, color: '#fff' })} numberOfLines={1} ellipsizeMode="tail">
|
|
133
133
|
{item.title}
|
|
134
134
|
</Text>
|
|
135
|
+
{/* 底部分割线 */}
|
|
136
|
+
{/* <View style={styles.bottomDivider} /> */}
|
|
135
137
|
</TouchableOpacity>
|
|
136
138
|
);
|
|
137
139
|
}, [updateState, item, index, selectedItem, rootViewsRef, themeMode]);
|
|
@@ -143,35 +145,36 @@ interface AutoPositionedPopupListProps {
|
|
|
143
145
|
tag: string;
|
|
144
146
|
updateState: (key: string, value: any) => void;
|
|
145
147
|
fetchData: ({
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
pageIndex,
|
|
149
|
+
pageSize,
|
|
150
|
+
searchQuery,
|
|
151
|
+
}: {
|
|
150
152
|
pageIndex: number;
|
|
151
153
|
pageSize: number;
|
|
152
154
|
searchQuery?: string;
|
|
153
155
|
}) => Promise<Data | null>;
|
|
154
156
|
keyExtractor?: (item: SelectedItem) => string; //keyExtractor={item => item?.id}
|
|
155
|
-
renderItem?: ({item, index}: { item: SelectedItem; index: number }) => React.ReactElement;
|
|
157
|
+
renderItem?: ({ item, index }: { item: SelectedItem; index: number }) => React.ReactElement;
|
|
156
158
|
selectedItem?: SelectedItem;
|
|
157
159
|
localSearch?: boolean;
|
|
158
160
|
pageSize?: number;
|
|
159
161
|
showListEmptyComponent?: boolean;
|
|
160
162
|
emptyText?: string;
|
|
161
163
|
themeMode?: string | null | undefined;
|
|
164
|
+
internalSearchTextInput?: React.ReactNode;
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
165
168
|
({
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
tag,
|
|
170
|
+
updateState,
|
|
171
|
+
fetchData,
|
|
172
|
+
keyExtractor = (item) => String(item.id),
|
|
173
|
+
renderItem,
|
|
174
|
+
selectedItem,
|
|
175
|
+
localSearch,
|
|
176
|
+
pageSize, showListEmptyComponent, emptyText, themeMode, internalSearchTextInput
|
|
177
|
+
}: AutoPositionedPopupListProps): React.JSX.Element => {
|
|
175
178
|
const [state, setState] = useState<{
|
|
176
179
|
selectedItem?: SelectedItem;
|
|
177
180
|
localData: SelectedItem[];
|
|
@@ -182,7 +185,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
182
185
|
// Define an interface that matches the methods we need from CsxFlatList
|
|
183
186
|
const ref_list = useRef<{ scrollToTop: () => void; refresh: () => void } | null>(null);
|
|
184
187
|
const ref_searchQuery = useRef<string>('');
|
|
185
|
-
const {searchQuery, setSearchQuery, rootViews} = useRootView();
|
|
188
|
+
const { searchQuery, setSearchQuery, rootViews } = useRootView();
|
|
186
189
|
const rootViewsRef = useRef(rootViews);
|
|
187
190
|
useEffect(() => {
|
|
188
191
|
rootViewsRef.current = rootViews;
|
|
@@ -221,10 +224,10 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
221
224
|
updateState(key, value);
|
|
222
225
|
};
|
|
223
226
|
const _fetchData = async ({
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
debugLog('AutoPositionedPopupList _fetchData=', {pageIndex, pageSize: currentPageSize, 'state.localData': state.localData, 'ref_searchQuery.current': ref_searchQuery.current, localSearch});
|
|
227
|
+
pageIndex,
|
|
228
|
+
pageSize: currentPageSize,
|
|
229
|
+
}: FetchDataParams): Promise<ListData | null> => {
|
|
230
|
+
debugLog('AutoPositionedPopupList _fetchData=', { pageIndex, pageSize: currentPageSize, 'state.localData': state.localData, 'ref_searchQuery.current': ref_searchQuery.current, localSearch });
|
|
228
231
|
if (localSearch && state.localData.length > 0) {
|
|
229
232
|
const result: SelectedItem[] = state.localData.filter((item: SelectedItem) => {
|
|
230
233
|
return `${item.title}`?.toLowerCase().includes(ref_searchQuery.current.toLowerCase());
|
|
@@ -267,7 +270,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
267
270
|
return null;
|
|
268
271
|
};
|
|
269
272
|
const _renderItem = useCallback(
|
|
270
|
-
({item, index}: { item: SelectedItem; index: number }) => {
|
|
273
|
+
({ item, index }: { item: SelectedItem; index: number }) => {
|
|
271
274
|
return <ListItem item={item} index={index} updateState={_updateState} selectedItem={state.selectedItem} themeMode={themeMode} />;
|
|
272
275
|
},
|
|
273
276
|
[state.selectedItem, themeMode]
|
|
@@ -277,14 +280,15 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
277
280
|
// Babel configuration handles the path redirection based on global.$fake
|
|
278
281
|
// No need for conditional import here
|
|
279
282
|
return (
|
|
280
|
-
<View style={[styles.baseModalView, styles.autoPositionedPopupList, {backgroundColor: themeMode === 'light' ? '#fff' : 'rgba(44, 44, 46, 1)',}]}>
|
|
283
|
+
<View style={[styles.baseModalView, styles.autoPositionedPopupList, { backgroundColor: themeMode === 'light' ? '#fff' : 'rgba(44, 44, 46, 1)', }]}>
|
|
284
|
+
{internalSearchTextInput}
|
|
281
285
|
<AdvancedFlatList
|
|
282
|
-
style={[{borderRadius: 0}]}
|
|
283
|
-
{...(ref_list && {ref: ref_list})}
|
|
286
|
+
style={[{ borderRadius: 0 }, internalSearchTextInput ? { flex: 1 } : undefined]}
|
|
287
|
+
{...(ref_list && { ref: ref_list })}
|
|
284
288
|
keyExtractor={(item, index) => keyExtractor ? keyExtractor(item as SelectedItem) : (item as SelectedItem).id}
|
|
285
289
|
keyboardShouldPersistTaps={'always'}
|
|
286
290
|
fetchData={_fetchData}
|
|
287
|
-
renderItem={renderItem ? ({item, index}) => renderItem({item: item as SelectedItem, index}) : ({item, index}) => _renderItem({item: item as SelectedItem, index})}
|
|
291
|
+
renderItem={renderItem ? ({ item, index }) => renderItem({ item: item as SelectedItem, index }) : ({ item, index }) => _renderItem({ item: item as SelectedItem, index })}
|
|
288
292
|
showListEmptyComponent={showListEmptyComponent}
|
|
289
293
|
emptyText={emptyText}
|
|
290
294
|
/>
|
|
@@ -300,7 +304,7 @@ const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
|
300
304
|
searchQuery,
|
|
301
305
|
localSearch,
|
|
302
306
|
pageSize,
|
|
303
|
-
rootViewsRef, showListEmptyComponent, emptyText, themeMode
|
|
307
|
+
rootViewsRef, showListEmptyComponent, emptyText, themeMode, internalSearchTextInput
|
|
304
308
|
]);
|
|
305
309
|
}
|
|
306
310
|
);
|
|
@@ -315,6 +319,12 @@ interface StateProps {
|
|
|
315
319
|
const listLayout = {
|
|
316
320
|
height: 200,
|
|
317
321
|
};
|
|
322
|
+
const internalSearchListLayout = {
|
|
323
|
+
height: 300,
|
|
324
|
+
};
|
|
325
|
+
const internalSearchInputLayout = {
|
|
326
|
+
height: 50,
|
|
327
|
+
};
|
|
318
328
|
|
|
319
329
|
// Main AutoPositionedPopup component
|
|
320
330
|
const AutoPositionedPopup = memo(
|
|
@@ -330,12 +340,12 @@ const AutoPositionedPopup = memo(
|
|
|
330
340
|
TextInputProps,//= {autoFocus: true},
|
|
331
341
|
inputStyle,
|
|
332
342
|
labelStyle,
|
|
333
|
-
popUpViewStyle = {left: '5%', width: '90%'},
|
|
343
|
+
popUpViewStyle = { left: '5%', width: '90%' },
|
|
334
344
|
fetchData = async ({
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
345
|
+
pageIndex,
|
|
346
|
+
pageSize,
|
|
347
|
+
searchQuery,
|
|
348
|
+
}: {
|
|
339
349
|
pageIndex: number;
|
|
340
350
|
pageSize: number;
|
|
341
351
|
searchQuery?: string;
|
|
@@ -362,7 +372,7 @@ const AutoPositionedPopup = memo(
|
|
|
362
372
|
selectedItem,
|
|
363
373
|
useTextInput = false,
|
|
364
374
|
btwChildren,
|
|
365
|
-
CustomRow = ({children}) => <View>{children}</View>,
|
|
375
|
+
CustomRow = ({ children }) => <View>{children}</View>,
|
|
366
376
|
keyExtractor = (item: any) => String(item?.id || ''),
|
|
367
377
|
AutoPositionedPopupBtnDisabled = false,
|
|
368
378
|
forceRemoveAllRootViewOnItemSelected = false,
|
|
@@ -370,7 +380,7 @@ const AutoPositionedPopup = memo(
|
|
|
370
380
|
selectedItemBackgroundColor = 'rgba(116, 116, 128, 0.08)',
|
|
371
381
|
// textAlign = 'right',
|
|
372
382
|
CustomPopView = undefined, CustomPopViewStyle, showListEmptyComponent = true, emptyText = '', onChangeText, themeMode = 'light',
|
|
373
|
-
parentScrollViewRef, scrollExtraHeight = 100,
|
|
383
|
+
parentScrollViewRef, scrollExtraHeight = 100, internalSearch = false,
|
|
374
384
|
} = props;
|
|
375
385
|
// State management similar to project implementation
|
|
376
386
|
const [state, setState] = useState<StateProps>({
|
|
@@ -378,7 +388,7 @@ const AutoPositionedPopup = memo(
|
|
|
378
388
|
selectedItem: selectedItem,
|
|
379
389
|
});
|
|
380
390
|
// Use RootView context
|
|
381
|
-
const {addRootView, setRootViewNativeStyle, updateRootView, removeRootView, rootViews, setSearchQuery} = useRootView();
|
|
391
|
+
const { addRootView, setRootViewNativeStyle, updateRootView, removeRootView, rootViews, setSearchQuery } = useRootView();
|
|
382
392
|
const rootViewsRef = useRef(rootViews);
|
|
383
393
|
// Track TextInput focus and RootView states like project implementation
|
|
384
394
|
const hasTriggeredFocus = useRef(false);
|
|
@@ -391,7 +401,7 @@ const AutoPositionedPopup = memo(
|
|
|
391
401
|
const refAutoPositionedPopup = useRef<View>(null);
|
|
392
402
|
const ref_searchQuery = useRef<string>('');
|
|
393
403
|
// Store trigger button position when clicked (before it's replaced by TextInput)
|
|
394
|
-
const triggerPositionRef = useRef<{x: number; y: number; width: number; height: number} | null>(null);
|
|
404
|
+
const triggerPositionRef = useRef<{ x: number; y: number; width: number; height: number } | null>(null);
|
|
395
405
|
// V19: Track keyboard height for accurate popup positioning
|
|
396
406
|
const keyboardHeightRef = useRef<number>(0);
|
|
397
407
|
// Add ref to track previous keyboard state to avoid false triggers during parent component re-renders
|
|
@@ -417,7 +427,7 @@ const AutoPositionedPopup = memo(
|
|
|
417
427
|
top: number;
|
|
418
428
|
left: number;
|
|
419
429
|
width: number;
|
|
420
|
-
}>({top: 0, left: 0, width: 0});
|
|
430
|
+
}>({ top: 0, left: 0, width: 0 });
|
|
421
431
|
// Refs for performance optimization
|
|
422
432
|
const containerRef = useRef<View>(null);
|
|
423
433
|
const textInputRef = useRef<RNTextInput>(null);
|
|
@@ -580,7 +590,7 @@ const AutoPositionedPopup = memo(
|
|
|
580
590
|
useEffect(() => {
|
|
581
591
|
(async () => {
|
|
582
592
|
})();
|
|
583
|
-
debugLog(`AutoPositionedPopup componentDidMount=`, {tag, CustomPopView});
|
|
593
|
+
debugLog(`AutoPositionedPopup componentDidMount=`, { tag, CustomPopView });
|
|
584
594
|
//componentWillUnmount
|
|
585
595
|
return () => {
|
|
586
596
|
debugLog(`AutoPositionedPopup componentWillUnmount tag=`, tag);
|
|
@@ -596,7 +606,7 @@ const AutoPositionedPopup = memo(
|
|
|
596
606
|
};
|
|
597
607
|
}, []);
|
|
598
608
|
useEffect(() => {
|
|
599
|
-
debugLog('AutoPositionedPopup rootViews=', {tag, rootViews});
|
|
609
|
+
debugLog('AutoPositionedPopup rootViews=', { tag, rootViews });
|
|
600
610
|
rootViewsRef.current = rootViews;
|
|
601
611
|
if (rootViews.length === 0) {
|
|
602
612
|
hasAddedRootView.current = false;
|
|
@@ -613,7 +623,7 @@ const AutoPositionedPopup = memo(
|
|
|
613
623
|
}
|
|
614
624
|
}, [rootViews]);
|
|
615
625
|
useEffect(() => {
|
|
616
|
-
debugLog('AutoPositionedPopup useEffect [selectedItem, state.selectedItem, tag]=', {tag, selectedItem, 'state.selectedItem': state.selectedItem});
|
|
626
|
+
debugLog('AutoPositionedPopup useEffect [selectedItem, state.selectedItem, tag]=', { tag, selectedItem, 'state.selectedItem': state.selectedItem });
|
|
617
627
|
debugLog('AutoPositionedPopup useEffect state.selectedItem=', state.selectedItem);
|
|
618
628
|
if (state.selectedItem?.id !== selectedItem?.id || state.selectedItem?.title != selectedItem?.title) {
|
|
619
629
|
debugLog('AutoPositionedPopup useEffect selectedItem!=state.selectedItem');
|
|
@@ -682,8 +692,8 @@ const AutoPositionedPopup = memo(
|
|
|
682
692
|
return StatusBar.currentHeight || 24; // Fallback to 24 if undefined
|
|
683
693
|
} else {
|
|
684
694
|
// iOS: Calculate from screen vs window height difference
|
|
685
|
-
const {height: screenHeightFull} = Dimensions.get('screen');
|
|
686
|
-
const {height: windowHeight} = Dimensions.get('window');
|
|
695
|
+
const { height: screenHeightFull } = Dimensions.get('screen');
|
|
696
|
+
const { height: windowHeight } = Dimensions.get('window');
|
|
687
697
|
return screenHeightFull - windowHeight; // Safe area top (status bar)
|
|
688
698
|
}
|
|
689
699
|
};
|
|
@@ -743,10 +753,10 @@ const AutoPositionedPopup = memo(
|
|
|
743
753
|
// Measure BOTH refs for comparison
|
|
744
754
|
if (textInputRef.current && refAutoPositionedPopup.current) {
|
|
745
755
|
textInputRef.current.measureInWindow((tx: number | undefined, ty: number | undefined, tw: number | undefined, th: number | undefined) => {
|
|
746
|
-
debugLog('AutoPositionedPopup DEBUG: textInputRef position=', {x: tx, y: ty, width: tw, height: th});
|
|
756
|
+
debugLog('AutoPositionedPopup DEBUG: textInputRef position=', { x: tx, y: ty, width: tw, height: th });
|
|
747
757
|
});
|
|
748
758
|
refAutoPositionedPopup.current.measureInWindow((rx: number | undefined, ry: number | undefined, rw: number | undefined, rh: number | undefined) => {
|
|
749
|
-
debugLog('AutoPositionedPopup DEBUG: refAutoPositionedPopup position=', {x: rx, y: ry, width: rw, height: rh});
|
|
759
|
+
debugLog('AutoPositionedPopup DEBUG: refAutoPositionedPopup position=', { x: rx, y: ry, width: rw, height: rh });
|
|
750
760
|
});
|
|
751
761
|
}
|
|
752
762
|
|
|
@@ -761,7 +771,7 @@ const AutoPositionedPopup = memo(
|
|
|
761
771
|
const screenHeightFallback = Dimensions.get('window').height;
|
|
762
772
|
const screenWidthFallback = Dimensions.get('window').width;
|
|
763
773
|
const fallbackY = (screenHeightFallback - listLayout.height) / 2;
|
|
764
|
-
ref_listPos.current = {x: screenWidthFallback * 0.05, y: fallbackY, width: screenWidthFallback * 0.9};
|
|
774
|
+
ref_listPos.current = { x: screenWidthFallback * 0.05, y: fallbackY, width: screenWidthFallback * 0.9 };
|
|
765
775
|
updateRootView(tag, {
|
|
766
776
|
style: {
|
|
767
777
|
top: ref_listPos.current?.y,
|
|
@@ -837,7 +847,7 @@ const AutoPositionedPopup = memo(
|
|
|
837
847
|
positionDebugLog(`V19f_RESULT: position=${position} popupY=${popupY} popupBottom=${popupBottom}`);
|
|
838
848
|
positionDebugLog(`V19f_GAP: trigger_top=${triggerTop} - popup_bottom=${popupBottom} = gap=${gapPixels}px`);
|
|
839
849
|
|
|
840
|
-
ref_listPos.current = {x, y: popupY, width};
|
|
850
|
+
ref_listPos.current = { x, y: popupY, width };
|
|
841
851
|
updateRootView(tag, {
|
|
842
852
|
style: { top: popupY, left: popUpViewStyle?.left, width: popUpViewStyle?.width, height: listLayout.height, opacity: 1 }
|
|
843
853
|
});
|
|
@@ -849,7 +859,7 @@ const AutoPositionedPopup = memo(
|
|
|
849
859
|
// Only execute close logic when keyboard state actually changes from true to false
|
|
850
860
|
debugLog(
|
|
851
861
|
'AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView (keyboard state changed)=',
|
|
852
|
-
{tag, forceRemoveAllRootViewOnItemSelected, keyboardStateChanged}
|
|
862
|
+
{ tag, forceRemoveAllRootViewOnItemSelected, keyboardStateChanged }
|
|
853
863
|
);
|
|
854
864
|
removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
|
|
855
865
|
setState((prevState) => {
|
|
@@ -867,16 +877,71 @@ const AutoPositionedPopup = memo(
|
|
|
867
877
|
// User request: "只要传入的 useTextInput 是 false, 弹框都显示在屏幕中间"
|
|
868
878
|
// This avoids all complex positioning calculations that kept failing
|
|
869
879
|
if (state.isFocus) {
|
|
880
|
+
if (internalSearch && isKeyboardFullyShown && hasAddedRootView.current) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
870
883
|
if (isKeyboardFullyShown) {
|
|
871
884
|
Keyboard.dismiss();
|
|
872
885
|
return;
|
|
873
886
|
}
|
|
874
887
|
|
|
888
|
+
if (internalSearch) {
|
|
889
|
+
if (hasAddedRootView.current) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
hasAddedRootView.current = true;
|
|
893
|
+
hasShownRootView.current = true;
|
|
894
|
+
const internalSearchPopupTop = (Platform.OS === 'android' ? StatusBar.currentHeight || 0 : 0) + 52;
|
|
895
|
+
const internalSearchPopupHeight = internalSearchListLayout.height + internalSearchInputLayout.height;
|
|
896
|
+
debugLog('AutoPositionedPopup internalSearch useTextInput=false, showing popup below top navigation', {
|
|
897
|
+
tag,
|
|
898
|
+
internalSearchPopupTop,
|
|
899
|
+
internalSearchPopupHeight,
|
|
900
|
+
});
|
|
901
|
+
addRootView({
|
|
902
|
+
id: tag,
|
|
903
|
+
style: {
|
|
904
|
+
top: internalSearchPopupTop,
|
|
905
|
+
left: popUpViewStyle?.left,
|
|
906
|
+
width: popUpViewStyle?.width,
|
|
907
|
+
height: internalSearchPopupHeight,
|
|
908
|
+
opacity: 1,
|
|
909
|
+
},
|
|
910
|
+
component: (
|
|
911
|
+
<AutoPositionedPopupList
|
|
912
|
+
tag={tag}
|
|
913
|
+
updateState={updateState}
|
|
914
|
+
fetchData={fetchData}
|
|
915
|
+
pageSize={pageSize}
|
|
916
|
+
renderItem={renderItem}
|
|
917
|
+
selectedItem={state.selectedItem}
|
|
918
|
+
localSearch={localSearch}
|
|
919
|
+
showListEmptyComponent={showListEmptyComponent}
|
|
920
|
+
emptyText={emptyText}
|
|
921
|
+
themeMode={themeMode}
|
|
922
|
+
internalSearchTextInput={internalSearchMemoizedTextInput}
|
|
923
|
+
/>
|
|
924
|
+
),
|
|
925
|
+
useModal: true,
|
|
926
|
+
onModalClose: () => {
|
|
927
|
+
debugLog('AutoPositionedPopup internalSearch onModalClose tag=', tag);
|
|
928
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
929
|
+
hasAddedRootView.current = false;
|
|
930
|
+
hasShownRootView.current = false;
|
|
931
|
+
hasTriggeredFocus.current = false;
|
|
932
|
+
ref_isFocus.current = false;
|
|
933
|
+
setState((prevState) => ({ ...prevState }));
|
|
934
|
+
setSearchQuery('');
|
|
935
|
+
},
|
|
936
|
+
});
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
875
940
|
debugLog('🟢🟢🟢 POPUP_V17 useTextInput=false, showing popup in CENTER of screen');
|
|
876
941
|
|
|
877
|
-
const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
|
|
878
|
-
|
|
879
|
-
|
|
942
|
+
// const actualPopupHeight = CustomPopView && CustomPopViewStyle && typeof CustomPopViewStyle.height === 'number'
|
|
943
|
+
// ? CustomPopViewStyle.height
|
|
944
|
+
// : listLayout.height;
|
|
880
945
|
|
|
881
946
|
if (CustomPopView && CustomPopViewStyle) {
|
|
882
947
|
const PopViewComponent = CustomPopView();
|
|
@@ -920,22 +985,22 @@ const AutoPositionedPopup = memo(
|
|
|
920
985
|
}
|
|
921
986
|
}
|
|
922
987
|
}, [isKeyboardFullyShown,
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
988
|
+
state.isFocus,
|
|
989
|
+
useTextInput,
|
|
990
|
+
CustomPopView,
|
|
991
|
+
CustomPopViewStyle,
|
|
992
|
+
forceRemoveAllRootViewOnItemSelected,
|
|
993
|
+
tag, TextInputProps,
|
|
994
|
+
state.selectedItem, showListEmptyComponent, themeMode
|
|
995
|
+
]);
|
|
931
996
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
997
|
+
// V18: All positioning logic is now in the useEffect above
|
|
998
|
+
// V18 FIX (2025-01-04): Wait 1000ms after keyboard appears before measuring position
|
|
999
|
+
// This ensures trigger position is stable after KeyboardAwareScrollView scrolls
|
|
1000
|
+
// Formula: top = componentY - popupHeight (popup bottom touches trigger top exactly)
|
|
936
1001
|
|
|
937
|
-
|
|
938
|
-
|
|
1002
|
+
// Imperative handle for parent component access
|
|
1003
|
+
useImperativeHandle(
|
|
939
1004
|
parentRef,
|
|
940
1005
|
() => ({
|
|
941
1006
|
clearSelectedItem: () => {
|
|
@@ -954,14 +1019,14 @@ const AutoPositionedPopup = memo(
|
|
|
954
1019
|
removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
|
|
955
1020
|
setSearchQuery('');
|
|
956
1021
|
if (textInputRef.current) {
|
|
957
|
-
textInputRef.current.setNativeProps({text: ''});
|
|
1022
|
+
textInputRef.current.setNativeProps({ text: '' });
|
|
958
1023
|
}
|
|
959
1024
|
},
|
|
960
1025
|
}),
|
|
961
1026
|
[]
|
|
962
1027
|
);
|
|
963
1028
|
const updateState = (key: string, value: SelectedItem) => {
|
|
964
|
-
debugLog('AutoPositionedPopup updateState=', {key, value});
|
|
1029
|
+
debugLog('AutoPositionedPopup updateState=', { key, value });
|
|
965
1030
|
setState((prevState) => ({
|
|
966
1031
|
...prevState,
|
|
967
1032
|
[key]: value,
|
|
@@ -1003,7 +1068,7 @@ const AutoPositionedPopup = memo(
|
|
|
1003
1068
|
// Only update when deep comparison detects real changes to avoid TextInput recreation due to reference changes during parent component redraws
|
|
1004
1069
|
const stableInputStyle = useMemo(() => {
|
|
1005
1070
|
if (!shallowEqual(stableInputStyleRef.current, inputStyle)) {
|
|
1006
|
-
debugLog(`AutoPositionedPopup stableInputStyle: `, {tag, inputStyle, themeMode});
|
|
1071
|
+
debugLog(`AutoPositionedPopup stableInputStyle: `, { tag, inputStyle, themeMode });
|
|
1007
1072
|
stableInputStyleRef.current = inputStyle;
|
|
1008
1073
|
}
|
|
1009
1074
|
return stableInputStyleRef.current;
|
|
@@ -1014,7 +1079,7 @@ const AutoPositionedPopup = memo(
|
|
|
1014
1079
|
debugLog(`AutoPositionedPopup TextInputProps deep change detected, updating stable reference - tag: ${tag}`);
|
|
1015
1080
|
stableTextInputPropsRef.current = TextInputProps;
|
|
1016
1081
|
}
|
|
1017
|
-
debugLog('AutoPositionedPopup stableTextInputProps=', {tag, TextInputProps, 'stableTextInputPropsRef.current': stableTextInputPropsRef.current})
|
|
1082
|
+
debugLog('AutoPositionedPopup stableTextInputProps=', { tag, TextInputProps, 'stableTextInputPropsRef.current': stableTextInputPropsRef.current })
|
|
1018
1083
|
return stableTextInputPropsRef.current;
|
|
1019
1084
|
}, [TextInputProps, tag]);
|
|
1020
1085
|
|
|
@@ -1106,7 +1171,7 @@ const AutoPositionedPopup = memo(
|
|
|
1106
1171
|
removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
|
|
1107
1172
|
setSearchQuery('');
|
|
1108
1173
|
if (textInputRef.current) {
|
|
1109
|
-
textInputRef.current.setNativeProps({text: ''});
|
|
1174
|
+
textInputRef.current.setNativeProps({ text: '' });
|
|
1110
1175
|
ref_searchQuery.current = '';
|
|
1111
1176
|
// Remove textInputRef.current.blur() - avoid forcing blur causing keyboard to close
|
|
1112
1177
|
}
|
|
@@ -1116,7 +1181,7 @@ const AutoPositionedPopup = memo(
|
|
|
1116
1181
|
// Wrap TextInput independently in useMemo to recreate only when key props change
|
|
1117
1182
|
// This avoids repeated ref callback triggers due to other props changes during parent component redraws
|
|
1118
1183
|
const memoizedTextInput = useMemo(() => {
|
|
1119
|
-
debugLog('AutoPositionedPopup memoizedTextInput=', {tag, useTextInput, 'state.isFocus': state.isFocus, stableTextInputProps});
|
|
1184
|
+
debugLog('AutoPositionedPopup memoizedTextInput=', { tag, useTextInput, 'state.isFocus': state.isFocus, stableTextInputProps });
|
|
1120
1185
|
if (!useTextInput || !state.isFocus) {
|
|
1121
1186
|
return null;
|
|
1122
1187
|
}
|
|
@@ -1137,7 +1202,7 @@ const AutoPositionedPopup = memo(
|
|
|
1137
1202
|
style={[
|
|
1138
1203
|
styles.inputStyle,
|
|
1139
1204
|
stableInputStyle,
|
|
1140
|
-
(themeMode==='dark' && {color:'#fff'}),
|
|
1205
|
+
(themeMode === 'dark' && { color: '#fff' }),
|
|
1141
1206
|
]}
|
|
1142
1207
|
textAlign={stableTextInputProps && stableTextInputProps['textAlign'] || 'left'}
|
|
1143
1208
|
multiline={stableTextInputProps && stableTextInputProps['multiline'] || false}
|
|
@@ -1204,6 +1269,106 @@ const AutoPositionedPopup = memo(
|
|
|
1204
1269
|
// No longer use original inputStyle and TextInputProps, use stable references instead
|
|
1205
1270
|
// Stable references only update when deep comparison detects actual content changes, avoiding frequent TextInput recreation during parent component redraws
|
|
1206
1271
|
]);
|
|
1272
|
+
const internalSearchMemoizedTextInput = useMemo(() => {
|
|
1273
|
+
debugLog('AutoPositionedPopup internalSearchMemoizedTextInput=', { tag, internalSearch, 'state.isFocus': state.isFocus, stableTextInputProps });
|
|
1274
|
+
if (!internalSearch || !state.isFocus) {
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
return (
|
|
1278
|
+
<RNTextInput
|
|
1279
|
+
ref={(ref) => {
|
|
1280
|
+
if (ref && !textInputRef.current) {
|
|
1281
|
+
debugLog(`AutoPositionedPopup internalSearch TextInput created/mounted - tag: ${tag}, ref:`, ref);
|
|
1282
|
+
} else if (!ref && textInputRef.current) {
|
|
1283
|
+
debugLog(`AutoPositionedPopup internalSearch TextInput unmounted - tag: ${tag}`);
|
|
1284
|
+
} else if (ref && textInputRef.current && ref !== textInputRef.current) {
|
|
1285
|
+
debugLog(`AutoPositionedPopup internalSearch TextInput replaced - tag: ${tag}, oldRef:`, textInputRef.current, 'newRef:', ref);
|
|
1286
|
+
}
|
|
1287
|
+
textInputRef.current = ref;
|
|
1288
|
+
}}
|
|
1289
|
+
key={`internal-search-textinput-${tag}`}
|
|
1290
|
+
style={[
|
|
1291
|
+
styles.inputStyle,
|
|
1292
|
+
stableInputStyle,
|
|
1293
|
+
{
|
|
1294
|
+
flex: 0,
|
|
1295
|
+
height: internalSearchInputLayout.height,
|
|
1296
|
+
width: '100%',
|
|
1297
|
+
paddingHorizontal: 12,
|
|
1298
|
+
},
|
|
1299
|
+
(themeMode === 'dark' && { color: '#fff' }),
|
|
1300
|
+
]}
|
|
1301
|
+
textAlign={stableTextInputProps && stableTextInputProps['textAlign'] || 'left'}
|
|
1302
|
+
multiline={stableTextInputProps && stableTextInputProps['multiline'] || false}
|
|
1303
|
+
numberOfLines={stableTextInputProps && stableTextInputProps['numberOfLines'] || 1}
|
|
1304
|
+
onChangeText={(searchQuery) => {
|
|
1305
|
+
ref_searchQuery.current = searchQuery;
|
|
1306
|
+
debugLog('AutoPositionedPopup internalSearch onChangeText rootViews=', rootViews);
|
|
1307
|
+
if (!localSearch) {
|
|
1308
|
+
if (debounceTimerRef.current) {
|
|
1309
|
+
clearTimeout(debounceTimerRef.current);
|
|
1310
|
+
}
|
|
1311
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
1312
|
+
emitQueryChange(ref_searchQuery.current);
|
|
1313
|
+
if (!internalSearch) {
|
|
1314
|
+
onChangeText && onChangeText(ref_searchQuery.current);
|
|
1315
|
+
}
|
|
1316
|
+
}, 500);
|
|
1317
|
+
} else {
|
|
1318
|
+
emitQueryChange(ref_searchQuery.current);
|
|
1319
|
+
if (!internalSearch) {
|
|
1320
|
+
onChangeText && onChangeText(ref_searchQuery.current);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}}
|
|
1324
|
+
placeholderTextColor={stableTextInputProps && stableTextInputProps['placeholderTextColor'] || theme.colors.placeholderText}
|
|
1325
|
+
placeholder={placeholder}
|
|
1326
|
+
onKeyPress={(e) => {
|
|
1327
|
+
if (e.nativeEvent.key === 'Enter') {
|
|
1328
|
+
Keyboard.dismiss();
|
|
1329
|
+
}
|
|
1330
|
+
}}
|
|
1331
|
+
keyboardType={stableTextInputProps && stableTextInputProps['keyboardType'] || 'default'}
|
|
1332
|
+
clearButtonMode="while-editing"
|
|
1333
|
+
returnKeyType={stableTextInputProps && stableTextInputProps['returnKeyType'] || 'done'}
|
|
1334
|
+
maxLength={stableTextInputProps && stableTextInputProps['maxLength'] || 100}
|
|
1335
|
+
accessibilityLabel="selectInput"
|
|
1336
|
+
accessible={true}
|
|
1337
|
+
autoFocus={stableTextInputProps && stableTextInputProps['autoFocus'] || true}
|
|
1338
|
+
autoCorrect={false}
|
|
1339
|
+
underlineColorAndroid="transparent"
|
|
1340
|
+
editable={stableTextInputProps && stableTextInputProps['editable'] || true}
|
|
1341
|
+
secureTextEntry={stableTextInputProps && stableTextInputProps['secureTextEntry'] || false}
|
|
1342
|
+
defaultValue=""
|
|
1343
|
+
caretHidden={false}
|
|
1344
|
+
enablesReturnKeyAutomatically
|
|
1345
|
+
onFocus={handleTextInputFocus}
|
|
1346
|
+
onBlur={handleTextInputBlur}
|
|
1347
|
+
selectTextOnFocus={stableTextInputProps && stableTextInputProps['selectTextOnFocus'] || false}
|
|
1348
|
+
onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
|
|
1349
|
+
debugLog(
|
|
1350
|
+
'AutoPositionedPopup.tsx internalSearch onSubmitEditing e.nativeEvent.text=',
|
|
1351
|
+
e.nativeEvent.text
|
|
1352
|
+
);
|
|
1353
|
+
onSubmitEditing && onSubmitEditing(e);
|
|
1354
|
+
}}
|
|
1355
|
+
/>
|
|
1356
|
+
);
|
|
1357
|
+
}, [
|
|
1358
|
+
tag,
|
|
1359
|
+
internalSearch,
|
|
1360
|
+
state.isFocus,
|
|
1361
|
+
handleTextInputFocus,
|
|
1362
|
+
handleTextInputBlur,
|
|
1363
|
+
stableInputStyle,
|
|
1364
|
+
stableTextInputProps,
|
|
1365
|
+
placeholder,
|
|
1366
|
+
onSubmitEditing,
|
|
1367
|
+
localSearch,
|
|
1368
|
+
onChangeText,
|
|
1369
|
+
rootViews,
|
|
1370
|
+
themeMode,
|
|
1371
|
+
]);
|
|
1207
1372
|
|
|
1208
1373
|
// Render the component following project implementation
|
|
1209
1374
|
return useMemo(() => {
|
|
@@ -1233,9 +1398,9 @@ const AutoPositionedPopup = memo(
|
|
|
1233
1398
|
// IMPORTANT: Always capture position regardless of parentScrollViewRef
|
|
1234
1399
|
if (triggerBtnRef.current) {
|
|
1235
1400
|
triggerBtnRef.current.measureInWindow((x, y, width, height) => {
|
|
1236
|
-
debugLog('AutoPositionedPopup onPress: captured trigger position=', {tag, x, y, width, height});
|
|
1401
|
+
debugLog('AutoPositionedPopup onPress: captured trigger position=', { tag, x, y, width, height });
|
|
1237
1402
|
if (x !== undefined && y !== undefined && width !== undefined && height !== undefined) {
|
|
1238
|
-
triggerPositionRef.current = {x, y, width, height};
|
|
1403
|
+
triggerPositionRef.current = { x, y, width, height };
|
|
1239
1404
|
}
|
|
1240
1405
|
});
|
|
1241
1406
|
}
|
|
@@ -1311,7 +1476,7 @@ const AutoPositionedPopup = memo(
|
|
|
1311
1476
|
<Text
|
|
1312
1477
|
style={[
|
|
1313
1478
|
styles.searchQueryTxt,
|
|
1314
|
-
state.selectedItem && {color: theme.colors.text},
|
|
1479
|
+
state.selectedItem && { color: theme.colors.text },
|
|
1315
1480
|
labelStyle,
|
|
1316
1481
|
]}
|
|
1317
1482
|
numberOfLines={1}
|
|
@@ -1352,7 +1517,7 @@ const AutoPositionedPopup = memo(
|
|
|
1352
1517
|
forceRemoveAllRootViewOnItemSelected,
|
|
1353
1518
|
state.isFocus,
|
|
1354
1519
|
showListEmptyComponent,
|
|
1355
|
-
emptyText,
|
|
1520
|
+
emptyText, internalSearch
|
|
1356
1521
|
// ⚠Removed most dependencies that may cause re-rendering, keeping only core dependencies that truly affect component structure
|
|
1357
1522
|
// This prevents TextInput recreation due to inline functions/objects during parent component redraws
|
|
1358
1523
|
]);
|