react-native-auto-positioned-popup 1.0.2
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/LICENSE +21 -0
- package/README.md +425 -0
- package/README_zh.md +425 -0
- package/lib/AutoPositionedPopup.d.ts +5 -0
- package/lib/AutoPositionedPopup.d.ts.map +1 -0
- package/lib/AutoPositionedPopup.js +306 -0
- package/lib/AutoPositionedPopup.style.d.ts +80 -0
- package/lib/AutoPositionedPopup.style.d.ts.map +1 -0
- package/lib/AutoPositionedPopup.style.js +79 -0
- package/lib/AutoPositionedPopupProps.d.ts +58 -0
- package/lib/AutoPositionedPopupProps.d.ts.map +1 -0
- package/lib/AutoPositionedPopupProps.js +1 -0
- package/lib/RootViewContext.d.ts +31 -0
- package/lib/RootViewContext.d.ts.map +1 -0
- package/lib/RootViewContext.js +136 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +7 -0
- package/package.json +82 -0
- package/src/AutoPositionedPopup.style.ts +80 -0
- package/src/AutoPositionedPopup.tsx +529 -0
- package/src/AutoPositionedPopupProps.ts +61 -0
- package/src/RootViewContext.tsx +186 -0
- package/src/index.ts +16 -0
package/README_zh.md
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# React Native 自动定位弹窗组件
|
|
2
|
+
|
|
3
|
+
一个高度可定制的 React Native 自动定位弹窗组件,具有搜索功能和灵活的样式选项。非常适合用作下拉菜单、自动完成输入框和选择列表。
|
|
4
|
+
|
|
5
|
+
[English](./README.md) | 中文
|
|
6
|
+
|
|
7
|
+
## 特性
|
|
8
|
+
|
|
9
|
+
🚀 **自动定位**: 根据屏幕空间自动调整弹窗位置
|
|
10
|
+
🔍 **搜索功能**: 内置防抖搜索功能
|
|
11
|
+
📱 **跨平台**: 同时支持 iOS 和 Android
|
|
12
|
+
🎨 **可定制**: 丰富的样式和主题选项
|
|
13
|
+
⚡ **性能优化**: 使用 AdvancedFlatList 高效渲染
|
|
14
|
+
🎯 **TypeScript 支持**: 包含完整的 TypeScript 类型定义
|
|
15
|
+
🔄 **动态视图管理**: 基于 RootView 的弹窗系统
|
|
16
|
+
|
|
17
|
+
## 安装
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install react-native-auto-positioned-popup
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
或者
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
yarn add react-native-auto-positioned-popup
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 基本用法
|
|
30
|
+
|
|
31
|
+
首先,使用 `RootViewProvider` 包裹你的应用:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { RootViewProvider } from 'react-native-auto-positioned-popup';
|
|
35
|
+
|
|
36
|
+
const App = () => {
|
|
37
|
+
return (
|
|
38
|
+
<RootViewProvider>
|
|
39
|
+
{/* 你的应用内容 */}
|
|
40
|
+
</RootViewProvider>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
然后使用 `AutoPositionedPopup` 组件:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import React, { useState } from 'react';
|
|
49
|
+
import { View } from 'react-native';
|
|
50
|
+
import AutoPositionedPopup, { SelectedItem, Data } from 'react-native-auto-positioned-popup';
|
|
51
|
+
|
|
52
|
+
const MyComponent = () => {
|
|
53
|
+
const [selectedItem, setSelectedItem] = useState<SelectedItem | undefined>();
|
|
54
|
+
|
|
55
|
+
const fetchData = async ({ pageIndex, pageSize, searchQuery }): Promise<Data | null> => {
|
|
56
|
+
// 你的数据获取逻辑
|
|
57
|
+
return {
|
|
58
|
+
items: [
|
|
59
|
+
{ id: '1', title: '选项 1' },
|
|
60
|
+
{ id: '2', title: '选项 2' },
|
|
61
|
+
{ id: '3', title: '选项 3' },
|
|
62
|
+
],
|
|
63
|
+
pageIndex: 0,
|
|
64
|
+
needLoadMore: false,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<View style={{ padding: 20 }}>
|
|
70
|
+
<AutoPositionedPopup
|
|
71
|
+
tag="example-popup"
|
|
72
|
+
placeholder="请选择一个选项"
|
|
73
|
+
selectedItem={selectedItem}
|
|
74
|
+
fetchData={fetchData}
|
|
75
|
+
onItemSelected={(item) => setSelectedItem(item)}
|
|
76
|
+
useTextInput={true}
|
|
77
|
+
/>
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default MyComponent;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 高级用法
|
|
86
|
+
|
|
87
|
+
### 自定义行组件
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
<AutoPositionedPopup
|
|
91
|
+
tag="custom-popup"
|
|
92
|
+
CustomRow={({ children }) => (
|
|
93
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', padding: 10 }}>
|
|
94
|
+
<Text style={{ marginRight: 10 }}>选择:</Text>
|
|
95
|
+
{children}
|
|
96
|
+
</View>
|
|
97
|
+
)}
|
|
98
|
+
// ... 其他属性
|
|
99
|
+
/>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 自定义项目渲染
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<AutoPositionedPopup
|
|
106
|
+
tag="custom-items"
|
|
107
|
+
renderItem={({ item, index }) => (
|
|
108
|
+
<View style={{ padding: 15, borderBottomWidth: 1 }}>
|
|
109
|
+
<Text style={{ fontWeight: 'bold' }}>{item.title}</Text>
|
|
110
|
+
<Text style={{ color: '#666', fontSize: 12 }}>ID: {item.id}</Text>
|
|
111
|
+
</View>
|
|
112
|
+
)}
|
|
113
|
+
// ... 其他属性
|
|
114
|
+
/>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 自定义样式
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
<AutoPositionedPopup
|
|
121
|
+
tag="styled-popup"
|
|
122
|
+
style={{ backgroundColor: '#f5f5f5', borderRadius: 8 }}
|
|
123
|
+
AutoPositionedPopupBtnStyle={{
|
|
124
|
+
backgroundColor: '#e0e0e0',
|
|
125
|
+
padding: 15,
|
|
126
|
+
borderRadius: 8,
|
|
127
|
+
}}
|
|
128
|
+
inputStyle={{
|
|
129
|
+
fontSize: 16,
|
|
130
|
+
color: '#333',
|
|
131
|
+
}}
|
|
132
|
+
popUpViewStyle={{
|
|
133
|
+
left: '10%',
|
|
134
|
+
width: '80%',
|
|
135
|
+
}}
|
|
136
|
+
// ... 其他属性
|
|
137
|
+
/>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 完整下拉选择示例 (useTextInput=false)
|
|
141
|
+
|
|
142
|
+
此示例展示了无搜索输入的完整实现,适用于下拉选择器:
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
import React, { useState } from 'react';
|
|
146
|
+
import { View, Text, Image, StyleSheet } from 'react-native';
|
|
147
|
+
import AutoPositionedPopup, { SelectedItem, Data, RootViewProvider } from 'react-native-auto-positioned-popup';
|
|
148
|
+
|
|
149
|
+
// 支持颜色的数据类型示例
|
|
150
|
+
interface ClinicItem extends SelectedItem {
|
|
151
|
+
code: string;
|
|
152
|
+
textColor: string;
|
|
153
|
+
address?: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const ClinicSelector = () => {
|
|
157
|
+
const [selectedClinic, setSelectedClinic] = useState<ClinicItem | null>(null);
|
|
158
|
+
|
|
159
|
+
const fetchClinics = async ({ pageIndex, pageSize }): Promise<Data | null> => {
|
|
160
|
+
// 模拟 API 调用
|
|
161
|
+
const mockClinics = [
|
|
162
|
+
{ id: '1', title: '主诊所', code: 'MC001', textColor: '#4CAF50', address: '主街123号' },
|
|
163
|
+
{ id: '2', title: '市中心诊所', code: 'DC002', textColor: '#2196F3', address: '市中心大道456号' },
|
|
164
|
+
{ id: '3', title: '郊区诊所', code: 'SC003', textColor: '#FF9800', address: '郊区路789号' },
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
items: mockClinics.map(clinic => ({
|
|
169
|
+
title: clinic.code,
|
|
170
|
+
...clinic,
|
|
171
|
+
})),
|
|
172
|
+
pageIndex,
|
|
173
|
+
needLoadMore: false,
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<RootViewProvider>
|
|
179
|
+
<View style={styles.container}>
|
|
180
|
+
<AutoPositionedPopup
|
|
181
|
+
tag="clinic-selector"
|
|
182
|
+
useTextInput={false}
|
|
183
|
+
localSearch={false}
|
|
184
|
+
forceRemoveAllRootViewOnItemSelected={true}
|
|
185
|
+
selectedItem={selectedClinic ? {
|
|
186
|
+
title: selectedClinic.code,
|
|
187
|
+
...selectedClinic,
|
|
188
|
+
} : undefined}
|
|
189
|
+
CustomRow={({ children }) => (
|
|
190
|
+
<View style={styles.sectionRow}>
|
|
191
|
+
<Text style={styles.sectionRowLabel}>诊所</Text>
|
|
192
|
+
{children}
|
|
193
|
+
<Image
|
|
194
|
+
source={require('./assets/arrow-down.png')}
|
|
195
|
+
style={styles.selectArrow}
|
|
196
|
+
/>
|
|
197
|
+
</View>
|
|
198
|
+
)}
|
|
199
|
+
AutoPositionedPopupBtnStyle={styles.selectorButton}
|
|
200
|
+
btwChildren={() => (
|
|
201
|
+
<>
|
|
202
|
+
{!selectedClinic ? (
|
|
203
|
+
<Text style={styles.placeholderText} numberOfLines={1}>
|
|
204
|
+
请选择
|
|
205
|
+
</Text>
|
|
206
|
+
) : (
|
|
207
|
+
<View style={styles.selectedItemContainer}>
|
|
208
|
+
<View
|
|
209
|
+
style={[
|
|
210
|
+
styles.colorIndicator,
|
|
211
|
+
{ backgroundColor: selectedClinic.textColor }
|
|
212
|
+
]}
|
|
213
|
+
/>
|
|
214
|
+
<Text style={styles.selectedText} numberOfLines={1}>
|
|
215
|
+
{selectedClinic.code}
|
|
216
|
+
</Text>
|
|
217
|
+
</View>
|
|
218
|
+
)}
|
|
219
|
+
</>
|
|
220
|
+
)}
|
|
221
|
+
fetchData={fetchClinics}
|
|
222
|
+
onItemSelected={(item: ClinicItem) => {
|
|
223
|
+
console.log('选中的诊所:', item);
|
|
224
|
+
setSelectedClinic(item);
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
</View>
|
|
228
|
+
</RootViewProvider>
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const styles = StyleSheet.create({
|
|
233
|
+
container: {
|
|
234
|
+
padding: 20,
|
|
235
|
+
},
|
|
236
|
+
sectionRow: {
|
|
237
|
+
flexDirection: 'row',
|
|
238
|
+
alignItems: 'center',
|
|
239
|
+
paddingVertical: 15,
|
|
240
|
+
paddingHorizontal: 16,
|
|
241
|
+
backgroundColor: '#fff',
|
|
242
|
+
borderRadius: 8,
|
|
243
|
+
shadowColor: '#000',
|
|
244
|
+
shadowOffset: { width: 0, height: 1 },
|
|
245
|
+
shadowOpacity: 0.1,
|
|
246
|
+
shadowRadius: 2,
|
|
247
|
+
elevation: 2,
|
|
248
|
+
},
|
|
249
|
+
sectionRowLabel: {
|
|
250
|
+
fontSize: 16,
|
|
251
|
+
fontWeight: '600',
|
|
252
|
+
color: '#333',
|
|
253
|
+
marginRight: 12,
|
|
254
|
+
minWidth: 60,
|
|
255
|
+
},
|
|
256
|
+
selectorButton: {
|
|
257
|
+
flex: 1,
|
|
258
|
+
alignItems: 'flex-start',
|
|
259
|
+
},
|
|
260
|
+
selectArrow: {
|
|
261
|
+
width: 12,
|
|
262
|
+
height: 12,
|
|
263
|
+
marginLeft: 8,
|
|
264
|
+
},
|
|
265
|
+
placeholderText: {
|
|
266
|
+
fontSize: 15,
|
|
267
|
+
color: '#999',
|
|
268
|
+
},
|
|
269
|
+
selectedItemContainer: {
|
|
270
|
+
flexDirection: 'row',
|
|
271
|
+
alignItems: 'center',
|
|
272
|
+
},
|
|
273
|
+
colorIndicator: {
|
|
274
|
+
width: 12,
|
|
275
|
+
height: 12,
|
|
276
|
+
borderRadius: 6,
|
|
277
|
+
marginRight: 8,
|
|
278
|
+
},
|
|
279
|
+
selectedText: {
|
|
280
|
+
fontSize: 15,
|
|
281
|
+
fontWeight: '500',
|
|
282
|
+
color: '#333',
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
export default ClinicSelector;
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## API 参考
|
|
290
|
+
|
|
291
|
+
### 属性
|
|
292
|
+
|
|
293
|
+
| 属性 | 类型 | 默认值 | 描述 |
|
|
294
|
+
|------|------|---------|-------------|
|
|
295
|
+
| `tag` | `string` | **必需** | 弹窗的唯一标识符 |
|
|
296
|
+
| `fetchData` | `function` | `undefined` | 获取弹窗列表数据的函数 |
|
|
297
|
+
| `selectedItem` | `SelectedItem` | `undefined` | 当前选中的项目 |
|
|
298
|
+
| `onItemSelected` | `function` | `undefined` | 选中项目时的回调函数 |
|
|
299
|
+
| `placeholder` | `string` | `'Please Select'` | 占位符文本 |
|
|
300
|
+
| `useTextInput` | `boolean` | `false` | 启用搜索输入功能 |
|
|
301
|
+
| `localSearch` | `boolean` | `false` | 启用本地数据过滤 |
|
|
302
|
+
| `pageSize` | `number` | `20` | 每页项目数量 |
|
|
303
|
+
| `textAlign` | `'left' \| 'center' \| 'right'` | `'right'` | 文本对齐方式 |
|
|
304
|
+
| `AutoPositionedPopupBtnDisabled` | `boolean` | `false` | 禁用弹窗触发按钮 |
|
|
305
|
+
| `style` | `ViewStyle` | `undefined` | 容器样式 |
|
|
306
|
+
| `AutoPositionedPopupBtnStyle` | `ViewStyle` | `undefined` | 按钮样式 |
|
|
307
|
+
| `inputStyle` | `TextStyle` | `undefined` | 输入框样式 |
|
|
308
|
+
| `labelStyle` | `ViewStyle` | `undefined` | 标签文本样式 |
|
|
309
|
+
| `popUpViewStyle` | `ViewStyle` | `{ left: '5%', width: '90%' }` | 弹窗容器定位 |
|
|
310
|
+
|
|
311
|
+
### 数据结构
|
|
312
|
+
|
|
313
|
+
#### SelectedItem
|
|
314
|
+
```typescript
|
|
315
|
+
interface SelectedItem {
|
|
316
|
+
id: string;
|
|
317
|
+
title: string;
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### Data(fetchData 返回值)
|
|
322
|
+
```typescript
|
|
323
|
+
interface Data {
|
|
324
|
+
items: SelectedItem[];
|
|
325
|
+
pageIndex: number;
|
|
326
|
+
needLoadMore: boolean;
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### 方法(通过 ref)
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
const popupRef = useRef();
|
|
334
|
+
|
|
335
|
+
// 清除选中的项目
|
|
336
|
+
popupRef.current?.clearSelectedItem();
|
|
337
|
+
|
|
338
|
+
// 以编程方式显示弹窗
|
|
339
|
+
popupRef.current?.showPopup();
|
|
340
|
+
|
|
341
|
+
// 以编程方式隐藏弹窗
|
|
342
|
+
popupRef.current?.hidePopup();
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## 自定义示例
|
|
346
|
+
|
|
347
|
+
### 主题定制
|
|
348
|
+
|
|
349
|
+
组件支持通过覆盖默认样式来自定义主题:
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
const customTheme = {
|
|
353
|
+
colors: {
|
|
354
|
+
text: '#2c3e50',
|
|
355
|
+
placeholderText: '#95a5a6',
|
|
356
|
+
background: '#ecf0f1',
|
|
357
|
+
border: '#bdc3c7',
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### 自定义搜索逻辑
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
const fetchDataWithSearch = async ({ pageIndex, pageSize, searchQuery }) => {
|
|
366
|
+
const allItems = [
|
|
367
|
+
{ id: '1', title: '苹果' },
|
|
368
|
+
{ id: '2', title: '香蕉' },
|
|
369
|
+
{ id: '3', title: '樱桃' },
|
|
370
|
+
// ... 更多项目
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
const filteredItems = searchQuery
|
|
374
|
+
? allItems.filter(item =>
|
|
375
|
+
item.title.toLowerCase().includes(searchQuery.toLowerCase())
|
|
376
|
+
)
|
|
377
|
+
: allItems;
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
items: filteredItems.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize),
|
|
381
|
+
pageIndex,
|
|
382
|
+
needLoadMore: (pageIndex + 1) * pageSize < filteredItems.length,
|
|
383
|
+
};
|
|
384
|
+
};
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## 性能优化建议
|
|
388
|
+
|
|
389
|
+
1. **使用 keyExtractor**: 为列表项提供稳定的键
|
|
390
|
+
```tsx
|
|
391
|
+
keyExtractor={(item) => item.id}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
2. **优化 renderItem**: 对自定义项组件使用 React.memo
|
|
395
|
+
```tsx
|
|
396
|
+
const CustomItem = React.memo(({ item }) => (
|
|
397
|
+
<View>{/* 你的自定义项目 */}</View>
|
|
398
|
+
));
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
3. **防抖搜索**: 组件内置防抖搜索功能(300ms 延迟)
|
|
402
|
+
|
|
403
|
+
4. **本地 vs 远程搜索**: 对于小数据集使用 `localSearch={true}`,对于服务器端过滤使用 `false`
|
|
404
|
+
|
|
405
|
+
## 系统要求
|
|
406
|
+
|
|
407
|
+
- React Native >= 0.60.0
|
|
408
|
+
- React >= 16.8.0 (支持 Hooks)
|
|
409
|
+
|
|
410
|
+
## 贡献
|
|
411
|
+
|
|
412
|
+
欢迎贡献代码!请随时提交 Pull Request。
|
|
413
|
+
|
|
414
|
+
## 许可证
|
|
415
|
+
|
|
416
|
+
MIT © [Stark](https://github.com/your-username)
|
|
417
|
+
|
|
418
|
+
## 更新日志
|
|
419
|
+
|
|
420
|
+
### 1.0.0
|
|
421
|
+
- 初始发布
|
|
422
|
+
- 自动定位功能
|
|
423
|
+
- 搜索支持
|
|
424
|
+
- TypeScript 定义
|
|
425
|
+
- 跨平台兼容性
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ForwardRefExoticComponent, MemoExoticComponent } from 'react';
|
|
2
|
+
import { AutoPositionedPopupProps } from './AutoPositionedPopupProps';
|
|
3
|
+
declare const AutoPositionedPopup: MemoExoticComponent<ForwardRefExoticComponent<AutoPositionedPopupProps>>;
|
|
4
|
+
export default AutoPositionedPopup;
|
|
5
|
+
//# sourceMappingURL=AutoPositionedPopup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AutoPositionedPopup.d.ts","sourceRoot":"","sources":["../src/AutoPositionedPopup.tsx"],"names":[],"mappings":"AAAA,OAAc,EAGZ,yBAAyB,EAEzB,mBAAmB,EAOpB,MAAM,OAAO,CAAC;AAaf,OAAO,EAAC,wBAAwB,EAAqB,MAAM,4BAA4B,CAAC;AAqLxF,QAAA,MAAM,mBAAmB,EAAE,mBAAmB,CAC5C,yBAAyB,CAAC,wBAAwB,CAAC,CA+TpD,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
|