react-native-native-select 1.0.0
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/README.md +109 -0
- package/android/build.gradle +36 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/rtnselect/SelectView.java +127 -0
- package/android/src/main/java/com/rtnselect/SelectViewManager.java +83 -0
- package/android/src/main/java/com/rtnselect/SelectViewPackage.java +21 -0
- package/ios/RTNSelect.h +10 -0
- package/ios/RTNSelect.mm +245 -0
- package/ios/RTNSelectManager.mm +22 -0
- package/package.json +43 -0
- package/react-native-native-select.podspec +19 -0
- package/src/RTNSelectNativeComponent.ts +21 -0
- package/src/index.ts +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# react-native-native-select
|
|
2
|
+
|
|
3
|
+
A strictly native, performant Select component for React Native built exclusively for the **New Architecture**.
|
|
4
|
+
|
|
5
|
+
It leverages actual OS primitives: `UIMenu` (iOS 14+), `UIPickerView` (iOS Wheel), and `AppCompatSpinner` (Android), offering a truly native look and feel that JS-based libraries cannot match.
|
|
6
|
+
|
|
7
|
+
## Preview
|
|
8
|
+
|
|
9
|
+
| iOS (Dropdown Mode) | iOS (Dialog/Wheel Mode) | Android |
|
|
10
|
+
|:---:|:---:|:---:|
|
|
11
|
+
| *[Insert Screenshot of iOS Select with open Menu]* | *[Insert Screenshot of iOS Bottom Wheel picker]* | *[Insert Screenshot of Android Dropdown]* |
|
|
12
|
+
| *Native UIMenu (iOS 14+)* | *Classic UIPickerView* | *Native AppCompatSpinner* |
|
|
13
|
+
|
|
14
|
+
## Why this library?
|
|
15
|
+
|
|
16
|
+
Most Select/Picker libraries in the React Native ecosystem fall into two categories:
|
|
17
|
+
1. **JS-based Simulations:** They render a Modal with a FlatList. They are customizable but feel "off" compared to the OS native UI and often lack accessibility standards like [`react-native-picker-select`](https://www.npmjs.com/package/react-native-picker-select).
|
|
18
|
+
2. **Legacy Wrappers:** Libraries like [`@react-native-picker/picker`](https://www.npmjs.com/package/@react-native-picker/picker) are excellent but often rely on the old bridge or split functionality across different components.
|
|
19
|
+
|
|
20
|
+
**react-native-native-select** is designed for modern apps:
|
|
21
|
+
* **Zero JS Thread Overhead:** Fully native implementation using Fabric.
|
|
22
|
+
* **Modern iOS UI:** Supports the iOS 14+ `pull-down` menu style out of the box.
|
|
23
|
+
* **Native Performance:** Interactions run on the UI thread.
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
* **React Native:** >= 0.71.0
|
|
28
|
+
* **Architecture:** New Architecture **Enabled**
|
|
29
|
+
* **iOS:** 14.0+ (for Dropdown mode), 11.0+ (for Dialog mode)
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install react-native-native-select
|
|
35
|
+
# or
|
|
36
|
+
yarn add react-native-native-select
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### iOS Setup
|
|
40
|
+
|
|
41
|
+
Since this library uses native modules, you must run pod install:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cd ios && pod install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { StyleSheet, View, Text } from 'react-native';
|
|
51
|
+
import { Select } from 'react-native-native-select';
|
|
52
|
+
|
|
53
|
+
export default function App() {
|
|
54
|
+
return (
|
|
55
|
+
<View style={styles.container}>
|
|
56
|
+
<Text style={styles.label}>Choose a fruit:</Text>
|
|
57
|
+
|
|
58
|
+
<Select
|
|
59
|
+
style={styles.select}
|
|
60
|
+
mode="dropdown"
|
|
61
|
+
options={["Apple", "Banana", "Orange", "Mango"]}
|
|
62
|
+
selectedIndex={0}
|
|
63
|
+
onValueChange={(e) => {
|
|
64
|
+
console.log("Selected Item:", e.nativeEvent.value); // Ex: "Apple"
|
|
65
|
+
console.log("Selected Index:", e.nativeEvent.index); // Ex: 0
|
|
66
|
+
}}
|
|
67
|
+
/>
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const styles = StyleSheet.create({
|
|
73
|
+
container: {
|
|
74
|
+
flex: 1,
|
|
75
|
+
justifyContent: 'center',
|
|
76
|
+
padding: 20,
|
|
77
|
+
},
|
|
78
|
+
label: {
|
|
79
|
+
marginBottom: 10,
|
|
80
|
+
fontSize: 16,
|
|
81
|
+
},
|
|
82
|
+
select: {
|
|
83
|
+
width: '100%',
|
|
84
|
+
height: 50, // Height is required for layout calculation
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Android Usage
|
|
90
|
+
|
|
91
|
+
Please take a look at this [Android example usage](./tests/android_demo.tsx) because the native component is not updating the visual state at the moment
|
|
92
|
+
|
|
93
|
+
## Props
|
|
94
|
+
|
|
95
|
+
| Prop | Type | Required | Description |
|
|
96
|
+
| :--- | :--- | :---: | :--- |
|
|
97
|
+
| **`options`** | `string[]` | **Yes** | An array of strings to display in the list. |
|
|
98
|
+
| **`selectedIndex`** | `number` | No | The index of the currently selected item. Defaults to `0`. |
|
|
99
|
+
| **`mode`** | `'dropdown' \| 'dialog'` | No | **iOS Only.** <br>`dropdown`: Uses `UIMenu` (Modern iOS 14+). <br>`dialog`: Uses `UIPickerView` (Classic Wheel). <br> *On Android, this prop is ignored as it always uses the native Spinner.* |
|
|
100
|
+
| **`onValueChange`** | `function` | No | Callback fired when an item is selected. Returns `{ value: string, index: number }`. |
|
|
101
|
+
| **`style`** | `ViewStyle` | No | Standard style prop. **Note:** You must define `width` and `height` for the view to render correctly. |
|
|
102
|
+
|
|
103
|
+
## Troubleshooting
|
|
104
|
+
|
|
105
|
+
**The component is visible but empty (Empty space)**
|
|
106
|
+
Ensure you have defined a `width` and `height` in the `style` prop. Native views on iOS require explicit dimensions or Flexbox constraints to render their subviews correctly.
|
|
107
|
+
|
|
108
|
+
**App crashes on launch**
|
|
109
|
+
Ensure `RCT_NEW_ARCH_ENABLED=1` was present when you ran `pod install`. This library does not support the old React Native Bridge.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.safeExtGet = {prop, fallback ->
|
|
3
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
4
|
+
}
|
|
5
|
+
repositories {
|
|
6
|
+
google()
|
|
7
|
+
gradlePluginPortal()
|
|
8
|
+
}
|
|
9
|
+
dependencies {
|
|
10
|
+
classpath("com.android.tools.build:gradle:7.2.0")
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
apply plugin: 'com.android.library'
|
|
15
|
+
apply plugin: 'com.facebook.react'
|
|
16
|
+
|
|
17
|
+
android {
|
|
18
|
+
compileSdkVersion safeExtGet('compileSdkVersion', 31)
|
|
19
|
+
|
|
20
|
+
defaultConfig {
|
|
21
|
+
minSdkVersion safeExtGet('minSdkVersion', 21)
|
|
22
|
+
targetSdkVersion safeExtGet('targetSdkVersion', 31)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
repositories {
|
|
27
|
+
maven {
|
|
28
|
+
url "$projectDir/../node_modules/react-native/android"
|
|
29
|
+
}
|
|
30
|
+
mavenCentral()
|
|
31
|
+
google()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
dependencies {
|
|
35
|
+
implementation 'com.facebook.react:react-native:+'
|
|
36
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
package com.rtnselect;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.util.AttributeSet;
|
|
5
|
+
import android.util.Log;
|
|
6
|
+
import android.view.View;
|
|
7
|
+
import android.widget.AdapterView;
|
|
8
|
+
import android.widget.ArrayAdapter;
|
|
9
|
+
import androidx.annotation.Nullable;
|
|
10
|
+
import androidx.appcompat.widget.AppCompatSpinner;
|
|
11
|
+
|
|
12
|
+
import com.facebook.react.bridge.Arguments;
|
|
13
|
+
import com.facebook.react.bridge.ReactContext;
|
|
14
|
+
import com.facebook.react.bridge.WritableMap;
|
|
15
|
+
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
|
16
|
+
|
|
17
|
+
import java.util.List;
|
|
18
|
+
|
|
19
|
+
public class SelectView extends AppCompatSpinner implements AdapterView.OnItemSelectedListener {
|
|
20
|
+
|
|
21
|
+
private final String TAG = "RTNSelect";
|
|
22
|
+
|
|
23
|
+
public SelectView(Context context) {
|
|
24
|
+
super(context);
|
|
25
|
+
init();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public SelectView(Context context, @Nullable AttributeSet attrs) {
|
|
29
|
+
super(context, attrs);
|
|
30
|
+
init();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public SelectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
|
34
|
+
super(context, attrs, defStyleAttr);
|
|
35
|
+
init();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private void init() {
|
|
39
|
+
this.setOnItemSelectedListener(this);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Override
|
|
43
|
+
public boolean performClick() {
|
|
44
|
+
boolean result = super.performClick();
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Override
|
|
49
|
+
protected void onAttachedToWindow() {
|
|
50
|
+
super.onAttachedToWindow();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Override
|
|
54
|
+
protected void onDetachedFromWindow() {
|
|
55
|
+
super.onDetachedFromWindow();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public void setOptions(List<String> options) {
|
|
59
|
+
|
|
60
|
+
ArrayAdapter<String> adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, options);
|
|
61
|
+
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
62
|
+
|
|
63
|
+
this.setAdapter(adapter);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Override
|
|
67
|
+
public void setSelection(int position) {
|
|
68
|
+
|
|
69
|
+
this.post(new Runnable() {
|
|
70
|
+
@Override
|
|
71
|
+
public void run() {
|
|
72
|
+
SelectView.super.setSelection(position);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
ReactContext reactContext = (ReactContext) getContext();
|
|
77
|
+
if (reactContext != null) {
|
|
78
|
+
String val = "unknown";
|
|
79
|
+
if (getAdapter() != null && getAdapter().getCount() > position) {
|
|
80
|
+
Object item = getAdapter().getItem(position);
|
|
81
|
+
if (item != null) val = item.toString();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
WritableMap event = Arguments.createMap();
|
|
85
|
+
event.putString("value", val);
|
|
86
|
+
event.putInt("index", position);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
|
|
90
|
+
getId(),
|
|
91
|
+
"topValueChange",
|
|
92
|
+
event
|
|
93
|
+
);
|
|
94
|
+
} catch (Exception e) {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Override
|
|
100
|
+
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
|
101
|
+
String val = "null";
|
|
102
|
+
if (parent != null && parent.getItemAtPosition(position) != null) {
|
|
103
|
+
val = parent.getItemAtPosition(position).toString();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ReactContext reactContext = (ReactContext) getContext();
|
|
107
|
+
if (reactContext != null) {
|
|
108
|
+
WritableMap event = Arguments.createMap();
|
|
109
|
+
event.putString("value", val);
|
|
110
|
+
event.putInt("index", position);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
|
|
114
|
+
getId(),
|
|
115
|
+
"topValueChange",
|
|
116
|
+
event
|
|
117
|
+
);
|
|
118
|
+
} catch (Exception e) {
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@Override
|
|
125
|
+
public void onNothingSelected(AdapterView<?> parent) {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
package com.rtnselect;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
import androidx.annotation.Nullable;
|
|
5
|
+
|
|
6
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
7
|
+
import com.facebook.react.bridge.ReactContext;
|
|
8
|
+
import com.facebook.react.bridge.ReadableArray;
|
|
9
|
+
import com.facebook.react.common.MapBuilder;
|
|
10
|
+
import com.facebook.react.module.annotations.ReactModule;
|
|
11
|
+
import com.facebook.react.uimanager.SimpleViewManager;
|
|
12
|
+
import com.facebook.react.uimanager.ThemedReactContext;
|
|
13
|
+
import com.facebook.react.uimanager.ViewManagerDelegate;
|
|
14
|
+
import com.facebook.react.uimanager.annotations.ReactProp;
|
|
15
|
+
import com.facebook.react.viewmanagers.RTNSelectManagerDelegate;
|
|
16
|
+
import com.facebook.react.viewmanagers.RTNSelectManagerInterface;
|
|
17
|
+
|
|
18
|
+
import java.util.ArrayList;
|
|
19
|
+
import java.util.Map;
|
|
20
|
+
|
|
21
|
+
@ReactModule(name = SelectViewManager.REACT_CLASS)
|
|
22
|
+
public class SelectViewManager extends SimpleViewManager<SelectView>
|
|
23
|
+
implements RTNSelectManagerInterface<SelectView> {
|
|
24
|
+
|
|
25
|
+
public static final String REACT_CLASS = "RTNSelect";
|
|
26
|
+
private final ViewManagerDelegate<SelectView> mDelegate;
|
|
27
|
+
|
|
28
|
+
public SelectViewManager(ReactApplicationContext reactContext) {
|
|
29
|
+
mDelegate = new RTNSelectManagerDelegate<>(this);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Nullable
|
|
33
|
+
@Override
|
|
34
|
+
protected ViewManagerDelegate<SelectView> getDelegate() {
|
|
35
|
+
return mDelegate;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@NonNull
|
|
39
|
+
@Override
|
|
40
|
+
public String getName() {
|
|
41
|
+
return REACT_CLASS;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Nullable
|
|
45
|
+
@Override
|
|
46
|
+
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
|
|
47
|
+
// Strict mapping: Native 'topValueChange' -> JS 'onValueChange'
|
|
48
|
+
return MapBuilder.of(
|
|
49
|
+
"topValueChange",
|
|
50
|
+
MapBuilder.of("registrationName", "onValueChange")
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@NonNull
|
|
55
|
+
@Override
|
|
56
|
+
protected SelectView createViewInstance(@NonNull ThemedReactContext reactContext) {
|
|
57
|
+
return new SelectView(reactContext);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Override
|
|
61
|
+
@ReactProp(name = "options")
|
|
62
|
+
public void setOptions(SelectView view, @Nullable ReadableArray value) {
|
|
63
|
+
ArrayList<String> list = new ArrayList<>();
|
|
64
|
+
if (value != null) {
|
|
65
|
+
for (int i = 0; i < value.size(); i++) {
|
|
66
|
+
list.add(value.getString(i));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
view.setOptions(list);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Override
|
|
73
|
+
@ReactProp(name = "selectedIndex")
|
|
74
|
+
public void setSelectedIndex(SelectView view, int value) {
|
|
75
|
+
view.setSelection(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Override
|
|
79
|
+
@ReactProp(name = "mode")
|
|
80
|
+
public void setMode(SelectView view, @Nullable String value) {}
|
|
81
|
+
|
|
82
|
+
public void setOnValueChange(SelectView view, @Nullable Object value) {}
|
|
83
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
package com.rtnselect;
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage;
|
|
4
|
+
import com.facebook.react.bridge.NativeModule;
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
7
|
+
|
|
8
|
+
import java.util.Collections;
|
|
9
|
+
import java.util.List;
|
|
10
|
+
|
|
11
|
+
public class SelectViewPackage implements ReactPackage {
|
|
12
|
+
@Override
|
|
13
|
+
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
|
14
|
+
return Collections.singletonList(new SelectViewManager(reactContext));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Override
|
|
18
|
+
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
|
19
|
+
return Collections.emptyList();
|
|
20
|
+
}
|
|
21
|
+
}
|
package/ios/RTNSelect.h
ADDED
package/ios/RTNSelect.mm
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#import "RTNSelect.h"
|
|
2
|
+
#import <React/RCTComponent.h>
|
|
3
|
+
|
|
4
|
+
#import <react/renderer/components/RTNSelectSpec/ComponentDescriptors.h>
|
|
5
|
+
#import <react/renderer/components/RTNSelectSpec/EventEmitters.h>
|
|
6
|
+
#import <react/renderer/components/RTNSelectSpec/Props.h>
|
|
7
|
+
#import <react/renderer/components/RTNSelectSpec/RCTComponentViewHelpers.h>
|
|
8
|
+
|
|
9
|
+
#import "RCTFabricComponentsPlugins.h"
|
|
10
|
+
|
|
11
|
+
using namespace facebook::react;
|
|
12
|
+
|
|
13
|
+
@interface RTNSelect () <RCTRTNSelectViewProtocol>
|
|
14
|
+
@end
|
|
15
|
+
|
|
16
|
+
@implementation RTNSelect {
|
|
17
|
+
UIPickerView *_pickerView;
|
|
18
|
+
UIButton *_button;
|
|
19
|
+
|
|
20
|
+
std::vector<std::string> _options;
|
|
21
|
+
NSInteger _selectedIndex;
|
|
22
|
+
RCTDirectEventBlock _onValueChangeLegacy;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
26
|
+
{
|
|
27
|
+
return concreteComponentDescriptorProvider<RTNSelectComponentDescriptor>();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
31
|
+
{
|
|
32
|
+
if (self = [super initWithFrame:frame]) {
|
|
33
|
+
static const auto defaultProps = std::make_shared<const RTNSelectProps>();
|
|
34
|
+
_props = defaultProps;
|
|
35
|
+
_selectedIndex = 0;
|
|
36
|
+
|
|
37
|
+
_pickerView = [[UIPickerView alloc] initWithFrame:self.bounds];
|
|
38
|
+
_pickerView.dataSource = self;
|
|
39
|
+
_pickerView.delegate = self;
|
|
40
|
+
_pickerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
41
|
+
|
|
42
|
+
_button = [UIButton buttonWithType:UIButtonTypeSystem];
|
|
43
|
+
_button.frame = self.bounds;
|
|
44
|
+
_button.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
45
|
+
|
|
46
|
+
// Style du bouton (alignement à gauche, couleur noire pour le texte)
|
|
47
|
+
_button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
|
|
48
|
+
[_button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
|
49
|
+
|
|
50
|
+
if (@available(iOS 14.0, *)) {
|
|
51
|
+
_button.showsMenuAsPrimaryAction = YES;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_button.hidden = YES;
|
|
55
|
+
|
|
56
|
+
[self addSubview:_pickerView];
|
|
57
|
+
[self addSubview:_button];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return self;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
- (void)layoutSubviews {
|
|
64
|
+
[super layoutSubviews];
|
|
65
|
+
_pickerView.frame = self.bounds;
|
|
66
|
+
_button.frame = self.bounds;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Gestion du Menu Flottant (Dropdown)
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
- (void)updateButtonMenu {
|
|
74
|
+
if (@available(iOS 14.0, *)) {
|
|
75
|
+
NSMutableArray<UIAction *> *actions = [NSMutableArray array];
|
|
76
|
+
|
|
77
|
+
for (int i = 0; i < _options.size(); i++) {
|
|
78
|
+
std::string optionString = _options[i];
|
|
79
|
+
NSString *title = [NSString stringWithUTF8String:optionString.c_str()];
|
|
80
|
+
|
|
81
|
+
UIAction *action = [UIAction actionWithTitle:title image:nil identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
|
|
82
|
+
[self selectIndex:i fromSource:@"dropdown"];
|
|
83
|
+
}];
|
|
84
|
+
|
|
85
|
+
if (i == _selectedIndex) {
|
|
86
|
+
action.state = UIMenuElementStateOn;
|
|
87
|
+
[_button setTitle:title forState:UIControlStateNormal];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
[actions addObject:action];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
UIMenu *menu = [UIMenu menuWithTitle:@"" children:actions];
|
|
94
|
+
_button.menu = menu;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Méthode unifiée de sélection (appelée par Picker ou Dropdown)
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
- (void)selectIndex:(NSInteger)index fromSource:(NSString *)source {
|
|
103
|
+
if (index >= _options.size()) return;
|
|
104
|
+
|
|
105
|
+
_selectedIndex = index;
|
|
106
|
+
|
|
107
|
+
if ([source isEqualToString:@"dropdown"]) {
|
|
108
|
+
[_pickerView selectRow:index inComponent:0 animated:NO];
|
|
109
|
+
[self updateButtonMenu];
|
|
110
|
+
} else {
|
|
111
|
+
[self updateButtonMenu];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (_eventEmitter) {
|
|
115
|
+
auto emitter = std::static_pointer_cast<RTNSelectEventEmitter const>(_eventEmitter);
|
|
116
|
+
emitter->onValueChange({
|
|
117
|
+
.value = _options[index],
|
|
118
|
+
.index = (int)index
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (_onValueChangeLegacy) {
|
|
123
|
+
_onValueChangeLegacy(@{
|
|
124
|
+
@"value": [NSString stringWithUTF8String:_options[index].c_str()],
|
|
125
|
+
@"index": @(index)
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// 1. FABRIC (New Architecture) Implementation
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
135
|
+
{
|
|
136
|
+
const auto &oldViewProps = *std::static_pointer_cast<const RTNSelectProps>(_props);
|
|
137
|
+
const auto &newViewProps = *std::static_pointer_cast<const RTNSelectProps>(props);
|
|
138
|
+
|
|
139
|
+
bool optionsChanged = oldViewProps.options != newViewProps.options;
|
|
140
|
+
bool indexChanged = oldViewProps.selectedIndex != newViewProps.selectedIndex;
|
|
141
|
+
bool modeChanged = oldViewProps.mode != newViewProps.mode;
|
|
142
|
+
|
|
143
|
+
if (optionsChanged) {
|
|
144
|
+
_options = newViewProps.options;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (indexChanged) {
|
|
148
|
+
_selectedIndex = newViewProps.selectedIndex;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
[super updateProps:props oldProps:oldProps];
|
|
152
|
+
|
|
153
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
154
|
+
if (optionsChanged) {
|
|
155
|
+
[self->_pickerView reloadAllComponents];
|
|
156
|
+
[self updateButtonMenu];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (indexChanged && newViewProps.selectedIndex < self->_options.size()) {
|
|
160
|
+
[self->_pickerView selectRow:newViewProps.selectedIndex inComponent:0 animated:YES];
|
|
161
|
+
[self updateButtonMenu];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (modeChanged || optionsChanged) {
|
|
165
|
+
if (newViewProps.mode == RTNSelectMode::Dropdown) {
|
|
166
|
+
self->_pickerView.hidden = YES;
|
|
167
|
+
self->_button.hidden = NO;
|
|
168
|
+
} else {
|
|
169
|
+
self->_pickerView.hidden = NO;
|
|
170
|
+
self->_button.hidden = YES;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// 2. LEGACY / INTEROP Implementation
|
|
178
|
+
// ============================================================================
|
|
179
|
+
|
|
180
|
+
- (void)setOptions:(NSArray<NSString *> *)options {
|
|
181
|
+
_options.clear();
|
|
182
|
+
for (NSString *option in options) {
|
|
183
|
+
_options.push_back([option UTF8String]);
|
|
184
|
+
}
|
|
185
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
186
|
+
[self->_pickerView reloadAllComponents];
|
|
187
|
+
[self updateButtonMenu];
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
- (void)setSelectedIndex:(NSInteger)selectedIndex {
|
|
192
|
+
_selectedIndex = selectedIndex;
|
|
193
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
194
|
+
if (selectedIndex < self->_options.size()) {
|
|
195
|
+
[self->_pickerView selectRow:selectedIndex inComponent:0 animated:YES];
|
|
196
|
+
[self updateButtonMenu];
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
- (void)setMode:(NSString *)mode {
|
|
202
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
203
|
+
if ([mode isEqualToString:@"dropdown"]) {
|
|
204
|
+
self->_pickerView.hidden = YES;
|
|
205
|
+
self->_button.hidden = NO;
|
|
206
|
+
} else {
|
|
207
|
+
self->_pickerView.hidden = NO;
|
|
208
|
+
self->_button.hidden = YES;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
- (void)setOnValueChange:(RCTDirectEventBlock)onValueChange {
|
|
214
|
+
_onValueChangeLegacy = onValueChange;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// 3. UIPickerView Delegate Methods
|
|
219
|
+
// ============================================================================
|
|
220
|
+
|
|
221
|
+
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
|
|
226
|
+
return _options.size();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
|
|
230
|
+
if (row < _options.size()) {
|
|
231
|
+
return [NSString stringWithUTF8String:_options[row].c_str()];
|
|
232
|
+
}
|
|
233
|
+
return @"";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
|
|
237
|
+
[self selectIndex:row fromSource:@"picker"];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@end
|
|
241
|
+
|
|
242
|
+
Class<RCTComponentViewProtocol> RTNSelectCls(void)
|
|
243
|
+
{
|
|
244
|
+
return RTNSelect.class;
|
|
245
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#import <React/RCTLog.h>
|
|
2
|
+
#import <React/RCTUIManager.h>
|
|
3
|
+
#import <React/RCTViewManager.h>
|
|
4
|
+
#import "RTNSelect.h"
|
|
5
|
+
|
|
6
|
+
@interface RTNSelectManager : RCTViewManager
|
|
7
|
+
@end
|
|
8
|
+
|
|
9
|
+
@implementation RTNSelectManager
|
|
10
|
+
|
|
11
|
+
RCT_EXPORT_MODULE(RTNSelect)
|
|
12
|
+
|
|
13
|
+
- (UIView *)view {
|
|
14
|
+
return [[RTNSelect alloc] init];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
RCT_EXPORT_VIEW_PROPERTY(options, NSArray)
|
|
18
|
+
RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSInteger)
|
|
19
|
+
RCT_EXPORT_VIEW_PROPERTY(mode, NSString)
|
|
20
|
+
RCT_EXPORT_VIEW_PROPERTY(onValueChange, RCTDirectEventBlock)
|
|
21
|
+
|
|
22
|
+
@end
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-native-select",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "react-native-native-select is a strictly native, performant Select component for React Native built exclusively for the New Architecture.",
|
|
5
|
+
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"react-native": "src/index.ts",
|
|
8
|
+
"types": "src/index.ts",
|
|
9
|
+
|
|
10
|
+
"source": "src/index",
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"android",
|
|
14
|
+
"ios",
|
|
15
|
+
"react-native-native-select.podspec",
|
|
16
|
+
"!android/build",
|
|
17
|
+
"!ios/build",
|
|
18
|
+
"!tests",
|
|
19
|
+
"!**/__tests__",
|
|
20
|
+
"!**/__fixtures__",
|
|
21
|
+
"!**/__mocks__"
|
|
22
|
+
],
|
|
23
|
+
"keywords": ["react-native", "ios", "android", "picker", "dropdown", "select", "input select", "new architecture", "typescript", "native-module"],
|
|
24
|
+
"repository": "https://github.com/wneel/react-native-native-select",
|
|
25
|
+
"author": "Wayan NEEL <66263633+wneel@users.noreply.github.com> (https://github.com/wneel)",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/wneel/react-native-native-select/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/wneel/react-native-native-select#readme",
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": "*",
|
|
33
|
+
"react-native": ">=0.71.0"
|
|
34
|
+
},
|
|
35
|
+
"codegenConfig": {
|
|
36
|
+
"name": "RTNSelectSpec",
|
|
37
|
+
"type": "components",
|
|
38
|
+
"jsSrcsDir": "src",
|
|
39
|
+
"android": {
|
|
40
|
+
"javaPackageName": "com.rtnselect"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "react-native-native-select"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = "A strictly native, performant Select component for React Native."
|
|
9
|
+
s.description = package["description"]
|
|
10
|
+
s.homepage = "https://github.com/wneel/react-native-native-select"
|
|
11
|
+
s.license = package["license"]
|
|
12
|
+
s.platforms = { :ios => "11.0" }
|
|
13
|
+
s.author = package["author"]
|
|
14
|
+
s.source = { :git => "https://github.com/wneel/react-native-native-select.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
17
|
+
|
|
18
|
+
install_modules_dependencies(s)
|
|
19
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
2
|
+
|
|
3
|
+
import type { HostComponent } from 'react-native';
|
|
4
|
+
import type { ViewProps } from 'react-native/Libraries/Components/View/ViewPropTypes';
|
|
5
|
+
import type { Int32, DirectEventHandler, WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
|
|
6
|
+
|
|
7
|
+
type OnChangeEvent = Readonly<{
|
|
8
|
+
value: string;
|
|
9
|
+
index: Int32;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
export interface NativeProps extends ViewProps {
|
|
13
|
+
options: ReadonlyArray<string>;
|
|
14
|
+
selectedIndex?: Int32;
|
|
15
|
+
mode?: WithDefault<'dialog' | 'dropdown', 'dialog'>;
|
|
16
|
+
onValueChange?: DirectEventHandler<OnChangeEvent>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default codegenNativeComponent<NativeProps>(
|
|
20
|
+
'RTNSelect'
|
|
21
|
+
) as HostComponent<NativeProps>;
|
package/src/index.ts
ADDED