@applicaster/zapp-react-native-ui-components 16.0.0-alpha.6593152532 → 16.0.0-alpha.6918106967

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 (152) hide show
  1. package/Components/AudioPlayer/tv/__tests__/audioPlayer.test.js +19 -0
  2. package/Components/AudioPlayer/tv/__tests__/index.test.tsx +120 -0
  3. package/Components/AudioPlayer/tv/index.tsx +5 -1
  4. package/Components/BackgroundImage/BackgroundImage.android.tv.tsx +28 -0
  5. package/Components/BackgroundImage/BackgroundImage.ios.tv.tsx +24 -0
  6. package/Components/BackgroundImage/BackgroundImage.tsx +42 -0
  7. package/Components/BackgroundImage/index.ts +1 -0
  8. package/Components/Cell/Cell.tsx +3 -6
  9. package/Components/Cell/TvOSCellComponent.tsx +0 -1
  10. package/Components/CellRendererResolver/index.ts +2 -2
  11. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  12. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  13. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +54 -0
  14. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  15. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  16. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  17. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  18. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  19. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +188 -0
  20. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  21. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  22. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  23. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  24. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +90 -0
  25. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  26. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  27. package/Components/GeneralContentScreen/index.ts +2 -0
  28. package/Components/Layout/FullWidthRow.tsx +38 -0
  29. package/Components/Layout/TV/ScreenContainer.tsx +5 -0
  30. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +0 -1
  31. package/Components/Layout/TV/index.tsx +3 -4
  32. package/Components/Layout/TV/index.web.tsx +2 -3
  33. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +17 -5
  34. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  35. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  36. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -0
  37. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +1 -1
  38. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  39. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  40. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  41. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  42. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  43. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  44. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  45. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  46. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  47. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  48. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  49. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  50. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  51. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
  52. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  53. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  54. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  55. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  56. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  57. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  58. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  59. package/Components/MasterCell/dataAdapter.ts +15 -5
  60. package/Components/MasterCell/hooks/index.ts +2 -0
  61. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  62. package/Components/MasterCell/index.tsx +30 -5
  63. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  64. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  65. package/Components/MasterCell/utils/index.ts +85 -46
  66. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +54 -0
  67. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +21 -0
  68. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  69. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +58 -0
  70. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +314 -0
  71. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +407 -0
  72. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +763 -0
  73. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +653 -0
  74. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +960 -0
  75. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +89 -0
  76. package/Components/ModalComponent/AudioPlayer/Components/index.ts +11 -0
  77. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +230 -0
  78. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +332 -0
  79. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  80. package/Components/ModalComponent/BottomSheetModalContent.tsx +383 -42
  81. package/Components/ModalComponent/Button/index.tsx +5 -3
  82. package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
  83. package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
  84. package/Components/ModalComponent/Header/index.tsx +20 -7
  85. package/Components/ModalComponent/Header/utils.ts +1 -1
  86. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  87. package/Components/ModalComponent/SortableList.tsx +142 -0
  88. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  89. package/Components/ModalComponent/TextInputContent.tsx +100 -0
  90. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  91. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +238 -0
  92. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  93. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +120 -0
  94. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  95. package/Components/ModalComponent/index.tsx +11 -0
  96. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +66 -0
  97. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  98. package/Components/ModalComponent/presets/index.ts +27 -0
  99. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  100. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  101. package/Components/ModalComponent/presets/types.ts +35 -0
  102. package/Components/ModalComponent/showTextInput.ts +43 -0
  103. package/Components/ModalComponent/utils.ts +82 -21
  104. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  105. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  106. package/Components/OfflineHandler/hooks.ts +111 -0
  107. package/Components/OfflineHandler/index.tsx +51 -73
  108. package/Components/OfflineHandler/utils/index.ts +1 -13
  109. package/Components/PlayerContainer/PlayerContainer.tsx +25 -17
  110. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  111. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
  112. package/Components/PreloaderWrapper/index.tsx +1 -1
  113. package/Components/River/ComponentsMap/ComponentsMap.tsx +81 -53
  114. package/Components/River/River.tsx +4 -0
  115. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +3 -15
  116. package/Components/River/__tests__/resultCallback.test.tsx +47 -0
  117. package/Components/Screen/TV/hooks/__tests__/useAfterPaint.test.ts +60 -0
  118. package/Components/Screen/TV/hooks/index.ts +2 -0
  119. package/Components/Screen/TV/hooks/useAfterPaint.ts +23 -0
  120. package/Components/Screen/TV/index.web.tsx +16 -7
  121. package/Components/Screen/index.tsx +8 -4
  122. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  123. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  124. package/Components/ScreenRevealManager/Overlay.tsx +34 -0
  125. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  126. package/Components/ScreenRevealManager/__tests__/Overlay.test.tsx +88 -0
  127. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  128. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  129. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +28 -22
  130. package/Components/TopCutoffOverlay/index.tsx +2 -2
  131. package/Components/Transitioner/Transitioner.tsx +28 -17
  132. package/Components/VideoLive/LiveImageManager.ts +57 -46
  133. package/Components/VideoLive/PlayerLiveImageComponent.tsx +4 -2
  134. package/Components/VideoModal/utils.ts +6 -1
  135. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  136. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  137. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  138. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  139. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  140. package/Helpers/index.js +7 -40
  141. package/Hooks/useEntryActionsExpander.ts +63 -0
  142. package/package.json +6 -5
  143. package/Components/Layout/TV/LayoutBackground.tsx +0 -31
  144. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  145. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  146. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  147. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  148. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  149. package/Components/Screen/utils.ts +0 -16
  150. package/Helpers/Analytics/index.js +0 -95
  151. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
  152. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -10,11 +10,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
10
10
  >
11
11
  <View
12
12
  onLayout={[Function]}
13
- style={
14
- {
15
- "flex": 1,
16
- }
17
- }
13
+ style={{}}
18
14
  >
19
15
  <RCTScrollView
20
16
  ListFooterComponent={
@@ -167,11 +163,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
167
163
  >
168
164
  <View
169
165
  onLayout={[Function]}
170
- style={
171
- {
172
- "flex": 1,
173
- }
174
- }
166
+ style={{}}
175
167
  >
176
168
  <View />
177
169
  </View>
@@ -183,11 +175,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
183
175
  >
184
176
  <View
185
177
  onLayout={[Function]}
186
- style={
187
- {
188
- "flex": 1,
189
- }
190
- }
178
+ style={{}}
191
179
  />
192
180
  </View>
193
181
  <View
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+
4
+ import { RiverComponent } from "../River";
5
+
6
+ const mockResolverProps: any[] = [];
7
+
8
+ jest.mock("../../ScreenResolver", () => ({
9
+ ScreenResolver: (props: any) => {
10
+ mockResolverProps.push(props);
11
+
12
+ return null;
13
+ },
14
+ }));
15
+
16
+ const river = { id: "river-parent-lock", type: "parent_lock" } as any;
17
+
18
+ const baseProps = {
19
+ river,
20
+ screenData: {},
21
+ groupId: "group-1",
22
+ scrollViewExtraProps: {},
23
+ setIsScreenWrappedInContainer: jest.fn(),
24
+ screenContext: { navBar: { setTitle: jest.fn(), setSummary: jest.fn() } },
25
+ } as any;
26
+
27
+ const resolved = () => mockResolverProps[mockResolverProps.length - 1];
28
+
29
+ describe("RiverComponent", () => {
30
+ beforeEach(() => {
31
+ mockResolverProps.length = 0;
32
+ });
33
+
34
+ it("forwards resultCallback to the screen it resolves", () => {
35
+ const resultCallback = jest.fn();
36
+
37
+ render(<RiverComponent {...baseProps} resultCallback={resultCallback} />);
38
+
39
+ expect(resolved().resultCallback).toBe(resultCallback);
40
+ });
41
+
42
+ it("resolves the screen without a resultCallback when none was given", () => {
43
+ render(<RiverComponent {...baseProps} />);
44
+
45
+ expect(resolved().resultCallback).toBeUndefined();
46
+ });
47
+ });
@@ -0,0 +1,60 @@
1
+ import { renderHook, act } from "@testing-library/react-native";
2
+
3
+ import { useAfterPaint } from "../useAfterPaint";
4
+
5
+ describe("useAfterPaint", () => {
6
+ let frameCallbacks: FrameRequestCallback[];
7
+ let originalRaf: typeof requestAnimationFrame;
8
+ let originalCancelRaf: typeof cancelAnimationFrame;
9
+
10
+ const flushFrame = () => {
11
+ const callbacks = frameCallbacks;
12
+ frameCallbacks = [];
13
+
14
+ act(() => {
15
+ callbacks.forEach((cb) => cb(0));
16
+ });
17
+ };
18
+
19
+ beforeEach(() => {
20
+ frameCallbacks = [];
21
+ originalRaf = global.requestAnimationFrame;
22
+ originalCancelRaf = global.cancelAnimationFrame;
23
+
24
+ global.requestAnimationFrame = jest.fn((cb: FrameRequestCallback) => {
25
+ frameCallbacks.push(cb);
26
+
27
+ return frameCallbacks.length;
28
+ }) as unknown as typeof requestAnimationFrame;
29
+
30
+ global.cancelAnimationFrame = jest.fn();
31
+ });
32
+
33
+ afterEach(() => {
34
+ global.requestAnimationFrame = originalRaf;
35
+ global.cancelAnimationFrame = originalCancelRaf;
36
+ });
37
+
38
+ it("returns false on the initial render", () => {
39
+ const { result } = renderHook(() => useAfterPaint());
40
+
41
+ expect(result.current).toBe(false);
42
+ });
43
+
44
+ it("stays false after only one animation frame (before paint completes)", () => {
45
+ const { result } = renderHook(() => useAfterPaint());
46
+
47
+ flushFrame();
48
+
49
+ expect(result.current).toBe(false);
50
+ });
51
+
52
+ it("returns true after two animation frames (a real post-paint boundary)", () => {
53
+ const { result } = renderHook(() => useAfterPaint());
54
+
55
+ flushFrame();
56
+ flushFrame();
57
+
58
+ expect(result.current).toBe(true);
59
+ });
60
+ });
@@ -1 +1,3 @@
1
1
  export { useInitialFocus } from "./useInitialFocus";
2
+
3
+ export { useAfterPaint } from "./useAfterPaint";
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+
3
+ export const useAfterPaint = (): boolean => {
4
+ const [painted, setPainted] = React.useState(false);
5
+
6
+ React.useEffect(() => {
7
+ let secondFrame: number | undefined;
8
+
9
+ const firstFrame = requestAnimationFrame(() => {
10
+ secondFrame = requestAnimationFrame(() => setPainted(true));
11
+ });
12
+
13
+ return () => {
14
+ cancelAnimationFrame(firstFrame);
15
+
16
+ if (secondFrame !== undefined) {
17
+ cancelAnimationFrame(secondFrame);
18
+ }
19
+ };
20
+ }, []);
21
+
22
+ return painted;
23
+ };
@@ -21,7 +21,7 @@ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/he
21
21
 
22
22
  import { NavBarContainer } from "../../Layout/TV/NavBarContainer";
23
23
  import { ScreenResolver } from "../../ScreenResolver";
24
- import { useInitialFocus } from "./hooks";
24
+ import { useInitialFocus, useAfterPaint } from "./hooks";
25
25
  import { isPlayerPlugin } from "@applicaster/zapp-react-native-utils/pluginUtils";
26
26
  import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils";
27
27
  import { FreezeWithCallback } from "../../FreezeWithCallback";
@@ -157,6 +157,13 @@ export const Screen = ({ route, Components }: Props) => {
157
157
 
158
158
  const isScreenActive = useIsScreenActive();
159
159
 
160
+ // We need to render menu first and then proceed with screen content,
161
+ // otherwise screen will stay black until everything is loaded and screen
162
+ // rendering put huge load the CPU pushing rendering even further.
163
+ // With this approach, menu will be rendered immediately and screen content
164
+ // will be rendered after paint, which makes it more responsive and prevents black screen.
165
+ const isContentReady = useAfterPaint();
166
+
160
167
  return (
161
168
  <FreezeWithCallback freeze={!isScreenActive} onRelease={onRelease}>
162
169
  <View style={[styles.container, { backgroundColor }]}>
@@ -167,12 +174,14 @@ export const Screen = ({ route, Components }: Props) => {
167
174
  navigationProps={navigationProps}
168
175
  />
169
176
  </NavBarContainer>
170
- <ScreenResolver
171
- screenType={screenType}
172
- screenId={screenId}
173
- screenData={screenData}
174
- groupId={route}
175
- />
177
+ {isContentReady ? (
178
+ <ScreenResolver
179
+ screenType={screenType}
180
+ screenId={screenId}
181
+ screenData={screenData}
182
+ groupId={route}
183
+ />
184
+ ) : null}
176
185
  </View>
177
186
  </FreezeWithCallback>
178
187
  );
@@ -18,10 +18,13 @@ import {
18
18
  useScreenData,
19
19
  } from "@applicaster/zapp-react-native-utils/reactHooks";
20
20
  import { getNavigationPluginModule } from "@applicaster/zapp-react-native-app/App/Layout/layoutHelpers";
21
+ import {
22
+ isValidColor,
23
+ isTransparentColor,
24
+ } from "@applicaster/zapp-react-native-utils/colorUtils";
21
25
 
22
26
  import { RouteManager } from "../RouteManager";
23
27
  import { useScreenConfiguration } from "../River/useScreenConfiguration";
24
- import { isValidColor } from "./utils";
25
28
  import { useWaitForValidOrientation } from "./hooks";
26
29
 
27
30
  const screenStyles = {
@@ -82,9 +85,10 @@ export function Screen(_props: Props) {
82
85
  const style = React.useMemo(
83
86
  () => ({
84
87
  ...screenStyles,
85
- backgroundColor: isValidColor(backgroundColor)
86
- ? backgroundColor
87
- : theme.app_background_color,
88
+ backgroundColor:
89
+ isValidColor(backgroundColor) && !isTransparentColor(backgroundColor)
90
+ ? backgroundColor
91
+ : theme.app_background_color,
88
92
  }),
89
93
  [theme.app_background_color, backgroundColor]
90
94
  );
@@ -8,9 +8,11 @@ import { componentsLogger } from "../../Helpers/logger";
8
8
  const logger = componentsLogger.addSubsystem("ScreenFeedLoader");
9
9
 
10
10
  /** Loads and provides `feedData` and store to */
11
- export const ScreenFeedLoader: React.FC<
12
- React.PropsWithChildren<{ id: string; feedData: any }>
13
- > = ({ id, feedData, children }) => {
11
+ export const ScreenFeedLoader: React.FC<{
12
+ id: string;
13
+ feedData: any;
14
+ children: React.ReactElement | null;
15
+ }> = ({ id, feedData, children }) => {
14
16
  const { source: feedUrl, mapping } = feedData;
15
17
 
16
18
  const { data, loading, error } = useFeedLoader({
@@ -8,7 +8,7 @@ export const ScreenResolverFeedProvider = ({
8
8
  useFeedData,
9
9
  }: {
10
10
  id: string;
11
- children: React.ReactNode;
11
+ children: React.ReactElement | null;
12
12
  useFeedData: (id: string) => Option<ZappDataSource>;
13
13
  }) => {
14
14
  const feedData = useFeedData(id);
@@ -0,0 +1,34 @@
1
+ import * as React from "react";
2
+ import { Animated, StyleSheet } from "react-native";
3
+
4
+ import { BackgroundImage } from "@applicaster/zapp-react-native-ui-components/Components/BackgroundImage";
5
+
6
+ const styles = StyleSheet.create({
7
+ container: {
8
+ ...StyleSheet.absoluteFillObject,
9
+ position: "absolute",
10
+ },
11
+ });
12
+
13
+ export const Overlay = ({
14
+ opacity,
15
+ backgroundColor,
16
+ }: {
17
+ opacity: Animated.Value;
18
+ backgroundColor: string;
19
+ }) => {
20
+ return (
21
+ <Animated.View
22
+ style={[
23
+ styles.container,
24
+ {
25
+ opacity,
26
+ backgroundColor,
27
+ },
28
+ ]}
29
+ testID="animated-component"
30
+ >
31
+ <BackgroundImage />
32
+ </Animated.View>
33
+ );
34
+ };
@@ -42,9 +42,25 @@ export class ScreenRevealManager {
42
42
  private subject$ = new Subject<void>();
43
43
  private subscription: Subscription;
44
44
 
45
- constructor(componentsToRender: ZappUIComponent[], callback: Callback) {
45
+ /**
46
+ * @param initialNumberToLoad how many components to wait for before revealing
47
+ * the screen. Staggering exists so a screen full of network-backed components
48
+ * does not load them all at once. A caller that has nothing to stagger - see
49
+ * `disableIncrementalLoading` on the HOC - passes the full count here so the
50
+ * reveal waits for exactly what gets rendered. Omitted, the count is derived
51
+ * from the components themselves.
52
+ */
53
+ constructor(
54
+ componentsToRender: ZappUIComponent[],
55
+ callback: Callback,
56
+ initialNumberToLoad?: number
57
+ ) {
46
58
  this.numberOfComponentsWaitToLoadBeforePresent =
47
- getNumberOfComponentsWaitToLoadBeforePresent(componentsToRender);
59
+ initialNumberToLoad == null
60
+ ? getNumberOfComponentsWaitToLoadBeforePresent(componentsToRender)
61
+ : // Clamped like the computed path: waiting for components that do not
62
+ // exist would hold the screen behind its overlay until the timeout.
63
+ Math.min(initialNumberToLoad, componentsToRender.length);
48
64
 
49
65
  this.renderingState = makeListOf<ComponentLoadingState>(
50
66
  COMPONENT_LOADING_STATE.UNKNOWN,
@@ -0,0 +1,88 @@
1
+ import React from "react";
2
+ import { Animated } from "react-native";
3
+ import { render } from "@testing-library/react-native";
4
+
5
+ jest.mock(
6
+ "@applicaster/zapp-react-native-ui-components/Components/BackgroundImage",
7
+ () => {
8
+ const { Text } = require("react-native");
9
+
10
+ return {
11
+ BackgroundImage: () => (
12
+ <Text testID="background-image">BackgroundImage</Text>
13
+ ),
14
+ };
15
+ }
16
+ );
17
+
18
+ import { Overlay } from "../Overlay";
19
+
20
+ const flattenStyle = (style: unknown): Record<string, unknown> => {
21
+ if (!style) {
22
+ return {};
23
+ }
24
+
25
+ if (Array.isArray(style)) {
26
+ return style.reduce<Record<string, unknown>>(
27
+ (acc, item) => ({ ...acc, ...flattenStyle(item) }),
28
+ {}
29
+ );
30
+ }
31
+
32
+ if (typeof style === "object") {
33
+ return style as Record<string, unknown>;
34
+ }
35
+
36
+ return {};
37
+ };
38
+
39
+ describe("Overlay", () => {
40
+ it("renders the animated overlay container", () => {
41
+ const opacity = new Animated.Value(1);
42
+
43
+ const { getByTestId } = render(
44
+ <Overlay opacity={opacity} backgroundColor="#000000" />
45
+ );
46
+
47
+ expect(getByTestId("animated-component")).toBeTruthy();
48
+ });
49
+
50
+ it("applies opacity and backgroundColor to the overlay", () => {
51
+ const opacity = new Animated.Value(0.5);
52
+
53
+ const { getByTestId } = render(
54
+ <Overlay opacity={opacity} backgroundColor="#ff0000" />
55
+ );
56
+
57
+ const style = flattenStyle(getByTestId("animated-component").props.style);
58
+
59
+ expect(style.backgroundColor).toBe("#ff0000");
60
+ expect(style.opacity).toBe(0.5);
61
+ });
62
+
63
+ it("positions the overlay with absolute fill layout", () => {
64
+ const opacity = new Animated.Value(1);
65
+
66
+ const { getByTestId } = render(
67
+ <Overlay opacity={opacity} backgroundColor="#000000" />
68
+ );
69
+
70
+ const style = flattenStyle(getByTestId("animated-component").props.style);
71
+
72
+ expect(style.position).toBe("absolute");
73
+ expect(style.top).toBe(0);
74
+ expect(style.left).toBe(0);
75
+ expect(style.right).toBe(0);
76
+ expect(style.bottom).toBe(0);
77
+ });
78
+
79
+ it("renders BackgroundImage inside the overlay", () => {
80
+ const opacity = new Animated.Value(1);
81
+
82
+ const { getByTestId } = render(
83
+ <Overlay opacity={opacity} backgroundColor="#000000" />
84
+ );
85
+
86
+ expect(getByTestId("background-image")).toBeTruthy();
87
+ });
88
+ });
@@ -22,6 +22,42 @@ import { makeListOf } from "@applicaster/zapp-react-native-utils/arrayUtils";
22
22
  import { isFirstComponentGallery } from "@applicaster/zapp-react-native-utils/componentsUtils";
23
23
  import { withTimeout$ } from "@applicaster/zapp-react-native-utils/idleUtils";
24
24
 
25
+ describe("ScreenRevealManager explicit component count", () => {
26
+ beforeEach(() => {
27
+ (isFirstComponentGallery as jest.Mock).mockReturnValue(false);
28
+
29
+ (makeListOf as jest.Mock).mockImplementation((value, length) =>
30
+ Array(length).fill(value)
31
+ );
32
+
33
+ (withTimeout$ as jest.Mock).mockReturnValue(new Subject());
34
+ });
35
+
36
+ it("waits for the number of components it is given, not the computed default", () => {
37
+ const components = Array(6).fill({}) as any;
38
+
39
+ const manager = new ScreenRevealManager(components, jest.fn(), 6);
40
+
41
+ expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(6);
42
+ });
43
+
44
+ it("never waits for more components than it was handed", () => {
45
+ const components = Array(3).fill({}) as any;
46
+
47
+ const manager = new ScreenRevealManager(components, jest.fn(), 10);
48
+
49
+ expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(3);
50
+ });
51
+
52
+ it("falls back to the computed default when given no explicit count", () => {
53
+ const components = Array(6).fill({}) as any;
54
+
55
+ const manager = new ScreenRevealManager(components, jest.fn());
56
+
57
+ expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(3);
58
+ });
59
+ });
60
+
25
61
  describe("ScreenRevealManager", () => {
26
62
  let mockCallback: jest.Mock;
27
63
  let timeout$: Subject<void>;
@@ -9,6 +9,12 @@ import {
9
9
  TIMEOUT,
10
10
  } from "../withScreenRevealManager";
11
11
 
12
+ jest.mock("@applicaster/zapp-react-native-utils/theme", () => ({
13
+ useTheme: () => ({ app_background_color: "#000000" }),
14
+ }));
15
+
16
+ jest.mock("../Overlay", () => ({ Overlay: () => null }));
17
+
12
18
  // jest.mock("react-native/Libraries/Animated/NativeAnimatedHelper");
13
19
 
14
20
  const MockComponent = ({
@@ -37,6 +43,56 @@ const MockComponent = ({
37
43
 
38
44
  const WrappedComponent = withScreenRevealManager(MockComponent);
39
45
 
46
+ describe("withScreenRevealManager disableIncrementalLoading", () => {
47
+ const components = (count: number) =>
48
+ Array(count).fill({ component_type: "grid-qb" });
49
+
50
+ beforeEach(() => {
51
+ jest.clearAllMocks();
52
+ });
53
+
54
+ it("asks for every component at once when incremental loading is disabled", () => {
55
+ render(
56
+ <WrappedComponent
57
+ componentsToRender={components(6)}
58
+ disableIncrementalLoading
59
+ />
60
+ );
61
+
62
+ expect(screen.getByTestId("mock-component").props.initialNumberToLoad).toBe(
63
+ 6
64
+ );
65
+ });
66
+
67
+ it("still tracks the component count after it changes", () => {
68
+ const { rerender } = render(
69
+ <WrappedComponent
70
+ componentsToRender={components(6)}
71
+ disableIncrementalLoading
72
+ />
73
+ );
74
+
75
+ rerender(
76
+ <WrappedComponent
77
+ componentsToRender={components(9)}
78
+ disableIncrementalLoading
79
+ />
80
+ );
81
+
82
+ expect(screen.getByTestId("mock-component").props.initialNumberToLoad).toBe(
83
+ 9
84
+ );
85
+ });
86
+
87
+ it("loads incrementally when the flag is not set", () => {
88
+ render(<WrappedComponent componentsToRender={components(6)} />);
89
+
90
+ expect(screen.getByTestId("mock-component").props.initialNumberToLoad).toBe(
91
+ 3
92
+ );
93
+ });
94
+ });
95
+
40
96
  describe.skip("withScreenRevealManager", () => {
41
97
  beforeEach(() => {
42
98
  jest.clearAllMocks();
@@ -1,8 +1,10 @@
1
1
  import * as React from "react";
2
- import { Animated, StyleSheet } from "react-native";
2
+ import { Animated } from "react-native";
3
+
3
4
  import { isFirstComponentScreenPicker } from "@applicaster/zapp-react-native-utils/componentsUtils";
4
5
  import { useRefWithInitialValue } from "@applicaster/zapp-react-native-utils/reactHooks/state/useRefWithInitialValue";
5
6
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
7
+ import { Overlay } from "./Overlay";
6
8
 
7
9
  import { ScreenRevealManager } from "./ScreenRevealManager";
8
10
  import {
@@ -10,13 +12,6 @@ import {
10
12
  emitScreenRevealManagerIsNotReadyToShow,
11
13
  } from "./utils";
12
14
 
13
- const styles = StyleSheet.create({
14
- container: {
15
- ...StyleSheet.absoluteFillObject,
16
- position: "absolute",
17
- },
18
- });
19
-
20
15
  export const TIMEOUT = 300; // 300 ms
21
16
 
22
17
  const HIDDEN = 0; // opacity = 0
@@ -26,11 +21,23 @@ export const SHOWN = 1; // opacity = 1
26
21
  type Props = {
27
22
  componentsToRender: ZappUIComponent[];
28
23
  backgroundColor?: string;
24
+ /**
25
+ * Renders every component at once instead of revealing them a few at a time.
26
+ *
27
+ * For a screen that builds its own components and holds all of them already -
28
+ * a form, say - staggering buys nothing and costs: each rebuild of the list
29
+ * tears it back down to the first few and lets the rest crawl in again.
30
+ *
31
+ * This is a flag rather than a count on purpose. A count would have to be
32
+ * re-read whenever the number of components changes, while "do not stagger"
33
+ * stays true whatever the screen ends up holding.
34
+ */
35
+ disableIncrementalLoading?: boolean;
29
36
  };
30
37
 
31
38
  export const withScreenRevealManager = (Component) => {
32
39
  return function WithScreenRevealManager(props: Props) {
33
- const { componentsToRender } = props;
40
+ const { componentsToRender, disableIncrementalLoading } = props;
34
41
 
35
42
  const [isContentReadyToBeShown, setIsContentReadyToBeShown] =
36
43
  React.useState(false);
@@ -47,7 +54,8 @@ export const withScreenRevealManager = (Component) => {
47
54
  () =>
48
55
  new ScreenRevealManager(
49
56
  componentsToRender,
50
- handleSetIsContentReadyToBeShown
57
+ handleSetIsContentReadyToBeShown,
58
+ disableIncrementalLoading ? componentsToRender.length : undefined
51
59
  )
52
60
  );
53
61
 
@@ -85,7 +93,11 @@ export const withScreenRevealManager = (Component) => {
85
93
  <Component
86
94
  {...props}
87
95
  initialNumberToLoad={
88
- managerRef.current.numberOfComponentsWaitToLoadBeforePresent
96
+ // Recomputed on every render, so the count follows the components
97
+ // even though the manager settled its own expectation at mount.
98
+ disableIncrementalLoading
99
+ ? componentsToRender.length
100
+ : managerRef.current.numberOfComponentsWaitToLoadBeforePresent
89
101
  }
90
102
  onLoadFinishedFromScreenRevealManager={
91
103
  managerRef.current.onLoadFinished
@@ -93,17 +105,11 @@ export const withScreenRevealManager = (Component) => {
93
105
  onLoadFailedFromScreenRevealManager={managerRef.current.onLoadFailed}
94
106
  />
95
107
  {isShowOverlay ? (
96
- <Animated.View
97
- style={[
98
- styles.container,
99
- {
100
- opacity: opacityRef.current,
101
- // TODO: we should support background image as well, but for now we will use background color from theme
102
- backgroundColor:
103
- props.backgroundColor ?? theme.app_background_color,
104
- },
105
- ]}
106
- testID="animated-component"
108
+ <Overlay
109
+ opacity={opacityRef.current}
110
+ backgroundColor={
111
+ props.backgroundColor ?? theme.app_background_color
112
+ }
107
113
  />
108
114
  ) : null}
109
115
  </>
@@ -7,7 +7,7 @@ import { useMarginTop } from "./hooks";
7
7
 
8
8
  interface IProps {
9
9
  targetScreenId?: string;
10
- children?: React.ReactNode;
10
+ children: React.ReactElement | null;
11
11
  applyTopCutoff?: boolean;
12
12
  }
13
13
 
@@ -21,7 +21,7 @@ export const TopCutoffOverlay: React.FC<IProps> = ({
21
21
  targetScreenId,
22
22
  children,
23
23
  applyTopCutoff = true,
24
- }: IProps) => {
24
+ }) => {
25
25
  const cutoffHeight = useMarginTop(targetScreenId);
26
26
 
27
27
  const { backgroundColor: screenBackgroundColor } =