react-native-native-select 2.0.0 → 2.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/README.md +6 -7
- package/android/src/main/java/com/rtnselect/SelectView.java +120 -86
- package/android/src/main/java/com/rtnselect/SelectViewManager.java +5 -1
- package/android/src/main/java/com/rtnselect/ValueChangeEvent.java +41 -0
- package/package.json +2 -1
- package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
- package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/9.2.0/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
- package/android/.gradle/vcs-1/gc.properties +0 -0
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ There is no JS rendering logic in this package. When your user opens the list, t
|
|
|
30
30
|
| :--- | :--- | :--- | :--- |
|
|
31
31
|
| iOS | `dropdown` | A real `UIMenu` built from `UIAction`s, attached to a `UIButton` with `showsMenuAsPrimaryAction = YES` (iOS 14+). The selected row carries `UIMenuElementStateOn`, so the system draws its own checkmark. | `ios/RTNSelect.mm` |
|
|
32
32
|
| iOS | `dialog` (default) | `UIPickerView`, the classic wheel, with this view as its own dataSource and delegate. | `ios/RTNSelect.mm` |
|
|
33
|
-
| Android | any | `AppCompatSpinner` with an `ArrayAdapter` over the platform `simple_spinner_item` and `simple_spinner_dropdown_item` layouts. | `android/src/main/java/com/rtnselect/SelectView.java` |
|
|
33
|
+
| Android | any | `AppCompatSpinner`, pinned to `MODE_DROPDOWN` so the host app's theme cannot switch it to a dialog, with an `ArrayAdapter` over the platform `simple_spinner_item` and `simple_spinner_dropdown_item` layouts. | `android/src/main/java/com/rtnselect/SelectView.java` |
|
|
34
34
|
|
|
35
35
|
On iOS a single `RCTViewComponentView` holds both the picker and the button, and `mode` decides which one is hidden. Nothing is custom-drawn on either platform: no `Modal`, no `FlatList`, no wheel re-implemented in JS.
|
|
36
36
|
|
|
@@ -238,8 +238,8 @@ Three details make the difference between this working and nearly working:
|
|
|
238
238
|
| Prop | Type | Required | Description |
|
|
239
239
|
| :--- | :--- | :---: | :--- |
|
|
240
240
|
| **`options`** | `ReadonlyArray<string>` | **Yes** | The rows to display. Strings only. |
|
|
241
|
-
| **`selectedIndex`** | `number` | No | Index of the selected row. The native views start at `0`.
|
|
242
|
-
| **`mode`** | `'dialog' \| 'dropdown'` | No | **iOS only.** `dialog` (the codegen default) uses `UIPickerView`, the classic wheel. `dropdown` uses the iOS 14+ `UIMenu` pull-down. Ignored on Android, which always draws its spinner. |
|
|
241
|
+
| **`selectedIndex`** | `number` | No | Index of the selected row. The native views start at `0`. A negative index means "nothing selected yet" and is ignored rather than applied. |
|
|
242
|
+
| **`mode`** | `'dialog' \| 'dropdown'` | No | **iOS only.** `dialog` (the codegen default) uses `UIPickerView`, the classic wheel. `dropdown` uses the iOS 14+ `UIMenu` pull-down. Ignored on Android, which always draws its spinner dropdown. |
|
|
243
243
|
| **`textColor`** | `ColorValue` | No | **iOS only.** Overrides the text color of both the wheel and the button title. Defaults to `UIColor.labelColor`, which already follows light and dark mode, so set this only when you need to override the system. |
|
|
244
244
|
| **`onValueChange`** | `(event) => void` | No | Fired on selection. Read `event.nativeEvent.value` (`string`) and `event.nativeEvent.index` (`number`). |
|
|
245
245
|
| **`style`** | `ViewStyle` | No | Standard style prop. Give it a `width` and a `height`, or Flexbox constraints that produce them. |
|
|
@@ -255,9 +255,8 @@ onValueChange={(e) => {
|
|
|
255
255
|
|
|
256
256
|
Nothing here is a bug to be fixed later, it is what wrapping two different OS widgets behind one component costs. Better said out loud.
|
|
257
257
|
|
|
258
|
-
* **`mode` and `textColor` do nothing on Android.** In `SelectViewManager
|
|
259
|
-
* **Android
|
|
260
|
-
* **Writing `selectedIndex` on Android emits a change event.** `setSelection` reports back through `topValueChange` as well as the adapter's own `onItemSelected`, so a state update driven from JS can echo. If your handler does more than `setState`, guard it against the value it already holds.
|
|
258
|
+
* **`mode` and `textColor` do nothing on Android.** In `SelectViewManager` both setters carry a comment saying so. They exist only because the codegen-generated `RTNSelectManagerInterface` requires them, and a missing method there is a build failure (that is exactly what v1.1.1 fixed). Style Android text through the overlay pattern instead.
|
|
259
|
+
* **Android pins its spinner to `MODE_DROPDOWN`.** The mode an `AppCompatSpinner` takes otherwise comes from the host app theme's `spinnerStyle`. AppCompat's own sets `dropdown`, but a vendor theme can hand back `dialog`, where the popup is an `AlertDialog` built on the React context. Since 2.0.2 the view constructs itself in dropdown mode so the widget behaves the same on every device.
|
|
261
260
|
* **Below iOS 14, `dropdown` degrades to an inert button.** Both `showsMenuAsPrimaryAction` and the menu construction sit behind `@available(iOS 14.0, *)`, so on iOS 11 to 13 the button renders with no menu attached. Use `dialog` if you still support those versions.
|
|
262
261
|
* **iOS applies props on the main queue.** `updateProps` diffs `options`, `selectedIndex`, `mode` and `textColor`, then dispatches the UIKit work asynchronously. A `selectedIndex` change animates the wheel; expect the visual update one turn later, not synchronously with your `setState`.
|
|
263
262
|
|
|
@@ -368,7 +367,7 @@ Its `opacity` is `0` (or below `0.02`). See the overlay pattern section.
|
|
|
368
367
|
Expected, they are iOS only. See Platform differences.
|
|
369
368
|
|
|
370
369
|
**Android fires `onValueChange` when I set `selectedIndex` from JS.**
|
|
371
|
-
|
|
370
|
+
Fixed in 2.0.2. A selection this component makes on your behalf no longer travels back as a change, and neither does the row-0 selection the adapter takes when it attaches.
|
|
372
371
|
|
|
373
372
|
## Contributing
|
|
374
373
|
|
|
@@ -1,128 +1,162 @@
|
|
|
1
1
|
package com.rtnselect;
|
|
2
2
|
|
|
3
3
|
import android.content.Context;
|
|
4
|
-
import android.util.AttributeSet;
|
|
5
|
-
import android.util.Log;
|
|
6
4
|
import android.view.View;
|
|
7
5
|
import android.widget.AdapterView;
|
|
8
6
|
import android.widget.ArrayAdapter;
|
|
9
|
-
import
|
|
7
|
+
import android.widget.Spinner;
|
|
8
|
+
|
|
10
9
|
import androidx.appcompat.widget.AppCompatSpinner;
|
|
11
10
|
|
|
12
|
-
import com.facebook.react.bridge.Arguments;
|
|
13
11
|
import com.facebook.react.bridge.ReactContext;
|
|
14
|
-
import com.facebook.react.
|
|
15
|
-
import com.facebook.react.uimanager.events.
|
|
12
|
+
import com.facebook.react.uimanager.UIManagerHelper;
|
|
13
|
+
import com.facebook.react.uimanager.events.EventDispatcher;
|
|
16
14
|
|
|
15
|
+
import java.util.ArrayList;
|
|
17
16
|
import java.util.List;
|
|
18
17
|
|
|
19
18
|
public class SelectView extends AppCompatSpinner implements AdapterView.OnItemSelectedListener {
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
/** No index yet. Distinct from 0, which is a legitimate selection. */
|
|
21
|
+
private static final int NO_SELECTION = -1;
|
|
22
|
+
|
|
23
|
+
private final List<String> options = new ArrayList<>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The index JS asked for. Fabric writes props in whatever order RawProps yields, so
|
|
27
|
+
* `selectedIndex` regularly lands before `options` and there is no adapter to apply it to
|
|
28
|
+
* yet. Holding it here and replaying it once the adapter exists makes prop order irrelevant.
|
|
29
|
+
*/
|
|
30
|
+
private int requestedIndex = NO_SELECTION;
|
|
31
|
+
|
|
32
|
+
/** The last index announced to JS, so an unchanged selection stays quiet. */
|
|
33
|
+
private int announcedIndex = NO_SELECTION;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Set when the user opens the list. It is what tells a person's pick apart from a prop write,
|
|
37
|
+
* and the two are otherwise indistinguishable: the spinner's popup reports the row the user
|
|
38
|
+
* chose by calling this view's own `setSelection`, the very method the `selectedIndex` prop
|
|
39
|
+
* arrives through. Emitting from `setSelection` unconditionally is why writing the prop used
|
|
40
|
+
* to echo an onValueChange straight back to JS.
|
|
41
|
+
*/
|
|
42
|
+
private boolean openedByUser = false;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* React Native drives layout from the shadow tree and swallows a requestLayout() raised by a
|
|
46
|
+
* child, so AbsSpinner.setAdapter's own request never produces a measure pass and the spinner
|
|
47
|
+
* keeps drawing its previous row. Re-running measure and layout on our own frame is the
|
|
48
|
+
* standard workaround on this platform; ReactPicker and ReactSwitch both carry it.
|
|
49
|
+
*/
|
|
50
|
+
private final Runnable measureAndLayout = new Runnable() {
|
|
51
|
+
@Override
|
|
52
|
+
public void run() {
|
|
53
|
+
measure(
|
|
54
|
+
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
|
|
55
|
+
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY)
|
|
56
|
+
);
|
|
57
|
+
layout(getLeft(), getTop(), getRight(), getBottom());
|
|
58
|
+
}
|
|
59
|
+
};
|
|
22
60
|
|
|
23
61
|
public SelectView(Context context) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
super(context,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
super(context, attrs, defStyleAttr);
|
|
35
|
-
init();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
private void init() {
|
|
39
|
-
this.setOnItemSelectedListener(this);
|
|
40
|
-
this.setBackground(null);
|
|
62
|
+
// MODE_DROPDOWN is pinned rather than inherited. An AppCompatSpinner fixes its mode at
|
|
63
|
+
// construction, and the mode it would otherwise take comes from the host app theme's
|
|
64
|
+
// spinnerStyle: AppCompat's own sets `dropdown`, but a vendor theme can hand back
|
|
65
|
+
// `dialog`, where the popup is an AlertDialog built on the React context. Pinning it makes
|
|
66
|
+
// the widget behave the same whatever theme it is dropped into.
|
|
67
|
+
super(context, null, androidx.appcompat.R.attr.spinnerStyle, Spinner.MODE_DROPDOWN);
|
|
68
|
+
setOnItemSelectedListener(this);
|
|
69
|
+
// The overlay pattern this component is built for puts an invisible Select on top of
|
|
70
|
+
// app-drawn chrome, so the platform spinner background and its arrow must not show through.
|
|
71
|
+
setBackground(null);
|
|
41
72
|
}
|
|
42
73
|
|
|
43
74
|
@Override
|
|
44
|
-
public
|
|
45
|
-
|
|
46
|
-
|
|
75
|
+
public void requestLayout() {
|
|
76
|
+
super.requestLayout();
|
|
77
|
+
post(measureAndLayout);
|
|
47
78
|
}
|
|
48
79
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
public void setOptions(List<String> options) {
|
|
80
|
+
public void setOptions(List<String> next) {
|
|
81
|
+
// Fabric re-applies every prop whenever any one of them changes, and callers routinely
|
|
82
|
+
// build this array inline, so an identical list arrives on most renders. Rebuilding the
|
|
83
|
+
// adapter for it would reset the spinner's own selection for nothing.
|
|
84
|
+
if (options.equals(next)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
options.clear();
|
|
88
|
+
options.addAll(next);
|
|
60
89
|
|
|
61
90
|
ArrayAdapter<String> adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, options);
|
|
62
91
|
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
92
|
+
setAdapter(adapter);
|
|
63
93
|
|
|
64
|
-
|
|
94
|
+
applyRequestedSelection();
|
|
65
95
|
}
|
|
66
96
|
|
|
67
97
|
@Override
|
|
68
98
|
public void setSelection(int position) {
|
|
99
|
+
boolean userPick = openedByUser;
|
|
100
|
+
openedByUser = false;
|
|
69
101
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (item != null) val = item.toString();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
WritableMap event = Arguments.createMap();
|
|
86
|
-
event.putString("value", val);
|
|
87
|
-
event.putInt("index", position);
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
|
|
91
|
-
getId(),
|
|
92
|
-
"topValueChange",
|
|
93
|
-
event
|
|
94
|
-
);
|
|
95
|
-
} catch (Exception e) {
|
|
96
|
-
}
|
|
102
|
+
if (!userPick && requestedIndex == position) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
requestedIndex = position;
|
|
106
|
+
applyRequestedSelection();
|
|
107
|
+
|
|
108
|
+
if (position < 0 || position == announcedIndex) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
announcedIndex = position;
|
|
112
|
+
if (userPick) {
|
|
113
|
+
emitValueChange(position);
|
|
97
114
|
}
|
|
98
115
|
}
|
|
99
116
|
|
|
100
117
|
@Override
|
|
101
|
-
public
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
118
|
+
public boolean performClick() {
|
|
119
|
+
openedByUser = true;
|
|
120
|
+
return super.performClick();
|
|
121
|
+
}
|
|
106
122
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
|
|
115
|
-
getId(),
|
|
116
|
-
"topValueChange",
|
|
117
|
-
event
|
|
118
|
-
);
|
|
119
|
-
} catch (Exception e) {
|
|
120
|
-
}
|
|
121
|
-
} else {
|
|
123
|
+
private void applyRequestedSelection() {
|
|
124
|
+
// A -1 is how callers spell "nothing selected yet" after an indexOf, and a spinner has no
|
|
125
|
+
// empty state. Forwarding it used to reach ArrayAdapter.getItem and take the host app down
|
|
126
|
+
// with IndexOutOfBoundsException.
|
|
127
|
+
if (requestedIndex < 0 || getAdapter() == null || requestedIndex >= getAdapter().getCount()) {
|
|
128
|
+
return;
|
|
122
129
|
}
|
|
130
|
+
if (getSelectedItemPosition() != requestedIndex) {
|
|
131
|
+
super.setSelection(requestedIndex);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@Override
|
|
136
|
+
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
|
137
|
+
// Deliberately silent, and it leaves announcedIndex alone. Every selection that matters to
|
|
138
|
+
// JS already came through setSelection above, while this callback also fires for the row 0
|
|
139
|
+
// an attaching adapter picks on its own, which iOS never reports.
|
|
123
140
|
}
|
|
124
141
|
|
|
125
142
|
@Override
|
|
126
143
|
public void onNothingSelected(AdapterView<?> parent) {
|
|
127
144
|
}
|
|
145
|
+
|
|
146
|
+
private void emitValueChange(int index) {
|
|
147
|
+
if (index < 0 || index >= options.size()) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
Context context = getContext();
|
|
151
|
+
if (!(context instanceof ReactContext)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
EventDispatcher dispatcher = UIManagerHelper.getEventDispatcher((ReactContext) context);
|
|
155
|
+
if (dispatcher == null) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
dispatcher.dispatchEvent(
|
|
159
|
+
new ValueChangeEvent(UIManagerHelper.getSurfaceId(this), getId(), options.get(index), index)
|
|
160
|
+
);
|
|
161
|
+
}
|
|
128
162
|
}
|
|
@@ -77,7 +77,11 @@ public class SelectViewManager extends SimpleViewManager<SelectView>
|
|
|
77
77
|
|
|
78
78
|
@Override
|
|
79
79
|
@ReactProp(name = "mode")
|
|
80
|
-
public void setMode(SelectView view, @Nullable String value) {
|
|
80
|
+
public void setMode(SelectView view, @Nullable String value) {
|
|
81
|
+
// iOS-only feature: Android has one widget for both modes, and the view pins it to
|
|
82
|
+
// MODE_DROPDOWN so the host app's theme cannot change it. The method must exist to
|
|
83
|
+
// satisfy the interface generated by TypeScript.
|
|
84
|
+
}
|
|
81
85
|
|
|
82
86
|
@Override
|
|
83
87
|
@ReactProp(name = "textColor", customType = "Color")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package com.rtnselect;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.Nullable;
|
|
4
|
+
|
|
5
|
+
import com.facebook.react.bridge.Arguments;
|
|
6
|
+
import com.facebook.react.bridge.WritableMap;
|
|
7
|
+
import com.facebook.react.uimanager.events.Event;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The `onValueChange` payload, dispatched through the EventDispatcher so it reaches both
|
|
11
|
+
* architectures. The legacy `RCTEventEmitter` path this replaces only still worked under
|
|
12
|
+
* bridgeless through the `useFabricInterop` shim, which logs a soft exception per event and
|
|
13
|
+
* dispatches with a surface id of -1.
|
|
14
|
+
*/
|
|
15
|
+
public class ValueChangeEvent extends Event<ValueChangeEvent> {
|
|
16
|
+
|
|
17
|
+
public static final String EVENT_NAME = "topValueChange";
|
|
18
|
+
|
|
19
|
+
private final String value;
|
|
20
|
+
private final int index;
|
|
21
|
+
|
|
22
|
+
public ValueChangeEvent(int surfaceId, int viewId, String value, int index) {
|
|
23
|
+
super(surfaceId, viewId);
|
|
24
|
+
this.value = value;
|
|
25
|
+
this.index = index;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Override
|
|
29
|
+
public String getEventName() {
|
|
30
|
+
return EVENT_NAME;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Nullable
|
|
34
|
+
@Override
|
|
35
|
+
protected WritableMap getEventData() {
|
|
36
|
+
WritableMap event = Arguments.createMap();
|
|
37
|
+
event.putString("value", value);
|
|
38
|
+
event.putInt("index", index);
|
|
39
|
+
return event;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-native-select",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "react-native-native-select is a strictly native, performant Select component for React Native built exclusively for the New Architecture.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"ios",
|
|
13
13
|
"react-native-native-select.podspec",
|
|
14
14
|
"!android/build",
|
|
15
|
+
"!android/.gradle",
|
|
15
16
|
"!ios/build",
|
|
16
17
|
"!tests",
|
|
17
18
|
"!**/__tests__",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|