@octane-xplat/picker 0.0.0 → 0.7.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 +17 -0
- package/dist/native/MaterialDropdown.android.tsrx.js +49 -0
- package/dist/native/SwiftUIPicker.ios.tsrx.js +49 -0
- package/dist/native/android/index.js +2 -0
- package/dist/native/ios/index.js +2 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/web/Select.web.tsrx.js +92 -0
- package/dist/web/octane-client-build.json +8 -0
- package/dist/web/web/index.js +2 -0
- package/package.json +70 -5
- package/platforms/android/include.gradle +15 -0
- package/platforms/android/java/com/octanexplat/nativepicker/XplatPickerProvider.kt +112 -0
- package/platforms/ios/src/XplatPickerProvider.swift +90 -0
- package/src/MaterialDropdown.android.tsrx +52 -0
- package/src/Select.web.tsrx +33 -0
- package/src/SwiftUIPicker.ios.tsrx +52 -0
- package/src/android/index.ts +2 -0
- package/src/android/types.ts +18 -0
- package/src/ios/index.ts +2 -0
- package/src/ios/types.ts +18 -0
- package/src/web/index.ts +2 -0
- package/src/web/types.ts +18 -0
- package/types/android/index.d.ts +22 -0
- package/types/ios/index.d.ts +22 -0
- package/types/web/index.d.ts +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# `@octane-xplat/picker`
|
|
2
|
+
|
|
3
|
+
An install boundary for platform-specific selection controls: a SwiftUI `Picker`
|
|
4
|
+
on iOS, a Material 3 dropdown built with Jetpack Compose on Android, and an
|
|
5
|
+
HTML `<select>` on web.
|
|
6
|
+
|
|
7
|
+
Install the package in the app that renders a control. Import `SwiftUIPicker`
|
|
8
|
+
from `@octane-xplat/picker/ios`, `MaterialDropdown` from
|
|
9
|
+
`@octane-xplat/picker/android`, or `Select` from `@octane-xplat/picker/web` in
|
|
10
|
+
the matching platform-suffixed file. Each entry exports its own component and
|
|
11
|
+
types; there is no shared picker API at the package root.
|
|
12
|
+
|
|
13
|
+
The platform APIs use their own selection vocabulary and option shapes. The iOS
|
|
14
|
+
entry uses SwiftUI selection IDs, the Android entry uses Material dropdown keys
|
|
15
|
+
and enabled states, and the web entry follows browser select values. See the
|
|
16
|
+
framework guide for details and maintained examples:
|
|
17
|
+
[`docs/native-picker.md`](../../docs/native-picker.md).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineUniversalComponent, hookSlots, registerElement, universalPlan, universalValue, useState } from "@nativescript-community/octane";
|
|
2
|
+
import { ComposeDataDriver, JetpackCompose, registerJetpackCompose } from "@nativescript/jetpack-compose";
|
|
3
|
+
//#region src/MaterialDropdown.android.tsrx
|
|
4
|
+
var _h$0 = /* @__PURE__ */ Symbol(/* @__PURE__ */ hookSlots(1));
|
|
5
|
+
var __octaneUniversalPlan0 = universalPlan("nativescript", {
|
|
6
|
+
"kind": "host",
|
|
7
|
+
"type": "xplatandroidpicker",
|
|
8
|
+
"props": { "composeId": "xplat-material-dropdown" },
|
|
9
|
+
"bindings": [
|
|
10
|
+
["id", 0],
|
|
11
|
+
["className", 1],
|
|
12
|
+
["style", 2],
|
|
13
|
+
["data", 3],
|
|
14
|
+
["onComposeEvent", 4]
|
|
15
|
+
]
|
|
16
|
+
});
|
|
17
|
+
registerElement("xplatandroidpicker", JetpackCompose);
|
|
18
|
+
registerJetpackCompose("xplat-material-dropdown", (owner) => new ComposeDataDriver(new com.octanexplat.nativepicker.XplatPickerProvider(), owner));
|
|
19
|
+
var MaterialDropdown = defineUniversalComponent("nativescript", function MaterialDropdown(props) {
|
|
20
|
+
const controlled = props.selectedKey !== void 0;
|
|
21
|
+
const [internalKey, setInternalKey] = useState(props.defaultSelectedKey ?? props.items[0]?.key ?? "", _h$0);
|
|
22
|
+
const selectedKey = controlled ? props.selectedKey : internalKey;
|
|
23
|
+
const data = {
|
|
24
|
+
label: props.label,
|
|
25
|
+
accessibilityLabel: props.accessibilityLabel ?? props.label,
|
|
26
|
+
options: props.items.map((item) => ({
|
|
27
|
+
value: item.key,
|
|
28
|
+
label: item.text,
|
|
29
|
+
disabled: item.enabled === false ? 1 : 0
|
|
30
|
+
})),
|
|
31
|
+
value: selectedKey,
|
|
32
|
+
disabled: props.enabled === false ? 1 : 0
|
|
33
|
+
};
|
|
34
|
+
const update = (nextKey) => {
|
|
35
|
+
if (!controlled) setInternalKey(nextKey);
|
|
36
|
+
props.onSelectedKeyChange?.(nextKey);
|
|
37
|
+
};
|
|
38
|
+
return universalValue(__octaneUniversalPlan0, [
|
|
39
|
+
props.id,
|
|
40
|
+
props.className,
|
|
41
|
+
props.style,
|
|
42
|
+
data,
|
|
43
|
+
(event) => {
|
|
44
|
+
if (typeof event.data === "string") update(event.data);
|
|
45
|
+
}
|
|
46
|
+
]);
|
|
47
|
+
}, { "module": "@nativescript-community/octane" });
|
|
48
|
+
//#endregion
|
|
49
|
+
export { MaterialDropdown };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineUniversalComponent, hookSlots, registerElement, universalPlan, universalValue, useState } from "@nativescript-community/octane";
|
|
2
|
+
import { SwiftUI, UIDataDriver, registerSwiftUI } from "@nativescript/swift-ui";
|
|
3
|
+
//#region src/SwiftUIPicker.ios.tsrx
|
|
4
|
+
var _h$0 = /* @__PURE__ */ Symbol(/* @__PURE__ */ hookSlots(1));
|
|
5
|
+
var __octaneUniversalPlan0 = universalPlan("nativescript", {
|
|
6
|
+
"kind": "host",
|
|
7
|
+
"type": "xplatiospicker",
|
|
8
|
+
"props": { "swiftId": "xplat-swiftui-picker" },
|
|
9
|
+
"bindings": [
|
|
10
|
+
["id", 0],
|
|
11
|
+
["className", 1],
|
|
12
|
+
["style", 2],
|
|
13
|
+
["data", 3],
|
|
14
|
+
["onSwiftUIEvent", 4]
|
|
15
|
+
]
|
|
16
|
+
});
|
|
17
|
+
registerElement("xplatiospicker", SwiftUI);
|
|
18
|
+
registerSwiftUI("xplat-swiftui-picker", (owner) => new UIDataDriver(new XplatPickerProvider(), owner));
|
|
19
|
+
var SwiftUIPicker = defineUniversalComponent("nativescript", function SwiftUIPicker(props) {
|
|
20
|
+
const controlled = props.selection !== void 0;
|
|
21
|
+
const [internalSelection, setInternalSelection] = useState(props.defaultSelection ?? props.options[0]?.id ?? "", _h$0);
|
|
22
|
+
const selection = controlled ? props.selection : internalSelection;
|
|
23
|
+
const update = (nextSelection) => {
|
|
24
|
+
if (!controlled) setInternalSelection(nextSelection);
|
|
25
|
+
props.onSelectionChange?.(nextSelection);
|
|
26
|
+
};
|
|
27
|
+
return universalValue(__octaneUniversalPlan0, [
|
|
28
|
+
props.id,
|
|
29
|
+
props.className,
|
|
30
|
+
props.style,
|
|
31
|
+
{
|
|
32
|
+
label: props.label,
|
|
33
|
+
accessibilityLabel: props.accessibilityLabel ?? props.label,
|
|
34
|
+
options: props.options.map((option) => ({
|
|
35
|
+
value: option.id,
|
|
36
|
+
label: option.title,
|
|
37
|
+
disabled: option.disabled ?? false
|
|
38
|
+
})),
|
|
39
|
+
value: selection,
|
|
40
|
+
disabled: props.disabled ?? false
|
|
41
|
+
},
|
|
42
|
+
(event) => {
|
|
43
|
+
const nextSelection = event.data?.value;
|
|
44
|
+
if (typeof nextSelection === "string") update(nextSelection);
|
|
45
|
+
}
|
|
46
|
+
]);
|
|
47
|
+
}, { "module": "@nativescript-community/octane" });
|
|
48
|
+
//#endregion
|
|
49
|
+
export { SwiftUIPicker };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { __s, bag14, bag5, child, clone, createScopedElement, delegateEvents, hookSlots, mapSlot, setAriaAttribute, setAttribute, setBooleanAttribute, setEventHandler, setStyle, sibling, template, useState } from "octane";
|
|
2
|
+
import { bindSignalAttribute, bindSignalChild, bindSignalValue, compilerMemoRegion, enableSignalBindings, hookMemoEqual, setPlainAttribute } from "octane/internal/client";
|
|
3
|
+
//#region src/Select.web.tsrx
|
|
4
|
+
enableSignalBindings(1, true);
|
|
5
|
+
delegateEvents(["change"]);
|
|
6
|
+
var _t$0 = /* @__PURE__ */ template("<option></option>");
|
|
7
|
+
var _t$1 = /* @__PURE__ */ template("<label><span></span><select data-octane-input=\"i:d2ddab07\"><!></select></label>");
|
|
8
|
+
var _hs$ = /* @__PURE__ */ hookSlots(2);
|
|
9
|
+
var _key$1 = (option) => option.value;
|
|
10
|
+
function __item$2(option, __s, __extra) {
|
|
11
|
+
let _b = __s.slots[0];
|
|
12
|
+
if (_b === void 0) {
|
|
13
|
+
let _m1;
|
|
14
|
+
const _root = clone(_t$0);
|
|
15
|
+
_m1 = _root;
|
|
16
|
+
_b = bag5(__s, _root, void 0, _m1, void 0, null, void 0);
|
|
17
|
+
}
|
|
18
|
+
_b.a = bindSignalAttribute(__s, _b.a, _b.b, "value", option.value, "b:f8ba3234", setAttribute);
|
|
19
|
+
_b.c = bindSignalAttribute(__s, _b.c, _b.b, "disabled", option.disabled, "b:97cc1a6e", setBooleanAttribute);
|
|
20
|
+
_b.f = bindSignalChild(__s, _b.f, 1, _b.b, option.label, "b:f207a308", null, void 0, void 0, void 0, true);
|
|
21
|
+
}
|
|
22
|
+
var Select = /* @__PURE__ */ __s(function Select(props, __s, __extra) {
|
|
23
|
+
const __memoRegion = compilerMemoRegion(__s, _hs$ + 0);
|
|
24
|
+
const __memoCommitted = __memoRegion.auto;
|
|
25
|
+
let __memoCache = __memoCommitted === void 0 ? [] : __memoCommitted;
|
|
26
|
+
let __mapItems;
|
|
27
|
+
let __mapMethod;
|
|
28
|
+
const controlled = props.value !== void 0;
|
|
29
|
+
const [internalValue, setInternalValue] = useState(props.defaultValue ?? props.options[0]?.value ?? "", _hs$ + 0);
|
|
30
|
+
const value = controlled ? props.value : internalValue;
|
|
31
|
+
const labelId = props.id ? `${props.id}-label` : void 0;
|
|
32
|
+
if (__s.block.pending && !__s.block.crossRenderUpdate) return;
|
|
33
|
+
let _b = __s.slots[0];
|
|
34
|
+
if (_b === void 0) {
|
|
35
|
+
let _m1, _m4, _m6, _m9, _m11;
|
|
36
|
+
const _root = clone(_t$1);
|
|
37
|
+
_m1 = _root;
|
|
38
|
+
const _el0 = child(_root);
|
|
39
|
+
_m4 = _el0;
|
|
40
|
+
const _el1 = sibling(_el0, 1);
|
|
41
|
+
_m6 = _el1;
|
|
42
|
+
_m9 = bindSignalValue(__s, void 0, _el1, value, "i:d2ddab07");
|
|
43
|
+
setEventHandler(_el1, "$$change", (event) => {
|
|
44
|
+
const nextValue = event.target.value;
|
|
45
|
+
if (!controlled) setInternalValue(nextValue);
|
|
46
|
+
props.onChange?.(nextValue);
|
|
47
|
+
});
|
|
48
|
+
_m11 = child(_el1);
|
|
49
|
+
_b = bag14(__s, _root, void 0, _m1, void 0, void 0, _m4, void 0, _m6, void 0, void 0, _m9, void 0, _m11, null, void 0);
|
|
50
|
+
} else {
|
|
51
|
+
_b.j = bindSignalValue(__s, _b.j, _b.g, value, "i:d2ddab07");
|
|
52
|
+
setEventHandler(_b.g, "$$change", (event) => {
|
|
53
|
+
const nextValue = event.target.value;
|
|
54
|
+
if (!controlled) setInternalValue(nextValue);
|
|
55
|
+
props.onChange?.(nextValue);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
_b.a = bindSignalAttribute(__s, _b.a, _b.b, "class", props.className, "b:1fc7f020", "class");
|
|
59
|
+
{
|
|
60
|
+
const _v = props.style;
|
|
61
|
+
if (_b.c !== _v) {
|
|
62
|
+
setStyle(_b.b, _v, _b.c);
|
|
63
|
+
_b.c = _v;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
_b.d = bindSignalAttribute(__s, _b.d, _b.e, "id", labelId, "b:9b7dc37e", setPlainAttribute);
|
|
67
|
+
_b.f = bindSignalAttribute(__s, _b.f, _b.g, "id", props.id, "b:0291033d", setPlainAttribute);
|
|
68
|
+
_b.h = bindSignalAttribute(__s, _b.h, _b.g, "aria-label", props.accessibilityLabel ?? props.label, "b:fc00c409", setAriaAttribute);
|
|
69
|
+
_b.i = bindSignalAttribute(__s, _b.i, _b.g, "aria-labelledby", labelId, "b:adebc444", setAriaAttribute);
|
|
70
|
+
_b.k = bindSignalAttribute(__s, _b.k, _b.g, "disabled", props.disabled, "b:1f344a18", setBooleanAttribute);
|
|
71
|
+
_b.o = bindSignalChild(__s, _b.o, 1, _b.e, props.label, "b:7206b4a3", null, void 0, void 0, void 0, true);
|
|
72
|
+
{
|
|
73
|
+
const _v = (__mapItems = props.options, __mapMethod = __mapItems.map, __mapItems);
|
|
74
|
+
const __mapNative = mapSlot(_v, __mapMethod);
|
|
75
|
+
{
|
|
76
|
+
const __memoDep = _v;
|
|
77
|
+
if (__s.slots[2] === void 0 || !__mapNative || __memoCache[1] !== true || !hookMemoEqual(__memoCache[0], __memoDep)) {
|
|
78
|
+
mapSlot(__s, 2, _b.g, _v, __mapMethod, __mapNative, (option) => createScopedElement("option", {
|
|
79
|
+
key: option.value,
|
|
80
|
+
value: option.value,
|
|
81
|
+
disabled: option.disabled
|
|
82
|
+
}, () => option.label), _key$1, __item$2, 27, void 0, _b.l, 1);
|
|
83
|
+
if (__memoCache === __memoCommitted) __memoCache = __memoCache.slice();
|
|
84
|
+
__memoCache[0] = __memoDep;
|
|
85
|
+
__memoCache[1] = __mapNative;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (__memoCache !== __memoCommitted) __memoRegion.auto = __memoCache;
|
|
90
|
+
});
|
|
91
|
+
//#endregion
|
|
92
|
+
export { Select };
|
package/package.json
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@octane-xplat/picker",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Platform-specific selection controls for Octane xplat",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/octane-xplat/octane-xplat.git",
|
|
8
|
+
"directory": "packages/picker"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"types",
|
|
13
|
+
"src",
|
|
14
|
+
"platforms"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"exports": {
|
|
18
|
+
"./ios": {
|
|
19
|
+
"types": "./types/ios/index.d.ts",
|
|
20
|
+
"native": "./dist/native/ios/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./android": {
|
|
23
|
+
"types": "./types/android/index.d.ts",
|
|
24
|
+
"native": "./dist/native/android/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./web": {
|
|
27
|
+
"types": "./types/web/index.d.ts",
|
|
28
|
+
"web": "./dist/web/web/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@nativescript/jetpack-compose": "1.0.0-beta.0",
|
|
36
|
+
"@nativescript/swift-ui": "4.0.2"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@nativescript-community/octane": "0.2.1",
|
|
40
|
+
"@nativescript/core": "9.1.2",
|
|
41
|
+
"@octanejs/vite-plugin": "0.1.61",
|
|
42
|
+
"octane": "0.6.3",
|
|
43
|
+
"typescript": "5.9.3",
|
|
44
|
+
"vite": "8.3.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@nativescript-community/octane": ">=0.2.1 <1",
|
|
48
|
+
"@nativescript/core": ">=9.1.0 <10",
|
|
49
|
+
"octane": ">=0.6.3 <1"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"@nativescript-community/octane": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"@nativescript/core": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"octane": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"nativescript": {
|
|
63
|
+
"platforms": {
|
|
64
|
+
"android": "9.1.0",
|
|
65
|
+
"ios": "9.1.0"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "vite build && vite build --mode native && tsc -p tsconfig.types.json && node ../../scripts/check-native-dist.mjs"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
|
2
|
+
|
|
3
|
+
android {
|
|
4
|
+
buildFeatures {
|
|
5
|
+
compose true
|
|
6
|
+
}
|
|
7
|
+
kotlinOptions {
|
|
8
|
+
jvmTarget = '17'
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
dependencies {
|
|
13
|
+
implementation platform('androidx.compose:compose-bom:2024.12.01')
|
|
14
|
+
implementation 'androidx.compose.material3:material3'
|
|
15
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
package com.octanexplat.nativepicker
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
4
|
+
import androidx.compose.foundation.layout.Box
|
|
5
|
+
import androidx.compose.foundation.layout.Row
|
|
6
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
7
|
+
import androidx.compose.foundation.layout.padding
|
|
8
|
+
import androidx.compose.material3.Button
|
|
9
|
+
import androidx.compose.material3.DropdownMenu
|
|
10
|
+
import androidx.compose.material3.DropdownMenuItem
|
|
11
|
+
import androidx.compose.material3.MaterialTheme
|
|
12
|
+
import androidx.compose.material3.Text
|
|
13
|
+
import androidx.compose.runtime.Composable
|
|
14
|
+
import androidx.compose.runtime.getValue
|
|
15
|
+
import androidx.compose.runtime.mutableStateOf
|
|
16
|
+
import androidx.compose.runtime.remember
|
|
17
|
+
import androidx.compose.runtime.setValue
|
|
18
|
+
import androidx.compose.ui.Alignment
|
|
19
|
+
import androidx.compose.ui.Modifier
|
|
20
|
+
import androidx.compose.ui.semantics.contentDescription
|
|
21
|
+
import androidx.compose.ui.semantics.semantics
|
|
22
|
+
import androidx.compose.ui.platform.ComposeView
|
|
23
|
+
import androidx.compose.ui.unit.dp
|
|
24
|
+
|
|
25
|
+
private data class PickerOption(
|
|
26
|
+
val value: String,
|
|
27
|
+
val label: String,
|
|
28
|
+
val disabled: Boolean,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
private data class PickerState(
|
|
32
|
+
val label: String = "",
|
|
33
|
+
val accessibilityLabel: String = "",
|
|
34
|
+
val value: String = "",
|
|
35
|
+
val disabled: Boolean = false,
|
|
36
|
+
val options: List<PickerOption> = emptyList(),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
private fun Any?.asBoolean(): Boolean = when (this) {
|
|
40
|
+
is Boolean -> this
|
|
41
|
+
is Number -> toInt() != 0
|
|
42
|
+
else -> false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class XplatPickerProvider {
|
|
46
|
+
private var state by mutableStateOf(PickerState())
|
|
47
|
+
var onEvent: ((String) -> Unit)? = null
|
|
48
|
+
|
|
49
|
+
fun generateComposeView(view: ComposeView) {
|
|
50
|
+
view.setContent {
|
|
51
|
+
MaterialTheme {
|
|
52
|
+
PickerContent(
|
|
53
|
+
state = state,
|
|
54
|
+
onValueChange = { value -> onEvent?.invoke(value) },
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fun updateData(input: Map<Any, Any>) {
|
|
61
|
+
val data = input["data"] as? Map<*, *> ?: input
|
|
62
|
+
val options = (data["options"] as? List<*>) .orEmpty()
|
|
63
|
+
.mapNotNull { raw ->
|
|
64
|
+
val option = raw as? Map<*, *> ?: return@mapNotNull null
|
|
65
|
+
val value = option["value"] as? String ?: return@mapNotNull null
|
|
66
|
+
val label = option["label"] as? String ?: return@mapNotNull null
|
|
67
|
+
PickerOption(value, label, option["disabled"].asBoolean())
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
state = PickerState(
|
|
71
|
+
label = data["label"] as? String ?: "",
|
|
72
|
+
accessibilityLabel = data["accessibilityLabel"] as? String ?: "",
|
|
73
|
+
value = data["value"] as? String ?: options.firstOrNull()?.value.orEmpty(),
|
|
74
|
+
disabled = data["disabled"].asBoolean(),
|
|
75
|
+
options = options,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@Composable
|
|
81
|
+
private fun PickerContent(state: PickerState, onValueChange: (String) -> Unit) {
|
|
82
|
+
var expanded by remember { mutableStateOf(false) }
|
|
83
|
+
val selectedLabel = state.options.firstOrNull { it.value == state.value }?.label.orEmpty()
|
|
84
|
+
|
|
85
|
+
Row(
|
|
86
|
+
modifier = Modifier
|
|
87
|
+
.fillMaxWidth()
|
|
88
|
+
.padding(horizontal = 12.dp, vertical = 4.dp)
|
|
89
|
+
.semantics { contentDescription = state.accessibilityLabel },
|
|
90
|
+
horizontalArrangement = Arrangement.SpaceBetween,
|
|
91
|
+
verticalAlignment = Alignment.CenterVertically,
|
|
92
|
+
) {
|
|
93
|
+
Text(state.label)
|
|
94
|
+
Box {
|
|
95
|
+
Button(onClick = { expanded = !expanded }, enabled = !state.disabled) {
|
|
96
|
+
Text(selectedLabel)
|
|
97
|
+
}
|
|
98
|
+
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
|
99
|
+
state.options.forEach { option ->
|
|
100
|
+
DropdownMenuItem(
|
|
101
|
+
text = { Text(option.label) },
|
|
102
|
+
enabled = !option.disabled,
|
|
103
|
+
onClick = {
|
|
104
|
+
expanded = false
|
|
105
|
+
onValueChange(option.value)
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
private struct PickerOption: Identifiable {
|
|
4
|
+
let value: String
|
|
5
|
+
let label: String
|
|
6
|
+
let disabled: Bool
|
|
7
|
+
|
|
8
|
+
var id: String { value }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
private final class PickerModel: ObservableObject {
|
|
12
|
+
@Published var label = ""
|
|
13
|
+
@Published var accessibilityLabel = ""
|
|
14
|
+
@Published var value = ""
|
|
15
|
+
@Published var disabled = false
|
|
16
|
+
@Published var options: [PickerOption] = []
|
|
17
|
+
var onValueChange: ((String) -> Void)?
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private struct PickerContent: View {
|
|
21
|
+
@ObservedObject var model: PickerModel
|
|
22
|
+
|
|
23
|
+
var body: some View {
|
|
24
|
+
Picker(model.label, selection: Binding(
|
|
25
|
+
get: { model.value },
|
|
26
|
+
set: { nextValue in
|
|
27
|
+
model.value = nextValue
|
|
28
|
+
model.onValueChange?(nextValue)
|
|
29
|
+
},
|
|
30
|
+
)) {
|
|
31
|
+
ForEach(model.options) { option in
|
|
32
|
+
Text(option.label)
|
|
33
|
+
.tag(option.value)
|
|
34
|
+
.disabled(option.disabled)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
.pickerStyle(.menu)
|
|
38
|
+
.disabled(model.disabled)
|
|
39
|
+
.accessibilityLabel(model.accessibilityLabel)
|
|
40
|
+
.background(Color.clear)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@objc(XplatPickerProvider)
|
|
45
|
+
public final class XplatPickerProvider: UIViewController, SwiftUIProvider {
|
|
46
|
+
private let model = PickerModel()
|
|
47
|
+
|
|
48
|
+
public required init() {
|
|
49
|
+
super.init(nibName: nil, bundle: nil)
|
|
50
|
+
model.onValueChange = { [weak self] value in
|
|
51
|
+
self?.onEvent?(["value": value])
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public required init?(coder: NSCoder) {
|
|
56
|
+
super.init(coder: coder)
|
|
57
|
+
model.onValueChange = { [weak self] value in
|
|
58
|
+
self?.onEvent?(["value": value])
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public override func viewDidLoad() {
|
|
63
|
+
super.viewDidLoad()
|
|
64
|
+
setupSwiftUIView(content: PickerContent(model: model))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public func updateData(data: NSDictionary) {
|
|
68
|
+
if let label = data["label"] as? String { model.label = label }
|
|
69
|
+
if let accessibilityLabel = data["accessibilityLabel"] as? String {
|
|
70
|
+
model.accessibilityLabel = accessibilityLabel
|
|
71
|
+
}
|
|
72
|
+
if let value = data["value"] as? String { model.value = value }
|
|
73
|
+
if let disabled = data["disabled"] as? NSNumber { model.disabled = disabled.boolValue }
|
|
74
|
+
if let options = data["options"] as? [[String: Any]] {
|
|
75
|
+
model.options = options.compactMap { option in
|
|
76
|
+
guard let value = option["value"] as? String,
|
|
77
|
+
let label = option["label"] as? String
|
|
78
|
+
else { return nil }
|
|
79
|
+
|
|
80
|
+
return PickerOption(
|
|
81
|
+
value: value,
|
|
82
|
+
label: label,
|
|
83
|
+
disabled: (option["disabled"] as? NSNumber)?.boolValue ?? false,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public var onEvent: ((NSDictionary) -> Void)?
|
|
90
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
+
import { registerElement } from '@nativescript-community/octane'
|
|
3
|
+
import type { Attributes } from '@nativescript-community/octane/intrinsics'
|
|
4
|
+
import { ComposeDataDriver, JetpackCompose, registerJetpackCompose } from '@nativescript/jetpack-compose'
|
|
5
|
+
import { useState } from 'octane'
|
|
6
|
+
import type { MaterialDropdownProps } from './android/types'
|
|
7
|
+
|
|
8
|
+
declare const com: any
|
|
9
|
+
|
|
10
|
+
declare module '@nativescript-community/octane/intrinsics' {
|
|
11
|
+
interface NativeScriptElements {
|
|
12
|
+
xplatandroidpicker: Attributes<typeof JetpackCompose>
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
registerElement('xplatandroidpicker', JetpackCompose)
|
|
17
|
+
registerJetpackCompose('xplat-material-dropdown', (owner: any) =>
|
|
18
|
+
new ComposeDataDriver(new com.octanexplat.nativepicker.XplatPickerProvider(), owner),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
/** Renders a Material 3 dropdown through the NativeScript Jetpack Compose bridge. */
|
|
22
|
+
export function MaterialDropdown(props: MaterialDropdownProps) @{
|
|
23
|
+
const controlled = props.selectedKey !== undefined
|
|
24
|
+
const [internalKey, setInternalKey] = useState(props.defaultSelectedKey ?? props.items[0]?.key ?? '')
|
|
25
|
+
const selectedKey = controlled ? props.selectedKey! : internalKey
|
|
26
|
+
const data = {
|
|
27
|
+
label: props.label,
|
|
28
|
+
accessibilityLabel: props.accessibilityLabel ?? props.label,
|
|
29
|
+
options: props.items.map((item) => ({
|
|
30
|
+
value: item.key,
|
|
31
|
+
label: item.text,
|
|
32
|
+
disabled: item.enabled === false ? 1 : 0,
|
|
33
|
+
})),
|
|
34
|
+
value: selectedKey,
|
|
35
|
+
disabled: props.enabled === false ? 1 : 0,
|
|
36
|
+
}
|
|
37
|
+
const update = (nextKey: string) => {
|
|
38
|
+
if (!controlled) setInternalKey(nextKey)
|
|
39
|
+
props.onSelectedKeyChange?.(nextKey)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
<xplatandroidpicker
|
|
43
|
+
id={props.id}
|
|
44
|
+
className={props.className}
|
|
45
|
+
style={props.style}
|
|
46
|
+
composeId="xplat-material-dropdown"
|
|
47
|
+
data={data}
|
|
48
|
+
onComposeEvent={(event: any) => {
|
|
49
|
+
if (typeof event.data === 'string') update(event.data)
|
|
50
|
+
}}
|
|
51
|
+
/>;
|
|
52
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** @jsxImportSource octane */
|
|
2
|
+
import { useState } from 'octane'
|
|
3
|
+
import type { SelectProps } from './web/types'
|
|
4
|
+
|
|
5
|
+
/** Renders a labeled browser select. */
|
|
6
|
+
export function Select(props: SelectProps) @{
|
|
7
|
+
const controlled = props.value !== undefined
|
|
8
|
+
const [internalValue, setInternalValue] = useState(props.defaultValue ?? props.options[0]?.value ?? '')
|
|
9
|
+
const value = controlled ? props.value! : internalValue
|
|
10
|
+
const labelId = props.id ? `${props.id}-label` : undefined
|
|
11
|
+
|
|
12
|
+
<label className={props.className} style={props.style}>
|
|
13
|
+
<span id={labelId}>{props.label}</span>
|
|
14
|
+
<select
|
|
15
|
+
id={props.id}
|
|
16
|
+
aria-label={props.accessibilityLabel ?? props.label}
|
|
17
|
+
aria-labelledby={labelId}
|
|
18
|
+
value={value}
|
|
19
|
+
disabled={props.disabled}
|
|
20
|
+
onChange={(event: any) => {
|
|
21
|
+
const nextValue = event.target.value as string
|
|
22
|
+
if (!controlled) setInternalValue(nextValue)
|
|
23
|
+
props.onChange?.(nextValue)
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
{props.options.map((option) => (
|
|
27
|
+
<option key={option.value} value={option.value} disabled={option.disabled}>
|
|
28
|
+
{option.label}
|
|
29
|
+
</option>
|
|
30
|
+
))}
|
|
31
|
+
</select>
|
|
32
|
+
</label>
|
|
33
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
+
import { registerElement } from '@nativescript-community/octane'
|
|
3
|
+
import type { Attributes } from '@nativescript-community/octane/intrinsics'
|
|
4
|
+
import { SwiftUI, UIDataDriver, registerSwiftUI } from '@nativescript/swift-ui'
|
|
5
|
+
import { useState } from 'octane'
|
|
6
|
+
import type { SwiftUIPickerProps } from './ios/types'
|
|
7
|
+
|
|
8
|
+
declare const XplatPickerProvider: any
|
|
9
|
+
|
|
10
|
+
declare module '@nativescript-community/octane/intrinsics' {
|
|
11
|
+
interface NativeScriptElements {
|
|
12
|
+
xplatiospicker: Attributes<typeof SwiftUI>
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
registerElement('xplatiospicker', SwiftUI)
|
|
17
|
+
registerSwiftUI('xplat-swiftui-picker', (owner: any) =>
|
|
18
|
+
new UIDataDriver(new XplatPickerProvider(), owner),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
/** Renders a menu-style SwiftUI Picker through NativeScript's SwiftUI bridge. */
|
|
22
|
+
export function SwiftUIPicker(props: SwiftUIPickerProps) @{
|
|
23
|
+
const controlled = props.selection !== undefined
|
|
24
|
+
const [internalSelection, setInternalSelection] = useState(props.defaultSelection ?? props.options[0]?.id ?? '')
|
|
25
|
+
const selection = controlled ? props.selection! : internalSelection
|
|
26
|
+
const update = (nextSelection: string) => {
|
|
27
|
+
if (!controlled) setInternalSelection(nextSelection)
|
|
28
|
+
props.onSelectionChange?.(nextSelection)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
<xplatiospicker
|
|
32
|
+
id={props.id}
|
|
33
|
+
className={props.className}
|
|
34
|
+
style={props.style}
|
|
35
|
+
swiftId="xplat-swiftui-picker"
|
|
36
|
+
data={{
|
|
37
|
+
label: props.label,
|
|
38
|
+
accessibilityLabel: props.accessibilityLabel ?? props.label,
|
|
39
|
+
options: props.options.map((option) => ({
|
|
40
|
+
value: option.id,
|
|
41
|
+
label: option.title,
|
|
42
|
+
disabled: option.disabled ?? false,
|
|
43
|
+
})),
|
|
44
|
+
value: selection,
|
|
45
|
+
disabled: props.disabled ?? false,
|
|
46
|
+
}}
|
|
47
|
+
onSwiftUIEvent={(event: any) => {
|
|
48
|
+
const nextSelection = event.data?.value
|
|
49
|
+
if (typeof nextSelection === 'string') update(nextSelection)
|
|
50
|
+
}}
|
|
51
|
+
/>;
|
|
52
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface MaterialDropdownItem {
|
|
2
|
+
key: string
|
|
3
|
+
text: string
|
|
4
|
+
enabled?: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface MaterialDropdownProps {
|
|
8
|
+
id?: string
|
|
9
|
+
className?: any
|
|
10
|
+
style?: any
|
|
11
|
+
label: string
|
|
12
|
+
items: readonly MaterialDropdownItem[]
|
|
13
|
+
selectedKey?: string
|
|
14
|
+
defaultSelectedKey?: string
|
|
15
|
+
onSelectedKeyChange?: (key: string) => void
|
|
16
|
+
enabled?: boolean
|
|
17
|
+
accessibilityLabel?: string
|
|
18
|
+
}
|
package/src/ios/index.ts
ADDED
package/src/ios/types.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SwiftUIPickerOption {
|
|
2
|
+
id: string
|
|
3
|
+
title: string
|
|
4
|
+
disabled?: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface SwiftUIPickerProps {
|
|
8
|
+
id?: string
|
|
9
|
+
className?: any
|
|
10
|
+
style?: any
|
|
11
|
+
label: string
|
|
12
|
+
options: readonly SwiftUIPickerOption[]
|
|
13
|
+
selection?: string
|
|
14
|
+
defaultSelection?: string
|
|
15
|
+
onSelectionChange?: (selection: string) => void
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
accessibilityLabel?: string
|
|
18
|
+
}
|
package/src/web/index.ts
ADDED
package/src/web/types.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SelectOption {
|
|
2
|
+
value: string
|
|
3
|
+
label: string
|
|
4
|
+
disabled?: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface SelectProps {
|
|
8
|
+
id?: string
|
|
9
|
+
className?: any
|
|
10
|
+
style?: any
|
|
11
|
+
label: string
|
|
12
|
+
options: readonly SelectOption[]
|
|
13
|
+
value?: string
|
|
14
|
+
defaultValue?: string
|
|
15
|
+
onChange?: (value: string) => void
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
accessibilityLabel?: string
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { UniversalComponent } from 'octane/universal'
|
|
2
|
+
|
|
3
|
+
export interface MaterialDropdownItem {
|
|
4
|
+
key: string
|
|
5
|
+
text: string
|
|
6
|
+
enabled?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface MaterialDropdownProps {
|
|
10
|
+
id?: string
|
|
11
|
+
className?: any
|
|
12
|
+
style?: any
|
|
13
|
+
label: string
|
|
14
|
+
items: readonly MaterialDropdownItem[]
|
|
15
|
+
selectedKey?: string
|
|
16
|
+
defaultSelectedKey?: string
|
|
17
|
+
onSelectedKeyChange?: (key: string) => void
|
|
18
|
+
enabled?: boolean
|
|
19
|
+
accessibilityLabel?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare const MaterialDropdown: UniversalComponent<MaterialDropdownProps>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { UniversalComponent } from 'octane/universal'
|
|
2
|
+
|
|
3
|
+
export interface SwiftUIPickerOption {
|
|
4
|
+
id: string
|
|
5
|
+
title: string
|
|
6
|
+
disabled?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SwiftUIPickerProps {
|
|
10
|
+
id?: string
|
|
11
|
+
className?: any
|
|
12
|
+
style?: any
|
|
13
|
+
label: string
|
|
14
|
+
options: readonly SwiftUIPickerOption[]
|
|
15
|
+
selection?: string
|
|
16
|
+
defaultSelection?: string
|
|
17
|
+
onSelectionChange?: (selection: string) => void
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
accessibilityLabel?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare const SwiftUIPicker: UniversalComponent<SwiftUIPickerProps>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { UniversalComponent } from 'octane/universal'
|
|
2
|
+
|
|
3
|
+
export interface SelectOption {
|
|
4
|
+
value: string
|
|
5
|
+
label: string
|
|
6
|
+
disabled?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SelectProps {
|
|
10
|
+
id?: string
|
|
11
|
+
className?: any
|
|
12
|
+
style?: any
|
|
13
|
+
label: string
|
|
14
|
+
options: readonly SelectOption[]
|
|
15
|
+
value?: string
|
|
16
|
+
defaultValue?: string
|
|
17
|
+
onChange?: (value: string) => void
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
accessibilityLabel?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare const Select: UniversalComponent<SelectProps>
|