@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,
|
|
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:
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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
|
-
/*
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
+
const offsetY = Math.max(0, rawOffsetY);
|
|
184
203
|
|
|
185
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
208
|
+
const offset = snapsToCollapsed
|
|
209
|
+
? Math.round(offsetY + scrollState + headerHeight)
|
|
210
|
+
: Math.round(offsetY + scrollState);
|
|
205
211
|
|
|
206
|
-
|
|
212
|
+
flatListRef.current.scrollToOffset({ animated: true, offset });
|
|
213
|
+
},
|
|
214
|
+
[navBarStore]
|
|
215
|
+
);
|
|
207
216
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
() =>
|
|
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
|
-
|
|
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.
|
|
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.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "16.0.0-alpha.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "16.0.0-alpha.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.
|
|
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",
|