@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
@@ -0,0 +1,224 @@
1
+ import {
2
+ Children,
3
+ isValidElement,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type ReactElement,
10
+ } from 'react';
11
+ import { StyleSheet } from 'react-native';
12
+
13
+ import { wrapNativeEvent, type PagerViewProps } from './types';
14
+ import { worklets } from '../../State';
15
+ import { HorizontalPager, type HorizontalPagerHandle } from '../../jetpack-compose/HorizontalPager';
16
+ import { Host } from '../../jetpack-compose/Host';
17
+ import { RNHostView } from '../../jetpack-compose/RNHostView';
18
+ import { type BuiltinShape, Shapes, clip, fillMaxSize } from '../../jetpack-compose/modifiers';
19
+
20
+ /**
21
+ * Drop-in replacement for `react-native-pager-view` on Android.
22
+ * Renders a Jetpack Compose `HorizontalPager`.
23
+ */
24
+ export function PagerView(props: PagerViewProps) {
25
+ const {
26
+ ref,
27
+ initialPage = 0,
28
+ scrollEnabled = true,
29
+ pageMargin,
30
+ offscreenPageLimit,
31
+ layoutDirection,
32
+ onPageScroll,
33
+ onPageScrollStateChanged,
34
+ onPageSelected,
35
+ children,
36
+ style,
37
+ ...passthrough
38
+ } = props;
39
+
40
+ const pagerRef = useRef<HorizontalPagerHandle>(null);
41
+ const [scrollEnabledState, setScrollEnabledState] = useState(scrollEnabled);
42
+ useEffect(() => {
43
+ setScrollEnabledState(scrollEnabled);
44
+ }, [scrollEnabled]);
45
+
46
+ // Synthesize pager-view's `idle | dragging | settling` from Compose's raw
47
+ // signals: `isScrollInProgress` (drag or snap-animation in flight) plus
48
+ // drag interactions (start/stop/cancel).
49
+ const isScrollInProgressRef = useRef(false);
50
+ const isDraggingRef = useRef(false);
51
+ const lastEmittedScrollStateRef = useRef<'idle' | 'dragging' | 'settling' | null>(null);
52
+ const emitPageScrollStateIfChanged = (state: 'idle' | 'dragging' | 'settling') => {
53
+ if (state === lastEmittedScrollStateRef.current) return;
54
+ lastEmittedScrollStateRef.current = state;
55
+ onPageScrollStateChanged?.(wrapNativeEvent({ pageScrollState: state }));
56
+ };
57
+
58
+ useImperativeHandle(
59
+ ref,
60
+ () => ({
61
+ setPage: (page: number) => {
62
+ pagerRef.current?.animateScrollToPage(page);
63
+ },
64
+ setPageWithoutAnimation: (page: number) => {
65
+ pagerRef.current?.scrollToPage(page);
66
+ },
67
+ setScrollEnabled: setScrollEnabledState,
68
+ }),
69
+ []
70
+ );
71
+
72
+ const pages = Children.toArray(children)
73
+ .filter((child): child is ReactElement => isValidElement(child))
74
+ .map((child, index) => (
75
+ <RNHostView key={child.key ?? String(index)} modifiers={[fillMaxSize()]}>
76
+ {child}
77
+ </RNHostView>
78
+ ));
79
+
80
+ const pageScrollHandler = useMemo(
81
+ () => (onPageScroll ? buildOnPageScrollHandler(onPageScroll) : undefined),
82
+ [onPageScroll]
83
+ );
84
+
85
+ // RN's `borderRadius` on the host View doesn't reliably clip Compose's draw
86
+ // pass — translate it into a Compose `clip` modifier instead.
87
+ const pagerModifiers = [fillMaxSize()];
88
+ const cornerShape = borderRadiusShape(style, layoutDirection === 'rtl');
89
+ if (cornerShape) {
90
+ pagerModifiers.push(clip(cornerShape));
91
+ }
92
+
93
+ return (
94
+ <Host style={style ?? { flex: 1 }} {...passthrough}>
95
+ <HorizontalPager
96
+ ref={pagerRef}
97
+ initialPage={initialPage}
98
+ userScrollEnabled={scrollEnabledState}
99
+ reverseLayout={layoutDirection === 'rtl'}
100
+ pageSpacing={pageMargin}
101
+ beyondViewportPageCount={offscreenPageLimit}
102
+ modifiers={pagerModifiers}
103
+ onSettledPageChange={(page) => {
104
+ onPageSelected?.(wrapNativeEvent({ position: page }));
105
+ }}
106
+ onPageScroll={pageScrollHandler}
107
+ onScrollInProgressChange={
108
+ onPageScrollStateChanged
109
+ ? (inProgress) => {
110
+ isScrollInProgressRef.current = inProgress;
111
+ if (!inProgress) {
112
+ emitPageScrollStateIfChanged('idle');
113
+ } else if (!isDraggingRef.current) {
114
+ emitPageScrollStateIfChanged('settling');
115
+ }
116
+ }
117
+ : undefined
118
+ }
119
+ onDragInteraction={
120
+ onPageScrollStateChanged
121
+ ? (kind) => {
122
+ if (kind === 'start') {
123
+ isDraggingRef.current = true;
124
+ emitPageScrollStateIfChanged('dragging');
125
+ } else {
126
+ isDraggingRef.current = false;
127
+ emitPageScrollStateIfChanged(isScrollInProgressRef.current ? 'settling' : 'idle');
128
+ }
129
+ }
130
+ : undefined
131
+ }>
132
+ {pages}
133
+ </HorizontalPager>
134
+ </Host>
135
+ );
136
+ }
137
+
138
+ // Compose's `(currentPage, fraction ∈ [-0.5, 0.5))` is anchored to the snapped
139
+ // page; pager-view's `(position, offset ∈ [0, 1))` is anchored to the leading
140
+ // page. Re-anchor negative fractions onto the previous page.
141
+ function composePageToPageScroll(
142
+ currentPage: number,
143
+ currentPageOffsetFraction: number
144
+ ): { position: number; offset: number } {
145
+ 'worklet';
146
+ if (currentPageOffsetFraction >= 0) {
147
+ return { position: currentPage, offset: currentPageOffsetFraction };
148
+ }
149
+ return { position: currentPage - 1, offset: 1 + currentPageOffsetFraction };
150
+ }
151
+
152
+ // Mirrors the worklet-ness of the user's callback so the per-frame mapping
153
+ // stays on the UI runtime when the user is also on it.
154
+ function buildOnPageScrollHandler(
155
+ userOnPageScroll: NonNullable<PagerViewProps['onPageScroll']>
156
+ ): (currentPage: number, currentPageOffsetFraction: number) => void {
157
+ if (worklets?.isWorkletFunction?.(userOnPageScroll)) {
158
+ return (currentPage, currentPageOffsetFraction) => {
159
+ 'worklet';
160
+ userOnPageScroll(
161
+ wrapNativeEvent(composePageToPageScroll(currentPage, currentPageOffsetFraction))
162
+ );
163
+ };
164
+ }
165
+ return (currentPage, currentPageOffsetFraction) => {
166
+ userOnPageScroll(
167
+ wrapNativeEvent(composePageToPageScroll(currentPage, currentPageOffsetFraction))
168
+ );
169
+ };
170
+ }
171
+
172
+ // Translates RN border-radius style keys to a Compose `RoundedCornerShape`.
173
+ // Physical (`borderTopLeftRadius`) and logical (`borderTopStartRadius`) keys
174
+ // collapse onto Compose's start/end edges, swapped under RTL.
175
+ function borderRadiusShape(style: PagerViewProps['style'], rtl: boolean): BuiltinShape | undefined {
176
+ const flat = StyleSheet.flatten(style) as Record<string, unknown> | undefined;
177
+ if (!flat) return undefined;
178
+ // Compose `RoundedCornerShape` only takes Dp (numeric); RN's string values
179
+ // like `'50%'` are dropped — `__DEV__` warns once so the no-clip isn't silent.
180
+ const num = (key: string): number | undefined => {
181
+ const v = flat[key];
182
+ if (typeof v === 'number') return v > 0 ? v : undefined;
183
+ if (__DEV__ && typeof v === 'string') {
184
+ warnAboutStringBorderRadiusOnce(key, v);
185
+ }
186
+ return undefined;
187
+ };
188
+ const uniform = num('borderRadius');
189
+ const topLeft = num('borderTopLeftRadius');
190
+ const topRight = num('borderTopRightRadius');
191
+ const bottomLeft = num('borderBottomLeftRadius');
192
+ const bottomRight = num('borderBottomRightRadius');
193
+ const topStart = num('borderTopStartRadius') ?? (rtl ? topRight : topLeft);
194
+ const topEnd = num('borderTopEndRadius') ?? (rtl ? topLeft : topRight);
195
+ const bottomStart = num('borderBottomStartRadius') ?? (rtl ? bottomRight : bottomLeft);
196
+ const bottomEnd = num('borderBottomEndRadius') ?? (rtl ? bottomLeft : bottomRight);
197
+ const hasPerCorner =
198
+ topStart != null || topEnd != null || bottomStart != null || bottomEnd != null;
199
+ if (hasPerCorner) {
200
+ const fallback = uniform ?? 0;
201
+ return Shapes.RoundedCorner({
202
+ topStart: topStart ?? fallback,
203
+ topEnd: topEnd ?? fallback,
204
+ bottomStart: bottomStart ?? fallback,
205
+ bottomEnd: bottomEnd ?? fallback,
206
+ });
207
+ }
208
+ if (uniform != null) {
209
+ return Shapes.RoundedCorner(uniform);
210
+ }
211
+ return undefined;
212
+ }
213
+
214
+ let didWarnStringBorderRadius = false;
215
+ function warnAboutStringBorderRadiusOnce(key: string, value: string): void {
216
+ if (didWarnStringBorderRadius) return;
217
+ didWarnStringBorderRadius = true;
218
+ console.warn(
219
+ `[expo-ui PagerView] ${key}: ${JSON.stringify(value)} — string border-radius values ` +
220
+ `aren't supported on the Android pager (Jetpack Compose's RoundedCornerShape requires ` +
221
+ `numeric Dp values). The corner radius is being dropped, so the pager won't clip. ` +
222
+ `Use a numeric pixel value, or omit the style key.`
223
+ );
224
+ }
@@ -0,0 +1,273 @@
1
+ import {
2
+ Children,
3
+ isValidElement,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useLayoutEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type ReactElement,
11
+ } from 'react';
12
+ import { Platform } from 'react-native';
13
+
14
+ import { wrapNativeEvent, type PagerViewProps } from './types';
15
+ import { useNativeState, worklets } from '../../State';
16
+ import { Group } from '../../swift-ui/Group';
17
+ import { Host } from '../../swift-ui/Host';
18
+ import { LazyHStack } from '../../swift-ui/LazyHStack';
19
+ import { RNHostView } from '../../swift-ui/RNHostView';
20
+ import { ScrollView, type ScrollGeometry, type ScrollPhase } from '../../swift-ui/ScrollView';
21
+ import {
22
+ containerRelativeFrame,
23
+ id,
24
+ onScrollPhaseChange,
25
+ scrollDisabled,
26
+ scrollPosition,
27
+ scrollTargetBehavior,
28
+ scrollTargetLayout,
29
+ useScrollGeometryChange,
30
+ } from '../../swift-ui/modifiers';
31
+ import { Animation } from '../../swift-ui/modifiers/animation';
32
+ import { withAnimation } from '../../swift-ui/withAnimation';
33
+
34
+ function phaseToPageState(phase: ScrollPhase): 'idle' | 'dragging' | 'settling' {
35
+ switch (phase) {
36
+ case 'idle':
37
+ return 'idle';
38
+ case 'tracking':
39
+ case 'interacting':
40
+ return 'dragging';
41
+ case 'animating':
42
+ case 'decelerating':
43
+ return 'settling';
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Drop-in replacement for `react-native-pager-view` on iOS.
49
+ *
50
+ * Renders a SwiftUI `ScrollView` with paging behavior. Scroll position is the
51
+ * single source of truth: an `ObservableState` bound through the
52
+ * `scrollPosition` modifier drives initial placement, user-swipe writeback,
53
+ * and imperative `setPage` / `setPageWithoutAnimation`. The animated path
54
+ * routes the write through `withAnimation`; if `react-native-worklets` isn't
55
+ * installed, `setPage` falls back to a non-animated jump. Requires iOS 17+.
56
+ * Continuous progress (`onPageScroll`) and scroll state
57
+ * (`onPageScrollStateChanged`) events require iOS 18+.
58
+ */
59
+ export function PagerView(props: PagerViewProps) {
60
+ const {
61
+ ref,
62
+ initialPage = 0,
63
+ scrollEnabled = true,
64
+ onPageScroll,
65
+ onPageScrollStateChanged,
66
+ onPageSelected,
67
+ children,
68
+ style,
69
+ // Drop pager-view-only props that aren't meaningful on iOS so they don't
70
+ // collide with `Host`'s own prop surface.
71
+ layoutDirection: _layoutDirection,
72
+ offscreenPageLimit: _offscreenPageLimit,
73
+ pageMargin: _pageMargin,
74
+ ...passthrough
75
+ } = props;
76
+
77
+ warnIfPreIOS18ScrollCallbacksDropped(onPageScroll, onPageScrollStateChanged);
78
+
79
+ const [scrollEnabledState, setScrollEnabledState] = useState(scrollEnabled);
80
+ useEffect(() => {
81
+ setScrollEnabledState(scrollEnabled);
82
+ }, [scrollEnabled]);
83
+
84
+ const validChildren = useMemo(
85
+ () => Children.toArray(children).filter((c): c is ReactElement => isValidElement(c)),
86
+ [children]
87
+ );
88
+ const pageCount = validChildren.length;
89
+ const pageCountRef = useRef(0);
90
+ pageCountRef.current = pageCount;
91
+
92
+ // Clamp on first render — out-of-range initials produce an id that matches
93
+ // no `Group`, leaving `.scrollPosition(id:)` silently stuck at 0.
94
+ const [clampedInitialPage] = useState(() => {
95
+ if (pageCount === 0) return 0;
96
+ return Math.max(0, Math.min(pageCount - 1, initialPage));
97
+ });
98
+
99
+ const activePageState = useNativeState<string | null>(
100
+ clampedInitialPage > 0 ? String(clampedInitialPage) : null
101
+ );
102
+
103
+ // The SwiftUI writeback fires for both user swipes and our own imperative
104
+ // writes; dedup against this ref so `onPageSelected` fires once per change.
105
+ const lastSelectedPageRef = useRef(clampedInitialPage);
106
+
107
+ // Read the latest `onPageSelected` through a ref so the shrink-clamp effect
108
+ // doesn't need it in deps (and won't re-run / double-fire on callback
109
+ // identity changes).
110
+ const onPageSelectedRef = useRef(onPageSelected);
111
+ onPageSelectedRef.current = onPageSelected;
112
+
113
+ const handleScrolledIDChange = (newId: string | null) => {
114
+ if (newId == null) return;
115
+ const page = parseInt(newId, 10);
116
+ if (!Number.isFinite(page)) return;
117
+ if (page !== lastSelectedPageRef.current) {
118
+ lastSelectedPageRef.current = page;
119
+ onPageSelectedRef.current?.(wrapNativeEvent({ position: page }));
120
+ }
121
+ };
122
+
123
+ // Without clamping, an out-of-range id would silently no-op on
124
+ // `.scrollPosition(id:)` instead of jumping to the nearest page.
125
+ const clampPage = (page: number): number | null => {
126
+ const count = pageCountRef.current;
127
+ if (count === 0 || !Number.isFinite(page)) return null;
128
+ return Math.max(0, Math.min(count - 1, page));
129
+ };
130
+
131
+ // Bypasses the public `value` setter on JS-thread writes — its dev warning
132
+ // is aimed at user code; our own imperative API is an intentional JS-thread
133
+ // writer. Inside a worklet we use `activePageState.value = …` directly,
134
+ // which routes through the SharedObject prototype installed by `index.fx`.
135
+ const writePageFromJS = (id: string) => {
136
+ (activePageState as unknown as { setValue(v: { value: string }): void }).setValue({
137
+ value: id,
138
+ });
139
+ };
140
+
141
+ // Re-anchor when the page count drops past the current selection. Matches
142
+ // Android, where Compose's `PagerState` clamps `currentPage` to the new
143
+ // max and `settledPage` fires the corresponding event. Without this on
144
+ // iOS, `.scrollPosition(id:)` silently no-ops on the missing id and the
145
+ // pager visually drifts without firing `onPageSelected`.
146
+ useLayoutEffect(() => {
147
+ if (pageCount === 0) return;
148
+ if (lastSelectedPageRef.current < pageCount) return;
149
+ const clamped = pageCount - 1;
150
+ lastSelectedPageRef.current = clamped;
151
+ writePageFromJS(String(clamped));
152
+ onPageSelectedRef.current?.(wrapNativeEvent({ position: clamped }));
153
+ }, [pageCount]);
154
+
155
+ useImperativeHandle(
156
+ ref,
157
+ () => ({
158
+ setPage: (page: number) => {
159
+ const clamped = clampPage(page);
160
+ if (clamped == null) return;
161
+ const nextId = String(clamped);
162
+ // `withAnimation` requires `react-native-worklets`; fall back to an
163
+ // instant jump if it isn't installed.
164
+ if (worklets) {
165
+ // `null` would disable animation; `Animation.default` opts into
166
+ // SwiftUI's default paging animation.
167
+ withAnimation(Animation.default, () => {
168
+ 'worklet';
169
+ activePageState.value = nextId;
170
+ });
171
+ } else {
172
+ writePageFromJS(nextId);
173
+ }
174
+ },
175
+ setPageWithoutAnimation: (page: number) => {
176
+ const clamped = clampPage(page);
177
+ if (clamped == null) return;
178
+ writePageFromJS(String(clamped));
179
+ },
180
+ setScrollEnabled: setScrollEnabledState,
181
+ }),
182
+ [activePageState]
183
+ );
184
+
185
+ const handleScrollGeometry = useMemo(
186
+ () => (onPageScroll ? buildHandleScrollGeometry(onPageScroll) : undefined),
187
+ [onPageScroll]
188
+ );
189
+ const geometryModifier = useScrollGeometryChange(handleScrollGeometry);
190
+
191
+ const phaseModifier = onPageScrollStateChanged
192
+ ? onScrollPhaseChange((phase) =>
193
+ onPageScrollStateChanged(wrapNativeEvent({ pageScrollState: phaseToPageState(phase) }))
194
+ )
195
+ : null;
196
+
197
+ const pages = validChildren.map((child, index) => (
198
+ <Group
199
+ key={child.key ?? String(index)}
200
+ modifiers={[containerRelativeFrame({ axes: 'horizontal' }), id(String(index))]}>
201
+ <RNHostView>{child}</RNHostView>
202
+ </Group>
203
+ ));
204
+
205
+ // Toggle the flag rather than splicing the modifier in/out — SwiftUI diffs
206
+ // modifiers by position, so a shifting array resets the ScrollView's content.
207
+ const modifiers = [
208
+ scrollTargetBehavior('paging'),
209
+ scrollDisabled(!scrollEnabledState),
210
+ scrollPosition(activePageState, { onChange: handleScrolledIDChange }),
211
+ ...(geometryModifier ? [geometryModifier] : []),
212
+ ...(phaseModifier ? [phaseModifier] : []),
213
+ ];
214
+
215
+ return (
216
+ <Host style={style ?? { flex: 1 }} {...passthrough}>
217
+ <ScrollView axes="horizontal" showsIndicators={false} modifiers={modifiers}>
218
+ <LazyHStack spacing={0} modifiers={[scrollTargetLayout()]}>
219
+ {pages}
220
+ </LazyHStack>
221
+ </ScrollView>
222
+ </Host>
223
+ );
224
+ }
225
+
226
+ function geometryToPageScroll(geometry: ScrollGeometry): { position: number; offset: number } {
227
+ 'worklet';
228
+ // Clamp so rubber-band overscroll doesn't emit `position` outside
229
+ // `[0, pageCount - 1]`. `Math.round` tolerates sub-pixel content sizing.
230
+ const pageCount = Math.max(1, Math.round(geometry.contentWidth / geometry.containerWidth));
231
+ const positionFloat = Math.max(
232
+ 0,
233
+ Math.min(pageCount - 1, geometry.contentOffsetX / geometry.containerWidth)
234
+ );
235
+ const position = Math.floor(positionFloat);
236
+ return { position, offset: positionFloat - position };
237
+ }
238
+
239
+ // Mirrors the worklet-ness of the user's callback so the per-frame mapping
240
+ // stays on the UI runtime when the user is also on it.
241
+ function buildHandleScrollGeometry(
242
+ userOnPageScroll: NonNullable<PagerViewProps['onPageScroll']>
243
+ ): (geometry: ScrollGeometry) => void {
244
+ if (worklets?.isWorkletFunction?.(userOnPageScroll)) {
245
+ return (geometry) => {
246
+ 'worklet';
247
+ if (geometry.containerWidth <= 0) return;
248
+ userOnPageScroll(wrapNativeEvent(geometryToPageScroll(geometry)));
249
+ };
250
+ }
251
+ return (geometry) => {
252
+ if (geometry.containerWidth <= 0) return;
253
+ userOnPageScroll(wrapNativeEvent(geometryToPageScroll(geometry)));
254
+ };
255
+ }
256
+
257
+ let didWarnPreIOS18 = false;
258
+ function warnIfPreIOS18ScrollCallbacksDropped(
259
+ onPageScroll: PagerViewProps['onPageScroll'],
260
+ onPageScrollStateChanged: PagerViewProps['onPageScrollStateChanged']
261
+ ) {
262
+ if (!__DEV__ || didWarnPreIOS18) return;
263
+ if (!onPageScroll && !onPageScrollStateChanged) return;
264
+ const major = parseInt(String(Platform.Version), 10);
265
+ if (Number.isFinite(major) && major < 18) {
266
+ didWarnPreIOS18 = true;
267
+ console.warn(
268
+ `[expo-ui PagerView] onPageScroll and onPageScrollStateChanged require iOS 18+ ` +
269
+ `and will not fire on iOS ${Platform.Version}. Guard with Platform.Version or ` +
270
+ `provide a fallback for older iOS targets.`
271
+ );
272
+ }
273
+ }
@@ -0,0 +1,14 @@
1
+ import { Platform } from 'react-native';
2
+
3
+ import type { PagerViewProps } from './types';
4
+
5
+ /**
6
+ * A drop-in replacement for `react-native-pager-view`. Renders a horizontally
7
+ * paged view backed by Jetpack Compose's `HorizontalPager` on Android and
8
+ * SwiftUI on iOS. Each child is treated as a separate page.
9
+ */
10
+ export function PagerView(props: PagerViewProps): never {
11
+ throw new Error(
12
+ `@expo/ui/community/pager-view is not implemented on ${Platform.OS}. Supported platforms: android, ios.`
13
+ );
14
+ }
@@ -0,0 +1,13 @@
1
+ export type {
2
+ PagerViewProps,
3
+ PagerViewRef,
4
+ PagerViewOnPageScrollEventData,
5
+ PagerViewOnPageSelectedEventData,
6
+ PageScrollStateChangedEventData,
7
+ PagerViewOnPageScrollEvent,
8
+ PagerViewOnPageSelectedEvent,
9
+ PageScrollStateChangedEvent,
10
+ } from './types';
11
+
12
+ // named export is for the docs generator; default for normal consumption
13
+ export { PagerView, PagerView as default } from './PagerView';
@@ -0,0 +1,131 @@
1
+ import type { ReactNode, Ref } from 'react';
2
+ import type { NativeSyntheticEvent, ViewProps } from 'react-native';
3
+
4
+ export type PagerViewOnPageScrollEventData = Readonly<{
5
+ /** Index of the leading visible page. */
6
+ position: number;
7
+ /** Fractional progress toward the next page, in `[0, 1)`. */
8
+ offset: number;
9
+ }>;
10
+
11
+ export type PagerViewOnPageSelectedEventData = Readonly<{
12
+ /** Index of the newly selected page. */
13
+ position: number;
14
+ }>;
15
+
16
+ export type PageScrollStateChangedEventData = Readonly<{
17
+ /**
18
+ * `idle` when scrolling has stopped, `dragging` while the user is actively
19
+ * swiping, and `settling` while the pager animates to a target page.
20
+ */
21
+ pageScrollState: 'idle' | 'dragging' | 'settling';
22
+ }>;
23
+
24
+ export type PagerViewOnPageScrollEvent = NativeSyntheticEvent<PagerViewOnPageScrollEventData>;
25
+ export type PagerViewOnPageSelectedEvent = NativeSyntheticEvent<PagerViewOnPageSelectedEventData>;
26
+ export type PageScrollStateChangedEvent = NativeSyntheticEvent<PageScrollStateChangedEventData>;
27
+
28
+ /**
29
+ * Wraps a payload as `NativeSyntheticEvent`. We only populate `nativeEvent`
30
+ * since our consumers (mirroring upstream `react-native-pager-view`) read
31
+ * only `event.nativeEvent.X`; the unset SyntheticEvent fields would never
32
+ * be observed in practice.
33
+ */
34
+ export const wrapNativeEvent = <T,>(nativeEvent: T): NativeSyntheticEvent<T> => {
35
+ 'worklet';
36
+ return { nativeEvent } as NativeSyntheticEvent<T>;
37
+ };
38
+
39
+ /**
40
+ * Props for the `PagerView` component.
41
+ * Compatible with `react-native-pager-view`.
42
+ */
43
+ export type PagerViewProps = ViewProps & {
44
+ /**
45
+ * Ref handle exposing imperative `setPage`, `setPageWithoutAnimation`,
46
+ * and `setScrollEnabled` methods.
47
+ */
48
+ ref?: Ref<PagerViewRef>;
49
+ /**
50
+ * Index of the page that is initially selected. Read **once** on mount;
51
+ * later changes are ignored. To navigate after mount, call
52
+ * `ref.setPage()` or `ref.setPageWithoutAnimation()`.
53
+ * @default 0
54
+ */
55
+ initialPage?: number;
56
+ /**
57
+ * Whether the user can swipe between pages.
58
+ * @default true
59
+ */
60
+ scrollEnabled?: boolean;
61
+ /**
62
+ * Layout direction for paging.
63
+ * @default 'ltr'
64
+ * @platform android
65
+ */
66
+ layoutDirection?: 'ltr' | 'rtl';
67
+ /**
68
+ * Number of pages kept off-screen on each side of the visible page.
69
+ * @platform android
70
+ */
71
+ offscreenPageLimit?: number;
72
+ /**
73
+ * Pixels of padding between pages.
74
+ * @platform android
75
+ */
76
+ pageMargin?: number;
77
+ /**
78
+ * Fires continuously while a swipe is in progress. The event's `position`
79
+ * is the index of the leading visible page; `offset` is the fractional
80
+ * progress toward the next page in the `[0, 1)` range.
81
+ *
82
+ * Mark this handler with `'worklet'` (requires `react-native-worklets`)
83
+ * to run it synchronously on the UI thread every frame.
84
+ *
85
+ * @platform android
86
+ * @platform ios 18.0+
87
+ */
88
+ onPageScroll?: (event: PagerViewOnPageScrollEvent) => void;
89
+ /**
90
+ * Fires when a page is fully selected. The event's `position` is the
91
+ * index of the new page.
92
+ */
93
+ onPageSelected?: (event: PagerViewOnPageSelectedEvent) => void;
94
+ /**
95
+ * Fires when the scroll state changes between `idle`, `dragging`,
96
+ * and `settling`.
97
+ *
98
+ * @platform android
99
+ * @platform ios 18.0+
100
+ */
101
+ onPageScrollStateChanged?: (event: PageScrollStateChangedEvent) => void;
102
+ /**
103
+ * Pages of the pager. Each child is treated as a separate page and
104
+ * stretched to fill the pager. Each child should have a stable `key`.
105
+ */
106
+ children?: ReactNode;
107
+ };
108
+
109
+ /**
110
+ * Ref handle for the `PagerView` component.
111
+ */
112
+ export type PagerViewRef = {
113
+ /**
114
+ * Animate the pager to the given page index. Out-of-range indices are
115
+ * silently ignored. On iOS the animation requires `react-native-worklets`;
116
+ * without it, `setPage` falls back to a non-animated jump.
117
+ */
118
+ setPage: (selectedPage: number) => void;
119
+ /**
120
+ * Jump to the given page index without an animation.
121
+ */
122
+ setPageWithoutAnimation: (selectedPage: number) => void;
123
+ /**
124
+ * Imperatively enable or disable user scrolling.
125
+ *
126
+ * > **Note:** If the `scrollEnabled` prop is also provided, subsequent
127
+ * > prop changes win and reset the value set imperatively. To use the
128
+ * > imperative path exclusively, omit the prop.
129
+ */
130
+ setScrollEnabled: (scrollEnabled: boolean) => void;
131
+ };
@@ -7,7 +7,7 @@ import {
7
7
  type PickerItemValue,
8
8
  type PickerProps,
9
9
  } from './types';
10
- import { useNativeState } from '../../State/useNativeState';
10
+ import { useNativeState } from '../../State';
11
11
  import { DropdownMenuItem } from '../../jetpack-compose/DropdownMenu/DropdownMenuItem';
12
12
  import {
13
13
  ExposedDropdownMenuBox,
@@ -12,7 +12,8 @@ export function SegmentsSeparators({
12
12
  values: number;
13
13
  selectedIndex?: number;
14
14
  }) {
15
- const colorScheme = useColorScheme();
15
+ const isDark = useColorScheme() === 'dark';
16
+
16
17
  const hide = (val: number) => {
17
18
  return selectedIndex === val || selectedIndex === val + 1;
18
19
  };
@@ -22,11 +23,7 @@ export function SegmentsSeparators({
22
23
  {[...Array(values - 1).keys()].map((val) => (
23
24
  <View
24
25
  key={val}
25
- style={[
26
- styles.separator,
27
- colorScheme === 'dark' && styles.darkSeparator,
28
- hide(val) && styles.hide,
29
- ]}
26
+ style={[styles.separator, isDark && styles.darkSeparator, hide(val) && styles.hide]}
30
27
  />
31
28
  ))}
32
29
  </View>