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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Stark
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# React Native Auto Positioned Popup
|
|
2
|
+
|
|
3
|
+
A highly customizable React Native auto-positioned popup component with search functionality and flexible styling options. Perfect for dropdowns, autocomplete inputs, and selection lists.
|
|
4
|
+
|
|
5
|
+
English | [δΈζ](./README_zh.md)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
π **Auto-positioning**: Automatically adjusts popup position based on screen space
|
|
10
|
+
π **Search functionality**: Built-in search with debounced input
|
|
11
|
+
π± **Cross-platform**: Works on both iOS and Android
|
|
12
|
+
π¨ **Customizable**: Extensive styling and theming options
|
|
13
|
+
β‘ **Performance optimized**: Efficient rendering with AdvancedFlatList
|
|
14
|
+
π― **TypeScript support**: Full TypeScript definitions included
|
|
15
|
+
π **Dynamic view management**: RootView-based popup system
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install react-native-auto-positioned-popup
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
or
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
yarn add react-native-auto-positioned-popup
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Basic Usage
|
|
30
|
+
|
|
31
|
+
First, wrap your app with the `RootViewProvider`:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { RootViewProvider } from 'react-native-auto-positioned-popup';
|
|
35
|
+
|
|
36
|
+
const App = () => {
|
|
37
|
+
return (
|
|
38
|
+
<RootViewProvider>
|
|
39
|
+
{/* Your app content */}
|
|
40
|
+
</RootViewProvider>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then use the `AutoPositionedPopup` component:
|
|
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
|
+
// Your data fetching logic here
|
|
57
|
+
return {
|
|
58
|
+
items: [
|
|
59
|
+
{ id: '1', title: 'Option 1' },
|
|
60
|
+
{ id: '2', title: 'Option 2' },
|
|
61
|
+
{ id: '3', title: 'Option 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="Select an option"
|
|
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
|
+
## Advanced Usage
|
|
86
|
+
|
|
87
|
+
### Custom Row Component
|
|
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 }}>Select:</Text>
|
|
95
|
+
{children}
|
|
96
|
+
</View>
|
|
97
|
+
)}
|
|
98
|
+
// ... other props
|
|
99
|
+
/>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Custom Item Rendering
|
|
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
|
+
// ... other props
|
|
114
|
+
/>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### With Custom Styling
|
|
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
|
+
// ... other props
|
|
137
|
+
/>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Complete Dropdown Example (useTextInput=false)
|
|
141
|
+
|
|
142
|
+
This example shows a complete implementation without search input, suitable for dropdowns and selectors:
|
|
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
|
+
// Sample data type with color support
|
|
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
|
+
// Simulate API call
|
|
161
|
+
const mockClinics = [
|
|
162
|
+
{ id: '1', title: 'Main Clinic', code: 'MC001', textColor: '#4CAF50', address: '123 Main St' },
|
|
163
|
+
{ id: '2', title: 'Downtown Clinic', code: 'DC002', textColor: '#2196F3', address: '456 Downtown Ave' },
|
|
164
|
+
{ id: '3', title: 'Suburb Clinic', code: 'SC003', textColor: '#FF9800', address: '789 Suburb Rd' },
|
|
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}>Clinic</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
|
+
Please Select
|
|
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('Selected clinic:', 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 Reference
|
|
290
|
+
|
|
291
|
+
### Props
|
|
292
|
+
|
|
293
|
+
| Prop | Type | Default | Description |
|
|
294
|
+
|------|------|---------|-------------|
|
|
295
|
+
| `tag` | `string` | **Required** | Unique identifier for the popup |
|
|
296
|
+
| `fetchData` | `function` | `undefined` | Function to fetch data for the popup list |
|
|
297
|
+
| `selectedItem` | `SelectedItem` | `undefined` | Currently selected item |
|
|
298
|
+
| `onItemSelected` | `function` | `undefined` | Callback when an item is selected |
|
|
299
|
+
| `placeholder` | `string` | `'Please Select'` | Placeholder text |
|
|
300
|
+
| `useTextInput` | `boolean` | `false` | Enable search input functionality |
|
|
301
|
+
| `localSearch` | `boolean` | `false` | Enable local filtering of data |
|
|
302
|
+
| `pageSize` | `number` | `20` | Number of items per page |
|
|
303
|
+
| `textAlign` | `'left' \| 'center' \| 'right'` | `'right'` | Text alignment |
|
|
304
|
+
| `AutoPositionedPopupBtnDisabled` | `boolean` | `false` | Disable the popup trigger button |
|
|
305
|
+
| `style` | `ViewStyle` | `undefined` | Container style |
|
|
306
|
+
| `AutoPositionedPopupBtnStyle` | `ViewStyle` | `undefined` | Button style |
|
|
307
|
+
| `inputStyle` | `TextStyle` | `undefined` | Input field style |
|
|
308
|
+
| `labelStyle` | `ViewStyle` | `undefined` | Label text style |
|
|
309
|
+
| `popUpViewStyle` | `ViewStyle` | `{ left: '5%', width: '90%' }` | Popup container positioning |
|
|
310
|
+
|
|
311
|
+
### Data Structure
|
|
312
|
+
|
|
313
|
+
#### SelectedItem
|
|
314
|
+
```typescript
|
|
315
|
+
interface SelectedItem {
|
|
316
|
+
id: string;
|
|
317
|
+
title: string;
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### Data (for fetchData return)
|
|
322
|
+
```typescript
|
|
323
|
+
interface Data {
|
|
324
|
+
items: SelectedItem[];
|
|
325
|
+
pageIndex: number;
|
|
326
|
+
needLoadMore: boolean;
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Methods (via ref)
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
const popupRef = useRef();
|
|
334
|
+
|
|
335
|
+
// Clear the selected item
|
|
336
|
+
popupRef.current?.clearSelectedItem();
|
|
337
|
+
|
|
338
|
+
// Programmatically show popup
|
|
339
|
+
popupRef.current?.showPopup();
|
|
340
|
+
|
|
341
|
+
// Programmatically hide popup
|
|
342
|
+
popupRef.current?.hidePopup();
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Customization Examples
|
|
346
|
+
|
|
347
|
+
### Theming
|
|
348
|
+
|
|
349
|
+
The component supports custom theming by overriding the default styles:
|
|
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
|
+
### Custom Search Logic
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
const fetchDataWithSearch = async ({ pageIndex, pageSize, searchQuery }) => {
|
|
366
|
+
const allItems = [
|
|
367
|
+
{ id: '1', title: 'Apple' },
|
|
368
|
+
{ id: '2', title: 'Banana' },
|
|
369
|
+
{ id: '3', title: 'Cherry' },
|
|
370
|
+
// ... more items
|
|
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
|
+
## Performance Tips
|
|
388
|
+
|
|
389
|
+
1. **Use keyExtractor**: Provide a stable key for list items
|
|
390
|
+
```tsx
|
|
391
|
+
keyExtractor={(item) => item.id}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
2. **Optimize renderItem**: Use React.memo for custom item components
|
|
395
|
+
```tsx
|
|
396
|
+
const CustomItem = React.memo(({ item }) => (
|
|
397
|
+
<View>{/* Your custom item */}</View>
|
|
398
|
+
));
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
3. **Debounced Search**: The component includes built-in debounced search (300ms delay)
|
|
402
|
+
|
|
403
|
+
4. **Local vs Remote Search**: Use `localSearch={true}` for small datasets, `false` for server-side filtering
|
|
404
|
+
|
|
405
|
+
## Requirements
|
|
406
|
+
|
|
407
|
+
- React Native >= 0.60.0
|
|
408
|
+
- React >= 16.8.0 (Hooks support)
|
|
409
|
+
|
|
410
|
+
## Contributing
|
|
411
|
+
|
|
412
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
413
|
+
|
|
414
|
+
## License
|
|
415
|
+
|
|
416
|
+
MIT Β© [Stark](https://github.com/your-username)
|
|
417
|
+
|
|
418
|
+
## Changelog
|
|
419
|
+
|
|
420
|
+
### 1.0.0
|
|
421
|
+
- Initial release
|
|
422
|
+
- Auto-positioning functionality
|
|
423
|
+
- Search support
|
|
424
|
+
- TypeScript definitions
|
|
425
|
+
- Cross-platform compatibility
|