@expo/ui 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/android/build.gradle +5 -13
- package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +24 -1
- package/android/src/main/java/expo/modules/ui/PickerView.kt +108 -0
- package/android/src/main/java/expo/modules/ui/SliderView.kt +73 -0
- package/android/src/main/java/expo/modules/ui/SwitchView.kt +118 -0
- package/android/src/main/java/expo/modules/ui/Utils.kt +52 -0
- package/android/src/main/java/expo/modules/ui/button/Button.kt +140 -0
- package/android/src/main/java/expo/modules/ui/menu/ContextMenu.kt +160 -0
- package/android/src/main/java/expo/modules/ui/menu/ContextMenuRecords.kt +55 -0
- package/build/components/Button/index.d.ts +81 -0
- package/build/components/Button/index.d.ts.map +1 -0
- package/build/components/ContextMenu/index.d.ts +80 -0
- package/build/components/ContextMenu/index.d.ts.map +1 -0
- package/build/components/ContextMenu/utils.d.ts +24 -0
- package/build/components/ContextMenu/utils.d.ts.map +1 -0
- package/build/components/Picker/index.d.ts +65 -0
- package/build/components/Picker/index.d.ts.map +1 -0
- package/build/components/Section/index.d.ts +12 -0
- package/build/components/Section/index.d.ts.map +1 -0
- package/build/components/Section/index.ios.d.ts +8 -0
- package/build/components/Section/index.ios.d.ts.map +1 -0
- package/build/components/Slider/index.d.ts +54 -0
- package/build/components/Slider/index.d.ts.map +1 -0
- package/build/components/Switch/index.d.ts +101 -0
- package/build/components/Switch/index.d.ts.map +1 -0
- package/build/src/index.d.ts +13 -0
- package/build/src/index.d.ts.map +1 -0
- package/components/Button/index.tsx +132 -0
- package/components/ContextMenu/index.tsx +147 -0
- package/components/ContextMenu/utils.ts +139 -0
- package/components/Picker/index.tsx +83 -0
- package/components/Section/index.ios.tsx +58 -0
- package/components/Section/index.tsx +56 -0
- package/components/Slider/index.tsx +85 -0
- package/components/Switch/index.tsx +144 -0
- package/expo-module.config.json +3 -10
- package/ios/Button/Button.swift +54 -0
- package/ios/Button/ButtonProps.swift +43 -0
- package/ios/ContextMenu/ContextMenu.swift +106 -0
- package/ios/ContextMenu/ContextMenuRecords.swift +29 -0
- package/ios/ExpoUI.podspec +2 -1
- package/ios/ExpoUIModule.swift +6 -1
- package/ios/PickerView.swift +52 -0
- package/ios/SectionView.swift +27 -0
- package/ios/SliderView.swift +51 -0
- package/ios/SwitchView.swift +74 -0
- package/package.json +2 -2
- package/src/index.ts +22 -3
- package/tsconfig.json +1 -1
- package/android/src/main/java/expo/modules/ui/SingleChoiceSegmentedControlView.kt +0 -47
- package/build/ExpoUI.types.d.ts +0 -13
- package/build/ExpoUI.types.d.ts.map +0 -1
- package/build/ExpoUIModule.d.ts +0 -6
- package/build/ExpoUIModule.d.ts.map +0 -1
- package/build/ExpoUIView.d.ts +0 -4
- package/build/ExpoUIView.d.ts.map +0 -1
- package/build/index.d.ts +0 -4
- package/build/index.d.ts.map +0 -1
- package/ios/SingleChoiceSegmentedControlProps.swift +0 -10
- package/ios/SingleChoiceSegmentedControlView.swift +0 -29
- package/src/ExpoUI.types.ts +0 -8
- package/src/ExpoUIModule.ts +0 -5
- package/src/ExpoUIView.tsx +0 -10
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { requireNativeView } from 'expo';
|
|
2
|
+
import { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { ViewEvent } from '../../src';
|
|
5
|
+
|
|
6
|
+
export type SliderProps = {
|
|
7
|
+
/**
|
|
8
|
+
* Custom styles for the slider component.
|
|
9
|
+
*/
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
/**
|
|
12
|
+
* The current value of the slider.
|
|
13
|
+
* @default 0
|
|
14
|
+
*/
|
|
15
|
+
value?: number;
|
|
16
|
+
/**
|
|
17
|
+
* The number of steps between the minimum and maximum values. 0 signifies infinite steps.
|
|
18
|
+
* @default 0
|
|
19
|
+
*/
|
|
20
|
+
steps?: number;
|
|
21
|
+
/**
|
|
22
|
+
* The mininum value of the slider. Updating this value does not trigger callbacks if the current value is below `min`.
|
|
23
|
+
* @default 0
|
|
24
|
+
*/
|
|
25
|
+
min?: number;
|
|
26
|
+
/**
|
|
27
|
+
* The maximum value of the slider. Updating this value does not trigger callbacks if the current value is above `max`.
|
|
28
|
+
* @default 1
|
|
29
|
+
*/
|
|
30
|
+
max?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Colors for slider's core elements.
|
|
33
|
+
* @platform android
|
|
34
|
+
*/
|
|
35
|
+
elementColors?: {
|
|
36
|
+
thumbColor?: string;
|
|
37
|
+
activeTrackColor?: string;
|
|
38
|
+
inactiveTrackColor?: string;
|
|
39
|
+
activeTickColor?: string;
|
|
40
|
+
inactiveTickColor?: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Slider color.
|
|
44
|
+
*/
|
|
45
|
+
color?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Callback triggered on dragging along the slider.
|
|
48
|
+
*/
|
|
49
|
+
onValueChange?: (value: number) => void;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type NativeSliderProps = Omit<SliderProps, 'onValueChange'> &
|
|
53
|
+
ViewEvent<'onValueChanged', { value: number }>;
|
|
54
|
+
|
|
55
|
+
const SliderNativeView: React.ComponentType<NativeSliderProps> = requireNativeView(
|
|
56
|
+
'ExpoUI',
|
|
57
|
+
'SliderView'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export function transformSliderProps(props: SliderProps): NativeSliderProps {
|
|
61
|
+
return {
|
|
62
|
+
...props,
|
|
63
|
+
min: props.min ?? 0,
|
|
64
|
+
max: props.max ?? 1,
|
|
65
|
+
steps: props.steps ?? 0,
|
|
66
|
+
value: props.value ?? 0,
|
|
67
|
+
onValueChanged: ({ nativeEvent: { value } }) => {
|
|
68
|
+
props?.onValueChange?.(value);
|
|
69
|
+
},
|
|
70
|
+
elementColors: props.elementColors
|
|
71
|
+
? props.elementColors
|
|
72
|
+
: props.color
|
|
73
|
+
? {
|
|
74
|
+
thumbColor: props.color,
|
|
75
|
+
activeTrackColor: props.color,
|
|
76
|
+
activeTickColor: props.color,
|
|
77
|
+
}
|
|
78
|
+
: undefined,
|
|
79
|
+
color: props.color,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function Slider(props: SliderProps) {
|
|
84
|
+
return <SliderNativeView {...transformSliderProps(props)} />;
|
|
85
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { requireNativeView } from 'expo';
|
|
2
|
+
import { NativeSyntheticEvent, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
type SwitchElementColors = {
|
|
5
|
+
/**
|
|
6
|
+
* Only for switch.
|
|
7
|
+
*/
|
|
8
|
+
checkedThumbColor?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Only for switch.
|
|
11
|
+
*/
|
|
12
|
+
checkedTrackColor?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Only for switch.
|
|
15
|
+
*/
|
|
16
|
+
uncheckedThumbColor?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Only for switch.
|
|
19
|
+
*/
|
|
20
|
+
uncheckedTrackColor?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type CheckboxElementColors = {
|
|
24
|
+
/**
|
|
25
|
+
* Only for checkbox.
|
|
26
|
+
*/
|
|
27
|
+
checkedColor?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Only for checkbox.
|
|
30
|
+
*/
|
|
31
|
+
disabledCheckedColor?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Only for checkbox.
|
|
34
|
+
*/
|
|
35
|
+
uncheckedColor?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Only for checkbox.
|
|
38
|
+
*/
|
|
39
|
+
disabledUncheckedColor?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Only for checkbox.
|
|
42
|
+
*/
|
|
43
|
+
checkmarkColor?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Only for checkbox.
|
|
46
|
+
*/
|
|
47
|
+
disabledIndeterminateColor?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type SwitchProps = {
|
|
51
|
+
/**
|
|
52
|
+
* Indicates whether the switch is checked.
|
|
53
|
+
*/
|
|
54
|
+
value: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Label for the switch.
|
|
57
|
+
*
|
|
58
|
+
* > On Android the label has an effect only when the `Switch` is used inside a `ContextMenu`.
|
|
59
|
+
* @platform ios
|
|
60
|
+
*/
|
|
61
|
+
label?: string;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Type of the switch component. Can be 'checkbox', 'switch', or 'button'. The 'button' style is iOS only.
|
|
65
|
+
* @default 'switch'
|
|
66
|
+
*/
|
|
67
|
+
variant?: 'checkbox' | 'switch' | 'button';
|
|
68
|
+
/**
|
|
69
|
+
* Callback function that is called when the checked state changes.
|
|
70
|
+
*/
|
|
71
|
+
onValueChange?: (value: boolean) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Optional style for the switch component.
|
|
74
|
+
*/
|
|
75
|
+
style?: StyleProp<ViewStyle>;
|
|
76
|
+
/**
|
|
77
|
+
* Picker color. On iOS it only applies to the `menu` variant.
|
|
78
|
+
*/
|
|
79
|
+
color?: string;
|
|
80
|
+
} & (
|
|
81
|
+
| {
|
|
82
|
+
variant?: 'switch';
|
|
83
|
+
/**
|
|
84
|
+
* Colors for switch's core elements.
|
|
85
|
+
* @platform android
|
|
86
|
+
*/
|
|
87
|
+
elementColors?: SwitchElementColors;
|
|
88
|
+
}
|
|
89
|
+
| {
|
|
90
|
+
variant: 'checkbox';
|
|
91
|
+
/**
|
|
92
|
+
* Colors for checkbox core elements.
|
|
93
|
+
* @platform android
|
|
94
|
+
*/
|
|
95
|
+
elementColors?: CheckboxElementColors;
|
|
96
|
+
}
|
|
97
|
+
| {
|
|
98
|
+
variant: 'button';
|
|
99
|
+
elementColors?: undefined;
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
type NativeSwitchProps = Omit<SwitchProps, 'onValueChange'> & {
|
|
104
|
+
onValueChange: (event: NativeSyntheticEvent<{ value: boolean }>) => void;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const SwitchNativeView: React.ComponentType<NativeSwitchProps> = requireNativeView(
|
|
108
|
+
'ExpoUI',
|
|
109
|
+
'SwitchView'
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
function getElementColors(props: SwitchProps) {
|
|
113
|
+
if (props.variant === 'button') {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
if (!props.elementColors) {
|
|
117
|
+
if (props.variant === 'switch') {
|
|
118
|
+
return {
|
|
119
|
+
checkedTrackColor: props.color,
|
|
120
|
+
};
|
|
121
|
+
} else {
|
|
122
|
+
return {
|
|
123
|
+
checkedColor: props.color,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return props.elementColors;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function transformSwitchProps(props: SwitchProps): NativeSwitchProps {
|
|
131
|
+
return {
|
|
132
|
+
...props,
|
|
133
|
+
variant: props.variant ?? 'switch',
|
|
134
|
+
elementColors: getElementColors(props),
|
|
135
|
+
color: props.color,
|
|
136
|
+
onValueChange: ({ nativeEvent: { value } }) => {
|
|
137
|
+
props?.onValueChange?.(value);
|
|
138
|
+
},
|
|
139
|
+
} as NativeSwitchProps;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function Switch(props: SwitchProps) {
|
|
143
|
+
return <SwitchNativeView {...transformSwitchProps(props)} />;
|
|
144
|
+
}
|
package/expo-module.config.json
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"platforms": [
|
|
3
|
-
"apple",
|
|
4
|
-
"android"
|
|
5
|
-
],
|
|
2
|
+
"platforms": ["apple", "android"],
|
|
6
3
|
"android": {
|
|
7
|
-
"modules": [
|
|
8
|
-
"expo.modules.ui.ExpoUIModule"
|
|
9
|
-
]
|
|
4
|
+
"modules": ["expo.modules.ui.ExpoUIModule"]
|
|
10
5
|
},
|
|
11
6
|
"apple": {
|
|
12
|
-
"modules": [
|
|
13
|
-
"ExpoUIModule"
|
|
14
|
-
]
|
|
7
|
+
"modules": ["ExpoUIModule"]
|
|
15
8
|
}
|
|
16
9
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import SwiftUI
|
|
4
|
+
import ExpoModulesCore
|
|
5
|
+
|
|
6
|
+
struct Button: ExpoSwiftUI.View {
|
|
7
|
+
@EnvironmentObject var props: ButtonProps
|
|
8
|
+
|
|
9
|
+
var body: some View {
|
|
10
|
+
SwiftUI.Button(
|
|
11
|
+
role: props.buttonRole?.toNativeRole(),
|
|
12
|
+
action: {
|
|
13
|
+
props.onButtonPressed()
|
|
14
|
+
},
|
|
15
|
+
label: {
|
|
16
|
+
if let systemImage = props.systemImage {
|
|
17
|
+
Label(props.text, systemImage: systemImage)
|
|
18
|
+
} else {
|
|
19
|
+
Text(props.text)
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
.tint(props.color)
|
|
23
|
+
// TODO: Maybe there is a way to do a switch statement similarly to the `if` extension?
|
|
24
|
+
.if(props.variant == .bordered, {
|
|
25
|
+
$0.buttonStyle(.bordered)
|
|
26
|
+
})
|
|
27
|
+
.if(props.variant == .plain, {
|
|
28
|
+
$0.buttonStyle(.plain)
|
|
29
|
+
})
|
|
30
|
+
.if(props.variant == .borderedProminent, {
|
|
31
|
+
$0.buttonStyle(.borderedProminent)
|
|
32
|
+
})
|
|
33
|
+
#if !os(tvOS)
|
|
34
|
+
.if(props.variant == .borderless, {
|
|
35
|
+
$0.buttonStyle(.borderless)
|
|
36
|
+
})
|
|
37
|
+
#endif
|
|
38
|
+
|
|
39
|
+
#if os(macOS)
|
|
40
|
+
.if(props.variant == .accessoryBar, {
|
|
41
|
+
$0.buttonStyle(.accessoryBar)
|
|
42
|
+
})
|
|
43
|
+
.if(props.variant == .accessoryBarAction, {
|
|
44
|
+
$0.buttonStyle(.accessoryBarAction)
|
|
45
|
+
})
|
|
46
|
+
.if(props.variant == .card, {
|
|
47
|
+
$0.buttonStyle(.card)
|
|
48
|
+
})
|
|
49
|
+
.if(props.variant == .link, {
|
|
50
|
+
$0.buttonStyle(.link)
|
|
51
|
+
})
|
|
52
|
+
#endif
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import SwiftUI
|
|
4
|
+
import ExpoModulesCore
|
|
5
|
+
|
|
6
|
+
internal enum ButtonRole: String, Enumerable {
|
|
7
|
+
case `default`
|
|
8
|
+
case destructive
|
|
9
|
+
case cancel
|
|
10
|
+
|
|
11
|
+
func toNativeRole() -> SwiftUI.ButtonRole? {
|
|
12
|
+
switch self {
|
|
13
|
+
case .default:
|
|
14
|
+
return nil
|
|
15
|
+
case .destructive:
|
|
16
|
+
return SwiftUI.ButtonRole.destructive
|
|
17
|
+
case .cancel:
|
|
18
|
+
return SwiftUI.ButtonRole.cancel
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
internal enum ButtonVariant: String, Enumerable {
|
|
24
|
+
case `default`
|
|
25
|
+
case bordered
|
|
26
|
+
case accessoryBar
|
|
27
|
+
case accessoryBarAction
|
|
28
|
+
case borderedProminent
|
|
29
|
+
case borderless
|
|
30
|
+
case card
|
|
31
|
+
case link
|
|
32
|
+
case plain
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class ButtonProps: ExpoSwiftUI.ViewProps, Observable {
|
|
36
|
+
required init() {}
|
|
37
|
+
@Field var text: String = ""
|
|
38
|
+
@Field var systemImage: String?
|
|
39
|
+
@Field var color: Color?
|
|
40
|
+
@Field var buttonRole: ButtonRole? = .default
|
|
41
|
+
@Field var variant: ButtonVariant? = .default
|
|
42
|
+
var onButtonPressed = EventDispatcher()
|
|
43
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import ExpoModulesCore
|
|
3
|
+
|
|
4
|
+
struct MenuItems: View {
|
|
5
|
+
let fromElements: [ContextMenuElement]?
|
|
6
|
+
let props: ContextMenuProps?
|
|
7
|
+
// We have to create a non-functional shadow node proxy, so that the elements don't send sizing changes to the
|
|
8
|
+
// root proxy - we won't be leaving the SwiftUI hierarchy.
|
|
9
|
+
let shadowNodeProxy = ExpoSwiftUI.ShadowNodeProxy()
|
|
10
|
+
|
|
11
|
+
init(fromElements: [ContextMenuElement]?, props: ContextMenuProps?) {
|
|
12
|
+
self.fromElements = fromElements
|
|
13
|
+
self.props = props
|
|
14
|
+
|
|
15
|
+
fromElements?.forEach { element in
|
|
16
|
+
let id = element.contextMenuElementID
|
|
17
|
+
if let button = element.button {
|
|
18
|
+
button.onButtonPressed.onEventSent = { _ in
|
|
19
|
+
props?.onContextMenuButtonPressed(addId(id, toMap: nil))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if let `switch` = element.switch {
|
|
23
|
+
`switch`.onValueChange.onEventSent = { map in
|
|
24
|
+
props?.onContextMenuSwitchCheckedChanged(addId(id, toMap: map))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if let picker = element.picker {
|
|
28
|
+
picker.onOptionSelected.onEventSent = { map in
|
|
29
|
+
props?.onContextMenuPickerOptionSelected(addId(id, toMap: map))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var body: some View {
|
|
36
|
+
ForEach(fromElements ?? []) { elem in
|
|
37
|
+
if let button = elem.button {
|
|
38
|
+
ExpoUI.Button().environmentObject(button)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if let picker = elem.picker {
|
|
42
|
+
ExpoUI.PickerView().environmentObject(picker)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if let `switch` = elem.switch {
|
|
46
|
+
ExpoUI.SwitchView().environmentObject(`switch`).environmentObject(shadowNodeProxy)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if let submenu = elem.submenu {
|
|
50
|
+
SinglePressContextMenu(
|
|
51
|
+
elements: submenu.elements,
|
|
52
|
+
activationElement: ExpoUI.Button().environmentObject(submenu.button),
|
|
53
|
+
props: props
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
struct SinglePressContextMenu<ActivationElement: View>: View {
|
|
61
|
+
let elements: [ContextMenuElement]?
|
|
62
|
+
let activationElement: ActivationElement
|
|
63
|
+
let props: ContextMenuProps?
|
|
64
|
+
|
|
65
|
+
var body: some View {
|
|
66
|
+
#if !os(tvOS)
|
|
67
|
+
SwiftUI.Menu {
|
|
68
|
+
MenuItems(fromElements: elements, props: props)
|
|
69
|
+
} label: {
|
|
70
|
+
activationElement
|
|
71
|
+
}
|
|
72
|
+
#else
|
|
73
|
+
Text("SinglePressContextMenu is not supported on this platform")
|
|
74
|
+
#endif
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
struct LongPressContextMenu<ActivationElement: View>: View {
|
|
79
|
+
let elements: [ContextMenuElement]?
|
|
80
|
+
let activationElement: ActivationElement
|
|
81
|
+
let props: ContextMenuProps?
|
|
82
|
+
|
|
83
|
+
var body: some View {
|
|
84
|
+
activationElement.contextMenu(menuItems: {
|
|
85
|
+
MenuItems(fromElements: elements, props: props)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
struct ContextMenu: ExpoSwiftUI.View {
|
|
91
|
+
@EnvironmentObject var props: ContextMenuProps
|
|
92
|
+
|
|
93
|
+
var body: some View {
|
|
94
|
+
if props.activationMethod == .singlePress {
|
|
95
|
+
SinglePressContextMenu(elements: props.elements, activationElement: Children(), props: props)
|
|
96
|
+
} else {
|
|
97
|
+
LongPressContextMenu(elements: props.elements, activationElement: Children(), props: props)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private func addId(_ id: String?, toMap initialMap: [String: Any]?) -> [String: Any] {
|
|
103
|
+
var newMap = initialMap ?? [:]
|
|
104
|
+
newMap["contextMenuElementID"] = id
|
|
105
|
+
return newMap
|
|
106
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
internal enum ActivationMethod: String, Enumerable {
|
|
4
|
+
case singlePress
|
|
5
|
+
case longPress
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
internal class Submenu: Record, Identifiable {
|
|
9
|
+
required init() { }
|
|
10
|
+
@Field var elements: [ContextMenuElement]
|
|
11
|
+
@Field var button: ButtonProps
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
internal class ContextMenuElement: Record, Identifiable {
|
|
15
|
+
required init() { }
|
|
16
|
+
@Field var button: ButtonProps?
|
|
17
|
+
@Field var picker: PickerProps?
|
|
18
|
+
@Field var `switch`: SwitchProps?
|
|
19
|
+
@Field var submenu: Submenu?
|
|
20
|
+
@Field var contextMenuElementID: String?
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
internal class ContextMenuProps: ExpoSwiftUI.ViewProps {
|
|
24
|
+
@Field var elements: [ContextMenuElement]
|
|
25
|
+
var onContextMenuButtonPressed = EventDispatcher()
|
|
26
|
+
var onContextMenuPickerOptionSelected = EventDispatcher()
|
|
27
|
+
var onContextMenuSwitchCheckedChanged = EventDispatcher()
|
|
28
|
+
@Field var activationMethod: ActivationMethod? = .singlePress
|
|
29
|
+
}
|
package/ios/ExpoUI.podspec
CHANGED
package/ios/ExpoUIModule.swift
CHANGED
|
@@ -6,6 +6,11 @@ public class ExpoUIModule: Module {
|
|
|
6
6
|
public func definition() -> ModuleDefinition {
|
|
7
7
|
Name("ExpoUI")
|
|
8
8
|
|
|
9
|
-
View(
|
|
9
|
+
View(Button.self)
|
|
10
|
+
View(PickerView.self)
|
|
11
|
+
View(SwitchView.self)
|
|
12
|
+
View(SectionView.self)
|
|
13
|
+
View(SliderView.self)
|
|
14
|
+
View(ExpoUI.ContextMenu.self)
|
|
10
15
|
}
|
|
11
16
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import SwiftUI
|
|
4
|
+
import ExpoModulesCore
|
|
5
|
+
|
|
6
|
+
class PickerProps: ExpoSwiftUI.ViewProps {
|
|
7
|
+
@Field var options: [String] = []
|
|
8
|
+
@Field var selectedIndex: Int?
|
|
9
|
+
@Field var variant: String?
|
|
10
|
+
@Field var label: String?
|
|
11
|
+
@Field var color: Color?
|
|
12
|
+
var onOptionSelected = EventDispatcher()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
struct PickerView: ExpoSwiftUI.View {
|
|
16
|
+
@State var selection: Int?
|
|
17
|
+
@State var prevSelectedIndex: Int?
|
|
18
|
+
@EnvironmentObject var props: PickerProps
|
|
19
|
+
|
|
20
|
+
var body: some View {
|
|
21
|
+
if #available(iOS 17.0, tvOS 17.0, *) {
|
|
22
|
+
Picker(props.label ?? "", selection: $selection) {
|
|
23
|
+
ForEach(Array(props.options.enumerated()), id: \.element) { index, option in
|
|
24
|
+
Text(option).tag(index)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
.tint(props.color)
|
|
28
|
+
#if !os(tvOS)
|
|
29
|
+
.if(props.variant == "wheel", { $0.pickerStyle(.wheel) })
|
|
30
|
+
#endif
|
|
31
|
+
.if(props.variant == "segmented", { $0.pickerStyle(.segmented) })
|
|
32
|
+
.if(props.variant == "menu", { $0.pickerStyle(.menu) })
|
|
33
|
+
.onChange(of: selection, perform: { newValue in
|
|
34
|
+
if props.selectedIndex == newValue {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
let payload = [
|
|
38
|
+
"index": newValue ?? 0,
|
|
39
|
+
"label": props.options[newValue ?? 0]
|
|
40
|
+
]
|
|
41
|
+
props.onOptionSelected(payload)
|
|
42
|
+
})
|
|
43
|
+
.onReceive(props.selectedIndex.publisher, perform: { newValue in
|
|
44
|
+
if prevSelectedIndex == newValue {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
selection = newValue
|
|
48
|
+
prevSelectedIndex = newValue
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import SwiftUI
|
|
4
|
+
import ExpoModulesCore
|
|
5
|
+
|
|
6
|
+
class SectionProps: ExpoSwiftUI.ViewProps {
|
|
7
|
+
@Field var title: String?
|
|
8
|
+
@Field var heightOffset: CGFloat = 0
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
struct SectionView: ExpoSwiftUI.View {
|
|
12
|
+
@EnvironmentObject var props: SectionProps
|
|
13
|
+
|
|
14
|
+
var body: some View {
|
|
15
|
+
let form = Form {
|
|
16
|
+
Section(header: Text(props.title ?? "")) {
|
|
17
|
+
Children().padding(EdgeInsets(top: 0, leading: 0, bottom: props.heightOffset, trailing: 0))
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if #available(iOS 16.0, tvOS 16.0, *) {
|
|
22
|
+
form.scrollDisabled(true)
|
|
23
|
+
} else {
|
|
24
|
+
form
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import SwiftUI
|
|
4
|
+
import ExpoModulesCore
|
|
5
|
+
|
|
6
|
+
class SliderProps: ExpoSwiftUI.ViewProps {
|
|
7
|
+
@Field var value: Float?
|
|
8
|
+
@Field var steps: Int = 0
|
|
9
|
+
@Field var min: Float = 0.0
|
|
10
|
+
@Field var max: Float = 1.0
|
|
11
|
+
@Field var color: Color?
|
|
12
|
+
var onValueChanged = EventDispatcher()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func getStep(_ min: Float, _ max: Float, _ steps: Int) -> Float {
|
|
16
|
+
if steps == 0 {
|
|
17
|
+
// Continous (no steps)
|
|
18
|
+
return 0.00001
|
|
19
|
+
}
|
|
20
|
+
// Matching Jetpack Compose where steps is the number of discreete points
|
|
21
|
+
return (max - min) / Float(steps + 1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
struct SliderView: ExpoSwiftUI.View {
|
|
25
|
+
@EnvironmentObject var props: SliderProps
|
|
26
|
+
@State var value: Float = 0.0
|
|
27
|
+
|
|
28
|
+
var body: some View {
|
|
29
|
+
#if !os(tvOS)
|
|
30
|
+
Slider(value: $value, in: props.min...props.max, step: getStep(props.min, props.max, props.steps) )
|
|
31
|
+
.onChange(of: value, perform: { newValue in
|
|
32
|
+
if props.value == newValue {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
// TODO: onChange(of: Float) action tried to update multiple times per frame.
|
|
36
|
+
props.onValueChanged([
|
|
37
|
+
"value": newValue
|
|
38
|
+
])
|
|
39
|
+
})
|
|
40
|
+
.tint(props.color)
|
|
41
|
+
.onReceive(props.value.publisher, perform: { newValue in
|
|
42
|
+
var sliderValue = newValue
|
|
43
|
+
sliderValue = max(sliderValue, props.min)
|
|
44
|
+
sliderValue = min(sliderValue, props.max)
|
|
45
|
+
value = sliderValue
|
|
46
|
+
})
|
|
47
|
+
#else
|
|
48
|
+
Text("Slider not supported on this platform")
|
|
49
|
+
#endif
|
|
50
|
+
}
|
|
51
|
+
}
|