@expo/ui 1.0.0-canary-20250306-d9d3e02 → 1.0.0-canary-20250320-7a205d3

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +4 -0
  3. package/android/src/main/java/expo/modules/ui/PickerView.kt +3 -0
  4. package/android/src/main/java/expo/modules/ui/TextInputView.kt +75 -0
  5. package/android/src/main/java/expo/modules/ui/menu/ContextMenu.kt +7 -3
  6. package/build/components/BottomSheet/index.d.ts +9 -0
  7. package/build/components/BottomSheet/index.d.ts.map +1 -0
  8. package/build/components/BottomSheet/index.ios.d.ts +11 -0
  9. package/build/components/BottomSheet/index.ios.d.ts.map +1 -0
  10. package/build/components/Button/index.d.ts +1 -1
  11. package/build/components/ContextMenu/index.android.d.ts +23 -0
  12. package/build/components/ContextMenu/index.android.d.ts.map +1 -0
  13. package/build/components/ContextMenu/index.d.ts +54 -20
  14. package/build/components/ContextMenu/index.d.ts.map +1 -1
  15. package/build/components/DatePicker/index.d.ts +6 -6
  16. package/build/components/Picker/index.d.ts +8 -5
  17. package/build/components/Picker/index.d.ts.map +1 -1
  18. package/build/components/Progress/index.d.ts +1 -1
  19. package/build/components/Section/index.d.ts +6 -1
  20. package/build/components/Section/index.d.ts.map +1 -1
  21. package/build/components/Slider/index.d.ts +5 -2
  22. package/build/components/Slider/index.d.ts.map +1 -1
  23. package/build/components/Switch/index.d.ts +18 -36
  24. package/build/components/Switch/index.d.ts.map +1 -1
  25. package/build/components/TextInput/index.d.ts +24 -1
  26. package/build/components/TextInput/index.d.ts.map +1 -1
  27. package/build/src/types.d.ts +2 -2
  28. package/build/src/types.d.ts.map +1 -1
  29. package/components/BottomSheet/index.ios.tsx +34 -0
  30. package/components/BottomSheet/index.tsx +12 -0
  31. package/components/Button/index.tsx +1 -1
  32. package/components/ContextMenu/index.android.tsx +72 -0
  33. package/components/ContextMenu/index.tsx +67 -33
  34. package/components/DatePicker/index.tsx +6 -6
  35. package/components/Picker/index.tsx +8 -5
  36. package/components/Progress/index.tsx +1 -1
  37. package/components/Section/index.tsx +6 -1
  38. package/components/Slider/index.tsx +5 -2
  39. package/components/Switch/index.tsx +37 -54
  40. package/components/TextInput/index.tsx +30 -3
  41. package/ios/BottomSheetView.swift +82 -0
  42. package/ios/ContextMenu/ContextMenu.swift +65 -2
  43. package/ios/ContextMenu/ContextMenuRecords.swift +6 -0
  44. package/ios/ExpoUIModule.swift +3 -0
  45. package/ios/PickerView.swift +32 -27
  46. package/package.json +3 -4
  47. package/src/types.ts +2 -2
@@ -3,6 +3,9 @@ import { StyleProp, ViewStyle } from 'react-native';
3
3
 
4
4
  import { ViewEvent } from '../../src/types';
5
5
 
6
+ /**
7
+ * @hidden Not used anywhere yet.
8
+ */
6
9
  export type TextInputRole = 'default' | 'cancel' | 'destructive';
7
10
 
8
11
  /**
@@ -34,6 +37,27 @@ export type TextInputProps = {
34
37
  numberOfLines?: number;
35
38
  /**
36
39
  * Determines which keyboard to open, e.g., numeric.
40
+ *
41
+ * Types that work on both platforms:
42
+ * - default
43
+ * - numeric
44
+ * - email-address
45
+ * - phone-pad
46
+ * - decimal-pad
47
+ * - ascii-capable
48
+ * - url
49
+ *
50
+ * Types that only work on Android:
51
+ * - password
52
+ * - password-numeric
53
+ *
54
+ * Types that only work on iOS:
55
+ * - numbers-and-punctuation
56
+ * - name-phone-pad
57
+ * - twitter
58
+ * - web-search
59
+ * - ascii-capable-number-pad
60
+ *
37
61
  * @default default
38
62
  */
39
63
  keyboardType?:
@@ -58,7 +82,7 @@ export type TextInputProps = {
58
82
 
59
83
  export type NativeTextInputProps = Omit<TextInputProps, 'onChangeText'> & {} & ViewEvent<
60
84
  'onValueChanged',
61
- { value: string; eventIndex: number }
85
+ { value: string }
62
86
  >;
63
87
 
64
88
  // We have to work around the `role` and `onPress` props being reserved by React Native.
@@ -67,11 +91,14 @@ const TextInputNativeView: React.ComponentType<NativeTextInputProps> = requireNa
67
91
  'TextInputView'
68
92
  );
69
93
 
94
+ /**
95
+ * @hidden
96
+ */
70
97
  function transformTextInputProps(props: TextInputProps): NativeTextInputProps {
71
98
  return {
72
99
  ...props,
73
- onValueChanged: (e) => {
74
- props.onChangeText?.(e.nativeEvent.value);
100
+ onValueChanged: (event) => {
101
+ props.onChangeText?.(event.nativeEvent.value);
75
102
  },
76
103
  };
77
104
  }
@@ -0,0 +1,82 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ import SwiftUI
4
+ import ExpoModulesCore
5
+
6
+ class BottomSheetProps: ExpoSwiftUI.ViewProps {
7
+ @Field var isOpened: Bool = false
8
+ var onIsOpenedChange = EventDispatcher()
9
+ }
10
+
11
+ struct HeightPreferenceKey: PreferenceKey {
12
+ static var defaultValue: CGFloat?
13
+
14
+ static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
15
+ guard let nextValue = nextValue() else {
16
+ return
17
+ }
18
+ value = nextValue
19
+ }
20
+ }
21
+
22
+ private struct ReadHeightModifier: ViewModifier {
23
+ private var sizeView: some View {
24
+ GeometryReader { geometry in
25
+ Color.clear.preference(key: HeightPreferenceKey.self, value: geometry.size.height)
26
+ }
27
+ }
28
+
29
+ func body(content: Content) -> some View {
30
+ content.background(sizeView)
31
+ }
32
+ }
33
+
34
+ struct BottomSheetView: ExpoSwiftUI.View {
35
+ @EnvironmentObject var props: BottomSheetProps
36
+
37
+ @State private var isOpened = true
38
+ @State var height: CGFloat = 0
39
+
40
+ var body: some View {
41
+ if #available(iOS 16.0, tvOS 16.0, *) {
42
+ Rectangle().hidden()
43
+ .sheet(isPresented: $isOpened) {
44
+ Children()
45
+ .modifier(ReadHeightModifier())
46
+ .onPreferenceChange(HeightPreferenceKey.self) { height in
47
+ if let height {
48
+ self.height = height
49
+ }
50
+ }
51
+ .presentationDetents([.height(self.height)])
52
+ }
53
+ .onChange(of: isOpened, perform: { newIsOpened in
54
+ if props.isOpened == newIsOpened {
55
+ return
56
+ }
57
+ props.onIsOpenedChange([
58
+ "isOpened": newIsOpened
59
+ ])
60
+ })
61
+ .onReceive(props.objectWillChange, perform: {
62
+ isOpened = props.isOpened
63
+ })
64
+ } else {
65
+ Rectangle().hidden()
66
+ .sheet(isPresented: $isOpened) {
67
+ Children()
68
+ }
69
+ .onChange(of: isOpened, perform: { newIsOpened in
70
+ if props.isOpened == newIsOpened {
71
+ return
72
+ }
73
+ props.onIsOpenedChange([
74
+ "isOpened": newIsOpened
75
+ ])
76
+ })
77
+ .onReceive(props.objectWillChange, perform: {
78
+ isOpened = props.isOpened
79
+ })
80
+ }
81
+ }
82
+ }
@@ -87,14 +87,77 @@ struct LongPressContextMenu<ActivationElement: View>: View {
87
87
  }
88
88
  }
89
89
 
90
+ struct LongPressContextMenuWithPreview<ActivationElement: View, Preview: View>: View {
91
+ let elements: [ContextMenuElement]?
92
+ let activationElement: ActivationElement
93
+ let preview: Preview
94
+ let props: ContextMenuProps?
95
+
96
+ var body: some View {
97
+ if #available(iOS 16.0, tvOS 16.0, *) {
98
+ activationElement.contextMenu(menuItems: {
99
+ MenuItems(fromElements: elements, props: props)
100
+ }, preview: {
101
+ preview
102
+ })
103
+ } else {
104
+ activationElement.contextMenu(menuItems: {
105
+ MenuItems(fromElements: elements, props: props)
106
+ })
107
+ }
108
+ }
109
+ }
110
+
111
+ struct ContextMenuPreview: ExpoSwiftUI.View {
112
+ @EnvironmentObject var props: ContextMenuPreviewProps
113
+
114
+ var body: some View {
115
+ Children()
116
+ }
117
+ }
118
+
119
+ struct ContextMenuActivationElement: ExpoSwiftUI.View {
120
+ @EnvironmentObject var props: ContextMenuActivationElementProps
121
+
122
+ var body: some View {
123
+ Children()
124
+ }
125
+ }
126
+
90
127
  struct ContextMenu: ExpoSwiftUI.View {
91
128
  @EnvironmentObject var props: ContextMenuProps
92
129
 
93
130
  var body: some View {
94
131
  if props.activationMethod == .singlePress {
95
- SinglePressContextMenu(elements: props.elements, activationElement: Children(), props: props)
132
+ let activationElement = props.children?.filter({
133
+ $0.view is ExpoSwiftUI.HostingView<ContextMenuActivationElementProps, ContextMenuActivationElement>
134
+ })
135
+ SinglePressContextMenu(
136
+ elements: props.elements,
137
+ activationElement: UnwrappedChildren(children: activationElement),
138
+ props: props
139
+ )
96
140
  } else {
97
- LongPressContextMenu(elements: props.elements, activationElement: Children(), props: props)
141
+ let preview = props.children?.filter({
142
+ $0.view is ExpoSwiftUI.HostingView<ContextMenuPreviewProps, ContextMenuPreview>
143
+ })
144
+ let activationElement = props.children?.filter({
145
+ $0.view is ExpoSwiftUI.HostingView<ContextMenuActivationElementProps, ContextMenuActivationElement>
146
+ })
147
+ if preview?.count ?? 0 > 0 {
148
+ LongPressContextMenuWithPreview(
149
+ elements: props.elements,
150
+ activationElement: UnwrappedChildren(children: activationElement),
151
+ preview: UnwrappedChildren(children: preview),
152
+ props: props
153
+ )
154
+ } else {
155
+ LongPressContextMenu(
156
+ elements: props.elements,
157
+ activationElement: UnwrappedChildren(children: activationElement),
158
+ props: props
159
+ )
160
+ }
98
161
  }
99
162
  }
100
163
  }
@@ -27,3 +27,9 @@ internal class ContextMenuProps: ExpoSwiftUI.ViewProps {
27
27
  var onContextMenuSwitchCheckedChanged = EventDispatcher()
28
28
  @Field var activationMethod: ActivationMethod? = .singlePress
29
29
  }
30
+
31
+ internal class ContextMenuPreviewProps: ExpoSwiftUI.ViewProps {
32
+ }
33
+
34
+ internal class ContextMenuActivationElementProps: ExpoSwiftUI.ViewProps {
35
+ }
@@ -10,8 +10,11 @@ public class ExpoUIModule: Module {
10
10
  View(PickerView.self)
11
11
  View(SwitchView.self)
12
12
  View(SectionView.self)
13
+ View(BottomSheetView.self)
13
14
  View(SliderView.self)
14
15
  View(ExpoUI.ContextMenu.self)
16
+ View(ExpoUI.ContextMenuActivationElement.self)
17
+ View(ExpoUI.ContextMenuPreview.self)
15
18
  View(ColorPickerView.self)
16
19
  View(DateTimePickerView.self)
17
20
  View(TextInputView.self)
@@ -13,40 +13,45 @@ class PickerProps: ExpoSwiftUI.ViewProps {
13
13
  }
14
14
 
15
15
  struct PickerView: ExpoSwiftUI.View {
16
- @State var selection: Int?
16
+ @State var selection: Int = 0
17
17
  @State var prevSelectedIndex: Int?
18
18
  @EnvironmentObject var props: PickerProps
19
+ @EnvironmentObject var shadowNodeProxy: ExpoSwiftUI.ShadowNodeProxy
19
20
 
20
21
  var body: some View {
21
22
  if #available(iOS 17.0, tvOS 17.0, *) {
22
- Picker(props.label ?? "", selection: $selection) {
23
- ForEach(Array(props.options.enumerated()), id: \.0) { index, option in
24
- Text(option).tag(index)
23
+ ExpoSwiftUI.AutoSizingStack(shadowNodeProxy: shadowNodeProxy) {
24
+ Picker(props.label ?? "", selection: $selection) {
25
+ ForEach(Array(props.options.enumerated()), id: \.0) { index, option in
26
+ Text(option).tag(index)
27
+ }
25
28
  }
29
+ .tint(props.color)
30
+ #if !os(tvOS)
31
+ .if(props.variant == "wheel", { $0.pickerStyle(.wheel) })
32
+ .if(props.variant == "palette", { $0.pickerStyle(.palette) })
33
+ #endif
34
+ .if(props.variant == "segmented", { $0.pickerStyle(.segmented) })
35
+ .if(props.variant == "inline", { $0.pickerStyle(.inline) })
36
+ .if(props.variant == "menu", { $0.pickerStyle(.menu) })
37
+ .onChange(of: selection, perform: { newValue in
38
+ if props.selectedIndex == newValue {
39
+ return
40
+ }
41
+ let payload = [
42
+ "index": newValue,
43
+ "label": props.options[newValue]
44
+ ]
45
+ props.onOptionSelected(payload)
46
+ })
47
+ .onReceive(props.selectedIndex.publisher, perform: { newValue in
48
+ if prevSelectedIndex == newValue {
49
+ return
50
+ }
51
+ selection = newValue
52
+ prevSelectedIndex = newValue
53
+ })
26
54
  }
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
55
  }
51
56
  }
52
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/ui",
3
- "version": "1.0.0-canary-20250306-d9d3e02",
3
+ "version": "1.0.0-canary-20250320-7a205d3",
4
4
  "description": "A collection of UI components",
5
5
  "scripts": {
6
6
  "build": "expo-module build",
@@ -27,12 +27,11 @@
27
27
  "dependencies": {},
28
28
  "devDependencies": {
29
29
  "@types/react": "~19.0.10",
30
- "expo-module-scripts": "4.0.5-canary-20250306-d9d3e02"
30
+ "expo-module-scripts": "4.0.5-canary-20250320-7a205d3"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "expo": "*",
34
34
  "react": "*",
35
35
  "react-native": "*"
36
- },
37
- "gitHead": "d9d3e024d8742099c307754673f17117a20c1dea"
36
+ }
38
37
  }
package/src/types.ts CHANGED
@@ -1,9 +1,9 @@
1
- export type * from '../components/Switch';
2
- export type * from '../components/Picker';
3
1
  export type * from '../components/Button';
4
2
  export type * from '../components/ContextMenu';
3
+ export type * from '../components/Picker';
5
4
  export type * from '../components/Section';
6
5
  export type * from '../components/Slider';
6
+ export type * from '../components/Switch';
7
7
 
8
8
  /**
9
9
  * @hidden