@expo/ui 56.0.9 → 56.0.11

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 (157) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/CLAUDE.md +1 -1
  3. package/android/build.gradle +2 -2
  4. package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +11 -5
  5. package/android/src/main/java/expo/modules/ui/HorizontalPagerView.kt +97 -16
  6. package/android/src/main/java/expo/modules/ui/colors/MaterialColors.kt +2 -0
  7. package/build/State/index.d.ts +6 -0
  8. package/build/State/index.d.ts.map +1 -0
  9. package/build/community/pager-view/PagerView.android.d.ts +7 -0
  10. package/build/community/pager-view/PagerView.android.d.ts.map +1 -0
  11. package/build/community/pager-view/PagerView.d.ts +8 -0
  12. package/build/community/pager-view/PagerView.d.ts.map +1 -0
  13. package/build/community/pager-view/PagerView.ios.d.ts +15 -0
  14. package/build/community/pager-view/PagerView.ios.d.ts.map +1 -0
  15. package/build/community/pager-view/index.d.ts +3 -0
  16. package/build/community/pager-view/index.d.ts.map +1 -0
  17. package/build/community/pager-view/types.d.ts +122 -0
  18. package/build/community/pager-view/types.d.ts.map +1 -0
  19. package/build/community/segmented-control/vendor/SegmentsSeparators.d.ts.map +1 -1
  20. package/build/jetpack-compose/HorizontalPager/index.d.ts +27 -0
  21. package/build/jetpack-compose/HorizontalPager/index.d.ts.map +1 -1
  22. package/build/jetpack-compose/Host/index.d.ts +1 -0
  23. package/build/jetpack-compose/Host/index.d.ts.map +1 -1
  24. package/build/jetpack-compose/LoadingIndicator/index.d.ts +1 -1
  25. package/build/jetpack-compose/LoadingIndicator/index.d.ts.map +1 -1
  26. package/build/jetpack-compose/SyncSwitch/index.d.ts +1 -1
  27. package/build/jetpack-compose/SyncSwitch/index.d.ts.map +1 -1
  28. package/build/jetpack-compose/TextField/index.d.ts +1 -1
  29. package/build/jetpack-compose/TextField/index.d.ts.map +1 -1
  30. package/build/jetpack-compose/index.d.ts +1 -2
  31. package/build/jetpack-compose/index.d.ts.map +1 -1
  32. package/build/swift-ui/Host/index.d.ts +1 -0
  33. package/build/swift-ui/Host/index.d.ts.map +1 -1
  34. package/build/swift-ui/ScrollView/index.d.ts +30 -0
  35. package/build/swift-ui/ScrollView/index.d.ts.map +1 -1
  36. package/build/swift-ui/SecureField/index.d.ts +1 -1
  37. package/build/swift-ui/SecureField/index.d.ts.map +1 -1
  38. package/build/swift-ui/SyncToggle/index.d.ts +1 -1
  39. package/build/swift-ui/SyncToggle/index.d.ts.map +1 -1
  40. package/build/swift-ui/TextField/index.d.ts +1 -1
  41. package/build/swift-ui/TextField/index.d.ts.map +1 -1
  42. package/build/swift-ui/index.d.ts +1 -2
  43. package/build/swift-ui/index.d.ts.map +1 -1
  44. package/build/swift-ui/modifiers/index.d.ts +25 -15
  45. package/build/swift-ui/modifiers/index.d.ts.map +1 -1
  46. package/build/swift-ui/modifiers/scrollObservation.d.ts +52 -0
  47. package/build/swift-ui/modifiers/scrollObservation.d.ts.map +1 -0
  48. package/build/swift-ui/modifiers/scrollPosition.d.ts +1 -1
  49. package/build/swift-ui/modifiers/scrollPosition.d.ts.map +1 -1
  50. package/build/swift-ui/modifiers/symbolEffect.d.ts +1 -1
  51. package/build/swift-ui/modifiers/symbolEffect.d.ts.map +1 -1
  52. package/build/universal/BottomSheet/index.android.d.ts.map +1 -1
  53. package/build/universal/BottomSheet/index.d.ts.map +1 -1
  54. package/build/universal/BottomSheet/index.ios.d.ts.map +1 -1
  55. package/build/universal/Checkbox/index.d.ts.map +1 -1
  56. package/build/universal/Collapsible/index.d.ts.map +1 -1
  57. package/build/universal/ListItem/ListItem.d.ts.map +1 -1
  58. package/build/universal/RNHostView/index.android.d.ts +7 -0
  59. package/build/universal/RNHostView/index.android.d.ts.map +1 -0
  60. package/build/universal/RNHostView/index.d.ts +7 -0
  61. package/build/universal/RNHostView/index.d.ts.map +1 -0
  62. package/build/universal/RNHostView/index.ios.d.ts +7 -0
  63. package/build/universal/RNHostView/index.ios.d.ts.map +1 -0
  64. package/build/universal/RNHostView/types.d.ts +23 -0
  65. package/build/universal/RNHostView/types.d.ts.map +1 -0
  66. package/build/universal/Switch/index.d.ts.map +1 -1
  67. package/build/universal/TextInput/index.d.ts.map +1 -1
  68. package/build/universal/index.d.ts +1 -0
  69. package/build/universal/index.d.ts.map +1 -1
  70. package/expo-module.config.json +1 -1
  71. package/ios/ExpoUIModule.swift +6 -3
  72. package/ios/HostView.swift +21 -18
  73. package/ios/Modifiers/FontModifier.swift +72 -20
  74. package/ios/Modifiers/ScrollObservationModifiers.swift +107 -0
  75. package/ios/Modifiers/ViewModifierRegistry.swift +8 -0
  76. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.9/expo.modules.ui-56.0.9-sources.jar → 56.0.11/expo.modules.ui-56.0.11-sources.jar} +0 -0
  77. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11-sources.jar.md5 +1 -0
  78. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11-sources.jar.sha1 +1 -0
  79. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11-sources.jar.sha256 +1 -0
  80. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11-sources.jar.sha512 +1 -0
  81. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11.aar +0 -0
  82. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11.aar.md5 +1 -0
  83. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11.aar.sha1 +1 -0
  84. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11.aar.sha256 +1 -0
  85. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11.aar.sha512 +1 -0
  86. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.9/expo.modules.ui-56.0.9.module → 56.0.11/expo.modules.ui-56.0.11.module} +22 -22
  87. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11.module.md5 +1 -0
  88. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11.module.sha1 +1 -0
  89. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11.module.sha256 +1 -0
  90. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11.module.sha512 +1 -0
  91. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.9/expo.modules.ui-56.0.9.pom → 56.0.11/expo.modules.ui-56.0.11.pom} +1 -1
  92. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11.pom.md5 +1 -0
  93. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11.pom.sha1 +1 -0
  94. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11.pom.sha256 +1 -0
  95. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.11/expo.modules.ui-56.0.11.pom.sha512 +1 -0
  96. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml +4 -4
  97. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.md5 +1 -1
  98. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha1 +1 -1
  99. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha256 +1 -1
  100. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha512 +1 -1
  101. package/package.json +7 -3
  102. package/src/State/index.ts +10 -0
  103. package/src/State/useNativeState.ts +8 -0
  104. package/src/community/pager-view/PagerView.android.tsx +224 -0
  105. package/src/community/pager-view/PagerView.ios.tsx +273 -0
  106. package/src/community/pager-view/PagerView.tsx +14 -0
  107. package/src/community/pager-view/index.tsx +13 -0
  108. package/src/community/pager-view/types.tsx +131 -0
  109. package/src/community/picker/Picker.android.tsx +1 -1
  110. package/src/community/segmented-control/vendor/SegmentsSeparators.tsx +3 -6
  111. package/src/jetpack-compose/HorizontalPager/index.tsx +89 -18
  112. package/src/jetpack-compose/Host/index.tsx +1 -0
  113. package/src/jetpack-compose/LoadingIndicator/index.tsx +1 -2
  114. package/src/jetpack-compose/SyncSwitch/index.tsx +1 -3
  115. package/src/jetpack-compose/TextField/index.tsx +1 -4
  116. package/src/jetpack-compose/index.ts +1 -2
  117. package/src/swift-ui/Host/index.tsx +1 -0
  118. package/src/swift-ui/ScrollView/index.tsx +33 -0
  119. package/src/swift-ui/SecureField/index.tsx +1 -4
  120. package/src/swift-ui/SyncToggle/index.tsx +1 -3
  121. package/src/swift-ui/TextField/index.tsx +1 -4
  122. package/src/swift-ui/index.tsx +1 -3
  123. package/src/swift-ui/modifiers/index.ts +37 -14
  124. package/src/swift-ui/modifiers/scrollObservation.ts +80 -0
  125. package/src/swift-ui/modifiers/scrollPosition.ts +1 -2
  126. package/src/swift-ui/modifiers/symbolEffect.ts +1 -2
  127. package/src/swift-ui/withAnimation.ts +1 -1
  128. package/src/universal/BottomSheet/index.android.tsx +33 -10
  129. package/src/universal/BottomSheet/index.ios.tsx +12 -10
  130. package/src/universal/BottomSheet/index.tsx +3 -0
  131. package/src/universal/Checkbox/index.tsx +14 -2
  132. package/src/universal/Collapsible/index.tsx +17 -4
  133. package/src/universal/ListItem/ListItem.tsx +7 -2
  134. package/src/universal/RNHostView/index.android.tsx +33 -0
  135. package/src/universal/RNHostView/index.ios.tsx +12 -0
  136. package/src/universal/RNHostView/index.tsx +39 -0
  137. package/src/universal/RNHostView/types.ts +25 -0
  138. package/src/universal/Switch/index.tsx +7 -2
  139. package/src/universal/TextInput/index.tsx +12 -2
  140. package/src/universal/index.ts +1 -0
  141. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.md5 +0 -1
  142. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha1 +0 -1
  143. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha256 +0 -1
  144. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha512 +0 -1
  145. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar +0 -0
  146. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.md5 +0 -1
  147. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha1 +0 -1
  148. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha256 +0 -1
  149. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha512 +0 -1
  150. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.md5 +0 -1
  151. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha1 +0 -1
  152. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha256 +0 -1
  153. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha512 +0 -1
  154. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.md5 +0 -1
  155. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha1 +0 -1
  156. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha256 +0 -1
  157. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha512 +0 -1
@@ -1,6 +1,7 @@
1
1
  import { requireNativeView } from 'expo';
2
2
  import type { Ref } from 'react';
3
3
 
4
+ import { getStateId, useWorkletProp, worklets } from '../../State';
4
5
  import { type ModifierConfig, type ViewEvent } from '../../types';
5
6
  import { type PaddingValuesRecord } from '../Carousel';
6
7
  import { createViewModifierEventListener } from '../modifiers/utils';
@@ -19,6 +20,12 @@ export type HorizontalPagerHandle = {
19
20
  scrollToPage: (page: number) => Promise<void>;
20
21
  };
21
22
 
23
+ /**
24
+ * Kind of drag interaction reported by `onDragInteraction`. Mirrors Compose's
25
+ * `DragInteraction.Start` / `DragInteraction.Stop` / `DragInteraction.Cancel`.
26
+ */
27
+ export type HorizontalPagerDragInteraction = 'start' | 'stop' | 'cancel';
28
+
22
29
  export type HorizontalPagerProps = {
23
30
  /**
24
31
  * Imperative handle for programmatic navigation. Mirrors the methods on
@@ -42,6 +49,28 @@ export type HorizontalPagerProps = {
42
49
  * swipe or programmatic scroll has fully settled.
43
50
  */
44
51
  onSettledPageChange?: (page: number) => void;
52
+ /**
53
+ * Fires continuously while a swipe is in progress. Mirrors Compose's
54
+ * `PagerState.currentPage` and `currentPageOffsetFraction` — the latter is
55
+ * the signed fractional offset from `currentPage`, in the `[-0.5, 0.5]` range.
56
+ *
57
+ * If the callback is marked with the `'worklet'` directive, it runs
58
+ * synchronously on the UI thread; otherwise it is delivered asynchronously
59
+ * as a regular JS event.
60
+ */
61
+ onPageScroll?: (currentPage: number, currentPageOffsetFraction: number) => void;
62
+ /**
63
+ * Fires when Compose's `PagerState.isScrollInProgress` toggles — true while
64
+ * the pager is being dragged or animating to a snap target, false once it
65
+ * has settled.
66
+ */
67
+ onScrollInProgressChange?: (isScrollInProgress: boolean) => void;
68
+ /**
69
+ * Fires for each drag interaction emitted by `PagerState.interactionSource`.
70
+ * Combine with `onScrollInProgressChange` to distinguish user dragging from
71
+ * fling/snap-settling.
72
+ */
73
+ onDragInteraction?: (kind: HorizontalPagerDragInteraction) => void;
45
74
  /**
46
75
  * Spacing between pages in dp.
47
76
  * @default 0
@@ -79,35 +108,77 @@ export type HorizontalPagerProps = {
79
108
 
80
109
  type NativeHorizontalPagerProps = Omit<
81
110
  HorizontalPagerProps,
82
- 'onCurrentPageChange' | 'onSettledPageChange'
111
+ | 'onCurrentPageChange'
112
+ | 'onSettledPageChange'
113
+ | 'onPageScroll'
114
+ | 'onScrollInProgressChange'
115
+ | 'onDragInteraction'
83
116
  > &
84
117
  ViewEvent<'onCurrentPageChange', { position: number }> &
85
- ViewEvent<'onSettledPageChange', { position: number }>;
118
+ ViewEvent<'onSettledPageChange', { position: number }> &
119
+ ViewEvent<'onPageScroll', { currentPage: number; currentPageOffsetFraction: number }> &
120
+ ViewEvent<'onScrollInProgressChange', { isScrollInProgress: boolean }> &
121
+ ViewEvent<'onDragInteraction', { kind: HorizontalPagerDragInteraction }> & {
122
+ onPageScrollSync?: number | null;
123
+ };
86
124
 
87
125
  const NativeView: React.ComponentType<NativeHorizontalPagerProps> = requireNativeView(
88
126
  'ExpoUI',
89
127
  'HorizontalPagerView'
90
128
  );
91
129
 
92
- function transformProps(props: HorizontalPagerProps): NativeHorizontalPagerProps {
93
- const { modifiers, onCurrentPageChange, onSettledPageChange, ...restProps } = props;
94
- return {
95
- modifiers,
96
- ...(modifiers ? createViewModifierEventListener(modifiers) : undefined),
97
- ...restProps,
98
- onCurrentPageChange: onCurrentPageChange
99
- ? ({ nativeEvent: { position } }) => onCurrentPageChange(position)
100
- : undefined,
101
- onSettledPageChange: onSettledPageChange
102
- ? ({ nativeEvent: { position } }) => onSettledPageChange(position)
103
- : undefined,
104
- };
105
- }
106
-
107
130
  /**
108
131
  * A horizontally scrolling pager that snaps to individual pages,
109
132
  * matching Compose's `HorizontalPager`.
110
133
  */
111
134
  export function HorizontalPager(props: HorizontalPagerProps) {
112
- return <NativeView {...transformProps(props)} />;
135
+ const {
136
+ modifiers,
137
+ onCurrentPageChange,
138
+ onSettledPageChange,
139
+ onPageScroll,
140
+ onScrollInProgressChange,
141
+ onDragInteraction,
142
+ ...restProps
143
+ } = props;
144
+
145
+ const isPageScrollWorklet = !!onPageScroll && !!worklets?.isWorkletFunction?.(onPageScroll);
146
+ const pageScrollWorkletCallback = useWorkletProp(
147
+ isPageScrollWorklet ? onPageScroll : undefined,
148
+ 'onPageScroll'
149
+ );
150
+
151
+ return (
152
+ <NativeView
153
+ {...restProps}
154
+ modifiers={modifiers}
155
+ {...(modifiers ? createViewModifierEventListener(modifiers) : undefined)}
156
+ onCurrentPageChange={
157
+ onCurrentPageChange
158
+ ? ({ nativeEvent: { position } }) => onCurrentPageChange(position)
159
+ : undefined
160
+ }
161
+ onSettledPageChange={
162
+ onSettledPageChange
163
+ ? ({ nativeEvent: { position } }) => onSettledPageChange(position)
164
+ : undefined
165
+ }
166
+ onPageScroll={
167
+ !isPageScrollWorklet && onPageScroll
168
+ ? ({ nativeEvent: { currentPage, currentPageOffsetFraction } }) =>
169
+ onPageScroll(currentPage, currentPageOffsetFraction)
170
+ : undefined
171
+ }
172
+ onPageScrollSync={getStateId(pageScrollWorkletCallback)}
173
+ onScrollInProgressChange={
174
+ onScrollInProgressChange
175
+ ? ({ nativeEvent: { isScrollInProgress } }) =>
176
+ onScrollInProgressChange(isScrollInProgress)
177
+ : undefined
178
+ }
179
+ onDragInteraction={
180
+ onDragInteraction ? ({ nativeEvent: { kind } }) => onDragInteraction(kind) : undefined
181
+ }
182
+ />
183
+ );
113
184
  }
@@ -64,6 +64,7 @@ export type HostProps = {
64
64
 
65
65
  children: React.ReactNode;
66
66
  style?: StyleProp<ViewStyle>;
67
+ pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto';
67
68
  } & PrimitiveBaseProps;
68
69
 
69
70
  type NativeHostProps = Omit<HostProps, 'colorScheme'> & {
@@ -1,8 +1,7 @@
1
1
  import { requireNativeView } from 'expo';
2
2
  import { type ColorValue } from 'react-native';
3
3
 
4
- import type { ObservableState } from '../../State/useNativeState';
5
- import { getStateId } from '../../State/utils';
4
+ import { getStateId, type ObservableState } from '../../State';
6
5
  import { type ModifierConfig } from '../../types';
7
6
  import { createViewModifierEventListener } from '../modifiers/utils';
8
7
 
@@ -1,8 +1,6 @@
1
1
  import { requireNativeView } from 'expo';
2
2
 
3
- import { type ObservableState } from '../../State/useNativeState';
4
- import { useWorkletProp } from '../../State/useWorkletProp';
5
- import { getStateId } from '../../State/utils';
3
+ import { getStateId, type ObservableState, useWorkletProp } from '../../State';
6
4
  import { type ModifierConfig } from '../../types';
7
5
  import { createViewModifierEventListener } from '../modifiers/utils';
8
6
 
@@ -2,10 +2,7 @@ import { requireNativeView } from 'expo';
2
2
  import type { Ref } from 'react';
3
3
  import type { ColorValue } from 'react-native';
4
4
 
5
- import { worklets } from '../../State/optionalWorklets';
6
- import type { ObservableState } from '../../State/useNativeState';
7
- import { useWorkletProp } from '../../State/useWorkletProp';
8
- import { getStateId } from '../../State/utils';
5
+ import { getStateId, type ObservableState, useWorkletProp, worklets } from '../../State';
9
6
  import type { ModifierConfig, ViewEvent } from '../../types';
10
7
  import { Slot } from '../SlotView';
11
8
  import { createViewModifierEventListener } from '../modifiers/utils';
@@ -1,4 +1,3 @@
1
- import '../State/index.fx';
2
1
  import './MaterialSymbolsAssetsTransformer.fx';
3
2
 
4
3
  export * from './AlertDialog';
@@ -65,6 +64,6 @@ export * from './Box';
65
64
  export * from './Row';
66
65
  export * from './Column';
67
66
  export * from './FlowRow';
68
- export { useNativeState } from '../State/useNativeState';
67
+ export { useNativeState } from '../State';
69
68
  export type { ViewEvent } from '../types';
70
69
  export type { PrimitiveBaseProps } from './layout-types';
@@ -45,6 +45,7 @@ export type HostProps = {
45
45
 
46
46
  children: React.ReactNode;
47
47
  style?: StyleProp<ViewStyle>;
48
+ pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto';
48
49
  } & CommonViewModifierProps;
49
50
 
50
51
  const HostNativeView: React.ComponentType<
@@ -3,6 +3,32 @@ import { requireNativeView } from 'expo';
3
3
  import { createViewModifierEventListener } from '../modifiers/utils';
4
4
  import { type CommonViewModifierProps } from '../types';
5
5
 
6
+ /**
7
+ * Scroll phase emitted by the `onScrollPhaseChange(...)` modifier. Mirrors
8
+ * SwiftUI's `ScrollPhase` (iOS 18+).
9
+ */
10
+ export type ScrollPhase = 'idle' | 'tracking' | 'interacting' | 'animating' | 'decelerating';
11
+
12
+ /**
13
+ * Snapshot of a `ScrollView`'s scroll geometry, emitted by the
14
+ * `useScrollGeometryChange(...)` and `onScrollPhaseChange(...)` modifiers
15
+ * (iOS 18+).
16
+ */
17
+ export type ScrollGeometry = {
18
+ /** Horizontal content offset, in points. */
19
+ contentOffsetX: number;
20
+ /** Vertical content offset, in points. */
21
+ contentOffsetY: number;
22
+ /** Width of the visible scroll container, in points. */
23
+ containerWidth: number;
24
+ /** Height of the visible scroll container, in points. */
25
+ containerHeight: number;
26
+ /** Total width of the scrollable content, in points. */
27
+ contentWidth: number;
28
+ /** Total height of the scrollable content, in points. */
29
+ contentHeight: number;
30
+ };
31
+
6
32
  export type ScrollViewProps = {
7
33
  children: React.ReactNode;
8
34
  /**
@@ -23,8 +49,15 @@ const ScrollViewNativeView: React.ComponentType<ScrollViewProps> = requireNative
23
49
  'ScrollViewComponent'
24
50
  );
25
51
 
52
+ /**
53
+ * SwiftUI `ScrollView` wrapper. To control scroll position, pair this with the
54
+ * `scrollPosition(state, { onChange })` modifier and a `useNativeState`-backed
55
+ * id. Write `state.value = targetId` for an instant scroll, or wrap the write
56
+ * in `withAnimation(...)` from `@expo/ui/swift-ui` for an animated one.
57
+ */
26
58
  export function ScrollView(props: ScrollViewProps) {
27
59
  const { modifiers, ...restProps } = props;
60
+
28
61
  return (
29
62
  <ScrollViewNativeView
30
63
  modifiers={modifiers}
@@ -1,10 +1,7 @@
1
1
  import { requireNativeView } from 'expo';
2
2
  import type { Ref } from 'react';
3
3
 
4
- import { worklets } from '../../State/optionalWorklets';
5
- import type { ObservableState } from '../../State/useNativeState';
6
- import { useWorkletProp } from '../../State/useWorkletProp';
7
- import { getStateId } from '../../State/utils';
4
+ import { getStateId, type ObservableState, useWorkletProp, worklets } from '../../State';
8
5
  import type { ViewEvent } from '../../types';
9
6
  import { Slot } from '../SlotView';
10
7
  import { createViewModifierEventListener } from '../modifiers/utils';
@@ -1,9 +1,7 @@
1
1
  import { requireNativeView } from 'expo';
2
2
  import { type SFSymbol } from 'sf-symbols-typescript';
3
3
 
4
- import { type ObservableState } from '../../State/useNativeState';
5
- import { useWorkletProp } from '../../State/useWorkletProp';
6
- import { getStateId } from '../../State/utils';
4
+ import { getStateId, type ObservableState, useWorkletProp } from '../../State';
7
5
  import { createViewModifierEventListener } from '../modifiers/utils';
8
6
  import { type CommonViewModifierProps } from '../types';
9
7
 
@@ -1,10 +1,7 @@
1
1
  import { requireNativeView } from 'expo';
2
2
  import type { Ref } from 'react';
3
3
 
4
- import { worklets } from '../../State/optionalWorklets';
5
- import type { ObservableState } from '../../State/useNativeState';
6
- import { useWorkletProp } from '../../State/useWorkletProp';
7
- import { getStateId } from '../../State/utils';
4
+ import { getStateId, type ObservableState, useWorkletProp, worklets } from '../../State';
8
5
  import type { ViewEvent } from '../../types';
9
6
  import { Slot } from '../SlotView';
10
7
  import { createViewModifierEventListener } from '../modifiers/utils';
@@ -1,5 +1,3 @@
1
- import '../State/index.fx';
2
-
3
1
  export * from './AccessoryWidgetBackground';
4
2
  export * from './Alert';
5
3
  export * from './BottomSheet';
@@ -36,7 +34,7 @@ export * from './Spacer';
36
34
  export * from './Stepper';
37
35
  export * from './SwipeActions';
38
36
  export * from './Text';
39
- export { useNativeState } from '../State/useNativeState';
37
+ export { useNativeState } from '../State';
40
38
  export { withAnimation, type WithAnimationCompletionCriteria } from './withAnimation';
41
39
  export * from './SyncToggle';
42
40
  export * from './TabView';
@@ -19,6 +19,7 @@ import { datePickerStyle } from './datePickerStyle';
19
19
  import { environment } from './environment';
20
20
  import { gaugeStyle } from './gaugeStyle';
21
21
  import { progressViewStyle } from './progressViewStyle';
22
+ import { onScrollPhaseChange, useScrollGeometryChange } from './scrollObservation';
22
23
  import { id, scrollPosition } from './scrollPosition';
23
24
  import { symbolEffect } from './symbolEffect';
24
25
  import type { Color } from './types';
@@ -981,30 +982,33 @@ export const listSectionMargins = (params?: {
981
982
 
982
983
  /**
983
984
  * Sets the font properties of a view.
984
- * Supports both custom font families and system fonts with weight and design options.
985
985
  *
986
- * @param params - The font configuration. When `family` is provided, it uses Font.custom().
987
- * When `family` is not provided, it uses Font.system() with the specified weight and design.
986
+ * Pass `textStyle` to scale with the user's Dynamic Type setting. Combine
987
+ * it with `family` to scale a custom font.
988
988
  *
989
989
  * @example
990
990
  * ```tsx
991
- * // Custom font family
992
- * <Text modifiers={[font({ family: 'Helvetica', size: 18 })]}>Custom Font Text</Text>
991
+ * // Scales with Dynamic Type
992
+ * <Text modifiers={[font({ textStyle: 'largeTitle', weight: 'bold' })]}>Hello</Text>
993
993
  *
994
- * // System font with weight and design
995
- * <Text modifiers={[font({ weight: 'bold', design: 'rounded', size: 16 })]}>System Font Text</Text>
994
+ * // Custom font that scales relative to the body text style
995
+ * <Text modifiers={[font({ textStyle: 'body', family: 'Helvetica', size: 18 })]}>Hi</Text>
996
+ *
997
+ * // Fixed-size system font (no Dynamic Type scaling)
998
+ * <Text modifiers={[font({ weight: 'bold', design: 'rounded', size: 16 })]}>Static</Text>
996
999
  * ```
997
- * @see Official [SwiftUI documentation for `custom(_:size:)`](https://developer.apple.com/documentation/swiftui/font/custom(_:size:)) and Official [SwiftUI documentation for `system(size:weight:design:)`](https://developer.apple.com/documentation/swiftui/font/system(size:weight:design:)).
1000
+ * @see Official SwiftUI documentation for [`system(_:design:weight:)`](https://developer.apple.com/documentation/swiftui/font/system(_:design:weight:)), and [`custom(_:size:relativeTo:)`](https://developer.apple.com/documentation/swiftui/font/custom(_:size:relativeto:)).
998
1001
  */
999
1002
  export const font = (params: {
1003
+ /** Custom font family name. */
1004
+ family?: string;
1000
1005
  /**
1001
- * Custom font family name.
1002
- * If provided, uses `Font.custom()`.
1006
+ * Font size in points. Ignored when only `textStyle` is set.
1007
+ *
1008
+ * @default 17
1003
1009
  */
1004
- family?: string;
1005
- /** Font size in points. */
1006
1010
  size?: number;
1007
- /** Font weight for system fonts. */
1011
+ /** Font weight. */
1008
1012
  weight?:
1009
1013
  | 'ultraLight'
1010
1014
  | 'thin'
@@ -1015,8 +1019,24 @@ export const font = (params: {
1015
1019
  | 'bold'
1016
1020
  | 'heavy'
1017
1021
  | 'black';
1018
- /** Font design for system fonts */
1022
+ /** Font design. Applied when no `family` is provided. `Font.custom` always uses the embedded font's own design. */
1019
1023
  design?: 'default' | 'rounded' | 'serif' | 'monospaced';
1024
+ /**
1025
+ * SwiftUI text style. When set, the resulting font scales with the user's
1026
+ * Dynamic Type setting.
1027
+ */
1028
+ textStyle?:
1029
+ | 'largeTitle'
1030
+ | 'title'
1031
+ | 'title2'
1032
+ | 'title3'
1033
+ | 'headline'
1034
+ | 'subheadline'
1035
+ | 'body'
1036
+ | 'callout'
1037
+ | 'footnote'
1038
+ | 'caption'
1039
+ | 'caption2';
1020
1040
  }) => createModifier('font', params);
1021
1041
  /**
1022
1042
  * Asks grid layouts not to offer the view extra size in the specified axes.
@@ -1339,6 +1359,8 @@ export type BuiltInModifier =
1339
1359
  | ReturnType<typeof scrollTargetLayout>
1340
1360
  | ReturnType<typeof id>
1341
1361
  | ReturnType<typeof scrollPosition>
1362
+ | ReturnType<typeof onScrollPhaseChange>
1363
+ | NonNullable<ReturnType<typeof useScrollGeometryChange>>
1342
1364
  | ReturnType<typeof moveDisabled>
1343
1365
  | ReturnType<typeof deleteDisabled>
1344
1366
  | ReturnType<typeof environment>
@@ -1429,6 +1451,7 @@ export * from './presentationModifiers';
1429
1451
  export * from './environment';
1430
1452
  export * from './scrollPosition';
1431
1453
  export * from './symbolEffect';
1454
+ export * from './scrollObservation';
1432
1455
  export * from './widgets';
1433
1456
  export type {
1434
1457
  TimingAnimationParams,
@@ -0,0 +1,80 @@
1
+ import { getStateId, useWorkletProp, worklets } from '../../State';
2
+ import type { ScrollGeometry, ScrollPhase } from '../ScrollView';
3
+ import { createModifier, createModifierWithEventListener } from './createModifier';
4
+
5
+ /**
6
+ * Fires when the scroll geometry changes — i.e., on every scroll update and
7
+ * on container/content size changes. Use to drive continuous progress UI
8
+ * such as page indicators, parallax, or fractional offsets.
9
+ *
10
+ * If the callback is marked with the `'worklet'` directive, it runs
11
+ * synchronously on the UI thread (no JS-thread round-trip); otherwise it is
12
+ * delivered asynchronously as a regular JS event. Both paths share the same
13
+ * native modifier — the worklet variant is automatically wrapped in a
14
+ * `WorkletCallback` shared object whose lifetime is managed by the hook.
15
+ *
16
+ * This is a hook because the worklet path requires a stable shared-object
17
+ * reference across renders. Call it at the top of your component, then
18
+ * include the returned modifier in your `modifiers` array.
19
+ *
20
+ * Apply to a SwiftUI `ScrollView` (and other scrollable views). On iOS below
21
+ * 18.0 the modifier is a no-op.
22
+ *
23
+ * @platform ios 18.0+
24
+ * @platform tvos 18.0+
25
+ *
26
+ * @see Official [SwiftUI documentation](https://developer.apple.com/documentation/swiftui/view/onscrollgeometrychange(for:of:_:)).
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * const geometryModifier = useScrollGeometryChange((g) => {
31
+ * 'worklet';
32
+ * progress.value = g.contentOffsetX / g.containerWidth;
33
+ * });
34
+ *
35
+ * <ScrollView modifiers={[geometryModifier]} />
36
+ * ```
37
+ */
38
+ export function useScrollGeometryChange(callback?: (geometry: ScrollGeometry) => void) {
39
+ const isWorklet = !!callback && !!worklets?.isWorkletFunction?.(callback);
40
+ const workletCallback = useWorkletProp(
41
+ isWorklet ? callback : undefined,
42
+ 'onScrollGeometryChange'
43
+ );
44
+
45
+ if (!callback) {
46
+ return null;
47
+ }
48
+ if (isWorklet && workletCallback) {
49
+ return createModifier('onScrollGeometryChange', {
50
+ workletCallback: getStateId(workletCallback),
51
+ });
52
+ }
53
+ return createModifierWithEventListener('onScrollGeometryChange', (event: ScrollGeometry) =>
54
+ callback(event)
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Fires when SwiftUI's scroll phase changes (e.g., the user begins dragging,
60
+ * the scroll view starts decelerating, or scrolling settles to idle). The
61
+ * second argument is the scroll geometry sampled at the phase transition,
62
+ * useful for reading the final offset on settle without subscribing to
63
+ * per-frame `onScrollGeometryChange`.
64
+ *
65
+ * Apply to a SwiftUI `ScrollView` (and other scrollable views). On iOS below
66
+ * 18.0 the modifier is a no-op.
67
+ *
68
+ * @platform ios 18.0+
69
+ * @platform tvos 18.0+
70
+ *
71
+ * @see Official [SwiftUI documentation](https://developer.apple.com/documentation/swiftui/view/onscrollphasechange(_:)).
72
+ */
73
+ export const onScrollPhaseChange = (
74
+ callback: (phase: ScrollPhase, geometry: ScrollGeometry) => void
75
+ ) =>
76
+ createModifierWithEventListener(
77
+ 'onScrollPhaseChange',
78
+ (event: { phase: ScrollPhase; geometry: ScrollGeometry }) =>
79
+ callback(event.phase, event.geometry)
80
+ );
@@ -1,7 +1,6 @@
1
1
  import type { UnitPointValue } from '.';
2
2
  import { createModifier, createModifierWithEventListener } from './createModifier';
3
- import { type ObservableState } from '../../State/useNativeState';
4
- import { getStateId } from '../../State/utils';
3
+ import { getStateId, type ObservableState } from '../../State';
5
4
 
6
5
  /**
7
6
  * Attaches a stable identifier to a view so it can be referenced by scroll target bindings.
@@ -1,6 +1,5 @@
1
1
  import { createModifier } from './createModifier';
2
- import { type ObservableState } from '../../State/useNativeState';
3
- import { getStateId } from '../../State/utils';
2
+ import { getStateId, type ObservableState } from '../../State';
4
3
 
5
4
  // https://developer.apple.com/documentation/symbols/appearsymboleffect
6
5
  type AppearSymbolEffect = {
@@ -1,6 +1,6 @@
1
1
  import { requireNativeModule } from 'expo';
2
2
 
3
- import { worklets } from '../State/optionalWorklets';
3
+ import { worklets } from '../State';
4
4
  import { VALUE_SYMBOL } from './modifiers/animation/constants';
5
5
  import type { AnimationObject, ChainableAnimationType } from './modifiers/animation/types';
6
6
 
@@ -1,10 +1,11 @@
1
- import { Column, ModalBottomSheet } from '@expo/ui/jetpack-compose';
1
+ import { Column, Host, ModalBottomSheet, type ModalBottomSheetRef } from '@expo/ui/jetpack-compose';
2
2
  import {
3
3
  fillMaxHeight,
4
4
  padding,
5
5
  testID as testIDModifier,
6
6
  type ModifierConfig,
7
7
  } from '@expo/ui/jetpack-compose/modifiers';
8
+ import { useEffect, useRef, useState } from 'react';
8
9
 
9
10
  import type { BottomSheetProps, SnapPoint } from './types';
10
11
 
@@ -38,21 +39,43 @@ export function BottomSheet({
38
39
  testID,
39
40
  modifiers,
40
41
  }: BottomSheetProps) {
41
- if (!isPresented) return null;
42
+ const sheetRef = useRef<ModalBottomSheetRef>(null);
43
+ const [mount, setMount] = useState(isPresented);
44
+
45
+ useEffect(() => {
46
+ if (isPresented) {
47
+ setMount(true);
48
+ return;
49
+ }
50
+ let cancelled = false;
51
+ sheetRef.current?.hide().then(() => {
52
+ if (!cancelled) setMount(false);
53
+ });
54
+ return () => {
55
+ cancelled = true;
56
+ };
57
+ }, [isPresented]);
58
+
59
+ if (!mount) {
60
+ return null;
61
+ }
42
62
 
43
63
  const contentModifiers: ModifierConfig[] = [padding(16, showDragIndicator ? 0 : 16, 16, 0)];
44
64
  if (shouldFillMaxHeight(snapPoints)) contentModifiers.push(fillMaxHeight());
45
65
  if (testID) contentModifiers.push(testIDModifier(testID));
46
66
 
47
67
  return (
48
- <ModalBottomSheet
49
- onDismissRequest={onDismiss}
50
- showDragHandle={showDragIndicator}
51
- skipPartiallyExpanded={shouldSkipPartiallyExpanded(snapPoints)}
52
- modifiers={modifiers}>
53
- {/* When the drag handle is hidden, add top padding so content doesn't crop against the top edge of the sheet. */}
54
- <Column modifiers={contentModifiers}>{children}</Column>
55
- </ModalBottomSheet>
68
+ <Host style={{ position: 'absolute' }} pointerEvents="none">
69
+ <ModalBottomSheet
70
+ ref={sheetRef}
71
+ onDismissRequest={onDismiss}
72
+ showDragHandle={showDragIndicator}
73
+ skipPartiallyExpanded={shouldSkipPartiallyExpanded(snapPoints)}
74
+ modifiers={modifiers}>
75
+ {/* When the drag handle is hidden, add top padding so content doesn't crop against the top edge of the sheet. */}
76
+ <Column modifiers={contentModifiers}>{children}</Column>
77
+ </ModalBottomSheet>
78
+ </Host>
56
79
  );
57
80
  }
58
81
 
@@ -1,4 +1,4 @@
1
- import { BottomSheet as SwiftUIBottomSheet, Group } from '@expo/ui/swift-ui';
1
+ import { BottomSheet as SwiftUIBottomSheet, Group, Host } from '@expo/ui/swift-ui';
2
2
  import {
3
3
  frame,
4
4
  padding,
@@ -38,15 +38,17 @@ export function BottomSheet({
38
38
  }
39
39
 
40
40
  return (
41
- <SwiftUIBottomSheet
42
- isPresented={isPresented}
43
- onIsPresentedChange={(presented) => {
44
- if (!presented) onDismiss();
45
- }}
46
- fitToContents={!snapPoints || snapPoints.length === 0}
47
- testID={testID}>
48
- <Group modifiers={presentationModifiers}>{children}</Group>
49
- </SwiftUIBottomSheet>
41
+ <Host style={{ position: 'absolute' }} pointerEvents="none">
42
+ <SwiftUIBottomSheet
43
+ isPresented={isPresented}
44
+ onIsPresentedChange={(presented) => {
45
+ if (!presented) onDismiss();
46
+ }}
47
+ fitToContents={!snapPoints || snapPoints.length === 0}
48
+ testID={testID}>
49
+ <Group modifiers={presentationModifiers}>{children}</Group>
50
+ </SwiftUIBottomSheet>
51
+ </Host>
50
52
  );
51
53
  }
52
54
 
@@ -1,3 +1,4 @@
1
+ import { useColorScheme } from 'react-native';
1
2
  import { Drawer } from 'vaul';
2
3
 
3
4
  import type { BottomSheetProps, SnapPoint } from './types';
@@ -33,6 +34,7 @@ export function BottomSheet({
33
34
  snapPoints,
34
35
  testID,
35
36
  }: BottomSheetProps) {
37
+ const isDark = useColorScheme() === 'dark';
36
38
  const vaulSnapPoints = snapPoints?.length ? snapPoints.map(snapPointToVaul) : undefined;
37
39
  const hasSnapPoints = vaulSnapPoints != null;
38
40
 
@@ -48,6 +50,7 @@ export function BottomSheet({
48
50
  <Drawer.Content
49
51
  style={{
50
52
  ...contentStyle,
53
+ ...(isDark && { backgroundColor: '#000' }),
51
54
  // Snap-points mode: vaul translates the drawer by `viewport - snapHeight`.
52
55
  // The drawer has to fill the viewport or it gets pushed off-screen.
53
56
  ...(hasSnapPoints ? snapPointContentStyle : noSnapPointContentStyle),