react-native-auto-positioned-popup 1.0.4 → 1.0.6
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 +523 -244
- package/lib/AutoPositionedPopup.js.map +1 -0
- package/lib/AutoPositionedPopup.style.d.ts +29 -0
- package/lib/AutoPositionedPopup.style.d.ts.map +1 -1
- package/lib/AutoPositionedPopup.style.js +20 -2
- package/lib/AutoPositionedPopup.style.js.map +1 -0
- package/lib/AutoPositionedPopupProps.js +1 -0
- package/lib/AutoPositionedPopupProps.js.map +1 -0
- package/lib/KeyboardManager.d.ts +2 -0
- package/lib/KeyboardManager.d.ts.map +1 -0
- package/lib/KeyboardManager.js +57 -0
- package/lib/KeyboardManager.js.map +1 -0
- package/lib/RootViewContext.d.ts.map +1 -1
- package/lib/RootViewContext.js +1 -1
- package/lib/RootViewContext.js.map +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -0
- package/package.json +6 -5
- package/src/AutoPositionedPopup.style.ts +24 -3
- package/src/AutoPositionedPopup.tsx +747 -383
- package/src/KeyboardManager.tsx +82 -0
- package/src/RootViewContext.tsx +0 -1
- package/src/types/react-native-advanced-flatlist.d.ts +19 -0
|
@@ -3,7 +3,7 @@ import React, {
|
|
|
3
3
|
forwardRef,
|
|
4
4
|
ForwardRefExoticComponent,
|
|
5
5
|
memo,
|
|
6
|
-
MemoExoticComponent,
|
|
6
|
+
MemoExoticComponent, MutableRefObject,
|
|
7
7
|
useCallback,
|
|
8
8
|
useEffect,
|
|
9
9
|
useImperativeHandle,
|
|
@@ -18,14 +18,18 @@ import {
|
|
|
18
18
|
TextInput as RNTextInput,
|
|
19
19
|
TouchableOpacity,
|
|
20
20
|
View,
|
|
21
|
-
ViewStyle,
|
|
22
21
|
} from 'react-native';
|
|
23
|
-
|
|
22
|
+
// @ts-ignore - Skip type checking for third-party library with type issues
|
|
23
|
+
import {AdvancedFlatList as AdvancedFlatListLib} from 'react-native-advanced-flatlist';
|
|
24
|
+
// @ts-ignore - Direct import from source when using fake data
|
|
25
|
+
import AdvancedFlatListSource from 'react-native-advanced-flatlist/src/AdvancedFlatList.tsx';
|
|
24
26
|
import {TextInputSubmitEditingEventData} from 'react-native/Libraries/Components/TextInput/TextInput';
|
|
25
27
|
import {LayoutRectangle, NativeSyntheticEvent} from 'react-native/Libraries/Types/CoreEventTypes';
|
|
26
28
|
import {AutoPositionedPopupProps, Data, SelectedItem} from './AutoPositionedPopupProps';
|
|
27
29
|
import styles from './AutoPositionedPopup.style';
|
|
28
30
|
import {useRootView} from './RootViewContext';
|
|
31
|
+
import {useKeyboardStatus} from './KeyboardManager';
|
|
32
|
+
import {useSafeAreaInsets} from "react-native-safe-area-context";
|
|
29
33
|
|
|
30
34
|
// Lightweight emitter to decouple TextInput and list without re-rendering context
|
|
31
35
|
type QueryListener = (query: string) => void;
|
|
@@ -64,144 +68,238 @@ const defaultTheme: Theme = {
|
|
|
64
68
|
|
|
65
69
|
// List item component for rendering individual items
|
|
66
70
|
const ListItem: React.FC<{
|
|
71
|
+
updateState: (key: string, value: SelectedItem) => void;
|
|
67
72
|
item: SelectedItem;
|
|
68
73
|
index: number;
|
|
69
74
|
selectedItem?: SelectedItem;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
75
|
+
}> = memo(
|
|
76
|
+
({
|
|
77
|
+
updateState,
|
|
78
|
+
item,
|
|
79
|
+
index,
|
|
80
|
+
selectedItem,
|
|
81
|
+
}: {
|
|
82
|
+
updateState: (key: string, value: SelectedItem) => void;
|
|
83
|
+
item: SelectedItem;
|
|
84
|
+
index: number;
|
|
85
|
+
selectedItem?: SelectedItem;
|
|
86
|
+
}): React.JSX.Element => {
|
|
87
|
+
const {addRootView, setRootViewNativeStyle, removeRootView, rootViews} = useRootView();
|
|
88
|
+
const rootViewsRef = useRef(rootViews);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
rootViewsRef.current = rootViews;
|
|
91
|
+
}, [rootViews]);
|
|
92
|
+
return useMemo(() => {
|
|
93
|
+
// console.log('AutoPositionedPopup.tsx ListItem index=', index);
|
|
94
|
+
// console.log('AutoPositionedPopup.tsx ListItem item=', item);
|
|
95
|
+
const isSelected = item.id === selectedItem?.id;
|
|
96
|
+
return (
|
|
97
|
+
<TouchableOpacity
|
|
98
|
+
key={item.id}
|
|
99
|
+
style={[
|
|
100
|
+
styles.commonModalRow,
|
|
101
|
+
{backgroundColor: isSelected ? 'rgba(116, 116, 128, 0.08)' : 'transparent'},
|
|
102
|
+
]}
|
|
103
|
+
onPress={() => {
|
|
104
|
+
// console.log('AutoPositionedPopup.tsx ListItem onPress item=', item); // Commented to prevent spam
|
|
105
|
+
// console.log('AutoPositionedPopup.tsx ListItem onPress rootViews=', rootViewsRef.current); // Commented to prevent spam
|
|
106
|
+
updateState('selectedItem', item);
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<Text style={styles.ListItemCode} numberOfLines={1} ellipsizeMode="tail">
|
|
110
|
+
{item.title}
|
|
111
|
+
</Text>
|
|
112
|
+
</TouchableOpacity>
|
|
113
|
+
);
|
|
114
|
+
}, [updateState, item, index, selectedItem, rootViewsRef]);
|
|
115
|
+
}
|
|
116
|
+
);
|
|
105
117
|
|
|
106
118
|
// Popup list component with AdvancedFlatList
|
|
107
|
-
interface
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
interface AutoPositionedPopupListProps {
|
|
120
|
+
tag: string;
|
|
121
|
+
updateState: (key: string, value: any) => void;
|
|
122
|
+
fetchData: ({
|
|
123
|
+
pageIndex,
|
|
124
|
+
pageSize,
|
|
125
|
+
searchQuery,
|
|
126
|
+
}: {
|
|
127
|
+
pageIndex: number;
|
|
128
|
+
pageSize: number;
|
|
129
|
+
searchQuery?: string;
|
|
130
|
+
}) => Promise<Data | null>;
|
|
131
|
+
keyExtractor?: (item: SelectedItem) => string; //keyExtractor={item => item?.id}
|
|
111
132
|
renderItem?: ({item, index}: { item: SelectedItem; index: number }) => React.ReactElement;
|
|
112
|
-
|
|
113
|
-
theme: Theme;
|
|
114
|
-
rootViewsRef?: React.MutableRefObject<any[]>;
|
|
115
|
-
fetchData?: (params: { pageIndex: number; pageSize: number; searchQuery?: string }) => Promise<Data | null>;
|
|
133
|
+
selectedItem?: SelectedItem;
|
|
116
134
|
localSearch?: boolean;
|
|
117
135
|
pageSize?: number;
|
|
118
|
-
onDataUpdate?: (newData: SelectedItem[]) => void;
|
|
119
|
-
selectedItemBackgroundColor?: string;
|
|
120
136
|
}
|
|
121
137
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
138
|
+
const AutoPositionedPopupList: React.FC<AutoPositionedPopupListProps> = memo(
|
|
139
|
+
({
|
|
140
|
+
tag,
|
|
141
|
+
updateState,
|
|
142
|
+
fetchData,
|
|
143
|
+
keyExtractor = (item) => String(item.id),
|
|
144
|
+
renderItem,
|
|
145
|
+
selectedItem,
|
|
146
|
+
localSearch,
|
|
147
|
+
pageSize,
|
|
148
|
+
}: AutoPositionedPopupListProps): React.JSX.Element => {
|
|
149
|
+
const [state, setState] = useState<{
|
|
150
|
+
selectedItem?: SelectedItem;
|
|
151
|
+
localData: SelectedItem[];
|
|
152
|
+
}>({
|
|
153
|
+
selectedItem: selectedItem,
|
|
154
|
+
localData: [],
|
|
155
|
+
});
|
|
156
|
+
// Define an interface that matches the methods we need from CsxFlatList
|
|
157
|
+
const ref_list = useRef<{ scrollToTop: () => void; refresh: () => void } | null>(null);
|
|
158
|
+
const ref_searchQuery = useRef<string>('');
|
|
159
|
+
const {searchQuery, setSearchQuery, rootViews} = useRootView();
|
|
160
|
+
const rootViewsRef = useRef(rootViews);
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
rootViewsRef.current = rootViews;
|
|
163
|
+
}, [rootViews]);
|
|
164
|
+
/**
|
|
165
|
+
* componentDidMount && componentWillUnmount
|
|
166
|
+
*/
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
(async () => {
|
|
169
|
+
})();
|
|
170
|
+
console.log(`AutoPositionedPopupList componentDidMount`);
|
|
171
|
+
//componentWillUnmount
|
|
172
|
+
return () => {
|
|
173
|
+
console.log(`AutoPositionedPopupList componentWillUnmount`);
|
|
174
|
+
setSearchQuery('');
|
|
175
|
+
};
|
|
176
|
+
}, []);
|
|
177
|
+
// useEffect(() => {
|
|
178
|
+
// // 監聽 TextInput 事件,收到就刷新列表,不依賴 global searchQuery
|
|
179
|
+
// // 將最新的 searchQuery 同步到 list 專用的 ref,供 _fetchData 使用
|
|
180
|
+
// ref_searchQuery.current = searchQuery;
|
|
181
|
+
// console.log('AutoPositionedPopupList useEffect searchQuery=', searchQuery);
|
|
182
|
+
// console.log('AutoPositionedPopupList useEffect state.localData=', state.localData);
|
|
183
|
+
// console.log('AutoPositionedPopupList useEffect ref_list.current=', ref_list.current);
|
|
184
|
+
// console.log('AutoPositionedPopupList useEffect localSearch=', localSearch);
|
|
185
|
+
// if (ref_list.current && (localSearch && state.localData.length > 0 || !localSearch)) {
|
|
186
|
+
// ref_list.current.scrollToTop();
|
|
187
|
+
// ref_list.current.refresh();
|
|
188
|
+
// }
|
|
189
|
+
// }, [searchQuery, state.localData, localSearch]);
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
const unsubscribe = subscribeQueryChange((newQuery: string) => {
|
|
192
|
+
console.log('AutoPositionedPopupList useEffect subscribeQueryChange newQuery=', newQuery);
|
|
193
|
+
ref_searchQuery.current = newQuery;
|
|
194
|
+
if (ref_list.current) {
|
|
195
|
+
ref_list.current.scrollToTop();
|
|
196
|
+
ref_list.current.refresh();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
return unsubscribe;
|
|
200
|
+
}, []);
|
|
201
|
+
const _updateState = (key: string, value: SelectedItem) => {
|
|
202
|
+
console.log('AutoPositionedPopupList _updateState key=', key, ' value=', value);
|
|
203
|
+
setState((prevState) => ({
|
|
204
|
+
...prevState,
|
|
205
|
+
[key]: value,
|
|
206
|
+
}));
|
|
207
|
+
console.log('AutoPositionedPopupList _updateState rootViews=', rootViewsRef.current);
|
|
208
|
+
updateState(key, value);
|
|
209
|
+
};
|
|
210
|
+
const _fetchData = async ({
|
|
211
|
+
pageIndex,
|
|
212
|
+
pageSize: currentPageSize,
|
|
213
|
+
}: {
|
|
214
|
+
pageIndex: number;
|
|
215
|
+
pageSize: number;
|
|
216
|
+
}): Promise<Data | null> => {
|
|
217
|
+
console.log('AutoPositionedPopupList _fetchData pageIndex=', pageIndex, ' pageSize=', currentPageSize);
|
|
218
|
+
console.log('AutoPositionedPopupList _fetchData state.localData=', state.localData);
|
|
219
|
+
console.log('AutoPositionedPopupList _fetchData ref_searchQuery.current=', ref_searchQuery.current);
|
|
220
|
+
console.log('AutoPositionedPopupList _fetchData localSearch=', localSearch);
|
|
221
|
+
if (localSearch && state.localData.length > 0) {
|
|
222
|
+
const result: SelectedItem[] = state.localData.filter((item: SelectedItem) => {
|
|
223
|
+
return item.title?.toLowerCase().includes(ref_searchQuery.current.toLowerCase());
|
|
224
|
+
});
|
|
225
|
+
console.log('AutoPositionedPopupList _fetchData localSearch result=', result);
|
|
226
|
+
return Promise.resolve({
|
|
227
|
+
items: result,
|
|
228
|
+
pageIndex: 0,
|
|
229
|
+
needLoadMore: false,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const res: Data | null = await fetchData({
|
|
234
|
+
pageIndex,
|
|
235
|
+
pageSize: pageSize || 10,
|
|
236
|
+
searchQuery: ref_searchQuery.current,
|
|
237
|
+
});
|
|
238
|
+
console.log('AutoPositionedPopupList _fetchData res=', res);
|
|
239
|
+
if (res?.items && localSearch) {
|
|
240
|
+
setState((prevState) => {
|
|
241
|
+
return {
|
|
242
|
+
...prevState,
|
|
243
|
+
localData: res.items,
|
|
244
|
+
};
|
|
156
245
|
});
|
|
157
|
-
|
|
158
|
-
if (result?.items) {
|
|
159
|
-
setInternalData(result.items);
|
|
160
|
-
onDataUpdate?.(result.items);
|
|
161
|
-
}
|
|
162
|
-
} catch (error) {
|
|
163
|
-
console.error('PopupList fetchData error:', error);
|
|
164
246
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
item.title.toLowerCase().includes(newQuery.toLowerCase())
|
|
169
|
-
);
|
|
170
|
-
setInternalData(filtered);
|
|
171
|
-
onDataUpdate?.(filtered);
|
|
247
|
+
return Promise.resolve(res);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
console.warn('Error in fetchData:', e);
|
|
172
250
|
}
|
|
173
|
-
|
|
251
|
+
console.log('AutoPositionedPopupList _fetchData res=', null);
|
|
252
|
+
return null;
|
|
253
|
+
};
|
|
254
|
+
const _renderItem = useCallback(
|
|
255
|
+
({item, index}: { item: SelectedItem; index: number }) => {
|
|
256
|
+
return <ListItem item={item} index={index} updateState={_updateState} selectedItem={state.selectedItem} />;
|
|
257
|
+
},
|
|
258
|
+
[state.selectedItem]
|
|
259
|
+
);
|
|
260
|
+
return useMemo(() => {
|
|
261
|
+
console.log('AutoPositionedPopupList (global as any)?.$fake=', (global as any)?.$fake);
|
|
262
|
+
// Choose AdvancedFlatList version based on global.$fake
|
|
263
|
+
const AdvancedFlatListComponent = (global as any)?.$fake ? AdvancedFlatListSource : AdvancedFlatListLib;
|
|
174
264
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
265
|
+
return (
|
|
266
|
+
<View style={[styles.baseModalView, styles.autoPositionedPopupList]}>
|
|
267
|
+
{/* @ts-ignore - Type assertion to bypass third-party library type issues */}
|
|
268
|
+
<AdvancedFlatListComponent
|
|
269
|
+
style={[{borderRadius: 0}]}
|
|
270
|
+
{...(ref_list && { ref: ref_list })}
|
|
271
|
+
keyExtractor={keyExtractor}
|
|
272
|
+
keyboardShouldPersistTaps={'always'}
|
|
273
|
+
{...({ fetchData: _fetchData })}
|
|
274
|
+
renderItem={renderItem || _renderItem}
|
|
275
|
+
/>
|
|
276
|
+
</View>
|
|
277
|
+
);
|
|
278
|
+
}, [tag,
|
|
279
|
+
updateState,
|
|
280
|
+
fetchData,
|
|
281
|
+
keyExtractor,
|
|
282
|
+
renderItem,
|
|
283
|
+
state.selectedItem,
|
|
284
|
+
state.localData,
|
|
285
|
+
searchQuery,
|
|
286
|
+
localSearch,
|
|
287
|
+
pageSize,
|
|
288
|
+
rootViewsRef,
|
|
289
|
+
]);
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// State interface for AutoPositionedPopup
|
|
294
|
+
interface StateProps {
|
|
295
|
+
isFocus?: boolean;
|
|
296
|
+
selectedItem?: SelectedItem | any;
|
|
297
|
+
}
|
|
191
298
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
keyExtractor={keyExtractor}
|
|
197
|
-
renderItem={renderItem || defaultRenderItem}
|
|
198
|
-
keyboardShouldPersistTaps="always"
|
|
199
|
-
showsVerticalScrollIndicator={true}
|
|
200
|
-
nestedScrollEnabled={true}
|
|
201
|
-
/>
|
|
202
|
-
</View>
|
|
203
|
-
);
|
|
204
|
-
});
|
|
299
|
+
// List layout constants
|
|
300
|
+
const listLayout = {
|
|
301
|
+
height: 200,
|
|
302
|
+
};
|
|
205
303
|
|
|
206
304
|
// Main AutoPositionedPopup component
|
|
207
305
|
const AutoPositionedPopup: MemoExoticComponent<
|
|
@@ -209,18 +307,41 @@ const AutoPositionedPopup: MemoExoticComponent<
|
|
|
209
307
|
> = memo(
|
|
210
308
|
forwardRef<unknown, AutoPositionedPopupProps>(
|
|
211
309
|
(props: AutoPositionedPopupProps, parentRef: ForwardedRef<unknown>): React.JSX.Element => {
|
|
310
|
+
console.log('AutoPositionedPopup props=', props);
|
|
212
311
|
const {
|
|
213
312
|
tag,
|
|
214
313
|
style,
|
|
215
314
|
AutoPositionedPopupBtnStyle,
|
|
216
315
|
placeholder = 'Please Select',
|
|
217
|
-
textAlign = 'right',
|
|
218
316
|
onSubmitEditing,
|
|
219
317
|
TextInputProps = {},
|
|
220
318
|
inputStyle,
|
|
221
319
|
labelStyle,
|
|
222
320
|
popUpViewStyle = {left: '5%', width: '90%'},
|
|
223
|
-
fetchData
|
|
321
|
+
fetchData = async ({
|
|
322
|
+
pageIndex,
|
|
323
|
+
pageSize,
|
|
324
|
+
searchQuery,
|
|
325
|
+
}: {
|
|
326
|
+
pageIndex: number;
|
|
327
|
+
pageSize: number;
|
|
328
|
+
searchQuery?: string;
|
|
329
|
+
}): Promise<Data | null> => {
|
|
330
|
+
const res = {
|
|
331
|
+
items: [] as any[],
|
|
332
|
+
pageIndex,
|
|
333
|
+
needLoadMore: false,
|
|
334
|
+
};
|
|
335
|
+
try {
|
|
336
|
+
// const res1: any[] = await $api.xxx(pageSize)
|
|
337
|
+
// console.log('${NAME} xxx res=', res)
|
|
338
|
+
// res.items = res1
|
|
339
|
+
// res.needLoadMore = res1.length === pageSize
|
|
340
|
+
} catch (e) {
|
|
341
|
+
console.warn('Error in fetch operation:', e);
|
|
342
|
+
}
|
|
343
|
+
return res;
|
|
344
|
+
},
|
|
224
345
|
renderItem,
|
|
225
346
|
onItemSelected,
|
|
226
347
|
localSearch = false,
|
|
@@ -229,22 +350,36 @@ const AutoPositionedPopup: MemoExoticComponent<
|
|
|
229
350
|
useTextInput = false,
|
|
230
351
|
btwChildren,
|
|
231
352
|
CustomRow = ({children}) => <View>{children}</View>,
|
|
232
|
-
keyExtractor = (item: any) => item?.id,
|
|
353
|
+
keyExtractor = (item: any) => String(item?.id || ''),
|
|
233
354
|
AutoPositionedPopupBtnDisabled = false,
|
|
234
355
|
forceRemoveAllRootViewOnItemSelected = false,
|
|
235
356
|
centerDisplay = false,
|
|
236
357
|
selectedItemBackgroundColor = 'rgba(116, 116, 128, 0.08)',
|
|
358
|
+
textAlign = 'right',
|
|
359
|
+
CustomPopView = undefined, CustomPopViewStyle
|
|
237
360
|
} = props;
|
|
238
|
-
|
|
361
|
+
// State management similar to project implementation
|
|
362
|
+
const [state, setState] = useState<StateProps>({
|
|
363
|
+
isFocus: false,
|
|
364
|
+
selectedItem: selectedItem,
|
|
365
|
+
});
|
|
239
366
|
// Use RootView context
|
|
240
|
-
const {addRootView, removeRootView, rootViews,
|
|
367
|
+
const {addRootView, setRootViewNativeStyle, removeRootView, rootViews, setSearchQuery} = useRootView();
|
|
368
|
+
const insets = useSafeAreaInsets();
|
|
241
369
|
const rootViewsRef = useRef(rootViews);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
370
|
+
// Track TextInput focus and RootView states like project implementation
|
|
371
|
+
const hasTriggeredFocus = useRef(false);
|
|
372
|
+
const hasAddedRootView = useRef(false);
|
|
373
|
+
const hasShownRootView = useRef(false);
|
|
374
|
+
// Additional refs for keyboard and position tracking
|
|
375
|
+
const ref_isFocus = useRef<boolean>();
|
|
376
|
+
const ref_isKeyboardFullyShown = useRef<boolean>();
|
|
377
|
+
const ref_listPos: MutableRefObject<any> = useRef<LayoutRectangle>()
|
|
378
|
+
const keyboardVisibleRef = useRef(false);
|
|
379
|
+
const refAutoPositionedPopup = useRef<View>(null);
|
|
380
|
+
const ref_searchQuery = useRef<string>('');
|
|
381
|
+
// Simple keyboard status tracking (alternative to useKeyboardStatus hook)
|
|
382
|
+
// Legacy state for compatibility
|
|
248
383
|
const [isVisible, setIsVisible] = useState(false);
|
|
249
384
|
const [data, setData] = useState<SelectedItem[]>([]);
|
|
250
385
|
const [loading, setLoading] = useState(false);
|
|
@@ -253,275 +388,504 @@ const AutoPositionedPopup: MemoExoticComponent<
|
|
|
253
388
|
left: number;
|
|
254
389
|
width: number;
|
|
255
390
|
}>({top: 0, left: 0, width: 0});
|
|
256
|
-
const popupId = useRef(`popup-${tag}-${Date.now()}`);
|
|
257
|
-
|
|
258
391
|
// Refs for performance optimization
|
|
259
392
|
const containerRef = useRef<View>(null);
|
|
260
393
|
const textInputRef = useRef<RNTextInput>(null);
|
|
261
394
|
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
262
395
|
const searchQueryRef = useRef<string>(''); // Use ref instead of state to avoid re-renders
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
396
|
+
// Refs to store latest values for useEffect without adding to dependency array
|
|
397
|
+
const dataRef = useRef<SelectedItem[]>(data);
|
|
398
|
+
const isKeyboardFullyShown = useKeyboardStatus();
|
|
266
399
|
const theme = defaultTheme;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
|
|
400
|
+
/**
|
|
401
|
+
* componentDidMount && componentWillUnmount
|
|
402
|
+
*/
|
|
403
|
+
useEffect(() => {
|
|
404
|
+
(async () => {
|
|
405
|
+
})();
|
|
406
|
+
console.log(`AutoPositionedPopup componentDidMount tag=`, tag);
|
|
407
|
+
console.log('AutoPositionedPopup componentDidMount CustomPopView=', CustomPopView);
|
|
408
|
+
//componentWillUnmount
|
|
409
|
+
return () => {
|
|
410
|
+
console.log(`AutoPositionedPopup componentWillUnmount tag=`, tag);
|
|
411
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
412
|
+
setSearchQuery('');
|
|
413
|
+
if (textInputRef.current) {
|
|
414
|
+
textInputRef.current.blur();
|
|
415
|
+
hasTriggeredFocus.current = false;
|
|
416
|
+
hasAddedRootView.current = false;
|
|
417
|
+
hasShownRootView.current = false;
|
|
418
|
+
ref_isFocus.current = false;
|
|
282
419
|
}
|
|
283
|
-
}
|
|
284
|
-
console.error('Error loading data:', error);
|
|
285
|
-
} finally {
|
|
286
|
-
setLoading(false);
|
|
287
|
-
}
|
|
288
|
-
}, [fetchData, pageSize]);
|
|
289
|
-
|
|
290
|
-
// Handle search query change with debounce and event emission
|
|
291
|
-
const handleSearchChange = useCallback((query: string) => {
|
|
292
|
-
// Store in ref to avoid re-renders
|
|
293
|
-
searchQueryRef.current = query;
|
|
294
|
-
|
|
295
|
-
// Update TextInput value directly if needed
|
|
296
|
-
if (textInputRef.current) {
|
|
297
|
-
// The TextInput's value will be controlled by its own state
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Clear previous debounce timer
|
|
301
|
-
if (debounceTimerRef.current) {
|
|
302
|
-
clearTimeout(debounceTimerRef.current);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Use debounce for performance optimization
|
|
306
|
-
debounceTimerRef.current = setTimeout(() => {
|
|
307
|
-
// Emit query change event to decouple components and avoid context re-rendering
|
|
308
|
-
emitQueryChange(searchQueryRef.current);
|
|
309
|
-
}, 300); // Use 300ms debounce like the original
|
|
420
|
+
};
|
|
310
421
|
}, []);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
top = y - LIST_HEIGHT;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Adjust horizontal position if needed
|
|
330
|
-
if (popUpViewStyle?.left && popUpViewStyle?.width) {
|
|
331
|
-
const leftPercent = parseFloat(String(popUpViewStyle.left).replace('%', '')) / 100;
|
|
332
|
-
const widthPercent = parseFloat(String(popUpViewStyle.width).replace('%', '')) / 100;
|
|
333
|
-
left = screenWidth * leftPercent;
|
|
334
|
-
popupWidth = screenWidth * widthPercent;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
setPopupPosition({top, left, width: popupWidth});
|
|
338
|
-
});
|
|
339
|
-
}, [popUpViewStyle]);
|
|
340
|
-
|
|
341
|
-
// Hide popup using RootView
|
|
342
|
-
const hidePopup = useCallback(() => {
|
|
343
|
-
setIsVisible(false);
|
|
344
|
-
// Reset search query
|
|
345
|
-
searchQueryRef.current = '';
|
|
346
|
-
if (textInputRef.current) {
|
|
347
|
-
textInputRef.current.blur();
|
|
348
|
-
textInputRef.current.clear?.(); // Clear the TextInput
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
console.log('AutoPositionedPopup rootViews=', rootViews);
|
|
424
|
+
rootViewsRef.current = rootViews;
|
|
425
|
+
if (rootViews.length === 0) {
|
|
426
|
+
hasAddedRootView.current = false;
|
|
427
|
+
hasShownRootView.current = false;
|
|
428
|
+
ref_isFocus.current = false;
|
|
429
|
+
ref_isKeyboardFullyShown.current = false;
|
|
430
|
+
hasTriggeredFocus.current = false;
|
|
431
|
+
setState((prevState) => {
|
|
432
|
+
return {
|
|
433
|
+
...prevState,
|
|
434
|
+
isFocus: false,
|
|
435
|
+
};
|
|
436
|
+
});
|
|
349
437
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}, [onItemSelected, hidePopup]);
|
|
363
|
-
|
|
364
|
-
// Show popup using RootView
|
|
365
|
-
const showPopup = useCallback(() => {
|
|
366
|
-
calculatePosition();
|
|
367
|
-
setIsVisible(true);
|
|
368
|
-
loadData(searchQueryRef.current);
|
|
369
|
-
|
|
370
|
-
// Wait for position to be calculated
|
|
371
|
-
setTimeout(() => {
|
|
372
|
-
const popupComponent = (
|
|
373
|
-
<TouchableOpacity
|
|
374
|
-
style={{
|
|
375
|
-
flex: 1,
|
|
376
|
-
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
377
|
-
}}
|
|
378
|
-
activeOpacity={1}
|
|
379
|
-
onPress={hidePopup}
|
|
380
|
-
>
|
|
381
|
-
<View
|
|
382
|
-
style={{
|
|
383
|
-
position: 'absolute',
|
|
384
|
-
top: popupPosition.top,
|
|
385
|
-
left: popupPosition.left,
|
|
386
|
-
width: popupPosition.width,
|
|
387
|
-
height: LIST_HEIGHT,
|
|
388
|
-
backgroundColor: theme.colors.background,
|
|
389
|
-
borderRadius: 8,
|
|
390
|
-
shadowColor: '#000',
|
|
391
|
-
shadowOffset: {width: 0, height: 2},
|
|
392
|
-
shadowOpacity: 0.25,
|
|
393
|
-
shadowRadius: 3.84,
|
|
394
|
-
elevation: 5,
|
|
395
|
-
}}
|
|
396
|
-
>
|
|
397
|
-
{useTextInput && (
|
|
398
|
-
<RNTextInput
|
|
399
|
-
ref={textInputRef}
|
|
400
|
-
style={[
|
|
401
|
-
styles.inputStyle,
|
|
402
|
-
{
|
|
403
|
-
height: 40,
|
|
404
|
-
borderBottomWidth: 1,
|
|
405
|
-
borderBottomColor: theme.colors.border,
|
|
406
|
-
paddingHorizontal: 12,
|
|
407
|
-
color: theme.colors.text,
|
|
408
|
-
},
|
|
409
|
-
inputStyle,
|
|
410
|
-
]}
|
|
411
|
-
placeholder={placeholder}
|
|
412
|
-
placeholderTextColor={theme.colors.placeholderText}
|
|
413
|
-
defaultValue={searchQueryRef.current}
|
|
414
|
-
onChangeText={handleSearchChange}
|
|
415
|
-
onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
|
|
416
|
-
onSubmitEditing?.(e);
|
|
417
|
-
Keyboard.dismiss();
|
|
418
|
-
}}
|
|
419
|
-
returnKeyType="done"
|
|
420
|
-
{...TextInputProps}
|
|
421
|
-
/>
|
|
422
|
-
)}
|
|
423
|
-
|
|
424
|
-
<PopupList
|
|
425
|
-
data={data}
|
|
426
|
-
selectedItem={selectedItem}
|
|
427
|
-
onItemPress={handleItemPress}
|
|
428
|
-
renderItem={renderItem}
|
|
429
|
-
keyExtractor={keyExtractor}
|
|
430
|
-
theme={theme}
|
|
431
|
-
rootViewsRef={rootViewsRef}
|
|
432
|
-
fetchData={fetchData}
|
|
433
|
-
localSearch={localSearch}
|
|
434
|
-
pageSize={pageSize}
|
|
435
|
-
onDataUpdate={handleDataUpdate}
|
|
436
|
-
selectedItemBackgroundColor={selectedItemBackgroundColor}
|
|
437
|
-
/>
|
|
438
|
-
</View>
|
|
439
|
-
</TouchableOpacity>
|
|
440
|
-
);
|
|
441
|
-
|
|
442
|
-
addRootView({
|
|
443
|
-
id: popupId.current,
|
|
444
|
-
style: {
|
|
445
|
-
position: 'absolute',
|
|
446
|
-
top: 0,
|
|
447
|
-
left: 0,
|
|
448
|
-
right: 0,
|
|
449
|
-
bottom: 0,
|
|
450
|
-
},
|
|
451
|
-
component: popupComponent,
|
|
452
|
-
useModal: true,
|
|
453
|
-
onModalClose: hidePopup,
|
|
454
|
-
centerDisplay: centerDisplay,
|
|
438
|
+
}, [rootViews]);
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
console.log('AutoPositionedPopup useEffect tag=', tag);
|
|
441
|
+
console.log('AutoPositionedPopup useEffect selectedItem=', selectedItem);
|
|
442
|
+
console.log('AutoPositionedPopup useEffect state.selectedItem=', state.selectedItem);
|
|
443
|
+
if (state.selectedItem?.id !== selectedItem?.id || state.selectedItem?.title !== selectedItem?.title) {
|
|
444
|
+
console.log('AutoPositionedPopup useEffect selectedItem!=state.selectedItem');
|
|
445
|
+
setState((prevState) => {
|
|
446
|
+
return {
|
|
447
|
+
...prevState,
|
|
448
|
+
selectedItem: selectedItem,
|
|
449
|
+
};
|
|
455
450
|
});
|
|
456
|
-
}
|
|
457
|
-
}, [
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
451
|
+
}
|
|
452
|
+
}, [selectedItem, state.selectedItem, tag]);
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
console.log('AutoPositionedPopup useEffect tag=', tag);
|
|
455
|
+
console.log('AutoPositionedPopup useEffect state.isFocus=', state.isFocus);
|
|
456
|
+
console.log('AutoPositionedPopup useEffect isKeyboardFullyShown=', isKeyboardFullyShown);
|
|
457
|
+
console.log('AutoPositionedPopup useEffect ref_isFocus.current=', ref_isFocus.current);
|
|
458
|
+
console.log(
|
|
459
|
+
'AutoPositionedPopup useEffect ref_isKeyboardFullyShown.current=',
|
|
460
|
+
ref_isKeyboardFullyShown.current
|
|
461
|
+
);
|
|
462
|
+
console.log('AutoPositionedPopup useEffect useTextInput=', useTextInput);
|
|
463
|
+
console.log('AutoPositionedPopup useEffect TextInputProps=', TextInputProps);
|
|
464
|
+
console.log('AutoPositionedPopup useEffect hasAddedRootView.current=', hasAddedRootView.current);
|
|
465
|
+
console.log('AutoPositionedPopup useEffect hasShownRootView.current=', hasShownRootView.current);
|
|
463
466
|
if (useTextInput) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
467
|
+
if (isKeyboardFullyShown && hasAddedRootView.current && !hasShownRootView.current && state.isFocus) {
|
|
468
|
+
refAutoPositionedPopup.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
|
|
469
|
+
console.log('AutoPositionedPopup measureInWindow x=', x, ' y=', y, ' width=', width, ' height=', height);
|
|
470
|
+
// SIMPLE CENTER-BASED POSITIONING STRATEGY
|
|
471
|
+
const screenHeight = Dimensions.get('screen').height;
|
|
472
|
+
const screenCenter = screenHeight / 2;
|
|
473
|
+
console.log('AutoPositionedPopup screenHeight=', screenHeight, ' screenCenter=', screenCenter, ' componentY=', y);
|
|
474
|
+
|
|
475
|
+
// Simple rule: if component Y > screen center, show popup above; otherwise show below
|
|
476
|
+
if (y > screenCenter) {
|
|
477
|
+
console.log('AutoPositionedPopup with keyboard: showing above (Y > center)');
|
|
478
|
+
ref_listPos.current = {x: x, y: y - listLayout.height, width: width};
|
|
479
|
+
} else {
|
|
480
|
+
console.log('AutoPositionedPopup with keyboard: showing below (Y <= center)');
|
|
481
|
+
ref_listPos.current = {x: x, y: y + height, width: width};
|
|
482
|
+
}
|
|
483
|
+
console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
|
|
484
|
+
setRootViewNativeStyle(tag, {
|
|
485
|
+
top: ref_listPos.current?.y,
|
|
486
|
+
left: popUpViewStyle?.left,
|
|
487
|
+
width: popUpViewStyle?.width,
|
|
488
|
+
height: listLayout.height,
|
|
489
|
+
opacity: 1,
|
|
490
|
+
});
|
|
491
|
+
hasShownRootView.current = true;
|
|
492
|
+
});
|
|
493
|
+
} else if (!isKeyboardFullyShown && ref_isFocus.current) {
|
|
494
|
+
console.log(
|
|
495
|
+
'AutoPositionedPopup isKeyboardFullyShown useEffect removeRootView tag=',
|
|
496
|
+
tag,
|
|
497
|
+
' forceRemoveAllRootViewOnItemSelected=',
|
|
498
|
+
forceRemoveAllRootViewOnItemSelected
|
|
499
|
+
);
|
|
500
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
|
|
501
|
+
setState((prevState) => {
|
|
502
|
+
return {
|
|
503
|
+
...prevState,
|
|
504
|
+
isFocus: false,
|
|
505
|
+
};
|
|
506
|
+
});
|
|
507
|
+
setSearchQuery('');
|
|
508
|
+
hasAddedRootView.current = false;
|
|
509
|
+
hasShownRootView.current = false;
|
|
510
|
+
}
|
|
469
511
|
} else {
|
|
470
|
-
|
|
512
|
+
if (state.isFocus) {
|
|
513
|
+
refAutoPositionedPopup.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
|
|
514
|
+
console.log('AutoPositionedPopup measureInWindow x=', x, ' y=', y, ' width=', width, ' height=', height);
|
|
515
|
+
// SIMPLE CENTER-BASED POSITIONING STRATEGY
|
|
516
|
+
const screenHeight = Dimensions.get('screen').height;
|
|
517
|
+
const screenCenter = screenHeight / 2;
|
|
518
|
+
console.log('AutoPositionedPopup screenHeight=', screenHeight, ' screenCenter=', screenCenter, ' componentY=', y);
|
|
519
|
+
|
|
520
|
+
// Simple rule: if component Y > screen center, show popup above; otherwise show below
|
|
521
|
+
if (y+insets. top > screenCenter) {
|
|
522
|
+
console.log('AutoPositionedPopup: showing above (Y > center)');
|
|
523
|
+
ref_listPos.current = {x: x, y: y - listLayout.height, width: width};
|
|
524
|
+
} else {
|
|
525
|
+
console.log('AutoPositionedPopup: showing below (Y <= center)');
|
|
526
|
+
ref_listPos.current = {x: x, y: y + height+insets.top, width: width};
|
|
527
|
+
}
|
|
528
|
+
console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
|
|
529
|
+
if (CustomPopView && CustomPopViewStyle) {
|
|
530
|
+
console.log('AutoPositionedPopup CustomPopViewStyle=', CustomPopViewStyle);
|
|
531
|
+
// Ensure CustomPopViewStyle.height is a number before using it in calculations
|
|
532
|
+
const customHeight =
|
|
533
|
+
typeof CustomPopViewStyle.height === 'number' ? CustomPopViewStyle.height : listLayout.height;
|
|
534
|
+
|
|
535
|
+
// Apply same simple center-based strategy for CustomPopView
|
|
536
|
+
console.log('AutoPositionedPopup CustomPopView using center-based positioning, customHeight=', customHeight);
|
|
537
|
+
|
|
538
|
+
// Simple rule: if component Y > screen center, show popup above; otherwise show below
|
|
539
|
+
if (y > screenCenter) {
|
|
540
|
+
console.log('AutoPositionedPopup CustomPopView: showing above (Y > center), tag=', tag);
|
|
541
|
+
ref_listPos.current = {x: x, y: y - customHeight, width: width};
|
|
542
|
+
} else {
|
|
543
|
+
console.log('AutoPositionedPopup CustomPopView: showing below (Y <= center), tag=', tag);
|
|
544
|
+
ref_listPos.current = {x: x, y: y + height, width: width};
|
|
545
|
+
}
|
|
546
|
+
const PopViewComponent = CustomPopView();
|
|
547
|
+
console.log('AutoPositionedPopup addRootView PopViewComponent=', PopViewComponent);
|
|
548
|
+
console.log('AutoPositionedPopup addRootView state.selectedItem=', state.selectedItem);
|
|
549
|
+
addRootView({
|
|
550
|
+
id: tag,
|
|
551
|
+
style: !centerDisplay
|
|
552
|
+
? {
|
|
553
|
+
top: ref_listPos.current.y,
|
|
554
|
+
left: popUpViewStyle?.left,
|
|
555
|
+
width: popUpViewStyle?.width,
|
|
556
|
+
height: listLayout.height,
|
|
557
|
+
opacity: 1,
|
|
558
|
+
...CustomPopViewStyle,
|
|
559
|
+
}
|
|
560
|
+
: {width: popUpViewStyle?.width, height: listLayout.height, ...CustomPopViewStyle},
|
|
561
|
+
component: <PopViewComponent selectedItem={state.selectedItem}></PopViewComponent>,
|
|
562
|
+
useModal: true,
|
|
563
|
+
onModalClose: () => {
|
|
564
|
+
console.log('AutoPositionedPopup onModalClose');
|
|
565
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
566
|
+
setState((prevState) => {
|
|
567
|
+
return {
|
|
568
|
+
...prevState,
|
|
569
|
+
isFocus: false,
|
|
570
|
+
};
|
|
571
|
+
});
|
|
572
|
+
hasAddedRootView.current = false;
|
|
573
|
+
hasShownRootView.current = false;
|
|
574
|
+
hasTriggeredFocus.current = false;
|
|
575
|
+
setSearchQuery('');
|
|
576
|
+
},
|
|
577
|
+
centerDisplay,
|
|
578
|
+
});
|
|
579
|
+
} else {
|
|
580
|
+
console.log('AutoPositionedPopup addRootView tag=', tag);
|
|
581
|
+
addRootView({
|
|
582
|
+
id: tag,
|
|
583
|
+
style: {
|
|
584
|
+
top: ref_listPos.current.y,
|
|
585
|
+
left: popUpViewStyle?.left,
|
|
586
|
+
width: popUpViewStyle?.width,
|
|
587
|
+
height: listLayout.height,
|
|
588
|
+
opacity: 1,
|
|
589
|
+
},
|
|
590
|
+
component: (
|
|
591
|
+
<AutoPositionedPopupList
|
|
592
|
+
tag={tag}
|
|
593
|
+
updateState={updateState}
|
|
594
|
+
fetchData={fetchData}
|
|
595
|
+
pageSize={pageSize}
|
|
596
|
+
renderItem={renderItem}
|
|
597
|
+
selectedItem={state.selectedItem}
|
|
598
|
+
localSearch={localSearch}
|
|
599
|
+
/>
|
|
600
|
+
),
|
|
601
|
+
useModal: true,
|
|
602
|
+
onModalClose: () => {
|
|
603
|
+
console.log('AutoPositionedPopup onModalClose tag=', tag);
|
|
604
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
605
|
+
setState((prevState) => {
|
|
606
|
+
return {
|
|
607
|
+
...prevState,
|
|
608
|
+
};
|
|
609
|
+
});
|
|
610
|
+
setSearchQuery('');
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
}
|
|
471
616
|
}
|
|
472
|
-
|
|
473
|
-
|
|
617
|
+
if (isKeyboardFullyShown) {
|
|
618
|
+
ref_isFocus.current = state.isFocus;
|
|
619
|
+
if (isKeyboardFullyShown !== keyboardVisibleRef.current) {
|
|
620
|
+
keyboardVisibleRef.current = isKeyboardFullyShown;
|
|
621
|
+
if (isKeyboardFullyShown && textInputRef.current) {
|
|
622
|
+
if (ref_searchQuery.current) {
|
|
623
|
+
textInputRef.current.setNativeProps({text: ref_searchQuery.current});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}, [insets,
|
|
629
|
+
isKeyboardFullyShown,
|
|
630
|
+
state.isFocus,
|
|
631
|
+
useTextInput,
|
|
632
|
+
CustomPopView,
|
|
633
|
+
CustomPopViewStyle,
|
|
634
|
+
forceRemoveAllRootViewOnItemSelected,
|
|
635
|
+
tag,
|
|
636
|
+
state.selectedItem,
|
|
637
|
+
]);
|
|
474
638
|
// Imperative handle for parent component access
|
|
475
639
|
useImperativeHandle(
|
|
476
640
|
parentRef,
|
|
477
641
|
() => ({
|
|
478
642
|
clearSelectedItem: () => {
|
|
479
|
-
|
|
480
|
-
|
|
643
|
+
console.log('AutoPositionedPopup clearSelectedItem tag=', tag);
|
|
644
|
+
setState((prevState) => {
|
|
645
|
+
return {
|
|
646
|
+
...prevState,
|
|
647
|
+
selectedItem: undefined,
|
|
648
|
+
};
|
|
649
|
+
});
|
|
481
650
|
},
|
|
482
|
-
showPopup,
|
|
483
|
-
hidePopup,
|
|
484
651
|
}),
|
|
485
|
-
[
|
|
652
|
+
[]
|
|
486
653
|
);
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
654
|
+
const updateState = (key: string, value: SelectedItem) => {
|
|
655
|
+
console.log('AutoPositionedPopup updateState key=', key, ' value=', value);
|
|
656
|
+
setState((prevState) => ({
|
|
657
|
+
...prevState,
|
|
658
|
+
[key]: value,
|
|
659
|
+
}));
|
|
660
|
+
if (key === 'selectedItem' && onItemSelected) {
|
|
661
|
+
onItemSelected(value);
|
|
662
|
+
console.log('AutoPositionedPopup updateState onItemSelected rootViewsRef.current=', rootViewsRef.current);
|
|
663
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
664
|
+
hasAddedRootView.current = false;
|
|
665
|
+
hasShownRootView.current = false;
|
|
666
|
+
hasTriggeredFocus.current = false;
|
|
667
|
+
setState((prevState) => {
|
|
668
|
+
return {
|
|
669
|
+
...prevState,
|
|
670
|
+
isFocus: false,
|
|
671
|
+
};
|
|
672
|
+
});
|
|
673
|
+
setSearchQuery('');
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
// Render the component following project implementation
|
|
677
|
+
return useMemo(() => {
|
|
678
|
+
console.log('AutoPositionedPopup render tag=', tag); // Now safe - circular dependency fixed
|
|
679
|
+
return (
|
|
680
|
+
<CustomRow>
|
|
681
|
+
<View style={[styles.contain, style]} ref={refAutoPositionedPopup}>
|
|
682
|
+
{!state.isFocus || !useTextInput ? (
|
|
683
|
+
<TouchableOpacity
|
|
684
|
+
style={[styles.AutoPositionedPopupBtn, AutoPositionedPopupBtnStyle]}
|
|
685
|
+
disabled={AutoPositionedPopupBtnDisabled}
|
|
686
|
+
onPress={() => {
|
|
687
|
+
console.log('AutoPositionedPopup onPress tag=', tag);
|
|
688
|
+
console.log('AutoPositionedPopup onPress state.isFocus=', state.isFocus);
|
|
689
|
+
console.log('AutoPositionedPopup onPress useTextInput=', useTextInput);
|
|
690
|
+
console.log(
|
|
691
|
+
'AutoPositionedPopup onPress hasAddedRootView.current=',
|
|
692
|
+
hasAddedRootView.current
|
|
693
|
+
);
|
|
694
|
+
console.log(
|
|
695
|
+
'AutoPositionedPopup onPress hasShownRootView.current=',
|
|
696
|
+
hasShownRootView.current
|
|
697
|
+
);
|
|
698
|
+
console.log(
|
|
699
|
+
'AutoPositionedPopup onPress hasTriggeredFocus.current=',
|
|
700
|
+
hasTriggeredFocus.current
|
|
701
|
+
);
|
|
702
|
+
console.log('AutoPositionedPopup onPress state.selectedItem=', state.selectedItem);
|
|
703
|
+
setState((prevState) => {
|
|
704
|
+
return {
|
|
705
|
+
...prevState,
|
|
706
|
+
isFocus: true,
|
|
707
|
+
};
|
|
708
|
+
});
|
|
709
|
+
if (!hasAddedRootView.current && useTextInput) {
|
|
710
|
+
hasAddedRootView.current = true;
|
|
711
|
+
hasShownRootView.current = false;
|
|
712
|
+
addRootView({
|
|
713
|
+
id: tag,
|
|
714
|
+
style: {
|
|
715
|
+
top: 0,
|
|
716
|
+
left: 0,
|
|
717
|
+
width: popUpViewStyle?.width,
|
|
718
|
+
height: listLayout.height,
|
|
719
|
+
opacity: 0,
|
|
720
|
+
},
|
|
721
|
+
component: (
|
|
722
|
+
<AutoPositionedPopupList
|
|
723
|
+
tag={tag}
|
|
724
|
+
updateState={updateState}
|
|
725
|
+
fetchData={fetchData}
|
|
726
|
+
pageSize={pageSize}
|
|
727
|
+
renderItem={renderItem}
|
|
728
|
+
selectedItem={state.selectedItem}
|
|
729
|
+
localSearch={localSearch}
|
|
730
|
+
/>
|
|
731
|
+
),
|
|
732
|
+
useModal: false,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}}
|
|
517
736
|
>
|
|
518
|
-
{
|
|
519
|
-
|
|
737
|
+
{!btwChildren ? (
|
|
738
|
+
<Text
|
|
739
|
+
style={[
|
|
740
|
+
styles.searchQueryTxt,
|
|
741
|
+
state.selectedItem && {color: theme.colors.text},
|
|
742
|
+
labelStyle,
|
|
743
|
+
]}
|
|
744
|
+
numberOfLines={1}
|
|
745
|
+
ellipsizeMode={'tail'}
|
|
746
|
+
>
|
|
747
|
+
{state.selectedItem?.title || placeholder}
|
|
748
|
+
</Text>
|
|
749
|
+
) : (
|
|
750
|
+
btwChildren()
|
|
751
|
+
)}
|
|
752
|
+
</TouchableOpacity>
|
|
753
|
+
) : (
|
|
754
|
+
useTextInput &&
|
|
755
|
+
state.isFocus && (
|
|
756
|
+
<RNTextInput
|
|
757
|
+
ref={textInputRef}
|
|
758
|
+
key="fixed-textinput-key"
|
|
759
|
+
style={[
|
|
760
|
+
styles.inputStyle,
|
|
761
|
+
inputStyle,
|
|
762
|
+
]}
|
|
763
|
+
textAlign={TextInputProps['textAlign'] || 'left'}
|
|
764
|
+
multiline={TextInputProps['multiline'] || false}
|
|
765
|
+
numberOfLines={TextInputProps['numberOfLines'] || 1}
|
|
766
|
+
onChangeText={(searchQuery) => {
|
|
767
|
+
ref_searchQuery.current = searchQuery;
|
|
768
|
+
console.log('AutoPositionedPopup onChangeText rootViews=', rootViews);
|
|
769
|
+
if (!localSearch) {
|
|
770
|
+
if (debounceTimerRef.current) {
|
|
771
|
+
clearTimeout(debounceTimerRef.current);
|
|
772
|
+
}
|
|
773
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
774
|
+
emitQueryChange(ref_searchQuery.current);
|
|
775
|
+
}, 500);
|
|
776
|
+
} else {
|
|
777
|
+
emitQueryChange(ref_searchQuery.current);
|
|
778
|
+
}
|
|
779
|
+
}}
|
|
780
|
+
placeholderTextColor={theme.colors.placeholderText}
|
|
781
|
+
placeholder={placeholder}
|
|
782
|
+
onKeyPress={(e) => {
|
|
783
|
+
if (e.nativeEvent.key === 'Enter') {
|
|
784
|
+
Keyboard.dismiss();
|
|
785
|
+
}
|
|
786
|
+
}}
|
|
787
|
+
keyboardType={TextInputProps['keyboardType'] || 'default'}
|
|
788
|
+
clearButtonMode="while-editing"
|
|
789
|
+
returnKeyType={TextInputProps['returnKeyType'] || 'done'}
|
|
790
|
+
maxLength={TextInputProps['maxLength'] || 100}
|
|
791
|
+
accessibilityLabel="selectInput"
|
|
792
|
+
accessible={true}
|
|
793
|
+
autoFocus={TextInputProps['autoFocus'] || false}
|
|
794
|
+
autoCorrect={false}
|
|
795
|
+
underlineColorAndroid="transparent"
|
|
796
|
+
editable={TextInputProps['editable'] || true}
|
|
797
|
+
secureTextEntry={TextInputProps['secureTextEntry'] || false}
|
|
798
|
+
defaultValue=""
|
|
799
|
+
caretHidden={false}
|
|
800
|
+
enablesReturnKeyAutomatically
|
|
801
|
+
onFocus={() => {
|
|
802
|
+
console.log(
|
|
803
|
+
'AutoPositionedPopup onFocus tag=',
|
|
804
|
+
tag,
|
|
805
|
+
' selectedItem=',
|
|
806
|
+
state.selectedItem,
|
|
807
|
+
' hasTriggeredFocus.current=',
|
|
808
|
+
hasTriggeredFocus.current,
|
|
809
|
+
' textInputRef.current=',
|
|
810
|
+
textInputRef.current,
|
|
811
|
+
' ref_searchQuery.current=',
|
|
812
|
+
ref_searchQuery.current
|
|
813
|
+
);
|
|
814
|
+
if (!hasTriggeredFocus.current) {
|
|
815
|
+
hasTriggeredFocus.current = true;
|
|
816
|
+
ref_isFocus.current = true;
|
|
817
|
+
if (state.selectedItem) {
|
|
818
|
+
ref_searchQuery.current = state.selectedItem.title;
|
|
819
|
+
}
|
|
820
|
+
if (textInputRef.current && ref_searchQuery.current) {
|
|
821
|
+
textInputRef.current.setNativeProps({
|
|
822
|
+
text: ref_searchQuery.current,
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}}
|
|
827
|
+
onBlur={() => {
|
|
828
|
+
console.log(
|
|
829
|
+
'AutoPositionedPopup onBlur tag=',
|
|
830
|
+
tag,
|
|
831
|
+
'textInputRef.current=',
|
|
832
|
+
textInputRef.current
|
|
833
|
+
);
|
|
834
|
+
hasTriggeredFocus.current = false;
|
|
835
|
+
hasAddedRootView.current = false; // 重置 RootView 狀態
|
|
836
|
+
hasShownRootView.current = false;
|
|
837
|
+
ref_isFocus.current = false;
|
|
838
|
+
setState((prevState) => {
|
|
839
|
+
return {
|
|
840
|
+
...prevState,
|
|
841
|
+
isFocus: false,
|
|
842
|
+
};
|
|
843
|
+
});
|
|
844
|
+
removeRootView(tag, forceRemoveAllRootViewOnItemSelected);
|
|
845
|
+
setSearchQuery('');
|
|
846
|
+
if (textInputRef.current) {
|
|
847
|
+
textInputRef.current.setNativeProps({text: ''});
|
|
848
|
+
ref_searchQuery.current = '';
|
|
849
|
+
textInputRef.current.blur();
|
|
850
|
+
}
|
|
851
|
+
Keyboard.dismiss();
|
|
852
|
+
}}
|
|
853
|
+
selectTextOnFocus={TextInputProps['selectTextOnFocus'] || false}
|
|
854
|
+
onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
|
|
855
|
+
console.log(
|
|
856
|
+
'AutoPositionedPopup.tsx onSubmitEditing e.nativeEvent.text=',
|
|
857
|
+
e.nativeEvent.text
|
|
858
|
+
);
|
|
859
|
+
onSubmitEditing && onSubmitEditing(e);
|
|
860
|
+
}}
|
|
861
|
+
/>
|
|
862
|
+
)
|
|
520
863
|
)}
|
|
521
|
-
</
|
|
522
|
-
</
|
|
523
|
-
|
|
524
|
-
|
|
864
|
+
</View>
|
|
865
|
+
</CustomRow>
|
|
866
|
+
);
|
|
867
|
+
}, [tag,
|
|
868
|
+
fetchData,
|
|
869
|
+
renderItem,
|
|
870
|
+
onItemSelected,
|
|
871
|
+
onSubmitEditing,
|
|
872
|
+
localSearch,
|
|
873
|
+
placeholder,
|
|
874
|
+
textAlign,
|
|
875
|
+
pageSize,
|
|
876
|
+
selectedItem,
|
|
877
|
+
CustomRow,
|
|
878
|
+
useTextInput,
|
|
879
|
+
btwChildren,
|
|
880
|
+
selectedItem,
|
|
881
|
+
keyExtractor,
|
|
882
|
+
AutoPositionedPopupBtnStyle,
|
|
883
|
+
CustomPopView,
|
|
884
|
+
CustomPopViewStyle,
|
|
885
|
+
forceRemoveAllRootViewOnItemSelected,
|
|
886
|
+
inputStyle,
|
|
887
|
+
TextInputProps,
|
|
888
|
+
state.isFocus,]);
|
|
525
889
|
}
|
|
526
890
|
)
|
|
527
891
|
);
|