@expo/ui 56.0.7 → 56.0.9

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 (208) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +44 -3
  4. package/android/src/main/java/expo/modules/ui/HostView.kt +2 -0
  5. package/android/src/main/java/expo/modules/ui/LoadingView.kt +80 -0
  6. package/android/src/main/java/expo/modules/ui/ModifierRegistry.kt +50 -4
  7. package/android/src/main/java/expo/modules/ui/RNHostView.kt +8 -3
  8. package/android/src/main/java/expo/modules/ui/ShadowNodeSyncFlush.kt +28 -0
  9. package/android/src/main/java/expo/modules/ui/SnackbarView.kt +126 -0
  10. package/android/src/main/java/expo/modules/ui/state/ObservableState.kt +10 -0
  11. package/assets/keyboard_arrow_down.xml +10 -0
  12. package/build/State/useNativeState.d.ts +32 -3
  13. package/build/State/useNativeState.d.ts.map +1 -1
  14. package/build/community/bottom-sheet/BottomSheet.ios.d.ts.map +1 -1
  15. package/build/community/menu/MenuView.android.d.ts +16 -0
  16. package/build/community/menu/MenuView.android.d.ts.map +1 -0
  17. package/build/community/menu/MenuView.d.ts +19 -0
  18. package/build/community/menu/MenuView.d.ts.map +1 -0
  19. package/build/community/menu/MenuView.ios.d.ts +10 -0
  20. package/build/community/menu/MenuView.ios.d.ts.map +1 -0
  21. package/build/community/menu/index.d.ts +5 -0
  22. package/build/community/menu/index.d.ts.map +1 -0
  23. package/build/community/menu/types.d.ts +166 -0
  24. package/build/community/menu/types.d.ts.map +1 -0
  25. package/build/jetpack-compose/LoadingIndicator/index.d.ts +41 -0
  26. package/build/jetpack-compose/LoadingIndicator/index.d.ts.map +1 -0
  27. package/build/jetpack-compose/Snackbar/index.d.ts +94 -0
  28. package/build/jetpack-compose/Snackbar/index.d.ts.map +1 -0
  29. package/build/jetpack-compose/index.d.ts +2 -0
  30. package/build/jetpack-compose/index.d.ts.map +1 -1
  31. package/build/jetpack-compose/modifiers/index.d.ts +21 -2
  32. package/build/jetpack-compose/modifiers/index.d.ts.map +1 -1
  33. package/build/swift-ui/Alert/index.d.ts +42 -0
  34. package/build/swift-ui/Alert/index.d.ts.map +1 -0
  35. package/build/swift-ui/BottomSheet/index.d.ts +5 -1
  36. package/build/swift-ui/BottomSheet/index.d.ts.map +1 -1
  37. package/build/swift-ui/SlotView.d.ts +5 -2
  38. package/build/swift-ui/SlotView.d.ts.map +1 -1
  39. package/build/swift-ui/SwipeActions/index.d.ts +38 -0
  40. package/build/swift-ui/SwipeActions/index.d.ts.map +1 -0
  41. package/build/swift-ui/index.d.ts +3 -0
  42. package/build/swift-ui/index.d.ts.map +1 -1
  43. package/build/swift-ui/modifiers/index.d.ts +3 -1
  44. package/build/swift-ui/modifiers/index.d.ts.map +1 -1
  45. package/build/swift-ui/modifiers/symbolEffect.d.ts +103 -0
  46. package/build/swift-ui/modifiers/symbolEffect.d.ts.map +1 -0
  47. package/build/swift-ui/withAnimation.d.ts +26 -0
  48. package/build/swift-ui/withAnimation.d.ts.map +1 -0
  49. package/build/universal/BottomSheet/index.android.d.ts +1 -1
  50. package/build/universal/BottomSheet/index.android.d.ts.map +1 -1
  51. package/build/universal/BottomSheet/index.d.ts +1 -1
  52. package/build/universal/BottomSheet/index.d.ts.map +1 -1
  53. package/build/universal/BottomSheet/index.ios.d.ts +1 -1
  54. package/build/universal/BottomSheet/index.ios.d.ts.map +1 -1
  55. package/build/universal/BottomSheet/types.d.ts +27 -0
  56. package/build/universal/BottomSheet/types.d.ts.map +1 -1
  57. package/build/universal/Collapsible/index.android.d.ts +8 -0
  58. package/build/universal/Collapsible/index.android.d.ts.map +1 -0
  59. package/build/universal/Collapsible/index.d.ts +8 -0
  60. package/build/universal/Collapsible/index.d.ts.map +1 -0
  61. package/build/universal/Collapsible/index.ios.d.ts +7 -0
  62. package/build/universal/Collapsible/index.ios.d.ts.map +1 -0
  63. package/build/universal/Collapsible/types.d.ts +23 -0
  64. package/build/universal/Collapsible/types.d.ts.map +1 -0
  65. package/build/universal/Column/index.d.ts.map +1 -1
  66. package/build/universal/Host/index.d.ts +5 -7
  67. package/build/universal/Host/index.d.ts.map +1 -1
  68. package/build/universal/Host/types.d.ts +72 -0
  69. package/build/universal/Host/types.d.ts.map +1 -0
  70. package/build/universal/List/index.android.d.ts +9 -0
  71. package/build/universal/List/index.android.d.ts.map +1 -0
  72. package/build/universal/List/index.d.ts +8 -0
  73. package/build/universal/List/index.d.ts.map +1 -0
  74. package/build/universal/List/index.ios.d.ts +8 -0
  75. package/build/universal/List/index.ios.d.ts.map +1 -0
  76. package/build/universal/List/types.d.ts +26 -0
  77. package/build/universal/List/types.d.ts.map +1 -0
  78. package/build/universal/ListItem/ListItem.android.d.ts +8 -0
  79. package/build/universal/ListItem/ListItem.android.d.ts.map +1 -0
  80. package/build/universal/ListItem/ListItem.d.ts +9 -0
  81. package/build/universal/ListItem/ListItem.d.ts.map +1 -0
  82. package/build/universal/ListItem/ListItem.ios.d.ts +8 -0
  83. package/build/universal/ListItem/ListItem.ios.d.ts.map +1 -0
  84. package/build/universal/ListItem/ListItemSlots.d.ts +21 -0
  85. package/build/universal/ListItem/ListItemSlots.d.ts.map +1 -0
  86. package/build/universal/ListItem/index.d.ts +10 -0
  87. package/build/universal/ListItem/index.d.ts.map +1 -0
  88. package/build/universal/ListItem/types.d.ts +59 -0
  89. package/build/universal/ListItem/types.d.ts.map +1 -0
  90. package/build/universal/Picker/Picker.android.d.ts +9 -0
  91. package/build/universal/Picker/Picker.android.d.ts.map +1 -0
  92. package/build/universal/Picker/Picker.d.ts +8 -0
  93. package/build/universal/Picker/Picker.d.ts.map +1 -0
  94. package/build/universal/Picker/Picker.ios.d.ts +9 -0
  95. package/build/universal/Picker/Picker.ios.d.ts.map +1 -0
  96. package/build/universal/Picker/PickerItem.d.ts +9 -0
  97. package/build/universal/Picker/PickerItem.d.ts.map +1 -0
  98. package/build/universal/Picker/index.d.ts +8 -0
  99. package/build/universal/Picker/index.d.ts.map +1 -0
  100. package/build/universal/Picker/types.d.ts +69 -0
  101. package/build/universal/Picker/types.d.ts.map +1 -0
  102. package/build/universal/index.d.ts +4 -0
  103. package/build/universal/index.d.ts.map +1 -1
  104. package/expo-module.config.json +1 -1
  105. package/ios/Alert/Alert.swift +56 -0
  106. package/ios/Alert/AlertProps.swift +8 -0
  107. package/ios/BottomSheetView.swift +4 -1
  108. package/ios/ExpoUIModule.swift +43 -1
  109. package/ios/ExpoUITouchHandlerHelper.h +4 -1
  110. package/ios/ExpoUITouchHandlerHelper.mm +1 -0
  111. package/ios/Modifiers/AnimationConfig.swift +109 -0
  112. package/ios/Modifiers/SwipeActionsModifier.swift +97 -0
  113. package/ios/Modifiers/SymbolEffectModifier.swift +452 -0
  114. package/ios/Modifiers/ViewModifierRegistry.swift +5 -112
  115. package/ios/SlotView.swift +5 -0
  116. package/ios/State/ObservableState.swift +12 -1
  117. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.7/expo.modules.ui-56.0.7-sources.jar → 56.0.9/expo.modules.ui-56.0.9-sources.jar} +0 -0
  118. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.md5 +1 -0
  119. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha1 +1 -0
  120. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha256 +1 -0
  121. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha512 +1 -0
  122. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar +0 -0
  123. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.md5 +1 -0
  124. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha1 +1 -0
  125. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha256 +1 -0
  126. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha512 +1 -0
  127. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.7/expo.modules.ui-56.0.7.module → 56.0.9/expo.modules.ui-56.0.9.module} +22 -22
  128. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.md5 +1 -0
  129. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha1 +1 -0
  130. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha256 +1 -0
  131. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha512 +1 -0
  132. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.7/expo.modules.ui-56.0.7.pom → 56.0.9/expo.modules.ui-56.0.9.pom} +1 -1
  133. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.md5 +1 -0
  134. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha1 +1 -0
  135. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha256 +1 -0
  136. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha512 +1 -0
  137. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml +4 -4
  138. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.md5 +1 -1
  139. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha1 +1 -1
  140. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha256 +1 -1
  141. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha512 +1 -1
  142. package/package.json +7 -3
  143. package/src/State/useNativeState.ts +70 -10
  144. package/src/community/bottom-sheet/BottomSheet.ios.tsx +0 -17
  145. package/src/community/menu/MenuView.android.tsx +224 -0
  146. package/src/community/menu/MenuView.ios.tsx +149 -0
  147. package/src/community/menu/MenuView.tsx +36 -0
  148. package/src/community/menu/index.tsx +14 -0
  149. package/src/community/menu/types.tsx +171 -0
  150. package/src/jetpack-compose/LoadingIndicator/index.tsx +92 -0
  151. package/src/jetpack-compose/Snackbar/index.tsx +135 -0
  152. package/src/jetpack-compose/index.ts +2 -0
  153. package/src/jetpack-compose/modifiers/index.ts +30 -2
  154. package/src/swift-ui/Alert/index.tsx +87 -0
  155. package/src/swift-ui/BottomSheet/index.tsx +32 -15
  156. package/src/swift-ui/SlotView.tsx +17 -4
  157. package/src/swift-ui/SwipeActions/index.tsx +73 -0
  158. package/src/swift-ui/index.tsx +3 -0
  159. package/src/swift-ui/modifiers/index.ts +3 -0
  160. package/src/swift-ui/modifiers/symbolEffect.ts +181 -0
  161. package/src/swift-ui/withAnimation.ts +71 -0
  162. package/src/ts-declarations/react-native-web.d.ts +27 -0
  163. package/src/universal/BottomSheet/index.android.tsx +27 -3
  164. package/src/universal/BottomSheet/index.ios.tsx +30 -12
  165. package/src/universal/BottomSheet/index.tsx +46 -4
  166. package/src/universal/BottomSheet/types.ts +25 -0
  167. package/src/universal/Collapsible/index.android.tsx +72 -0
  168. package/src/universal/Collapsible/index.ios.tsx +16 -0
  169. package/src/universal/Collapsible/index.tsx +58 -0
  170. package/src/universal/Collapsible/types.ts +25 -0
  171. package/src/universal/Column/index.tsx +3 -1
  172. package/src/universal/Host/index.tsx +69 -5
  173. package/src/universal/Host/types.ts +70 -0
  174. package/src/universal/List/index.android.tsx +44 -0
  175. package/src/universal/List/index.ios.tsx +19 -0
  176. package/src/universal/List/index.tsx +26 -0
  177. package/src/universal/List/types.ts +28 -0
  178. package/src/universal/ListItem/ListItem.android.tsx +52 -0
  179. package/src/universal/ListItem/ListItem.ios.tsx +58 -0
  180. package/src/universal/ListItem/ListItem.tsx +72 -0
  181. package/src/universal/ListItem/ListItemSlots.tsx +66 -0
  182. package/src/universal/ListItem/index.ts +15 -0
  183. package/src/universal/ListItem/types.ts +67 -0
  184. package/src/universal/Picker/Picker.android.tsx +69 -0
  185. package/src/universal/Picker/Picker.ios.tsx +45 -0
  186. package/src/universal/Picker/Picker.tsx +52 -0
  187. package/src/universal/Picker/PickerItem.tsx +27 -0
  188. package/src/universal/Picker/index.ts +11 -0
  189. package/src/universal/Picker/types.ts +79 -0
  190. package/src/universal/index.ts +4 -0
  191. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.md5 +0 -1
  192. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.sha1 +0 -1
  193. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.sha256 +0 -1
  194. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.sha512 +0 -1
  195. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar +0 -0
  196. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.md5 +0 -1
  197. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.sha1 +0 -1
  198. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.sha256 +0 -1
  199. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.sha512 +0 -1
  200. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.md5 +0 -1
  201. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.sha1 +0 -1
  202. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.sha256 +0 -1
  203. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.sha512 +0 -1
  204. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.md5 +0 -1
  205. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.sha1 +0 -1
  206. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.sha256 +0 -1
  207. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.sha512 +0 -1
  208. package/src/community/bottom-sheet/CLAUDE.md +0 -55
@@ -0,0 +1,149 @@
1
+ import * as React from 'react';
2
+
3
+ import type { MenuAction, MenuComponentProps, MenuComponentRef, NativeActionEvent } from './types';
4
+ import { Button } from '../../swift-ui/Button';
5
+ import { ContextMenu } from '../../swift-ui/ContextMenu';
6
+ import { Host } from '../../swift-ui/Host';
7
+ import { Menu } from '../../swift-ui/Menu';
8
+ import { RNHostView } from '../../swift-ui/RNHostView';
9
+ import { Section } from '../../swift-ui/Section';
10
+ import { Toggle } from '../../swift-ui/Toggle';
11
+ import {
12
+ disabled as disabledModifier,
13
+ foregroundColor as foregroundColorModifier,
14
+ tint as tintModifier,
15
+ } from '../../swift-ui/modifiers';
16
+ import type { ModifierConfig } from '../../types';
17
+
18
+ function actionId(action: MenuAction): string {
19
+ return action.id ?? action.title;
20
+ }
21
+
22
+ function makeEvent(action: MenuAction): NativeActionEvent {
23
+ return { nativeEvent: { event: actionId(action) } };
24
+ }
25
+
26
+ function renderAction(
27
+ action: MenuAction,
28
+ onPressAction: MenuComponentProps['onPressAction']
29
+ ): React.ReactNode {
30
+ if (action.attributes?.hidden) {
31
+ return null;
32
+ }
33
+
34
+ const { subactions, displayInline, state, attributes, image, imageColor, title } = action;
35
+ const key = actionId(action);
36
+ const systemImage = typeof image === 'string' ? image : undefined;
37
+ // `tint` is what SwiftUI's `Menu`/`Button` honor for the leading SF Symbol —
38
+ // `foregroundColor` would also affect the label text.
39
+ const tintMod = imageColor ? tintModifier(imageColor) : null;
40
+
41
+ if (subactions && subactions.length > 0) {
42
+ const children = subactions.map((sub) => renderAction(sub, onPressAction));
43
+ if (displayInline) {
44
+ return (
45
+ <Section key={key} title={title}>
46
+ {children}
47
+ </Section>
48
+ );
49
+ }
50
+ // SwiftUI's system menu UI ignores per-item color modifiers on submenu
51
+ // headers, so we don't forward `imageColor` here. Leaf `Button`s below
52
+ // tint via `foregroundColor`, which does take effect.
53
+ return (
54
+ <Menu key={key} label={title} systemImage={systemImage}>
55
+ {children}
56
+ </Menu>
57
+ );
58
+ }
59
+
60
+ const fire = () => onPressAction?.(makeEvent(action));
61
+
62
+ const modifiers: ModifierConfig[] = [];
63
+ if (attributes?.disabled) modifiers.push(disabledModifier(true));
64
+
65
+ if (state === 'on' || state === 'off') {
66
+ if (tintMod) modifiers.push(tintMod);
67
+ return (
68
+ <Toggle
69
+ key={key}
70
+ label={title}
71
+ systemImage={systemImage}
72
+ isOn={state === 'on'}
73
+ onIsOnChange={fire}
74
+ modifiers={modifiers.length > 0 ? modifiers : undefined}
75
+ />
76
+ );
77
+ }
78
+
79
+ // For a leaf `Button`, `foregroundColor` also tints the system image —
80
+ // upstream uses this to color both the label and the icon. Skip when the
81
+ // role is destructive so SwiftUI's red tint remains in effect.
82
+ if (imageColor && !attributes?.destructive) {
83
+ modifiers.push(foregroundColorModifier(imageColor));
84
+ }
85
+
86
+ return (
87
+ <Button
88
+ key={key}
89
+ label={title}
90
+ systemImage={systemImage}
91
+ role={attributes?.destructive ? 'destructive' : undefined}
92
+ modifiers={modifiers.length > 0 ? modifiers : undefined}
93
+ onPress={fire}
94
+ />
95
+ );
96
+ }
97
+
98
+ let warnedShowNoop = false;
99
+
100
+ /**
101
+ * A drop-in replacement for `@react-native-menu/menu` on iOS.
102
+ * Uses SwiftUI `Menu` for tap triggers and `ContextMenu` for long-press triggers.
103
+ */
104
+ export function MenuView(props: MenuComponentProps & { ref?: React.Ref<MenuComponentRef> }) {
105
+ const { ref, actions, onPressAction, shouldOpenOnLongPress, title, style, children, testID } =
106
+ props;
107
+
108
+ // SwiftUI `Menu`/`ContextMenu` expose no programmatic open API, so `show()`
109
+ // can't do anything on iOS. Surface that as a one-time dev warning instead of
110
+ // silently no-opping.
111
+ React.useImperativeHandle(
112
+ ref,
113
+ () => ({
114
+ show: () => {
115
+ if (__DEV__ && !warnedShowNoop) {
116
+ warnedShowNoop = true;
117
+ console.warn(
118
+ '[@expo/ui] MenuView.show() is a no-op on iOS. SwiftUI Menu/ContextMenu have no programmatic open API.'
119
+ );
120
+ }
121
+ },
122
+ }),
123
+ []
124
+ );
125
+
126
+ const items = actions.map((action) => renderAction(action, onPressAction));
127
+ const body = title ? <Section title={title}>{items}</Section> : items;
128
+
129
+ // RNHostView requires a single ReactElement; wrap in a fragment so callers
130
+ // can pass any `ReactNode` (string, array, etc.).
131
+ const trigger = (
132
+ <RNHostView matchContents>
133
+ <>{children}</>
134
+ </RNHostView>
135
+ );
136
+
137
+ return (
138
+ <Host matchContents style={style} testID={testID}>
139
+ {shouldOpenOnLongPress ? (
140
+ <ContextMenu>
141
+ <ContextMenu.Trigger>{trigger}</ContextMenu.Trigger>
142
+ <ContextMenu.Items>{body}</ContextMenu.Items>
143
+ </ContextMenu>
144
+ ) : (
145
+ <Menu label={trigger}>{body}</Menu>
146
+ )}
147
+ </Host>
148
+ );
149
+ }
@@ -0,0 +1,36 @@
1
+ import { type Ref, useEffect, useImperativeHandle } from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import type { MenuComponentProps, MenuComponentRef } from './types';
5
+
6
+ let warned = false;
7
+
8
+ /**
9
+ * A drop-in replacement for `@react-native-menu/menu`'s `MenuView`. Wrap any trigger
10
+ * view; long-pressing or tapping (per `shouldOpenOnLongPress`) shows a popup menu
11
+ * built from the `actions` tree.
12
+ *
13
+ * - On Android, renders via Compose's `DropdownMenu` anchored to a `Pressable`.
14
+ * - On iOS, renders via SwiftUI's `Menu` (tap) or `ContextMenu` (long-press).
15
+ * - On web, the trigger renders the trigger but actions do not fire;
16
+ * a one-time `console.warn` is emitted.
17
+ *
18
+ * @platform android
19
+ * @platform ios
20
+ */
21
+ export function MenuView(props: MenuComponentProps & { ref?: Ref<MenuComponentRef> }) {
22
+ useEffect(() => {
23
+ if (!warned) {
24
+ warned = true;
25
+ console.warn(
26
+ "[@expo/ui] MenuView is currently Android and iOS-only; the trigger will render but actions won't fire on this platform."
27
+ );
28
+ }
29
+ }, []);
30
+ useImperativeHandle(props.ref, () => ({ show: () => {} }), []);
31
+ return (
32
+ <View style={props.style} testID={props.testID}>
33
+ {props.children}
34
+ </View>
35
+ );
36
+ }
@@ -0,0 +1,14 @@
1
+ import { MenuView } from './MenuView';
2
+
3
+ export type {
4
+ MenuAction,
5
+ MenuAttributes,
6
+ MenuComponentProps,
7
+ MenuState,
8
+ MenuComponentRef,
9
+ NativeActionEvent,
10
+ } from './types';
11
+
12
+ export default MenuView;
13
+ // named export needed for docs generator
14
+ export { MenuView };
@@ -0,0 +1,171 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ColorValue, ImageSourcePropType, StyleProp, ViewStyle } from 'react-native';
3
+ import type { SFSymbol } from 'sf-symbols-typescript';
4
+
5
+ /**
6
+ * Visual and behavioral attributes of a menu action.
7
+ * Compatible with `@react-native-menu/menu`.
8
+ */
9
+ export type MenuAttributes = {
10
+ /**
11
+ * Renders the action with a destructive style (red text/icon).
12
+ */
13
+ destructive?: boolean;
14
+ /**
15
+ * Disables the action so it can't be activated.
16
+ */
17
+ disabled?: boolean;
18
+ /**
19
+ * Hides the action from the menu.
20
+ */
21
+ hidden?: boolean;
22
+ };
23
+
24
+ /**
25
+ * Selection state for a menu action.
26
+ * `'on'` renders a checkmark; `'off'` doesn't.
27
+ */
28
+ export type MenuState = 'on' | 'off';
29
+
30
+ /**
31
+ * A single action inside a `MenuView`.
32
+ * Compatible with `@react-native-menu/menu`.
33
+ */
34
+ export type MenuAction = {
35
+ /**
36
+ * Identifier passed back via `onPressAction.nativeEvent.event` when this action is selected.
37
+ * Defaults to `title` if omitted.
38
+ */
39
+ id?: string;
40
+ /**
41
+ * Action label shown in the menu.
42
+ */
43
+ title: string;
44
+ /**
45
+ * Text color of the action label.
46
+ * @platform android
47
+ */
48
+ titleColor?: ColorValue;
49
+ /**
50
+ * Icon to render beside the action label.
51
+ *
52
+ * - When an `SFSymbol` name (e.g. `'trash'`), rendered on iOS only.
53
+ * Not rendered on Android — pass an `ImageSourcePropType` instead to show an
54
+ * icon there.
55
+ * - When an `ImageSourcePropType` (e.g. `require('./trash.xml')` or
56
+ * `{ uri: '...' }`), rendered on Android via Compose `Icon`. Ignored on iOS;
57
+ * SwiftUI menus only accept SF Symbol names for built-in `Menu`/`Button`
58
+ * labels.
59
+ */
60
+ image?: SFSymbol | ImageSourcePropType;
61
+ /**
62
+ * Tint color applied to the action's icon.
63
+ *
64
+ * Visually applied on Android via the leading `Icon`'s tint. On iOS, the
65
+ * value is accepted but **may not render**: SwiftUI's `Menu`/`ContextMenu`
66
+ * draw their items via the system menu UI, which ignores per-item color
67
+ * modifiers.
68
+ */
69
+ imageColor?: ColorValue;
70
+ /**
71
+ * Selection state. When `'on'`, the action renders a checkmark.
72
+ */
73
+ state?: MenuState;
74
+ /**
75
+ * Visual/behavioral flags.
76
+ */
77
+ attributes?: MenuAttributes;
78
+ /**
79
+ * Nested actions. Without `displayInline`, renders as a submenu;
80
+ * with `displayInline: true`, renders as an inline section.
81
+ */
82
+ subactions?: MenuAction[];
83
+ /**
84
+ * When `true` and `subactions` is present, renders the children as an inline section
85
+ * inside the parent menu (with this action's `title` as the section header on iOS).
86
+ */
87
+ displayInline?: boolean;
88
+ };
89
+
90
+ /**
91
+ * Imperative handle exposed by `MenuView` via `ref`.
92
+ * Compatible with `@react-native-menu/menu`'s `ref.show()` API.
93
+ */
94
+ export type MenuComponentRef = {
95
+ /**
96
+ * Programmatically open the menu.
97
+ *
98
+ * On Android, opens the anchored `DropdownMenu` (equivalent to the user tapping
99
+ * the trigger). On iOS this is a no-op — SwiftUI `Menu`/`ContextMenu` have no
100
+ * programmatic open API; a one-time `console.warn` is emitted in development.
101
+ * @platform android
102
+ */
103
+ show: () => void;
104
+ };
105
+
106
+ /**
107
+ * Event payload delivered to `onPressAction` when an action is selected.
108
+ * Compatible with `@react-native-menu/menu`.
109
+ */
110
+ export type NativeActionEvent = {
111
+ nativeEvent: {
112
+ /** Identifier of the pressed action: `action.id ?? action.title`. */
113
+ event: string;
114
+ };
115
+ };
116
+
117
+ /**
118
+ * Props for the `MenuView` component.
119
+ * Drop-in compatible with `@react-native-menu/menu`.
120
+ */
121
+ export type MenuComponentProps = {
122
+ /**
123
+ * Menu title shown at the top of the menu.
124
+ * @platform ios
125
+ */
126
+ title?: string;
127
+ /**
128
+ * Callback invoked when a menu action is selected.
129
+ */
130
+ onPressAction?: (event: NativeActionEvent) => void;
131
+ /**
132
+ * Callback invoked when the menu opens.
133
+ *
134
+ * On Android, fires when the trigger's tap/long-press flips `expanded` to `true`.
135
+ * On iOS, SwiftUI `Menu`/`ContextMenu` do not expose an open hook, so this is not
136
+ * fired there.
137
+ * @platform android
138
+ */
139
+ onOpenMenu?: () => void;
140
+ /**
141
+ * Callback invoked when the menu closes (either via dismissal or after an action
142
+ * fires).
143
+ *
144
+ * On Android, fires from the controlled `DropdownMenu`'s dismiss path.
145
+ * On iOS, SwiftUI `Menu`/`ContextMenu` do not expose a close hook in a way we can
146
+ * forward, so this is not fired there.
147
+ * @platform android
148
+ */
149
+ onCloseMenu?: () => void;
150
+ /**
151
+ * The actions to display in the menu.
152
+ */
153
+ actions: MenuAction[];
154
+ /**
155
+ * When `true`, the menu opens on long-press of the trigger instead of a single tap.
156
+ * @default false
157
+ */
158
+ shouldOpenOnLongPress?: boolean;
159
+ /**
160
+ * Style applied to the trigger wrapper.
161
+ */
162
+ style?: StyleProp<ViewStyle>;
163
+ /**
164
+ * Test identifier passed through to the trigger view.
165
+ */
166
+ testID?: string;
167
+ /**
168
+ * Trigger view. Long-pressing or tapping (per `shouldOpenOnLongPress`) opens the menu.
169
+ */
170
+ children?: ReactNode;
171
+ };
@@ -0,0 +1,92 @@
1
+ import { requireNativeView } from 'expo';
2
+ import { type ColorValue } from 'react-native';
3
+
4
+ import type { ObservableState } from '../../State/useNativeState';
5
+ import { getStateId } from '../../State/utils';
6
+ import { type ModifierConfig } from '../../types';
7
+ import { createViewModifierEventListener } from '../modifiers/utils';
8
+
9
+ /**
10
+ * Common props shared by loading indicator variants.
11
+ */
12
+ export type LoadingIndicatorCommonConfig = {
13
+ /**
14
+ * An observable state that holds the current progress value.
15
+ * Create one with `useNativeState(0)`. Omit for indeterminate loading.
16
+ */
17
+ progress?: ObservableState<number | null>;
18
+ /**
19
+ * Loading indicator color.
20
+ */
21
+ color?: ColorValue;
22
+ /**
23
+ * Modifiers for the component.
24
+ */
25
+ modifiers?: ModifierConfig[];
26
+ };
27
+
28
+ type NativeLoadingIndicatorCommonConfig = Omit<
29
+ LoadingIndicatorCommonConfig,
30
+ 'progress' | 'modifiers'
31
+ > & {
32
+ progress?: number;
33
+ modifiers?: unknown;
34
+ };
35
+
36
+ function transformProps<T extends LoadingIndicatorCommonConfig>(
37
+ props: T
38
+ ): NativeLoadingIndicatorCommonConfig {
39
+ const { modifiers, progress, ...restProps } = props;
40
+ return {
41
+ modifiers,
42
+ ...(modifiers ? createViewModifierEventListener(modifiers) : undefined),
43
+ ...restProps,
44
+ progress: getStateId(progress),
45
+ };
46
+ }
47
+
48
+ function createLoadingIndicatorComponent<P extends LoadingIndicatorCommonConfig>(
49
+ viewName: string
50
+ ): React.ComponentType<P> {
51
+ const NativeView: React.ComponentType<NativeLoadingIndicatorCommonConfig> = requireNativeView(
52
+ 'ExpoUI',
53
+ viewName
54
+ );
55
+ function Component(props: P) {
56
+ return <NativeView {...transformProps(props)} />;
57
+ }
58
+ Component.displayName = viewName;
59
+ return Component;
60
+ }
61
+
62
+ // region LoadingIndicator
63
+
64
+ /**
65
+ * A loading indicator that displays loading using morphing shapes.
66
+ *
67
+ * Matches the Jetpack Compose `LoadingIndicator`.
68
+ */
69
+ export const LoadingIndicator = createLoadingIndicatorComponent('LoadingIndicatorView');
70
+
71
+ // endregion
72
+
73
+ // region ContainedLoadingIndicator
74
+
75
+ export type ContainedLoadingIndicatorProps = LoadingIndicatorCommonConfig & {
76
+ /**
77
+ * Loading indicator's container color
78
+ */
79
+ containerColor?: ColorValue;
80
+ };
81
+
82
+ /**
83
+ * A loading indicator that displays loading using morphing shapes inside a container.
84
+ *
85
+ * Matches the Jetpack Compose `ContainedLoadingIndicator`.
86
+ */
87
+ export const ContainedLoadingIndicator =
88
+ createLoadingIndicatorComponent<ContainedLoadingIndicatorProps>('ContainedLoadingIndicatorView');
89
+ // endregion
90
+
91
+ // Exported for docs api data
92
+ export { type ObservableState };
@@ -0,0 +1,135 @@
1
+ import { requireNativeView } from 'expo';
2
+ import { type Ref } from 'react';
3
+ import { type ColorValue } from 'react-native';
4
+
5
+ import { type ModifierConfig } from '../../types';
6
+ import { createViewModifierEventListener } from '../modifiers/utils';
7
+
8
+ export type SnackbarProps = {
9
+ /**
10
+ * The background color of the snackbar container.
11
+ */
12
+ containerColor?: ColorValue;
13
+ /**
14
+ * The preferred content color used for the message text.
15
+ */
16
+ contentColor?: ColorValue;
17
+ /**
18
+ * The content color used for the action button.
19
+ */
20
+ actionContentColor?: ColorValue;
21
+ /**
22
+ * The content color used for the dismiss-action icon button.
23
+ */
24
+ dismissActionContentColor?: ColorValue;
25
+ /**
26
+ * Whether the action should be placed on a new line below the message.
27
+ * Useful for long action labels.
28
+ * @default false
29
+ */
30
+ actionOnNewLine?: boolean;
31
+ /**
32
+ * Modifiers for the component.
33
+ */
34
+ modifiers?: ModifierConfig[];
35
+ };
36
+
37
+ const SnackbarNativeView: React.ComponentType<SnackbarProps> = requireNativeView(
38
+ 'ExpoUI',
39
+ 'SnackbarView'
40
+ );
41
+
42
+ /**
43
+ * Styling configuration for the snackbar shown by `SnackbarHost`. Pass as a
44
+ * child to override colors or place the action on a new line.
45
+ */
46
+ export function Snackbar(props: SnackbarProps) {
47
+ const { modifiers, ...rest } = props;
48
+ return (
49
+ <SnackbarNativeView
50
+ modifiers={modifiers}
51
+ {...(modifiers ? createViewModifierEventListener(modifiers) : undefined)}
52
+ {...rest}
53
+ />
54
+ );
55
+ }
56
+
57
+ // --- SnackbarHost ---
58
+
59
+ /**
60
+ * How long the snackbar is shown. Mirrors Compose's `SnackbarDuration` enum.
61
+ */
62
+ export type SnackbarDuration = 'short' | 'long' | 'indefinite';
63
+
64
+ /**
65
+ * Reason a snackbar invocation resolved. Mirrors Compose's `SnackbarResult` enum.
66
+ */
67
+ export type SnackbarResult = 'actionPerformed' | 'dismissed';
68
+
69
+ export type SnackbarShowOptions = {
70
+ /**
71
+ * The message body of the snackbar.
72
+ */
73
+ message: string;
74
+ /**
75
+ * Label for the optional action button. When omitted, no action button is shown.
76
+ */
77
+ actionLabel?: string;
78
+ /**
79
+ * Whether to show a trailing close (X) icon button to dismiss the snackbar.
80
+ * @default false
81
+ */
82
+ withDismissAction?: boolean;
83
+ /**
84
+ * How long to show the snackbar. Defaults to `'short'` when an `actionLabel`
85
+ * is not provided, and `'indefinite'` when it is, matching Compose.
86
+ */
87
+ duration?: SnackbarDuration;
88
+ };
89
+
90
+ export type SnackbarHostRef = {
91
+ /**
92
+ * Shows a snackbar and resolves with `'actionPerformed'` when the user taps
93
+ * the action, or `'dismissed'` when it times out or the dismiss-action
94
+ * button is tapped. Subsequent calls queue and show after the current
95
+ * snackbar is dismissed.
96
+ */
97
+ showSnackbar: (options: SnackbarShowOptions) => Promise<SnackbarResult>;
98
+ };
99
+
100
+ export type SnackbarHostProps = {
101
+ /**
102
+ * Ref exposing the imperative `showSnackbar` method.
103
+ */
104
+ ref?: Ref<SnackbarHostRef>;
105
+ /**
106
+ * Modifiers for the component.
107
+ */
108
+ modifiers?: ModifierConfig[];
109
+ /**
110
+ * Optional `Snackbar` child supplying styling for shown snackbars. Mirrors
111
+ * Compose's `SnackbarHost(hostState) { data -> Snackbar(data, ...) }` lambda.
112
+ */
113
+ children?: React.ReactNode;
114
+ };
115
+
116
+ const SnackbarHostNativeView: React.ComponentType<SnackbarHostProps> = requireNativeView(
117
+ 'ExpoUI',
118
+ 'SnackbarHostView'
119
+ );
120
+
121
+ /**
122
+ * A Material 3 [SnackbarHost](https://developer.android.com/develop/ui/compose/components/snackbar)
123
+ * that displays snackbars triggered via its ref's `showSnackbar` method.
124
+ */
125
+ export function SnackbarHost(props: SnackbarHostProps) {
126
+ const { modifiers, children, ...rest } = props;
127
+ return (
128
+ <SnackbarHostNativeView
129
+ modifiers={modifiers}
130
+ {...(modifiers ? createViewModifierEventListener(modifiers) : undefined)}
131
+ {...rest}>
132
+ {children}
133
+ </SnackbarHostNativeView>
134
+ );
135
+ }
@@ -49,6 +49,7 @@ export {
49
49
  type HorizontalPagerProps,
50
50
  } from './HorizontalPager';
51
51
  export * from './SearchBar';
52
+ export * from './Snackbar';
52
53
  export * from './DockedSearchBar';
53
54
  export * from './HorizontalFloatingToolbar';
54
55
  export * from './FloatingActionButton';
@@ -57,6 +58,7 @@ export * from './RadioButton';
57
58
  export * from './Surface';
58
59
  export { type TextProps, Text } from './Text';
59
60
  export * from './Tooltip';
61
+ export * from './LoadingIndicator';
60
62
 
61
63
  export * from './AnimatedVisibility';
62
64
  export * from './Box';
@@ -1,6 +1,6 @@
1
1
  import { type ColorValue } from 'react-native';
2
2
 
3
- import { type AnimatedValue } from './animation';
3
+ import { type AnimatedValue, type AnimationSpec } from './animation';
4
4
  import { createModifier, createModifierWithEventListener } from './createModifier';
5
5
  export { type ExpoModifier, type ModifierConfig } from '../../types';
6
6
  export {
@@ -142,9 +142,12 @@ export const offset = (x: number, y: number) => createModifier('offset', { x, y
142
142
 
143
143
  /**
144
144
  * Sets the background color.
145
+ * Pass an `animationSpec` to smoothly animate between colors when the prop changes (backed by `animateColorAsState`).
145
146
  * @param color - A color string (hex, e.g., `'#FF0000'`).
147
+ * @param options.animationSpec - Optional spec — animate between color changes.
146
148
  */
147
- export const background = (color: ColorValue) => createModifier('background', { color });
149
+ export const background = (color: ColorValue, options?: { animationSpec?: AnimationSpec }) =>
150
+ createModifier('background', { color, animationSpec: options?.animationSpec });
148
151
 
149
152
  /**
150
153
  * Adds a border around the view.
@@ -291,6 +294,31 @@ export const clickable = (handler: () => void, options?: { indication?: boolean
291
294
  indication: options?.indication ?? true,
292
295
  });
293
296
 
297
+ /**
298
+ * Makes the view respond to both click and long-click gestures.
299
+ * Wraps Compose's `Modifier.combinedClickable`. Useful for triggering a `DropdownMenu`
300
+ * on long-press while keeping a separate short-press action.
301
+ * @param handlers.onClick - Function to call on a short tap.
302
+ * @param handlers.onLongClick - Function to call on a long press.
303
+ * @param options - Optional configuration.
304
+ * @param options.indication - Whether to show a ripple indication. Defaults to `true`.
305
+ */
306
+ export const combinedClickable = (
307
+ handlers: { onClick?: () => void; onLongClick?: () => void },
308
+ options?: { indication?: boolean }
309
+ ) =>
310
+ createModifierWithEventListener(
311
+ 'combinedClickable',
312
+ (params: { event: 'click' | 'longClick' }) => {
313
+ if (params.event === 'click') {
314
+ handlers.onClick?.();
315
+ } else if (params.event === 'longClick') {
316
+ handlers.onLongClick?.();
317
+ }
318
+ },
319
+ { indication: options?.indication ?? true }
320
+ );
321
+
294
322
  /**
295
323
  * Makes the view selectable, like a radio button row.
296
324
  * @param selected - Whether the item is currently selected.