@applicaster/zapp-react-native-ui-components 16.0.0-alpha.2320942372 → 16.0.0-alpha.2628821467

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.
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import * as R from "ramda";
3
- import { FlatList, StyleSheet, View } from "react-native";
3
+ import { FlatList, ScrollView, ScrollViewProps, View } from "react-native";
4
4
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
5
5
  import { RiverItem } from "../RiverItem";
6
6
  import { RiverFooter } from "../RiverFooter";
@@ -36,18 +36,16 @@ type Props = {
36
36
  groupId?: string;
37
37
  isScreenWrappedInContainer?: boolean;
38
38
  riverComponents: ZappUIComponent[];
39
- scrollViewExtraProps?: {};
39
+ scrollViewExtraProps?: Partial<ScrollViewProps>;
40
40
  riverId?: string;
41
- getStaticComponentFeed: any;
41
+ getStaticComponentFeed: (args: {
42
+ component: ZappUIComponent;
43
+ index: number;
44
+ }) => ZappFeed | Promise<ZappFeed>;
42
45
  stickyHeaderIndices?: number[];
46
+ useScrollView?: boolean;
43
47
  };
44
48
 
45
- const styles = StyleSheet.create({
46
- container: {
47
- flex: 1,
48
- },
49
- });
50
-
51
49
  const getNearestValue = (value, optionA, optionB) =>
52
50
  Math.abs(value - optionA) < Math.abs(value - optionB) ? optionA : optionB;
53
51
 
@@ -64,11 +62,14 @@ function ComponentsMapComponent(props: Props) {
64
62
  riverId,
65
63
  getStaticComponentFeed,
66
64
  stickyHeaderIndices,
65
+ useScrollView,
67
66
  } = props;
68
67
 
69
68
  const flatListRef = React.useRef<FlatList | null>(null);
69
+ const scrollViewRef = React.useRef<ScrollView | null>(null);
70
70
  const flatListWrapperRef = React.useRef<View | null>(null);
71
71
  const hasUserScrolledRef = React.useRef(false);
72
+ const scrollEndReachedRef = React.useRef(false);
72
73
  const screenConfig = useScreenConfiguration(riverId);
73
74
  const screenData = useScreenData(riverId);
74
75
  const pullToRefreshEnabled = screenData?.rules?.pull_to_refresh_enabled;
@@ -87,7 +88,7 @@ function ComponentsMapComponent(props: Props) {
87
88
 
88
89
  const onLoadDone = React.useCallback(() => {
89
90
  logTimestamp(riverId?.toString());
90
- }, [logTimestamp]);
91
+ }, [logTimestamp, riverId]);
91
92
 
92
93
  const { loadingState, onLoadFinished, onLoadFailed, shouldShowLoadingError } =
93
94
  useLoadingState(riverComponents.length, onLoadDone);
@@ -116,7 +117,17 @@ function ComponentsMapComponent(props: Props) {
116
117
  </ScreenLoadingMeasurementsListItemWrapper>
117
118
  );
118
119
  },
119
- [feed, getStaticComponentFeed, onLoadFailed, onLoadFinished]
120
+ [
121
+ feed,
122
+ getStaticComponentFeed,
123
+ groupId,
124
+ isScreenWrappedInContainer,
125
+ loadingState,
126
+ onLoadFailed,
127
+ onLoadFinished,
128
+ riverComponents.length,
129
+ riverId,
130
+ ]
120
131
  );
121
132
 
122
133
  const screenStyle = React.useMemo(
@@ -138,7 +149,13 @@ function ComponentsMapComponent(props: Props) {
138
149
  R.prop("screen_padding_right")(theme)
139
150
  ),
140
151
  }),
141
- [riverId]
152
+ [
153
+ screenConfig.paddingTop,
154
+ screenConfig.paddingBottom,
155
+ screenConfig.paddingLeft,
156
+ screenConfig.paddingRight,
157
+ theme,
158
+ ]
142
159
  );
143
160
 
144
161
  const handleOnLayout = React.useCallback(
@@ -173,55 +190,50 @@ function ComponentsMapComponent(props: Props) {
173
190
  canMomentum.current = true;
174
191
  }, []);
175
192
 
176
- /* onMomentumScrollEnd: Handling snap to expanded/collapsed state ( for navbar ) */
177
- const _onMomentumScrollEnd = React.useCallback(({ nativeEvent }) => {
178
- if (!canMomentum.current) return;
179
- canMomentum.current = false;
193
+ /* Snap the navbar to its expanded/collapsed state based on current offset. */
194
+ const snapToHeaderState = React.useCallback(
195
+ (rawOffsetY: number) => {
196
+ const { height: headerHeight, scrollState } = navBarStore.getState();
180
197
 
181
- const { height: headerHeight, scrollState } = navBarStore.getState();
198
+ // Already fully expanded or fully collapsed — nothing to snap.
199
+ if (scrollState === 0 || scrollState === -headerHeight) return;
200
+ if (!flatListRef.current) return;
182
201
 
183
- const offsetY = Math.max(0, nativeEvent.contentOffset.y);
202
+ const offsetY = Math.max(0, rawOffsetY);
184
203
 
185
- if (
186
- !(scrollState === 0 || scrollState === -headerHeight) &&
187
- flatListRef.current
188
- ) {
189
- const offset =
204
+ const snapsToCollapsed =
190
205
  getNearestValue(Math.round(scrollState), -headerHeight, 0) ===
191
- -headerHeight
192
- ? Math.round(offsetY + scrollState + headerHeight)
193
- : Math.round(offsetY + scrollState);
194
-
195
- flatListRef.current.scrollToOffset({
196
- animated: true,
197
- offset,
198
- });
199
- }
200
- }, []);
206
+ -headerHeight;
201
207
 
202
- /* onScrollEnd: Handling snap to expanded/collapsed state ( for navbar ) */
203
- const _onScrollEndDrag = React.useCallback(({ nativeEvent }) => {
204
- const { height: headerHeight, scrollState } = navBarStore.getState();
208
+ const offset = snapsToCollapsed
209
+ ? Math.round(offsetY + scrollState + headerHeight)
210
+ : Math.round(offsetY + scrollState);
205
211
 
206
- const offsetY = Math.max(0, nativeEvent.contentOffset.y);
212
+ flatListRef.current.scrollToOffset({ animated: true, offset });
213
+ },
214
+ [navBarStore]
215
+ );
207
216
 
208
- if (
209
- !(scrollState === 0 || scrollState === -headerHeight) &&
210
- flatListRef.current &&
211
- nativeEvent.velocity.y === 0
212
- ) {
213
- const offset =
214
- getNearestValue(Math.round(scrollState), -headerHeight, 0) ===
215
- -headerHeight
216
- ? Math.round(offsetY + scrollState + headerHeight)
217
- : Math.round(offsetY + scrollState);
218
-
219
- flatListRef.current.scrollToOffset({
220
- animated: true,
221
- offset,
222
- });
223
- }
224
- }, []);
217
+ /* onMomentumScrollEnd: Handling snap to expanded/collapsed state ( for navbar ) */
218
+ const _onMomentumScrollEnd = React.useCallback(
219
+ ({ nativeEvent }) => {
220
+ if (!canMomentum.current) return;
221
+ canMomentum.current = false;
222
+ snapToHeaderState(nativeEvent.contentOffset.y);
223
+ },
224
+ [snapToHeaderState]
225
+ );
226
+
227
+ /* onScrollEndDrag: Handling snap to expanded/collapsed state ( for navbar ) */
228
+ const _onScrollEndDrag = React.useCallback(
229
+ ({ nativeEvent }) => {
230
+ // Only snap on drag-end if there's no momentum to follow;
231
+ // otherwise _onMomentumScrollEnd handles it.
232
+ if (nativeEvent.velocity?.y !== 0) return;
233
+ snapToHeaderState(nativeEvent.contentOffset.y);
234
+ },
235
+ [snapToHeaderState]
236
+ );
225
237
 
226
238
  const onScroll = React.useCallback((event) => {
227
239
  hasUserScrolledRef.current = true;
@@ -245,8 +257,42 @@ function ComponentsMapComponent(props: Props) {
245
257
  }
246
258
  }, []);
247
259
 
260
+ const onScrollWithEndReached = React.useCallback(
261
+ (event) => {
262
+ onScroll(event);
263
+
264
+ if (!hasUserScrolledRef.current) return;
265
+ if (isScreenWrappedInContainer) return;
266
+
267
+ const {
268
+ nativeEvent: {
269
+ contentOffset: { y },
270
+ layoutMeasurement: { height },
271
+ contentSize: { height: contentHeight },
272
+ },
273
+ } = event;
274
+
275
+ const distanceFromEnd = contentHeight - y - height;
276
+
277
+ if (distanceFromEnd <= height * 0.5) {
278
+ if (!scrollEndReachedRef.current) {
279
+ scrollEndReachedRef.current = true;
280
+ emitScrollEndReached();
281
+ }
282
+ } else {
283
+ scrollEndReachedRef.current = false;
284
+ }
285
+ },
286
+ [onScroll, isScreenWrappedInContainer]
287
+ );
288
+
248
289
  const contentContainerStyle = React.useMemo(
249
- () => (isScreenWrappedInContainer ? {} : screenStyle),
290
+ () =>
291
+ isScreenWrappedInContainer
292
+ ? {
293
+ flexGrow: 1,
294
+ }
295
+ : screenStyle,
250
296
  [isScreenWrappedInContainer, screenStyle]
251
297
  );
252
298
 
@@ -257,61 +303,98 @@ function ComponentsMapComponent(props: Props) {
257
303
  // TODO: support both "isScreenWrappedInContainer" and hasScreenPicker().
258
304
  // The Screen Picker in Mobile is completly different than the TV
259
305
  // so the various offsets / margins in TV do not apply here.
306
+ // Fix for WebView rerender crashes on Android API 28+
307
+ // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
308
+ const sharedScrollProps = {
309
+ overScrollMode: isAndroid ? ("never" as const) : ("auto" as const),
310
+ scrollIndicatorInsets,
311
+ onLayout: handleOnLayout,
312
+ contentContainerStyle,
313
+ refreshControl,
314
+ onScrollBeginDrag,
315
+ onMomentumScrollEnd: _onMomentumScrollEnd,
316
+ onScrollEndDrag: _onScrollEndDrag,
317
+ scrollEventThrottle: 16,
318
+ ...scrollViewExtraProps,
319
+ };
320
+
321
+ const listContent = useScrollView ? (
322
+ <ScrollView
323
+ testID="components-map-scroll-view"
324
+ // ViewportTracker clones this child and calls its `ref` as a function
325
+ // (childrenRef.current.ref(node)), so a callback ref is required here
326
+ // — an object ref would throw "ref is not a function".
327
+ ref={(ref) => {
328
+ scrollViewRef.current = ref;
329
+ }}
330
+ onScroll={onScrollWithEndReached}
331
+ {...sharedScrollProps}
332
+ >
333
+ {riverComponents.map((item, index) => (
334
+ <React.Fragment key={keyExtractor(item)}>
335
+ {renderRiverItem({ item, index })}
336
+ </React.Fragment>
337
+ ))}
338
+ <RiverFooter
339
+ flatListHeight={flatListHeight}
340
+ loadingState={loadingState}
341
+ />
342
+ </ScrollView>
343
+ ) : (
344
+ <FlatList
345
+ testID="components-map-flat-list"
346
+ // ViewportTracker clones this child and calls its `ref` as a function
347
+ // (childrenRef.current.ref(node)), so a callback ref is required here
348
+ // — an object ref would throw "ref is not a function".
349
+ ref={(ref) => {
350
+ flatListRef.current = ref;
351
+ }}
352
+ extraData={feed}
353
+ stickyHeaderIndices={stickyHeaderIndices}
354
+ removeClippedSubviews={isAndroid}
355
+ initialNumToRender={3}
356
+ maxToRenderPerBatch={10}
357
+ windowSize={12}
358
+ keyExtractor={keyExtractor}
359
+ renderItem={renderRiverItem}
360
+ data={riverComponents}
361
+ ListFooterComponent={
362
+ <RiverFooter
363
+ flatListHeight={flatListHeight}
364
+ loadingState={loadingState}
365
+ />
366
+ }
367
+ onScroll={onScroll}
368
+ onEndReached={
369
+ /* TODO: end-reached detection is inconsistent between the two paths:
370
+ the ScrollView branch uses manual pixel-offset math (onScrollWithEndReached)
371
+ with a scrollEndReachedRef debounce, while this FlatList relies on the
372
+ native onEndReached. Consider unifying the threshold/debounce behavior. */
373
+ /* When wrapped in a parent ScrollView (e.g. tabs),
374
+ this FlatList doesn't scroll so onEndReached can fire repeatedly;
375
+ skip it here and let the parent ScrollView emit scroll-end instead. */
376
+ isScreenWrappedInContainer
377
+ ? undefined
378
+ : () => {
379
+ if (!hasUserScrolledRef.current) return;
380
+ emitScrollEndReached();
381
+ }
382
+ }
383
+ {...sharedScrollProps}
384
+ />
385
+ );
386
+
260
387
  return (
261
- <View style={styles.container} ref={flatListWrapperRef}>
388
+ // Wrapper View exists only to expose a ref (via ComponentsMapRefContext)
389
+ // for measuring the list's position/height; it intentionally has no style.
390
+ <View ref={flatListWrapperRef}>
262
391
  <ComponentsMapHeightContext.Provider value={flatListHeight}>
263
392
  <ComponentsMapRefContext.Provider value={flatListWrapperRef}>
264
393
  <ScreenLoadingMeasurements
265
394
  riverId={riverId}
266
395
  numberOfComponents={riverComponents.length}
267
396
  >
268
- <ViewportTracker>
269
- <FlatList
270
- testID="components-map-flat-list"
271
- ref={(ref) => {
272
- flatListRef.current = ref;
273
- }}
274
- // Fix for WebView rerender crashes on Android API 28+
275
- // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
276
- overScrollMode={isAndroid ? "never" : "auto"}
277
- scrollIndicatorInsets={scrollIndicatorInsets}
278
- extraData={feed}
279
- stickyHeaderIndices={stickyHeaderIndices}
280
- removeClippedSubviews={isAndroid}
281
- onLayout={handleOnLayout}
282
- initialNumToRender={3}
283
- maxToRenderPerBatch={10}
284
- windowSize={12}
285
- keyExtractor={keyExtractor}
286
- renderItem={renderRiverItem}
287
- data={riverComponents}
288
- contentContainerStyle={contentContainerStyle}
289
- ListFooterComponent={
290
- <RiverFooter
291
- flatListHeight={flatListHeight}
292
- loadingState={loadingState}
293
- />
294
- }
295
- refreshControl={refreshControl}
296
- onScrollBeginDrag={onScrollBeginDrag}
297
- onScroll={onScroll}
298
- onMomentumScrollEnd={_onMomentumScrollEnd}
299
- onScrollEndDrag={_onScrollEndDrag}
300
- scrollEventThrottle={16}
301
- {...scrollViewExtraProps}
302
- onEndReached={
303
- /* When wrapped in a parent ScrollView (e.g. tabs),
304
- this FlatList doesn't scroll so onEndReached can fire repeatedly;
305
- skip it here and let the parent ScrollView emit scroll-end instead. */
306
- isScreenWrappedInContainer
307
- ? undefined
308
- : () => {
309
- if (!hasUserScrolledRef.current) return;
310
- emitScrollEndReached();
311
- }
312
- }
313
- />
314
- </ViewportTracker>
397
+ <ViewportTracker>{listContent}</ViewportTracker>
315
398
  </ScreenLoadingMeasurements>
316
399
  </ComponentsMapRefContext.Provider>
317
400
  </ComponentsMapHeightContext.Provider>
@@ -1,20 +1,9 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
3
  exports[`componentsMap renders renders components map correctly 1`] = `
4
- <View
5
- style={
6
- {
7
- "flex": 1,
8
- }
9
- }
10
- >
4
+ <View>
11
5
  <View
12
6
  onLayout={[Function]}
13
- style={
14
- {
15
- "flex": 1,
16
- }
17
- }
18
7
  >
19
8
  <RCTScrollView
20
9
  ListFooterComponent={
@@ -167,11 +156,6 @@ exports[`componentsMap renders renders components map correctly 1`] = `
167
156
  >
168
157
  <View
169
158
  onLayout={[Function]}
170
- style={
171
- {
172
- "flex": 1,
173
- }
174
- }
175
159
  >
176
160
  <View />
177
161
  </View>
@@ -183,11 +167,6 @@ exports[`componentsMap renders renders components map correctly 1`] = `
183
167
  >
184
168
  <View
185
169
  onLayout={[Function]}
186
- style={
187
- {
188
- "flex": 1,
189
- }
190
- }
191
170
  />
192
171
  </View>
193
172
  <View
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "16.0.0-alpha.2320942372",
3
+ "version": "16.0.0-alpha.2628821467",
4
4
  "description": "Applicaster Zapp React Native ui components for the Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,10 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "16.0.0-alpha.2320942372",
32
- "@applicaster/zapp-react-native-bridge": "16.0.0-alpha.2320942372",
33
- "@applicaster/zapp-react-native-redux": "16.0.0-alpha.2320942372",
34
- "@applicaster/zapp-react-native-utils": "16.0.0-alpha.2320942372",
31
+ "@applicaster/applicaster-types": "16.0.0-alpha.2628821467",
32
+ "@applicaster/zapp-react-native-bridge": "16.0.0-alpha.2628821467",
33
+ "@applicaster/zapp-react-native-redux": "16.0.0-alpha.2628821467",
34
+ "@applicaster/zapp-react-native-utils": "16.0.0-alpha.2628821467",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "url": "^0.11.0",