@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 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,2 @@
1
+ import { MaterialDropdown } from "../MaterialDropdown.android.tsrx.js";
2
+ export { MaterialDropdown };
@@ -0,0 +1,2 @@
1
+ import { SwiftUIPicker } from "../SwiftUIPicker.ios.tsrx.js";
2
+ export { SwiftUIPicker };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "8caff11e-e823-4ca1-ac41-17c9fcfe51b1",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
@@ -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 };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "0fac786e-79d2-4761-a4d6-2161f37aace5",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
@@ -0,0 +1,2 @@
1
+ import { Select } from "../Select.web.tsrx.js";
2
+ export { Select };
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
- "name": "@octane-xplat/picker",
3
- "version": "0.0.0",
4
- "description": "Stub reserving the name; real releases publish from CI via trusted publishing.",
5
- "publishConfig": {"access": "public"}
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,2 @@
1
+ export { MaterialDropdown } from '../MaterialDropdown.android.tsrx'
2
+ export type { MaterialDropdownItem, MaterialDropdownProps } from './types'
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { SwiftUIPicker } from '../SwiftUIPicker.ios.tsrx'
2
+ export type { SwiftUIPickerOption, SwiftUIPickerProps } from './types'
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { Select } from '../Select.web.tsrx'
2
+ export type { SelectOption, SelectProps } from './types'
@@ -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>