@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
@@ -1,33 +1,51 @@
1
1
  import { BottomSheet as SwiftUIBottomSheet, Group } from '@expo/ui/swift-ui';
2
- import { frame, padding, presentationDragIndicator } from '@expo/ui/swift-ui/modifiers';
2
+ import {
3
+ frame,
4
+ padding,
5
+ presentationDetents,
6
+ presentationDragIndicator,
7
+ type ModifierConfig,
8
+ type PresentationDetent,
9
+ } from '@expo/ui/swift-ui/modifiers';
3
10
 
4
- import type { BottomSheetProps } from './types';
11
+ import type { BottomSheetProps, SnapPoint } from './types';
12
+
13
+ function snapPointToDetent(snapPoint: SnapPoint): PresentationDetent {
14
+ if (snapPoint === 'half') return 'medium';
15
+ if (snapPoint === 'full') return 'large';
16
+ return snapPoint;
17
+ }
5
18
 
6
19
  export function BottomSheet({
7
20
  children,
8
21
  isPresented,
9
22
  onDismiss,
10
23
  showDragIndicator = true,
24
+ snapPoints,
11
25
  testID,
12
26
  modifiers,
13
27
  }: BottomSheetProps) {
28
+ const presentationModifiers: ModifierConfig[] = [
29
+ frame({ maxWidth: Infinity, alignment: 'topLeading' }),
30
+ padding({ top: 16, leading: 16, trailing: 16 }),
31
+ presentationDragIndicator(showDragIndicator ? 'visible' : 'hidden'),
32
+ ];
33
+ if (snapPoints && snapPoints.length > 0) {
34
+ presentationModifiers.push(presentationDetents(snapPoints.map(snapPointToDetent)));
35
+ }
36
+ if (modifiers?.length) {
37
+ presentationModifiers.push(...modifiers);
38
+ }
39
+
14
40
  return (
15
41
  <SwiftUIBottomSheet
16
42
  isPresented={isPresented}
17
43
  onIsPresentedChange={(presented) => {
18
44
  if (!presented) onDismiss();
19
45
  }}
20
- fitToContents
46
+ fitToContents={!snapPoints || snapPoints.length === 0}
21
47
  testID={testID}>
22
- <Group
23
- modifiers={[
24
- frame({ maxWidth: Infinity, alignment: 'topLeading' }),
25
- padding({ top: 16, leading: 16, trailing: 16 }),
26
- presentationDragIndicator(showDragIndicator ? 'visible' : 'hidden'),
27
- ...(modifiers ?? []),
28
- ]}>
29
- {children}
30
- </Group>
48
+ <Group modifiers={presentationModifiers}>{children}</Group>
31
49
  </SwiftUIBottomSheet>
32
50
  );
33
51
  }
@@ -1,6 +1,26 @@
1
1
  import { Drawer } from 'vaul';
2
2
 
3
- import type { BottomSheetProps } from './types';
3
+ import type { BottomSheetProps, SnapPoint } from './types';
4
+
5
+ // Visually-hidden style for the screen-reader-only Drawer.Title.
6
+ const visuallyHiddenStyle: React.CSSProperties = {
7
+ position: 'absolute',
8
+ width: 1,
9
+ height: 1,
10
+ padding: 0,
11
+ margin: -1,
12
+ overflow: 'hidden',
13
+ clip: 'rect(0, 0, 0, 0)',
14
+ whiteSpace: 'nowrap',
15
+ border: 0,
16
+ };
17
+
18
+ function snapPointToVaul(snapPoint: SnapPoint): string | number {
19
+ if (snapPoint === 'half') return 0.5;
20
+ if (snapPoint === 'full') return 1;
21
+ if ('fraction' in snapPoint) return snapPoint.fraction;
22
+ return `${snapPoint.height}px`;
23
+ }
4
24
 
5
25
  /**
6
26
  * A modal sheet that slides up from the bottom of the screen.
@@ -10,17 +30,32 @@ export function BottomSheet({
10
30
  isPresented,
11
31
  onDismiss,
12
32
  showDragIndicator = true,
33
+ snapPoints,
13
34
  testID,
14
35
  }: BottomSheetProps) {
36
+ const vaulSnapPoints = snapPoints?.length ? snapPoints.map(snapPointToVaul) : undefined;
37
+ const hasSnapPoints = vaulSnapPoints != null;
38
+
15
39
  return (
16
40
  <Drawer.Root
17
41
  open={isPresented}
18
42
  onOpenChange={(open) => {
19
43
  if (!open) onDismiss();
20
- }}>
44
+ }}
45
+ snapPoints={vaulSnapPoints}>
21
46
  <Drawer.Portal>
22
47
  <Drawer.Overlay style={overlayStyle} />
23
- <Drawer.Content style={{ ...contentStyle, ...(showDragIndicator && dragIndicatorSpacing) }}>
48
+ <Drawer.Content
49
+ style={{
50
+ ...contentStyle,
51
+ // Snap-points mode: vaul translates the drawer by `viewport - snapHeight`.
52
+ // The drawer has to fill the viewport or it gets pushed off-screen.
53
+ ...(hasSnapPoints ? snapPointContentStyle : noSnapPointContentStyle),
54
+ ...(showDragIndicator && dragIndicatorSpacing),
55
+ }}
56
+ aria-describedby={undefined}>
57
+ {/* Radix Dialog requires a title for a11y; render visually-hidden. */}
58
+ <Drawer.Title style={visuallyHiddenStyle}>Bottom sheet</Drawer.Title>
24
59
  {showDragIndicator && <Drawer.Handle />}
25
60
  <div style={innerStyle} data-testid={testID}>
26
61
  {children}
@@ -47,12 +82,19 @@ const contentStyle: React.CSSProperties = {
47
82
  borderTopLeftRadius: 16,
48
83
  borderTopRightRadius: 16,
49
84
  zIndex: 50,
50
- maxHeight: '85vh',
51
85
  outline: 'none',
52
86
  display: 'flex',
53
87
  flexDirection: 'column',
54
88
  };
55
89
 
90
+ const snapPointContentStyle: React.CSSProperties = {
91
+ height: '96vh',
92
+ };
93
+
94
+ const noSnapPointContentStyle: React.CSSProperties = {
95
+ maxHeight: '85vh',
96
+ };
97
+
56
98
  const innerStyle: React.CSSProperties = {
57
99
  padding: 16,
58
100
  overflow: 'auto',
@@ -1,5 +1,20 @@
1
1
  import type { ModifierConfig } from '../../types';
2
2
 
3
+ /**
4
+ * A snap point describing one of the heights a [`BottomSheet`](#bottomsheet) can rest at.
5
+ *
6
+ * - `'half'` — Approximately half-screen.
7
+ * - `'full'` — Fully expanded.
8
+ * - `{ fraction }` — A fraction of the screen height (0–1).
9
+ * iOS / web only.
10
+ * - `{ height }` — A fixed pixel height.
11
+ * iOS / web only.
12
+ *
13
+ * On Android, `{ fraction }` and `{ height }` snap to the nearest of `'half'` / `'full'`.
14
+ * See the component docs for platform behavior notes.
15
+ */
16
+ export type SnapPoint = 'half' | 'full' | { fraction: number } | { height: number };
17
+
3
18
  /**
4
19
  * Props for the [`BottomSheet`](#bottomsheet) component, a modal sheet that slides up from the bottom of the screen.
5
20
  */
@@ -25,6 +40,16 @@ export interface BottomSheetProps {
25
40
  */
26
41
  showDragIndicator?: boolean;
27
42
 
43
+ /**
44
+ * Heights the sheet can rest at.
45
+ * When omitted, the sheet auto-sizes to its content.
46
+ * See [`SnapPoint`](#snappoint) for the supported values.
47
+ *
48
+ * @example `['half', 'full']` — draggable between half and full
49
+ * @example `['full']` — always full height
50
+ */
51
+ snapPoints?: SnapPoint[];
52
+
28
53
  /**
29
54
  * Identifier used to locate the component in end-to-end tests.
30
55
  */
@@ -0,0 +1,72 @@
1
+ import {
2
+ AnimatedVisibility,
3
+ Column,
4
+ EnterTransition,
5
+ ExitTransition,
6
+ Icon,
7
+ ListItem,
8
+ Text,
9
+ useMaterialColors,
10
+ } from '@expo/ui/jetpack-compose';
11
+ import {
12
+ animated,
13
+ background,
14
+ clickable,
15
+ clip,
16
+ graphicsLayer,
17
+ padding,
18
+ Shapes,
19
+ spring,
20
+ } from '@expo/ui/jetpack-compose/modifiers';
21
+
22
+ import type { CollapsibleProps } from './types';
23
+
24
+ const KEYBOARD_ARROW_DOWN = require('../../../assets/keyboard_arrow_down.xml');
25
+
26
+ // M3 large-corner token (16dp) — the Expressive expandable-list-item pattern.
27
+ const CONTAINER_SHAPE = Shapes.RoundedCorner(16);
28
+
29
+ // expandVertically + fadeIn keeps motion strictly vertical.
30
+ // (Compose's default expandIn includes a horizontal component.)
31
+ const ENTER = EnterTransition.expandVertically().plus(EnterTransition.fadeIn());
32
+ const EXIT = ExitTransition.shrinkVertically().plus(ExitTransition.fadeOut());
33
+
34
+ /**
35
+ * Android implementation of `Collapsible`.
36
+ * A rounded M3 card whose container tint fades between `transparent` (collapsed) and `surfaceContainer` (expanded).
37
+ */
38
+ export function Collapsible({ isOpen, onOpenChange, label = '', children }: CollapsibleProps) {
39
+ const colors = useMaterialColors();
40
+ const containerColor = isOpen ? colors.surfaceContainer : 'transparent';
41
+
42
+ return (
43
+ <Column
44
+ modifiers={[
45
+ // `clip` first so background paint and the inner ListItem's ripple both respect the rounded shape.
46
+ clip(CONTAINER_SHAPE),
47
+ background(containerColor, { animationSpec: spring() }),
48
+ ]}>
49
+ <ListItem
50
+ // Transparent so the outer `background` is the sole tint source.
51
+ colors={{ containerColor: 'transparent' }}
52
+ modifiers={[clickable(() => onOpenChange(!isOpen))]}>
53
+ <ListItem.HeadlineContent>
54
+ <Text>{label}</Text>
55
+ </ListItem.HeadlineContent>
56
+ <ListItem.TrailingContent>
57
+ <Icon
58
+ source={KEYBOARD_ARROW_DOWN}
59
+ modifiers={[graphicsLayer({ rotationZ: animated(isOpen ? 180 : 0, spring()) })]}
60
+ />
61
+ </ListItem.TrailingContent>
62
+ </ListItem>
63
+ <AnimatedVisibility visible={isOpen} enterTransition={ENTER} exitTransition={EXIT}>
64
+ {/* 16dp matches the M3 large-corner radius.
65
+ ListItem's own bottom padding adds the M3 header–body separation on top. */}
66
+ <Column modifiers={[padding(16, 16, 16, 16)]}>{children}</Column>
67
+ </AnimatedVisibility>
68
+ </Column>
69
+ );
70
+ }
71
+
72
+ export * from './types';
@@ -0,0 +1,16 @@
1
+ import { DisclosureGroup } from '@expo/ui/swift-ui';
2
+
3
+ import type { CollapsibleProps } from './types';
4
+
5
+ /**
6
+ * iOS implementation of `Collapsible`. Wraps SwiftUI's `DisclosureGroup`.
7
+ */
8
+ export function Collapsible({ isOpen, onOpenChange, label = '', children }: CollapsibleProps) {
9
+ return (
10
+ <DisclosureGroup label={label} isExpanded={isOpen} onIsExpandedChange={onOpenChange}>
11
+ {children}
12
+ </DisclosureGroup>
13
+ );
14
+ }
15
+
16
+ export * from './types';
@@ -0,0 +1,58 @@
1
+ import type { ComponentProps, SyntheticEvent } from 'react';
2
+ import { StyleSheet, Text, unstable_createElement, View, type ViewProps } from 'react-native';
3
+
4
+ import type { CollapsibleProps } from './types';
5
+
6
+ const Details = (
7
+ props: Omit<ComponentProps<'details'>, 'style'> & { style?: ViewProps['style'] }
8
+ ) => unstable_createElement('details', props);
9
+
10
+ const Summary = (
11
+ props: Omit<ComponentProps<'summary'>, 'style'> & { style?: ViewProps['style'] }
12
+ ) => unstable_createElement('summary', props);
13
+
14
+ /**
15
+ * A primitive that toggles visibility of its content via a labelled tappable
16
+ * header. Controlled via `isOpen` + `onOpenChange`.
17
+ */
18
+ export function Collapsible({ isOpen, onOpenChange, label = '', children }: CollapsibleProps) {
19
+ return (
20
+ <Details
21
+ open={isOpen}
22
+ onToggle={(event: SyntheticEvent<HTMLDetailsElement>) => {
23
+ const nextOpen = event.currentTarget.open;
24
+ if (nextOpen !== isOpen) {
25
+ onOpenChange(nextOpen);
26
+ }
27
+ }}
28
+ style={styles.container}>
29
+ <Summary style={styles.summary}>
30
+ <Text>{label}</Text>
31
+ </Summary>
32
+ <View style={styles.content}>{children}</View>
33
+ </Details>
34
+ );
35
+ }
36
+
37
+ const styles = StyleSheet.create({
38
+ container: {
39
+ flexDirection: 'column',
40
+ width: '100%',
41
+ },
42
+ summary: {
43
+ flexDirection: 'row',
44
+ alignItems: 'center',
45
+ gap: 8,
46
+ width: '100%',
47
+ paddingHorizontal: 16,
48
+ paddingVertical: 12,
49
+ userSelect: 'none',
50
+ cursor: 'pointer',
51
+ },
52
+ content: {
53
+ paddingHorizontal: 16,
54
+ paddingBottom: 12,
55
+ },
56
+ });
57
+
58
+ export * from './types';
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Props for the [`Collapsible`](#collapsible) component, a primitive that
3
+ * shows or hides its content with a tap on a labelled header.
4
+ */
5
+ export interface CollapsibleProps {
6
+ /**
7
+ * Whether the content is currently expanded.
8
+ */
9
+ isOpen: boolean;
10
+
11
+ /**
12
+ * Called when the user taps the header to toggle the open state.
13
+ */
14
+ onOpenChange: (isOpen: boolean) => void;
15
+
16
+ /**
17
+ * Text rendered in the tappable header.
18
+ */
19
+ label?: string;
20
+
21
+ /**
22
+ * Content rendered when `isOpen` is `true`.
23
+ */
24
+ children?: React.ReactNode;
25
+ }
@@ -5,7 +5,9 @@ import { useUniversalLifecycle } from '../hooks';
5
5
  import type { UniversalAlignment } from '../types';
6
6
 
7
7
  const styles = StyleSheet.create({
8
- column: { flexDirection: 'column' },
8
+ // `alignSelf: 'stretch'` — match SwiftUI `VStack` / Compose `Column`, which fill their parent's cross-axis.
9
+ // Without this, a nested Column inherits its parent's `alignItems` (often `flex-start`) and shrinks to content.
10
+ column: { flexDirection: 'column', alignSelf: 'stretch' },
9
11
  hidden: { display: 'none' },
10
12
  disabled: {
11
13
  opacity: 0.5,
@@ -1,18 +1,82 @@
1
- import { View, type ViewProps } from 'react-native';
1
+ import { StyleSheet, View, type LayoutChangeEvent } from 'react-native';
2
+
3
+ import type { UniversalHostProps } from './types';
4
+
5
+ const styles = StyleSheet.create({
6
+ matchContents: {
7
+ alignSelf: 'flex-start',
8
+ },
9
+ matchViewport: {
10
+ height: '100dvh',
11
+ width: '100dvw',
12
+ },
13
+ safeArea: {
14
+ paddingLeft: 'max(env(safe-area-inset-left, 0px), env(keyboard-inset-left, 0px))',
15
+ paddingRight: 'max(env(safe-area-inset-right, 0px), env(keyboard-inset-right, 0px))',
16
+ paddingTop: 'max(env(safe-area-inset-top, 0px), env(keyboard-inset-top, 0px))',
17
+ paddingBottom: 'max(env(safe-area-inset-bottom, 0px), env(keyboard-inset-bottom, 0px))',
18
+ },
19
+ safeAreaWithoutKeyboard: {
20
+ paddingLeft: 'env(safe-area-inset-left, 0px)',
21
+ paddingRight: 'env(safe-area-inset-right, 0px)',
22
+ paddingTop: 'env(safe-area-inset-top, 0px)',
23
+ paddingBottom: 'env(safe-area-inset-bottom, 0px)',
24
+ },
25
+ });
2
26
 
3
27
  /**
4
28
  * A bridging container that hosts SwiftUI views on iOS and Jetpack Compose views on Android.
29
+ * On platforms without a native UI-toolkit binding (web, RN fallback), renders a plain `View`.
30
+ * The `colorScheme`, `layoutDirection`, and `matchContents` props are accepted for API parity but have no effect.
5
31
  */
6
32
  export function Host({
7
33
  children,
34
+ ignoreSafeArea,
35
+ layoutDirection,
36
+ matchContents = false,
37
+ onLayout,
38
+ onLayoutContent,
8
39
  style,
40
+ useViewportSizeMeasurement = false,
41
+ colorScheme: _colorScheme,
9
42
  ...rest
10
- }: ViewProps & {
11
- matchContents?: boolean | { vertical?: boolean; horizontal?: boolean };
12
- }) {
43
+ }: UniversalHostProps) {
44
+ const shouldMatchContents =
45
+ typeof matchContents === 'object'
46
+ ? matchContents.horizontal || matchContents.vertical
47
+ : matchContents;
48
+
13
49
  return (
14
- <View style={style} {...rest}>
50
+ <View
51
+ dir={
52
+ layoutDirection === 'leftToRight'
53
+ ? 'ltr'
54
+ : layoutDirection === 'rightToLeft'
55
+ ? 'rtl'
56
+ : undefined
57
+ }
58
+ onLayout={(event: LayoutChangeEvent) => {
59
+ onLayout?.(event);
60
+
61
+ onLayoutContent?.({
62
+ nativeEvent: {
63
+ width: event.nativeEvent.layout.width,
64
+ height: event.nativeEvent.layout.height,
65
+ },
66
+ });
67
+ }}
68
+ style={[
69
+ ignoreSafeArea !== 'all' &&
70
+ (ignoreSafeArea === 'keyboard' ? styles.safeAreaWithoutKeyboard : styles.safeArea),
71
+ shouldMatchContents
72
+ ? styles.matchContents
73
+ : useViewportSizeMeasurement && styles.matchViewport,
74
+ style,
75
+ ]}
76
+ {...rest}>
15
77
  {children}
16
78
  </View>
17
79
  );
18
80
  }
81
+
82
+ export type { UniversalHostProps } from './types';
@@ -0,0 +1,70 @@
1
+ import type { ColorSchemeName, ViewProps } from 'react-native';
2
+
3
+ /**
4
+ * Props for the [`Host`](#host) component.
5
+ */
6
+ export interface UniversalHostProps extends ViewProps {
7
+ /**
8
+ * When `true`, the host updates its size in the React Native view tree to match the content's layout from the underlying platform UI toolkit.
9
+ * Can only be set once on mount.
10
+ *
11
+ * @default false
12
+ * @platform android
13
+ * @platform ios
14
+ * @platform web
15
+ */
16
+ matchContents?: boolean | { vertical?: boolean; horizontal?: boolean };
17
+
18
+ /**
19
+ * The color scheme to apply to descendant native views.
20
+ * `'light'` / `'dark'` force a specific appearance; omitted follows the device setting.
21
+ *
22
+ * @platform android
23
+ * @platform ios
24
+ */
25
+ colorScheme?: ColorSchemeName;
26
+
27
+ /**
28
+ * Layout direction for the platform UI content.
29
+ * Defaults to the current locale direction from `I18nManager`.
30
+ *
31
+ * @platform android
32
+ * @platform ios
33
+ * @platform web
34
+ */
35
+ layoutDirection?: 'leftToRight' | 'rightToLeft';
36
+
37
+ /**
38
+ * Controls which safe area regions the hosting view should ignore. Can only be set once on mount.
39
+ * - `'all'`- ignores all safe area insets.
40
+ * - `'keyboard'` - ignores only the keyboard safe area.
41
+ *
42
+ * @platform android
43
+ * @platform ios
44
+ * @platform web
45
+ */
46
+ ignoreSafeArea?: 'all' | 'keyboard';
47
+
48
+ /**
49
+ * When true and no explicit size is provided, the host will use the viewport size as the proposed size for layout.
50
+ * This is particularly useful for views that need to fill their available space, such as `List`.
51
+ * @default false
52
+ *
53
+ * @platform android
54
+ * @platform ios
55
+ * @platform web
56
+ */
57
+ useViewportSizeMeasurement?: boolean;
58
+
59
+ /**
60
+ * Callback function that is triggered when the content completes its layout.
61
+ * Provides the current dimensions of the content, which may change as the content updates.
62
+ *
63
+ * @platform android
64
+ * @platform ios
65
+ * @platform web
66
+ */
67
+ onLayoutContent?: (event: { nativeEvent: { width: number; height: number } }) => void;
68
+
69
+ children?: React.ReactNode;
70
+ }
@@ -0,0 +1,44 @@
1
+ import { LazyColumn, PullToRefreshBox } from '@expo/ui/jetpack-compose';
2
+ import { testID as testIDModifier, type ModifierConfig } from '@expo/ui/jetpack-compose/modifiers';
3
+ import { useCallback, useState } from 'react';
4
+
5
+ import type { ListProps } from './types';
6
+
7
+ /**
8
+ * Android implementation of `List`.
9
+ * Composes `LazyColumn` and wraps with `PullToRefreshBox` when `onRefresh` is provided.
10
+ * The returned promise drives the refresh indicator's visibility.
11
+ */
12
+ export function List({ children, onRefresh, testID }: ListProps) {
13
+ const [isRefreshing, setIsRefreshing] = useState(false);
14
+
15
+ const handleRefresh = useCallback(async () => {
16
+ if (!onRefresh) return;
17
+ setIsRefreshing(true);
18
+ try {
19
+ await onRefresh();
20
+ } finally {
21
+ setIsRefreshing(false);
22
+ }
23
+ }, [onRefresh]);
24
+
25
+ const listModifiers: ModifierConfig[] | undefined = testID ? [testIDModifier(testID)] : undefined;
26
+ const listContent = <LazyColumn modifiers={listModifiers}>{children}</LazyColumn>;
27
+
28
+ if (!onRefresh) {
29
+ return listContent;
30
+ }
31
+
32
+ return (
33
+ // `contentAlignment="topCenter"` keeps the refresh indicator centered.
34
+ // The indicator's own `Modifier.align` resolves outside `BoxScope` and becomes a no-op, so we set it on the parent.
35
+ <PullToRefreshBox
36
+ isRefreshing={isRefreshing}
37
+ onRefresh={handleRefresh}
38
+ contentAlignment="topCenter">
39
+ {listContent}
40
+ </PullToRefreshBox>
41
+ );
42
+ }
43
+
44
+ export * from './types';
@@ -0,0 +1,19 @@
1
+ import { List as SwiftUIList } from '@expo/ui/swift-ui';
2
+ import { refreshable, type ModifierConfig } from '@expo/ui/swift-ui/modifiers';
3
+
4
+ import type { ListProps } from './types';
5
+
6
+ /**
7
+ * iOS implementation of `List`.
8
+ * Delegates to SwiftUI's `List` and applies `.refreshable` when `onRefresh` is provided.
9
+ */
10
+ export function List({ children, onRefresh, testID }: ListProps) {
11
+ const modifiers: ModifierConfig[] | undefined = onRefresh ? [refreshable(onRefresh)] : undefined;
12
+ return (
13
+ <SwiftUIList modifiers={modifiers} testID={testID}>
14
+ {children}
15
+ </SwiftUIList>
16
+ );
17
+ }
18
+
19
+ export * from './types';
@@ -0,0 +1,26 @@
1
+ import { StyleSheet, View } from 'react-native';
2
+
3
+ import type { ListProps } from './types';
4
+
5
+ /**
6
+ * A vertical container of rows.
7
+ * Typically populated with [`ListItem`](#listitem) children.
8
+ */
9
+ export function List({ children, testID }: ListProps) {
10
+ return (
11
+ <View style={styles.container} testID={testID}>
12
+ {children}
13
+ </View>
14
+ );
15
+ }
16
+
17
+ const styles = StyleSheet.create({
18
+ container: {
19
+ flexDirection: 'column',
20
+ width: '100%',
21
+ overflowX: 'auto',
22
+ overflowY: 'auto',
23
+ },
24
+ });
25
+
26
+ export * from './types';
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ /**
4
+ * Props for the [`List`](#list) component.
5
+ * A virtualized vertical container of rows.
6
+ * Typically populated with [`ListItem`](#listitem) children, though any node is accepted.
7
+ */
8
+ export interface ListProps {
9
+ /**
10
+ * The list rows. Usually `<ListItem>` elements.
11
+ */
12
+ children?: ReactNode;
13
+
14
+ /**
15
+ * Optional pull-to-refresh handler.
16
+ * When provided, the list shows the platform-native refresh affordance.
17
+ * The returned promise drives the indicator's visibility.
18
+ *
19
+ * @platform android
20
+ * @platform ios
21
+ */
22
+ onRefresh?: () => Promise<void>;
23
+
24
+ /**
25
+ * Identifier used to locate the component in end-to-end tests.
26
+ */
27
+ testID?: string;
28
+ }