react-native-auto-positioned-popup 1.0.5 → 1.0.7
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 +410 -365
- 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 +599 -571
- 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,
|
|
@@ -19,12 +19,17 @@ import {
|
|
|
19
19
|
TouchableOpacity,
|
|
20
20
|
View,
|
|
21
21
|
} from 'react-native';
|
|
22
|
-
|
|
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';
|
|
23
26
|
import {TextInputSubmitEditingEventData} from 'react-native/Libraries/Components/TextInput/TextInput';
|
|
24
|
-
import {NativeSyntheticEvent} from 'react-native/Libraries/Types/CoreEventTypes';
|
|
27
|
+
import {LayoutRectangle, NativeSyntheticEvent} from 'react-native/Libraries/Types/CoreEventTypes';
|
|
25
28
|
import {AutoPositionedPopupProps, Data, SelectedItem} from './AutoPositionedPopupProps';
|
|
26
29
|
import styles from './AutoPositionedPopup.style';
|
|
27
30
|
import {useRootView} from './RootViewContext';
|
|
31
|
+
import {useKeyboardStatus} from './KeyboardManager';
|
|
32
|
+
import {useSafeAreaInsets} from "react-native-safe-area-context";
|
|
28
33
|
|
|
29
34
|
// Lightweight emitter to decouple TextInput and list without re-rendering context
|
|
30
35
|
type QueryListener = (query: string) => void;
|
|
@@ -63,144 +68,227 @@ const defaultTheme: Theme = {
|
|
|
63
68
|
|
|
64
69
|
// List item component for rendering individual items
|
|
65
70
|
const ListItem: React.FC<{
|
|
71
|
+
updateState: (key: string, value: SelectedItem) => void;
|
|
66
72
|
item: SelectedItem;
|
|
67
73
|
index: number;
|
|
68
74
|
selectedItem?: SelectedItem;
|
|
69
|
-
|
|
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
|
-
}
|
|
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
|
+
);
|
|
104
117
|
|
|
105
118
|
// Popup list component with AdvancedFlatList
|
|
106
|
-
interface
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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}
|
|
110
132
|
renderItem?: ({item, index}: { item: SelectedItem; index: number }) => React.ReactElement;
|
|
111
|
-
|
|
112
|
-
theme: Theme;
|
|
113
|
-
rootViewsRef?: React.MutableRefObject<any[]>;
|
|
114
|
-
fetchData?: (params: { pageIndex: number; pageSize: number; searchQuery?: string }) => Promise<Data | null>;
|
|
133
|
+
selectedItem?: SelectedItem;
|
|
115
134
|
localSearch?: boolean;
|
|
116
135
|
pageSize?: number;
|
|
117
|
-
onDataUpdate?: (newData: SelectedItem[]) => void;
|
|
118
|
-
selectedItemBackgroundColor?: string;
|
|
119
136
|
}
|
|
120
137
|
|
|
121
|
-
const
|
|
122
|
-
|
|
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
|
-
|
|
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
|
+
};
|
|
155
245
|
});
|
|
156
|
-
|
|
157
|
-
if (result?.items) {
|
|
158
|
-
setInternalData(result.items);
|
|
159
|
-
onDataUpdate?.(result.items);
|
|
160
|
-
}
|
|
161
|
-
} catch (error) {
|
|
162
|
-
console.error('PopupList fetchData error:', error);
|
|
163
246
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
item.title.toLowerCase().includes(newQuery.toLowerCase())
|
|
168
|
-
);
|
|
169
|
-
setInternalData(filtered);
|
|
170
|
-
onDataUpdate?.(filtered);
|
|
247
|
+
return Promise.resolve(res);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
console.warn('Error in fetchData:', e);
|
|
171
250
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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;
|
|
264
|
+
|
|
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
|
+
);
|
|
204
292
|
|
|
205
293
|
// State interface for AutoPositionedPopup
|
|
206
294
|
interface StateProps {
|
|
@@ -230,7 +318,30 @@ const AutoPositionedPopup: MemoExoticComponent<
|
|
|
230
318
|
inputStyle,
|
|
231
319
|
labelStyle,
|
|
232
320
|
popUpViewStyle = {left: '5%', width: '90%'},
|
|
233
|
-
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
|
+
},
|
|
234
345
|
renderItem,
|
|
235
346
|
onItemSelected,
|
|
236
347
|
localSearch = false,
|
|
@@ -239,60 +350,83 @@ const AutoPositionedPopup: MemoExoticComponent<
|
|
|
239
350
|
useTextInput = false,
|
|
240
351
|
btwChildren,
|
|
241
352
|
CustomRow = ({children}) => <View>{children}</View>,
|
|
242
|
-
keyExtractor = (item: any) => item?.id,
|
|
353
|
+
keyExtractor = (item: any) => String(item?.id || ''),
|
|
243
354
|
AutoPositionedPopupBtnDisabled = false,
|
|
244
355
|
forceRemoveAllRootViewOnItemSelected = false,
|
|
245
356
|
centerDisplay = false,
|
|
246
357
|
selectedItemBackgroundColor = 'rgba(116, 116, 128, 0.08)',
|
|
358
|
+
textAlign = 'right',
|
|
359
|
+
CustomPopView = undefined, CustomPopViewStyle
|
|
247
360
|
} = props;
|
|
248
|
-
|
|
249
361
|
// State management similar to project implementation
|
|
250
362
|
const [state, setState] = useState<StateProps>({
|
|
251
363
|
isFocus: false,
|
|
252
364
|
selectedItem: selectedItem,
|
|
253
365
|
});
|
|
254
|
-
|
|
255
366
|
// Use RootView context
|
|
256
|
-
const {addRootView, removeRootView, rootViews, setSearchQuery
|
|
367
|
+
const {addRootView, setRootViewNativeStyle, removeRootView, rootViews, setSearchQuery} = useRootView();
|
|
368
|
+
const insets = useSafeAreaInsets();
|
|
257
369
|
const rootViewsRef = useRef(rootViews);
|
|
258
|
-
|
|
259
370
|
// Track TextInput focus and RootView states like project implementation
|
|
260
371
|
const hasTriggeredFocus = useRef(false);
|
|
261
372
|
const hasAddedRootView = useRef(false);
|
|
262
373
|
const hasShownRootView = useRef(false);
|
|
263
|
-
|
|
264
374
|
// Additional refs for keyboard and position tracking
|
|
265
375
|
const ref_isFocus = useRef<boolean>();
|
|
266
376
|
const ref_isKeyboardFullyShown = useRef<boolean>();
|
|
267
|
-
const ref_listPos = useRef<
|
|
377
|
+
const ref_listPos: MutableRefObject<any> = useRef<LayoutRectangle>()
|
|
268
378
|
const keyboardVisibleRef = useRef(false);
|
|
269
379
|
const refAutoPositionedPopup = useRef<View>(null);
|
|
270
|
-
|
|
380
|
+
const ref_searchQuery = useRef<string>('');
|
|
271
381
|
// Simple keyboard status tracking (alternative to useKeyboardStatus hook)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
382
|
+
// Legacy state for compatibility
|
|
383
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
384
|
+
const [data, setData] = useState<SelectedItem[]>([]);
|
|
385
|
+
const [loading, setLoading] = useState(false);
|
|
386
|
+
const [popupPosition, setPopupPosition] = useState<{
|
|
387
|
+
top: number;
|
|
388
|
+
left: number;
|
|
389
|
+
width: number;
|
|
390
|
+
}>({top: 0, left: 0, width: 0});
|
|
391
|
+
// Refs for performance optimization
|
|
392
|
+
const containerRef = useRef<View>(null);
|
|
393
|
+
const textInputRef = useRef<RNTextInput>(null);
|
|
394
|
+
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
395
|
+
const searchQueryRef = useRef<string>(''); // Use ref instead of state to avoid re-renders
|
|
396
|
+
// Refs to store latest values for useEffect without adding to dependency array
|
|
397
|
+
const dataRef = useRef<SelectedItem[]>(data);
|
|
398
|
+
const isKeyboardFullyShown = useKeyboardStatus();
|
|
399
|
+
const theme = defaultTheme;
|
|
400
|
+
/**
|
|
401
|
+
* componentDidMount && componentWillUnmount
|
|
402
|
+
*/
|
|
275
403
|
useEffect(() => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
setIsKeyboardFullyShown(false);
|
|
282
|
-
});
|
|
283
|
-
|
|
404
|
+
(async () => {
|
|
405
|
+
})();
|
|
406
|
+
console.log(`AutoPositionedPopup componentDidMount tag=`, tag);
|
|
407
|
+
console.log('AutoPositionedPopup componentDidMount CustomPopView=', CustomPopView);
|
|
408
|
+
//componentWillUnmount
|
|
284
409
|
return () => {
|
|
285
|
-
|
|
286
|
-
|
|
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;
|
|
419
|
+
}
|
|
287
420
|
};
|
|
288
421
|
}, []);
|
|
289
|
-
|
|
290
422
|
useEffect(() => {
|
|
291
423
|
console.log('AutoPositionedPopup rootViews=', rootViews);
|
|
292
424
|
rootViewsRef.current = rootViews;
|
|
293
425
|
if (rootViews.length === 0) {
|
|
294
426
|
hasAddedRootView.current = false;
|
|
295
427
|
hasShownRootView.current = false;
|
|
428
|
+
ref_isFocus.current = false;
|
|
429
|
+
ref_isKeyboardFullyShown.current = false;
|
|
296
430
|
hasTriggeredFocus.current = false;
|
|
297
431
|
setState((prevState) => {
|
|
298
432
|
return {
|
|
@@ -302,8 +436,6 @@ const AutoPositionedPopup: MemoExoticComponent<
|
|
|
302
436
|
});
|
|
303
437
|
}
|
|
304
438
|
}, [rootViews]);
|
|
305
|
-
|
|
306
|
-
// Sync selectedItem changes like project implementation
|
|
307
439
|
useEffect(() => {
|
|
308
440
|
console.log('AutoPositionedPopup useEffect tag=', tag);
|
|
309
441
|
console.log('AutoPositionedPopup useEffect selectedItem=', selectedItem);
|
|
@@ -318,323 +450,6 @@ const AutoPositionedPopup: MemoExoticComponent<
|
|
|
318
450
|
});
|
|
319
451
|
}
|
|
320
452
|
}, [selectedItem, state.selectedItem, tag]);
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
// Legacy state for compatibility
|
|
324
|
-
const [isVisible, setIsVisible] = useState(false);
|
|
325
|
-
const [data, setData] = useState<SelectedItem[]>([]);
|
|
326
|
-
const [loading, setLoading] = useState(false);
|
|
327
|
-
const [popupPosition, setPopupPosition] = useState<{
|
|
328
|
-
top: number;
|
|
329
|
-
left: number;
|
|
330
|
-
width: number;
|
|
331
|
-
}>({top: 0, left: 0, width: 0});
|
|
332
|
-
const popupId = useRef(`popup-${tag}-${Date.now()}`);
|
|
333
|
-
|
|
334
|
-
// Refs for performance optimization
|
|
335
|
-
const containerRef = useRef<View>(null);
|
|
336
|
-
const textInputRef = useRef<RNTextInput>(null);
|
|
337
|
-
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
338
|
-
const searchQueryRef = useRef<string>(''); // Use ref instead of state to avoid re-renders
|
|
339
|
-
|
|
340
|
-
// Constants
|
|
341
|
-
const LIST_HEIGHT = 200;
|
|
342
|
-
const theme = defaultTheme;
|
|
343
|
-
|
|
344
|
-
// Fetch data function
|
|
345
|
-
const loadData = useCallback(async (query: string = '') => {
|
|
346
|
-
if (!fetchData) return;
|
|
347
|
-
|
|
348
|
-
setLoading(true);
|
|
349
|
-
try {
|
|
350
|
-
const result = await fetchData({
|
|
351
|
-
pageIndex: 0,
|
|
352
|
-
pageSize,
|
|
353
|
-
searchQuery: query,
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
if (result?.items) {
|
|
357
|
-
setData(result.items);
|
|
358
|
-
}
|
|
359
|
-
} catch (error) {
|
|
360
|
-
console.error('Error loading data:', error);
|
|
361
|
-
} finally {
|
|
362
|
-
setLoading(false);
|
|
363
|
-
}
|
|
364
|
-
}, [fetchData, pageSize]);
|
|
365
|
-
|
|
366
|
-
// Handle search query change with debounce and event emission
|
|
367
|
-
const handleSearchChange = useCallback((query: string) => {
|
|
368
|
-
// Store in ref to avoid re-renders
|
|
369
|
-
searchQueryRef.current = query;
|
|
370
|
-
|
|
371
|
-
// Update TextInput value directly if needed
|
|
372
|
-
if (textInputRef.current) {
|
|
373
|
-
// The TextInput's value will be controlled by its own state
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Clear previous debounce timer
|
|
377
|
-
if (debounceTimerRef.current) {
|
|
378
|
-
clearTimeout(debounceTimerRef.current);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Use debounce for performance optimization
|
|
382
|
-
debounceTimerRef.current = setTimeout(() => {
|
|
383
|
-
// Emit query change event to decouple components and avoid context re-rendering
|
|
384
|
-
emitQueryChange(searchQueryRef.current);
|
|
385
|
-
}, 300); // Use 300ms debounce like the original
|
|
386
|
-
}, []);
|
|
387
|
-
|
|
388
|
-
// Calculate popup position
|
|
389
|
-
const calculatePosition = useCallback(() => {
|
|
390
|
-
if (!containerRef.current) return;
|
|
391
|
-
|
|
392
|
-
containerRef.current.measureInWindow((x, y, width, height) => {
|
|
393
|
-
const screenHeight = Dimensions.get('screen').height;
|
|
394
|
-
const screenWidth = Dimensions.get('screen').width;
|
|
395
|
-
|
|
396
|
-
let top = y + height;
|
|
397
|
-
let left = x;
|
|
398
|
-
let popupWidth = width;
|
|
399
|
-
|
|
400
|
-
// Check if popup should appear above the input
|
|
401
|
-
if (y + height + LIST_HEIGHT > screenHeight) {
|
|
402
|
-
top = y - LIST_HEIGHT;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Adjust horizontal position if needed
|
|
406
|
-
if (popUpViewStyle?.left && popUpViewStyle?.width) {
|
|
407
|
-
const leftPercent = parseFloat(String(popUpViewStyle.left).replace('%', '')) / 100;
|
|
408
|
-
const widthPercent = parseFloat(String(popUpViewStyle.width).replace('%', '')) / 100;
|
|
409
|
-
left = screenWidth * leftPercent;
|
|
410
|
-
popupWidth = screenWidth * widthPercent;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
setPopupPosition({top, left, width: popupWidth});
|
|
414
|
-
});
|
|
415
|
-
}, [popUpViewStyle]);
|
|
416
|
-
|
|
417
|
-
// Hide popup using RootView
|
|
418
|
-
const hidePopup = useCallback(() => {
|
|
419
|
-
setIsVisible(false);
|
|
420
|
-
// Reset search query
|
|
421
|
-
searchQueryRef.current = '';
|
|
422
|
-
if (textInputRef.current) {
|
|
423
|
-
textInputRef.current.blur();
|
|
424
|
-
textInputRef.current.clear?.(); // Clear the TextInput
|
|
425
|
-
}
|
|
426
|
-
removeRootView(popupId.current, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
427
|
-
}, [removeRootView, forceRemoveAllRootViewOnItemSelected]);
|
|
428
|
-
|
|
429
|
-
// Handle data updates from PopupList
|
|
430
|
-
const handleDataUpdate = useCallback((newData: SelectedItem[]) => {
|
|
431
|
-
setData(newData);
|
|
432
|
-
}, []);
|
|
433
|
-
|
|
434
|
-
// Handle item selection
|
|
435
|
-
const handleItemPress = useCallback((item: SelectedItem) => {
|
|
436
|
-
onItemSelected?.(item);
|
|
437
|
-
hidePopup();
|
|
438
|
-
}, [onItemSelected, hidePopup]);
|
|
439
|
-
|
|
440
|
-
// Show popup using RootView
|
|
441
|
-
const showPopup = useCallback(() => {
|
|
442
|
-
calculatePosition();
|
|
443
|
-
setIsVisible(true);
|
|
444
|
-
loadData(searchQueryRef.current);
|
|
445
|
-
|
|
446
|
-
// Wait for position to be calculated
|
|
447
|
-
setTimeout(() => {
|
|
448
|
-
const popupComponent = (
|
|
449
|
-
<TouchableOpacity
|
|
450
|
-
style={{
|
|
451
|
-
flex: 1,
|
|
452
|
-
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
453
|
-
}}
|
|
454
|
-
activeOpacity={1}
|
|
455
|
-
onPress={hidePopup}
|
|
456
|
-
>
|
|
457
|
-
<View
|
|
458
|
-
style={{
|
|
459
|
-
position: 'absolute',
|
|
460
|
-
top: popupPosition.top,
|
|
461
|
-
left: popupPosition.left,
|
|
462
|
-
width: popupPosition.width,
|
|
463
|
-
height: LIST_HEIGHT,
|
|
464
|
-
backgroundColor: theme.colors.background,
|
|
465
|
-
borderRadius: 8,
|
|
466
|
-
shadowColor: '#000',
|
|
467
|
-
shadowOffset: {width: 0, height: 2},
|
|
468
|
-
shadowOpacity: 0.25,
|
|
469
|
-
shadowRadius: 3.84,
|
|
470
|
-
elevation: 5,
|
|
471
|
-
}}
|
|
472
|
-
>
|
|
473
|
-
{useTextInput && (
|
|
474
|
-
<RNTextInput
|
|
475
|
-
ref={textInputRef}
|
|
476
|
-
style={[
|
|
477
|
-
styles.inputStyle,
|
|
478
|
-
{
|
|
479
|
-
height: 40,
|
|
480
|
-
borderBottomWidth: 1,
|
|
481
|
-
borderBottomColor: theme.colors.border,
|
|
482
|
-
paddingHorizontal: 12,
|
|
483
|
-
color: theme.colors.text,
|
|
484
|
-
},
|
|
485
|
-
inputStyle,
|
|
486
|
-
]}
|
|
487
|
-
placeholder={placeholder}
|
|
488
|
-
placeholderTextColor={theme.colors.placeholderText}
|
|
489
|
-
defaultValue={searchQueryRef.current}
|
|
490
|
-
onChangeText={handleSearchChange}
|
|
491
|
-
onSubmitEditing={(e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
|
|
492
|
-
onSubmitEditing?.(e);
|
|
493
|
-
Keyboard.dismiss();
|
|
494
|
-
}}
|
|
495
|
-
returnKeyType="done"
|
|
496
|
-
{...TextInputProps}
|
|
497
|
-
/>
|
|
498
|
-
)}
|
|
499
|
-
|
|
500
|
-
<PopupList
|
|
501
|
-
data={data}
|
|
502
|
-
selectedItem={selectedItem}
|
|
503
|
-
onItemPress={handleItemPress}
|
|
504
|
-
renderItem={renderItem}
|
|
505
|
-
keyExtractor={keyExtractor}
|
|
506
|
-
theme={theme}
|
|
507
|
-
rootViewsRef={rootViewsRef}
|
|
508
|
-
fetchData={fetchData}
|
|
509
|
-
localSearch={localSearch}
|
|
510
|
-
pageSize={pageSize}
|
|
511
|
-
onDataUpdate={handleDataUpdate}
|
|
512
|
-
selectedItemBackgroundColor={selectedItemBackgroundColor}
|
|
513
|
-
/>
|
|
514
|
-
</View>
|
|
515
|
-
</TouchableOpacity>
|
|
516
|
-
);
|
|
517
|
-
|
|
518
|
-
addRootView({
|
|
519
|
-
id: popupId.current,
|
|
520
|
-
style: {
|
|
521
|
-
position: 'absolute',
|
|
522
|
-
top: 0,
|
|
523
|
-
left: 0,
|
|
524
|
-
right: 0,
|
|
525
|
-
bottom: 0,
|
|
526
|
-
},
|
|
527
|
-
component: popupComponent,
|
|
528
|
-
useModal: true,
|
|
529
|
-
onModalClose: hidePopup,
|
|
530
|
-
centerDisplay: centerDisplay,
|
|
531
|
-
});
|
|
532
|
-
}, 100);
|
|
533
|
-
}, [calculatePosition, loadData, popupPosition, useTextInput, placeholder, theme, inputStyle, TextInputProps, data, selectedItem, renderItem, keyExtractor, centerDisplay, addRootView, hidePopup, handleSearchChange, handleItemPress, LIST_HEIGHT, selectedItemBackgroundColor]);
|
|
534
|
-
|
|
535
|
-
// Handle button press - following project implementation logic
|
|
536
|
-
const handleButtonPress = useCallback(() => {
|
|
537
|
-
if (AutoPositionedPopupBtnDisabled) return;
|
|
538
|
-
|
|
539
|
-
console.log('AutoPositionedPopup onPress tag=', tag);
|
|
540
|
-
console.log('AutoPositionedPopup onPress state.isFocus=', state.isFocus);
|
|
541
|
-
console.log('AutoPositionedPopup onPress useTextInput=', useTextInput);
|
|
542
|
-
console.log(
|
|
543
|
-
'AutoPositionedPopup onPress hasAddedRootView.current=',
|
|
544
|
-
hasAddedRootView.current
|
|
545
|
-
);
|
|
546
|
-
console.log(
|
|
547
|
-
'AutoPositionedPopup onPress hasShownRootView.current=',
|
|
548
|
-
hasShownRootView.current
|
|
549
|
-
);
|
|
550
|
-
console.log(
|
|
551
|
-
'AutoPositionedPopup onPress hasTriggeredFocus.current=',
|
|
552
|
-
hasTriggeredFocus.current
|
|
553
|
-
);
|
|
554
|
-
console.log('AutoPositionedPopup onPress state.selectedItem=', state.selectedItem);
|
|
555
|
-
|
|
556
|
-
setState((prevState) => {
|
|
557
|
-
return {
|
|
558
|
-
...prevState,
|
|
559
|
-
isFocus: true,
|
|
560
|
-
};
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
if (!hasAddedRootView.current && useTextInput) {
|
|
564
|
-
// TextInput version: hide first, show after keyboard fully appears
|
|
565
|
-
hasAddedRootView.current = true;
|
|
566
|
-
hasShownRootView.current = false;
|
|
567
|
-
addRootView({
|
|
568
|
-
id: tag,
|
|
569
|
-
style: {
|
|
570
|
-
top: 0,
|
|
571
|
-
left: 0,
|
|
572
|
-
width: popUpViewStyle?.width,
|
|
573
|
-
height: listLayout.height,
|
|
574
|
-
opacity: 0,
|
|
575
|
-
},
|
|
576
|
-
component: (
|
|
577
|
-
<PopupList
|
|
578
|
-
data={data}
|
|
579
|
-
selectedItem={state.selectedItem}
|
|
580
|
-
onItemPress={handleItemPress}
|
|
581
|
-
renderItem={renderItem}
|
|
582
|
-
keyExtractor={keyExtractor}
|
|
583
|
-
theme={theme}
|
|
584
|
-
rootViewsRef={rootViewsRef}
|
|
585
|
-
fetchData={fetchData}
|
|
586
|
-
localSearch={localSearch}
|
|
587
|
-
pageSize={pageSize}
|
|
588
|
-
onDataUpdate={handleDataUpdate}
|
|
589
|
-
selectedItemBackgroundColor={selectedItemBackgroundColor}
|
|
590
|
-
/>
|
|
591
|
-
),
|
|
592
|
-
useModal: false,
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
}, [AutoPositionedPopupBtnDisabled, useTextInput, state.isFocus, state.selectedItem, tag, hasAddedRootView, hasShownRootView, hasTriggeredFocus, addRootView, popUpViewStyle, data, handleItemPress, renderItem, keyExtractor, theme, rootViewsRef, fetchData, localSearch, pageSize, handleDataUpdate, selectedItemBackgroundColor]);
|
|
596
|
-
|
|
597
|
-
// Imperative handle for parent component access
|
|
598
|
-
useImperativeHandle(
|
|
599
|
-
parentRef,
|
|
600
|
-
() => ({
|
|
601
|
-
clearSelectedItem: () => {
|
|
602
|
-
// Clear selection logic can be implemented here
|
|
603
|
-
console.log('Clearing selected item for:', tag);
|
|
604
|
-
},
|
|
605
|
-
showPopup,
|
|
606
|
-
hidePopup,
|
|
607
|
-
}),
|
|
608
|
-
[tag, showPopup, hidePopup]
|
|
609
|
-
);
|
|
610
|
-
|
|
611
|
-
// Component lifecycle management like project implementation
|
|
612
|
-
useEffect(() => {
|
|
613
|
-
console.log(`AutoPositionedPopup componentDidMount tag=`, tag);
|
|
614
|
-
|
|
615
|
-
//componentWillUnmount
|
|
616
|
-
return () => {
|
|
617
|
-
console.log(`AutoPositionedPopup componentWillUnmount tag=`, tag);
|
|
618
|
-
removeRootView(tag, forceRemoveAllRootViewOnItemSelected, rootViewsRef.current);
|
|
619
|
-
setContextSearchQuery('');
|
|
620
|
-
if (textInputRef.current) {
|
|
621
|
-
textInputRef.current.blur();
|
|
622
|
-
hasTriggeredFocus.current = false;
|
|
623
|
-
hasAddedRootView.current = false;
|
|
624
|
-
hasShownRootView.current = false;
|
|
625
|
-
}
|
|
626
|
-
};
|
|
627
|
-
}, [tag, removeRootView, forceRemoveAllRootViewOnItemSelected, setContextSearchQuery]);
|
|
628
|
-
|
|
629
|
-
// Cleanup debounce timer
|
|
630
|
-
useEffect(() => {
|
|
631
|
-
return () => {
|
|
632
|
-
if (debounceTimerRef.current) {
|
|
633
|
-
clearTimeout(debounceTimerRef.current);
|
|
634
|
-
}
|
|
635
|
-
};
|
|
636
|
-
}, []);
|
|
637
|
-
|
|
638
453
|
useEffect(() => {
|
|
639
454
|
console.log('AutoPositionedPopup useEffect tag=', tag);
|
|
640
455
|
console.log('AutoPositionedPopup useEffect state.isFocus=', state.isFocus);
|
|
@@ -648,26 +463,26 @@ const AutoPositionedPopup: MemoExoticComponent<
|
|
|
648
463
|
console.log('AutoPositionedPopup useEffect TextInputProps=', TextInputProps);
|
|
649
464
|
console.log('AutoPositionedPopup useEffect hasAddedRootView.current=', hasAddedRootView.current);
|
|
650
465
|
console.log('AutoPositionedPopup useEffect hasShownRootView.current=', hasShownRootView.current);
|
|
651
|
-
|
|
652
466
|
if (useTextInput) {
|
|
653
467
|
if (isKeyboardFullyShown && hasAddedRootView.current && !hasShownRootView.current && state.isFocus) {
|
|
654
468
|
refAutoPositionedPopup.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
|
|
655
469
|
console.log('AutoPositionedPopup measureInWindow x=', x, ' y=', y, ' width=', width, ' height=', height);
|
|
656
|
-
//
|
|
470
|
+
// SIMPLE CENTER-BASED POSITIONING STRATEGY
|
|
657
471
|
const screenHeight = Dimensions.get('screen').height;
|
|
658
|
-
|
|
472
|
+
const screenCenter = screenHeight / 2;
|
|
473
|
+
console.log('AutoPositionedPopup screenHeight=', screenHeight, ' screenCenter=', screenCenter, ' componentY=', y);
|
|
659
474
|
|
|
660
|
-
if
|
|
661
|
-
|
|
662
|
-
|
|
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};
|
|
663
479
|
} else {
|
|
664
|
-
console.log('AutoPositionedPopup
|
|
665
|
-
ref_listPos.current = {
|
|
480
|
+
console.log('AutoPositionedPopup with keyboard: showing below (Y <= center)');
|
|
481
|
+
ref_listPos.current = {x: x, y: y + height, width: width};
|
|
666
482
|
}
|
|
667
483
|
console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
|
|
668
|
-
|
|
669
484
|
setRootViewNativeStyle(tag, {
|
|
670
|
-
top: ref_listPos.current
|
|
485
|
+
top: ref_listPos.current?.y,
|
|
671
486
|
left: popUpViewStyle?.left,
|
|
672
487
|
width: popUpViewStyle?.width,
|
|
673
488
|
height: listLayout.height,
|
|
@@ -689,7 +504,7 @@ const AutoPositionedPopup: MemoExoticComponent<
|
|
|
689
504
|
isFocus: false,
|
|
690
505
|
};
|
|
691
506
|
});
|
|
692
|
-
|
|
507
|
+
setSearchQuery('');
|
|
693
508
|
hasAddedRootView.current = false;
|
|
694
509
|
hasShownRootView.current = false;
|
|
695
510
|
}
|
|
@@ -697,107 +512,170 @@ const AutoPositionedPopup: MemoExoticComponent<
|
|
|
697
512
|
if (state.isFocus) {
|
|
698
513
|
refAutoPositionedPopup.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
|
|
699
514
|
console.log('AutoPositionedPopup measureInWindow x=', x, ' y=', y, ' width=', width, ' height=', height);
|
|
700
|
-
//
|
|
515
|
+
// SIMPLE CENTER-BASED POSITIONING STRATEGY
|
|
701
516
|
const screenHeight = Dimensions.get('screen').height;
|
|
702
|
-
|
|
517
|
+
const screenCenter = screenHeight / 2;
|
|
518
|
+
console.log('AutoPositionedPopup screenHeight=', screenHeight, ' screenCenter=', screenCenter, ' componentY=', y);
|
|
703
519
|
|
|
704
|
-
if
|
|
705
|
-
|
|
706
|
-
|
|
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};
|
|
707
524
|
} else {
|
|
708
|
-
console.log('AutoPositionedPopup
|
|
709
|
-
ref_listPos.current = {
|
|
525
|
+
console.log('AutoPositionedPopup: showing below (Y <= center)');
|
|
526
|
+
ref_listPos.current = {x: x, y: y + height+insets.top, width: width};
|
|
710
527
|
}
|
|
711
528
|
console.log('AutoPositionedPopup ref_listPos.current=', ref_listPos.current);
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
+
}
|
|
756
614
|
});
|
|
757
615
|
}
|
|
758
616
|
}
|
|
759
|
-
|
|
760
617
|
if (isKeyboardFullyShown) {
|
|
761
618
|
ref_isFocus.current = state.isFocus;
|
|
762
619
|
if (isKeyboardFullyShown !== keyboardVisibleRef.current) {
|
|
763
620
|
keyboardVisibleRef.current = isKeyboardFullyShown;
|
|
764
|
-
// Ensure TextInput has correct focus when keyboard is fully shown
|
|
765
621
|
if (isKeyboardFullyShown && textInputRef.current) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
textInputRef.current.setNativeProps({ text: searchQueryRef.current });
|
|
622
|
+
if (ref_searchQuery.current) {
|
|
623
|
+
textInputRef.current.setNativeProps({text: ref_searchQuery.current});
|
|
769
624
|
}
|
|
770
625
|
}
|
|
771
626
|
}
|
|
772
627
|
}
|
|
773
|
-
}, [
|
|
628
|
+
}, [insets,
|
|
774
629
|
isKeyboardFullyShown,
|
|
775
630
|
state.isFocus,
|
|
776
631
|
useTextInput,
|
|
632
|
+
CustomPopView,
|
|
633
|
+
CustomPopViewStyle,
|
|
777
634
|
forceRemoveAllRootViewOnItemSelected,
|
|
778
635
|
tag,
|
|
779
636
|
state.selectedItem,
|
|
780
|
-
popUpViewStyle,
|
|
781
|
-
data,
|
|
782
|
-
handleItemPress,
|
|
783
|
-
renderItem,
|
|
784
|
-
keyExtractor,
|
|
785
|
-
theme,
|
|
786
|
-
rootViewsRef,
|
|
787
|
-
fetchData,
|
|
788
|
-
localSearch,
|
|
789
|
-
pageSize,
|
|
790
|
-
handleDataUpdate,
|
|
791
|
-
selectedItemBackgroundColor,
|
|
792
|
-
removeRootView,
|
|
793
|
-
setContextSearchQuery,
|
|
794
|
-
addRootView,
|
|
795
|
-
centerDisplay,
|
|
796
|
-
setRootViewNativeStyle,
|
|
797
637
|
]);
|
|
798
|
-
|
|
638
|
+
// Imperative handle for parent component access
|
|
639
|
+
useImperativeHandle(
|
|
640
|
+
parentRef,
|
|
641
|
+
() => ({
|
|
642
|
+
clearSelectedItem: () => {
|
|
643
|
+
console.log('AutoPositionedPopup clearSelectedItem tag=', tag);
|
|
644
|
+
setState((prevState) => {
|
|
645
|
+
return {
|
|
646
|
+
...prevState,
|
|
647
|
+
selectedItem: undefined,
|
|
648
|
+
};
|
|
649
|
+
});
|
|
650
|
+
},
|
|
651
|
+
}),
|
|
652
|
+
[]
|
|
653
|
+
);
|
|
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
|
+
};
|
|
799
676
|
// Render the component following project implementation
|
|
800
677
|
return useMemo(() => {
|
|
678
|
+
console.log('AutoPositionedPopup render tag=', tag); // Now safe - circular dependency fixed
|
|
801
679
|
return (
|
|
802
680
|
<CustomRow>
|
|
803
681
|
<View style={[styles.contain, style]} ref={refAutoPositionedPopup}>
|
|
@@ -805,13 +683,62 @@ const AutoPositionedPopup: MemoExoticComponent<
|
|
|
805
683
|
<TouchableOpacity
|
|
806
684
|
style={[styles.AutoPositionedPopupBtn, AutoPositionedPopupBtnStyle]}
|
|
807
685
|
disabled={AutoPositionedPopupBtnDisabled}
|
|
808
|
-
onPress={
|
|
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
|
+
}}
|
|
809
736
|
>
|
|
810
737
|
{!btwChildren ? (
|
|
811
738
|
<Text
|
|
812
739
|
style={[
|
|
813
740
|
styles.searchQueryTxt,
|
|
814
|
-
state.selectedItem && {
|
|
741
|
+
state.selectedItem && {color: theme.colors.text},
|
|
815
742
|
labelStyle,
|
|
816
743
|
]}
|
|
817
744
|
numberOfLines={1}
|
|
@@ -831,33 +758,134 @@ const AutoPositionedPopup: MemoExoticComponent<
|
|
|
831
758
|
key="fixed-textinput-key"
|
|
832
759
|
style={[
|
|
833
760
|
styles.inputStyle,
|
|
834
|
-
{
|
|
835
|
-
textAlignVertical: 'center',
|
|
836
|
-
paddingVertical: 0,
|
|
837
|
-
paddingHorizontal: 0,
|
|
838
|
-
},
|
|
839
761
|
inputStyle,
|
|
840
762
|
]}
|
|
841
763
|
textAlign={TextInputProps['textAlign'] || 'left'}
|
|
842
764
|
multiline={TextInputProps['multiline'] || false}
|
|
843
765
|
numberOfLines={TextInputProps['numberOfLines'] || 1}
|
|
844
|
-
|
|
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
|
+
}}
|
|
845
780
|
placeholderTextColor={theme.colors.placeholderText}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
+
}
|
|
850
851
|
Keyboard.dismiss();
|
|
851
852
|
}}
|
|
852
|
-
|
|
853
|
-
{
|
|
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
|
+
}}
|
|
854
861
|
/>
|
|
855
862
|
)
|
|
856
863
|
)}
|
|
857
864
|
</View>
|
|
858
865
|
</CustomRow>
|
|
859
866
|
);
|
|
860
|
-
}, [
|
|
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,]);
|
|
861
889
|
}
|
|
862
890
|
)
|
|
863
891
|
);
|