@octane-xplat/picker 0.0.0 → 0.7.1
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 +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 +85 -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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alec Larson
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
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,86 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@octane-xplat/picker",
|
|
3
|
+
"version": "0.7.1",
|
|
4
|
+
"description": "Select control for Octane xplat — web select + SwiftUI picker (iOS) / Material dropdown (Android)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"octane",
|
|
7
|
+
"xplat",
|
|
8
|
+
"cross-platform",
|
|
9
|
+
"picker",
|
|
10
|
+
"select",
|
|
11
|
+
"dropdown",
|
|
12
|
+
"combobox",
|
|
13
|
+
"nativescript",
|
|
14
|
+
"ios",
|
|
15
|
+
"android",
|
|
16
|
+
"web"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/picker",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/octane-xplat/octane-xplat.git",
|
|
23
|
+
"directory": "packages/picker"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"types",
|
|
28
|
+
"src",
|
|
29
|
+
"platforms"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"exports": {
|
|
33
|
+
"./ios": {
|
|
34
|
+
"types": "./types/ios/index.d.ts",
|
|
35
|
+
"native": "./dist/native/ios/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./android": {
|
|
38
|
+
"types": "./types/android/index.d.ts",
|
|
39
|
+
"native": "./dist/native/android/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./web": {
|
|
42
|
+
"types": "./types/web/index.d.ts",
|
|
43
|
+
"web": "./dist/web/web/index.js"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@nativescript/jetpack-compose": "1.0.0-beta.0",
|
|
51
|
+
"@nativescript/swift-ui": "4.0.2"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@nativescript-community/octane": "0.2.1",
|
|
55
|
+
"@nativescript/core": "9.1.2",
|
|
56
|
+
"@octanejs/vite-plugin": "0.1.61",
|
|
57
|
+
"octane": "0.6.3",
|
|
58
|
+
"typescript": "5.9.3",
|
|
59
|
+
"vite": "8.3.0"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@nativescript-community/octane": ">=0.2.1 <1",
|
|
63
|
+
"@nativescript/core": ">=9.1.0 <10",
|
|
64
|
+
"octane": ">=0.6.3 <1"
|
|
65
|
+
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"@nativescript-community/octane": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"@nativescript/core": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"octane": {
|
|
74
|
+
"optional": true
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"nativescript": {
|
|
78
|
+
"platforms": {
|
|
79
|
+
"android": "9.1.0",
|
|
80
|
+
"ios": "9.1.0"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"scripts": {
|
|
84
|
+
"build": "vite build && vite build --mode native && tsc -p tsconfig.types.json && node ../../scripts/check-native-dist.mjs"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -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>
|