@applicaster/zapp-react-native-ui-components 16.0.0-alpha.2851031376 → 16.0.0-alpha.3029086545

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 (70) hide show
  1. package/Components/CellRendererResolver/index.ts +1 -1
  2. package/Components/Layout/FullWidthRow.tsx +38 -0
  3. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  4. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  5. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  6. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  7. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  8. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  9. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  10. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  11. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  12. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  13. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  14. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  15. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  16. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
  17. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  18. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  19. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  20. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  21. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  22. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  23. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  24. package/Components/MasterCell/dataAdapter.ts +15 -5
  25. package/Components/MasterCell/hooks/index.ts +2 -0
  26. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  27. package/Components/MasterCell/index.tsx +30 -5
  28. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  29. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  30. package/Components/MasterCell/utils/index.ts +85 -46
  31. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +332 -0
  32. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +402 -0
  33. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +811 -0
  34. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +671 -0
  35. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +878 -0
  36. package/Components/ModalComponent/AudioPlayer/Components/index.ts +9 -0
  37. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  38. package/Components/ModalComponent/BottomSheetModalContent.tsx +49 -26
  39. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  40. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  41. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  42. package/Components/OfflineHandler/hooks.ts +123 -0
  43. package/Components/OfflineHandler/index.tsx +51 -73
  44. package/Components/OfflineHandler/utils/index.ts +1 -13
  45. package/Components/PlayerContainer/PlayerContainer.tsx +7 -3
  46. package/Components/PreloaderWrapper/index.tsx +1 -1
  47. package/Components/River/ComponentsMap/ComponentsMap.tsx +2 -2
  48. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +3 -15
  49. package/Components/Screen/index.tsx +8 -4
  50. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  51. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  52. package/Components/TopCutoffOverlay/index.tsx +2 -2
  53. package/Components/Transitioner/Transitioner.tsx +28 -17
  54. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  55. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  56. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  57. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  58. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  59. package/Helpers/index.js +0 -39
  60. package/package.json +5 -5
  61. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  62. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  63. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  64. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  65. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  66. package/Components/Screen/utils.ts +0 -16
  67. package/Helpers/Analytics/index.js +0 -95
  68. /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
  69. /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
  70. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
@@ -1,105 +1,83 @@
1
- import React, { useCallback, useEffect, useRef, useState } from "react";
2
- import { View, StyleSheet } from "react-native";
3
- import { NotificationView as NotificationViewNative } from "./NotificationView/NotificationView";
4
- import { NotificationView as NotificationViewSamsung } from "./NotificationView/NotificationView.samsung";
5
- import { NotificationView as NotificationViewLG } from "./NotificationView/NotificationView.lg";
6
-
1
+ import React, { useCallback, useEffect } from "react";
7
2
  import { useConnectionInfo } from "@applicaster/zapp-react-native-utils/reactHooks/connection";
8
3
  import { usePrevious } from "@applicaster/zapp-react-native-utils/reactHooks/utils";
9
- import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
4
+ import { useSubscriberFor } from "@applicaster/zapp-react-native-utils/reactHooks/useSubscriberFor";
10
5
 
6
+ import { useOfflineHandlerProps } from "./hooks";
11
7
  import { componentsLogger } from "../../Helpers/logger";
12
8
  import { parseConnectionInfo } from "./utils";
13
9
 
14
10
  const logger = componentsLogger.addSubsystem("OfflineHandler");
15
11
 
12
+ // mirrors NOTIFICATION_TOAST_EVENTS.SHOW_TOAST in
13
+ // @applicaster/quick-brick-core/App/NotificationToastRenderer/NotificationToastManager
14
+ // (kept as a literal here since this package must not depend on quick-brick-core)
15
+ const SHOW_TOAST_EVENT = "showToast";
16
+ const OFFLINE_STATUS_TOAST_ID = "offline-status";
17
+
16
18
  type Props = {
17
19
  children?: React.ReactElement<any>;
18
20
  };
19
21
 
20
- const styles = StyleSheet.create({
21
- container: {
22
- flex: 1,
23
- position: "relative",
24
- },
25
- });
26
-
27
22
  const timers = {
28
23
  goingOffline: 5000,
29
24
  goingOnline: 2000,
30
25
  };
31
26
 
32
- export const NotificationView = platformSelect({
33
- samsung_tv: NotificationViewSamsung,
34
- lg_tv: NotificationViewLG,
35
- default: NotificationViewNative,
36
- });
37
-
38
27
  export const OfflineHandler = ({ children }: Props) => {
39
28
  const connectionInfo = useConnectionInfo(false);
40
- const { online, deviceOnline } = parseConnectionInfo(connectionInfo);
29
+ const { online } = parseConnectionInfo(connectionInfo);
30
+ const prevOnline = usePrevious(online);
41
31
 
42
- const [hidden, setHidden] = useState<boolean>(true);
43
- const previousOnline: boolean = usePrevious(online);
44
- const timer = useRef<NodeJS.Timeout | null>(null);
45
- const initialRender = useRef(false);
32
+ const emitShowToast = useSubscriberFor(SHOW_TOAST_EVENT);
46
33
 
47
- const dismiss = useCallback(() => {
48
- setHidden(true);
34
+ const { message, extraMessage, style } = useOfflineHandlerProps(online);
49
35
 
50
- if (timer.current) {
51
- clearTimeout(timer.current);
52
- timer.current = null;
53
- }
54
- }, []);
36
+ const showOnlineToast = useCallback(() => {
37
+ logger.log("Device is back online, emit toast");
55
38
 
56
- const setTimer = useCallback((cb, duration) => {
57
- if (timer.current) {
58
- clearTimeout(timer.current);
59
- }
39
+ emitShowToast({
40
+ id: OFFLINE_STATUS_TOAST_ID,
41
+ message,
42
+ extraMessage,
43
+ style,
44
+ timeout: timers.goingOnline,
45
+ });
46
+ }, [emitShowToast, message, extraMessage, style]);
47
+
48
+ const showOfflineToast = useCallback(() => {
49
+ logger.log("Device went offline, emit toast");
60
50
 
61
- timer.current = setTimeout(cb, duration);
62
- }, []);
51
+ emitShowToast({
52
+ id: OFFLINE_STATUS_TOAST_ID,
53
+ message,
54
+ extraMessage,
55
+ style,
56
+ timeout: timers.goingOffline,
57
+ });
58
+ }, [emitShowToast, message, extraMessage, style]);
63
59
 
64
60
  useEffect(() => {
65
- if (initialRender.current) {
66
- const wentOffline = previousOnline && online === false;
67
- const wentOnline = !previousOnline && online;
68
- const renderedOffline = !online && previousOnline === null;
69
-
70
- if (!deviceOnline || wentOffline || renderedOffline) {
71
- logger.log("Device went offline");
72
- setTimer(() => setHidden(true), timers.goingOffline);
73
- setHidden(false);
61
+ // On mount there is no previous state yet: only surface the offline toast
62
+ // for a device that starts offline. Don't announce "back online" for a
63
+ // device that was online to begin with.
64
+ if (prevOnline === undefined) {
65
+ if (!online) {
66
+ showOfflineToast();
74
67
  }
75
68
 
76
- if (wentOnline) {
77
- logger.log("Device is back online");
78
- setHidden(false);
79
- setTimer(() => setHidden(true), timers.goingOnline);
80
- }
81
- } else {
82
- initialRender.current = true;
69
+ return;
83
70
  }
84
71
 
85
- return () => {
86
- if (timer.current) {
87
- clearTimeout(timer.current);
88
- timer.current = null;
72
+ // On subsequent renders, only emit a toast when the online/offline state changes.
73
+ if (prevOnline !== online) {
74
+ if (!online) {
75
+ showOfflineToast();
76
+ } else {
77
+ showOnlineToast();
89
78
  }
90
- };
91
- }, [connectionInfo]);
92
-
93
- return (
94
- <View style={styles.container}>
95
- <NotificationView
96
- hidden={hidden}
97
- online={online}
98
- previousOnline={previousOnline}
99
- dismiss={dismiss}
100
- >
101
- {children}
102
- </NotificationView>
103
- </View>
104
- );
79
+ }
80
+ }, [online, prevOnline, showOfflineToast, showOnlineToast]);
81
+
82
+ return children;
105
83
  };
@@ -1,9 +1,8 @@
1
1
  import * as R from "ramda";
2
-
3
2
  import { NetInfoState } from "@react-native-community/netinfo";
4
3
  import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
5
4
  import { isOnline as isOnlineUtils } from "@applicaster/quick-brick-core/App/NetworkStatusProvider/utils";
6
- import { useSafeAreaInsets } from "react-native-safe-area-context";
5
+
7
6
  import { Platform } from "react-native";
8
7
 
9
8
  type ConnectionInfo = NetInfoState & { deviceStatus: NetStatus };
@@ -51,14 +50,3 @@ export const parseConnectionInfo: typeof parseConnectionInfoForDefault =
51
50
  samsung_tv: parseConnectionInfoForSamsung,
52
51
  default: parseConnectionInfoForDefault,
53
52
  });
54
-
55
- export const useNotificationHeight = () => {
56
- const insets = useSafeAreaInsets();
57
-
58
- const navBarHeight = platformSelect({ ios: 44, android: 56 });
59
-
60
- const statusHeight = insets.top;
61
- const notificationHeight = statusHeight + navBarHeight;
62
-
63
- return { statusHeight, notificationHeight };
64
- };
@@ -344,11 +344,15 @@ const PlayerContainerComponent = (props: Props) => {
344
344
  setIsLoadingNextVideo(false);
345
345
 
346
346
  const resumeTime = Number(item?.extensions?.resumeTime);
347
+ const activePlayer = playerManager.getActivePlayer();
347
348
 
348
- // Сhecking that the player itself knows where to start playing, and there is no need to call seekTo after returning from the Сhromecast
349
+ // Сhecking that the player itself knows where to start playing, and there is no need to call seekTo after returning from the Сhromecast.
350
+ // Live streams and a missing/zero resume position are skipped: seeking to 0 on
351
+ // a live stream pulls playback off the live edge into a buffer-starvation seek loop.
349
352
  if (
350
- !playerManager.getActivePlayer()?.hasResumePosition() &&
351
- !isNaN(resumeTime)
353
+ !activePlayer?.hasResumePosition() &&
354
+ resumeTime > 0 &&
355
+ !activePlayer?.isLive()
352
356
  ) {
353
357
  player?.seekTo(resumeTime);
354
358
  }
@@ -2,7 +2,7 @@ import React from "react";
2
2
 
3
3
  type PreloaderWrapperProps = {
4
4
  showPreloader?: boolean;
5
- children?: React.ReactNode;
5
+ children: React.ReactElement | null;
6
6
  };
7
7
 
8
8
  export const PreloaderWrapper: React.FC<PreloaderWrapperProps> = ({
@@ -9,7 +9,7 @@ import { useScreenConfiguration } from "../useScreenConfiguration";
9
9
  import { RefreshControl } from "../RefreshControl";
10
10
  import { ifEmptyUseFallback } from "@applicaster/zapp-react-native-utils/cellUtils";
11
11
  import {
12
- usePipesCacheReset,
12
+ useMarkPipesDataStale,
13
13
  useProfilerLogging,
14
14
  } from "@applicaster/zapp-react-native-utils/reactHooks";
15
15
  import { useLoadingState } from "./hooks/useLoadingState";
@@ -154,7 +154,7 @@ function ComponentsMapComponent(props: Props) {
154
154
  [flatListHeight]
155
155
  );
156
156
 
157
- usePipesCacheReset(riverId, riverComponents);
157
+ useMarkPipesDataStale(riverId, riverComponents);
158
158
 
159
159
  const refreshControl = React.useMemo(
160
160
  () => (pullToRefreshEnabled ? <RefreshControl /> : null),
@@ -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
@@ -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);
@@ -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 } =
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { View } from "react-native";
3
- import { append, compose, identity, isNil, tail, take, update } from "ramda";
3
+ import { append, compose, identity, tail, take, update } from "ramda";
4
4
 
5
5
  import { AnimationManager } from "./AnimationManager";
6
6
  import { Scene } from "./Scene";
@@ -10,6 +10,7 @@ import {
10
10
  shouldSkipAnimationForPreviousWebViewScene,
11
11
  } from "./utils";
12
12
  import { v4 as uuid_v4 } from "uuid";
13
+ import { isScreenActiveForRoute } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useIsScreenActive";
13
14
 
14
15
  export const NAV_ACTION_PUSH = "PUSH";
15
16
 
@@ -50,16 +51,16 @@ function scenesForBackTransition(props: SceneProps, { scenes }) {
50
51
  : [prepareScene(props.children), scenes[0]];
51
52
  }
52
53
 
53
- const isModalClosed = (navigator) => {
54
- return !!isNil(navigator?.modalData);
55
- };
56
-
57
54
  type Props = {
58
55
  transitionConfig: any;
59
56
  children: React.ReactNode;
60
57
  navigator: {
61
58
  previousAction: string;
62
59
  currentRoute: string;
60
+ videoModalState?: {
61
+ visible?: boolean;
62
+ mode?: string;
63
+ };
63
64
  screenData: {
64
65
  [key: string]: any;
65
66
  };
@@ -283,8 +284,10 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
283
284
  // so only a single scene should be rendered,
284
285
  // instead of dangerous nulls or duplicates.
285
286
  renderSingleScene({ scenes, to }) {
286
- const { contentStyle } = this.props;
287
- const screenData = this.props.children.props.screenData;
287
+ const { contentStyle, navigator, children } = this.props;
288
+
289
+ const { currentRoute, videoModalState } = navigator;
290
+ const screenData = children.props.screenData;
288
291
  const pathname = scenes[to.index].key;
289
292
 
290
293
  return (
@@ -293,7 +296,11 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
293
296
  <Scene
294
297
  {...{ style: to?.style || {} }}
295
298
  pathname={pathname}
296
- isActive={pathname === this.props.navigator.currentRoute}
299
+ isActive={isScreenActiveForRoute(
300
+ pathname,
301
+ currentRoute,
302
+ videoModalState
303
+ )}
297
304
  contentStyle={contentStyle}
298
305
  key={scenes[to.index].key}
299
306
  screenUniqueId={scenes[to.index].screenUniqueId}
@@ -311,16 +318,19 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
311
318
  // Looks redundant, but when written like this
312
319
  // we do not need to pass or generate keys.
313
320
  renderScenes({ scenes, from, to }) {
314
- const { contentStyle } = this.props;
321
+ const { contentStyle, navigator } = this.props;
322
+
323
+ const { currentRoute, videoModalState } = navigator;
315
324
 
316
325
  const fromScene = scenes[from.index] && (
317
326
  <Scene
318
327
  key={scenes[from.index].key}
319
328
  pathname={scenes[from.index].key}
320
- isActive={
321
- scenes[from.index].key === this.props.navigator.currentRoute &&
322
- isModalClosed(this.props.navigator)
323
- }
329
+ isActive={isScreenActiveForRoute(
330
+ scenes[from.index].key,
331
+ currentRoute,
332
+ videoModalState
333
+ )}
324
334
  style={from.style}
325
335
  pointerEvents="none"
326
336
  overlayStyle={from.overlayStyle}
@@ -336,10 +346,11 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
336
346
  const toScene = scenes[to.index] && (
337
347
  <Scene
338
348
  pathname={scenes[to.index].key}
339
- isActive={
340
- scenes[to.index].key === this.props.navigator.currentRoute &&
341
- isModalClosed(this.props.navigator)
342
- }
349
+ isActive={isScreenActiveForRoute(
350
+ scenes[to.index].key,
351
+ currentRoute,
352
+ videoModalState
353
+ )}
343
354
  key={scenes[to.index].key}
344
355
  style={to.style}
345
356
  overlayStyle={to.overlayStyle}
@@ -1,10 +1,11 @@
1
1
  import React from "react";
2
- import { StyleSheet, View } from "react-native";
3
-
2
+ import { StyleSheet, SafeAreaView as RNSafeAreaView } from "react-native";
3
+ import { SafeAreaView } from "react-native-safe-area-context";
4
4
  import { useIsRTL } from "@applicaster/zapp-react-native-utils/localizationUtils";
5
5
  import CloseButton from "./Buttons/CloseButton";
6
6
  import BackButton from "./Buttons/BackButton";
7
7
  import BarTitle from "./BarTitle";
8
+ import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
8
9
 
9
10
  type Props = {
10
11
  screenStyles: {
@@ -23,6 +24,11 @@ const defaultState: NavBarState = {
23
24
  title: "",
24
25
  };
25
26
 
27
+ const SafeAreaViewPlatform = platformSelect({
28
+ ios: RNSafeAreaView,
29
+ android: SafeAreaView,
30
+ });
31
+
26
32
  const BarView = ({ screenStyles, barState = defaultState }: Props) => {
27
33
  const isRTL = useIsRTL();
28
34
 
@@ -44,14 +50,15 @@ const BarView = ({ screenStyles, barState = defaultState }: Props) => {
44
50
  );
45
51
 
46
52
  return (
47
- <View
53
+ <SafeAreaViewPlatform
54
+ edges={["top", "left", "right"]}
48
55
  style={[style.view, isRTL ? style.rtlStyle : {}]}
49
- testID="BarView-Container"
56
+ testID="BarView-safeAreaView"
50
57
  >
51
58
  {backButton}
52
59
  <BarTitle title={barState.title} screenStyles={screenStyles} />
53
60
  {closeButton}
54
- </View>
61
+ </SafeAreaViewPlatform>
55
62
  );
56
63
  };
57
64
 
@@ -46,7 +46,7 @@ describe("BarView", () => {
46
46
  <BarView screenStyles={screenStyles} barState={barState} />
47
47
  );
48
48
 
49
- expect(getByTestId("BarView-Container").props.style).toContainEqual({
49
+ expect(getByTestId("BarView-safeAreaView").props.style).toContainEqual({
50
50
  transform: [{ scaleX: -1 }],
51
51
  });
52
52
  });
@@ -58,7 +58,7 @@ describe("BarView", () => {
58
58
  <BarView screenStyles={screenStyles} barState={barState} />
59
59
  );
60
60
 
61
- expect(getByTestId("BarView-Container").props.style).not.toContainEqual({
61
+ expect(getByTestId("BarView-safeAreaView").props.style).not.toContainEqual({
62
62
  transform: [{ scaleX: -1 }],
63
63
  });
64
64
  });
@@ -0,0 +1,154 @@
1
+ import { renderHook, act } from "@testing-library/react-native";
2
+
3
+ type DimensionsStore = {
4
+ setState: (
5
+ partial: { dimensions: Record<string, unknown>; ids: string[] },
6
+ replace?: boolean
7
+ ) => void;
8
+ };
9
+
10
+ const createdStores: DimensionsStore[] = [];
11
+
12
+ jest.mock("zustand", () => {
13
+ const actual = jest.requireActual<typeof import("zustand")>("zustand");
14
+
15
+ return {
16
+ ...actual,
17
+ create: ((initializer: Parameters<typeof actual.create>[0]) => {
18
+ const store = actual.create(initializer);
19
+ createdStores.push(store);
20
+
21
+ return store;
22
+ }) as typeof actual.create,
23
+ };
24
+ });
25
+
26
+ import { useCachedDimensions } from "../index";
27
+
28
+ const resetStore = () => {
29
+ const store = createdStores[createdStores.length - 1];
30
+
31
+ act(() => {
32
+ store?.setState({ dimensions: {}, ids: [] });
33
+ });
34
+ };
35
+
36
+ describe("useCachedDimensions", () => {
37
+ beforeEach(() => {
38
+ resetStore();
39
+ });
40
+
41
+ describe("getCachedDimensions", () => {
42
+ it("returns default dimensions when id has no cached value", () => {
43
+ const { result } = renderHook(() => useCachedDimensions("component-1"));
44
+
45
+ expect(result.current.getCachedDimensions()).toEqual({
46
+ width: undefined,
47
+ height: undefined,
48
+ });
49
+ });
50
+
51
+ it("returns cached dimensions after setCachedDimensions", () => {
52
+ const { result } = renderHook(() => useCachedDimensions("component-1"));
53
+ const dimensions = { width: 100, height: 200 };
54
+
55
+ act(() => {
56
+ result.current.setCachedDimensions(dimensions);
57
+ });
58
+
59
+ expect(result.current.getCachedDimensions()).toEqual(dimensions);
60
+ });
61
+ });
62
+
63
+ describe("setCachedDimensions", () => {
64
+ it("updates dimensions for the hook id", () => {
65
+ const { result } = renderHook(() => useCachedDimensions("component-1"));
66
+
67
+ act(() => {
68
+ result.current.setCachedDimensions({ width: 50, height: 75 });
69
+ });
70
+
71
+ expect(result.current.getCachedDimensions()).toEqual({
72
+ width: 50,
73
+ height: 75,
74
+ });
75
+ });
76
+
77
+ it("shares cached dimensions between hook instances with the same id", () => {
78
+ const { result: first } = renderHook(() =>
79
+ useCachedDimensions("shared-id")
80
+ );
81
+
82
+ const { result: second } = renderHook(() =>
83
+ useCachedDimensions("shared-id")
84
+ );
85
+
86
+ act(() => {
87
+ first.current.setCachedDimensions({ width: 10, height: 20 });
88
+ });
89
+
90
+ expect(second.current.getCachedDimensions()).toEqual({
91
+ width: 10,
92
+ height: 20,
93
+ });
94
+ });
95
+ });
96
+
97
+ describe("getLastCachedDimensions", () => {
98
+ it("returns undefined when no dimensions have been cached", () => {
99
+ const { result } = renderHook(() => useCachedDimensions("component-1"));
100
+
101
+ expect(result.current.getLastCachedDimensions()).toBeUndefined();
102
+ });
103
+
104
+ it("returns the most recently cached dimensions across ids", () => {
105
+ const { result: first } = renderHook(() => useCachedDimensions("first"));
106
+
107
+ const { result: second } = renderHook(() =>
108
+ useCachedDimensions("second")
109
+ );
110
+
111
+ act(() => {
112
+ first.current.setCachedDimensions({ width: 100, height: 100 });
113
+ });
114
+
115
+ act(() => {
116
+ second.current.setCachedDimensions({ width: 200, height: 200 });
117
+ });
118
+
119
+ expect(first.current.getLastCachedDimensions()).toEqual({
120
+ width: 200,
121
+ height: 200,
122
+ });
123
+ });
124
+
125
+ it("returns the latest dimensions when the same id is updated multiple times", () => {
126
+ const { result } = renderHook(() => useCachedDimensions("component-1"));
127
+
128
+ act(() => {
129
+ result.current.setCachedDimensions({ width: 100, height: 100 });
130
+ });
131
+
132
+ act(() => {
133
+ result.current.setCachedDimensions({ width: 300, height: 400 });
134
+ });
135
+
136
+ expect(result.current.getLastCachedDimensions()).toEqual({
137
+ width: 300,
138
+ height: 400,
139
+ });
140
+ });
141
+ });
142
+
143
+ describe("return value", () => {
144
+ it("exposes getCachedDimensions, setCachedDimensions, and getLastCachedDimensions", () => {
145
+ const { result } = renderHook(() => useCachedDimensions("component-1"));
146
+
147
+ expect(result.current).toEqual({
148
+ getCachedDimensions: expect.any(Function),
149
+ setCachedDimensions: expect.any(Function),
150
+ getLastCachedDimensions: expect.any(Function),
151
+ });
152
+ });
153
+ });
154
+ });