@applicaster/zapp-react-native-ui-components 12.0.0-alpha.4840257570 → 12.0.0-alpha.4885205607

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 (76) hide show
  1. package/Components/BaseFocusable/index.tsx +3 -3
  2. package/Components/Cell/Cell.tsx +30 -9
  3. package/Components/Cell/CellWithFocusable.tsx +23 -9
  4. package/Components/Cell/TvOSCellComponent.tsx +3 -7
  5. package/Components/Focusable/Focusable.tsx +6 -1
  6. package/Components/FocusableGroup/index.tsx +11 -2
  7. package/Components/HandlePlayable/HandlePlayable.tsx +3 -0
  8. package/Components/Layout/TV/ScreenContainer.tsx +79 -5
  9. package/Components/Layout/TV/ScreenLayoutContextProvider.tsx +2 -2
  10. package/Components/Layout/TV/__tests__/ScreenContainer.test.tsx +38 -28
  11. package/Components/Layout/TV/__tests__/__snapshots__/ScreenContainer.test.tsx.snap +25 -13
  12. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +10 -50
  13. package/Components/Layout/TV/__tests__/index.test.tsx +20 -75
  14. package/Components/Layout/TV/index.tsx +14 -67
  15. package/Components/Layout/TV/index.web.tsx +33 -0
  16. package/Components/Layout/index.tsx +15 -11
  17. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +2 -1
  18. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +88 -112
  19. package/Components/MasterCell/DefaultComponents/DynamicBadge.tsx +4 -2
  20. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -1
  21. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +20 -0
  22. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/index.android.tsx +4 -1
  23. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/index.tsx +10 -2
  24. package/Components/MasterCell/dataAdapter.ts +21 -7
  25. package/Components/MasterCell/index.tsx +5 -3
  26. package/Components/MasterCell/utils/__tests__/insertBetween.test.js +9 -0
  27. package/Components/MasterCell/utils/index.ts +32 -29
  28. package/Components/Navigator/StackNavigator.tsx +57 -0
  29. package/Components/Navigator/index.ts +1 -0
  30. package/Components/PlayerContainer/PlayerContainer.tsx +5 -1
  31. package/Components/River/ComponentsMap/ComponentsMap.tsx +6 -0
  32. package/Components/River/RefreshControl.tsx +2 -2
  33. package/Components/River/TV/River.tsx +0 -23
  34. package/Components/RouteManager/index.tsx +2 -2
  35. package/Components/Screen/TV/hooks/index.ts +3 -0
  36. package/Components/Screen/TV/hooks/useInitialFocus.ts +70 -0
  37. package/Components/Screen/TV/hooks/useRestoreFocus.ts +22 -0
  38. package/Components/Screen/TV/index.web.tsx +165 -0
  39. package/Components/Screen/__tests__/Screen.test.tsx +3 -0
  40. package/Components/Screen/index.tsx +55 -58
  41. package/Components/Screen/navigationHandler.ts +5 -0
  42. package/Components/ScreenResolver/index.tsx +9 -12
  43. package/Components/Transitioner/Scene.tsx +32 -26
  44. package/Components/Transitioner/Transitioner.tsx +38 -43
  45. package/Components/Transitioner/__tests__/__snapshots__/Scene.test.js.snap +12 -4
  46. package/Components/Transitioner/__tests__/__snapshots__/transitioner.test.js.snap +1 -0
  47. package/Components/VideoLive/LiveImageManager.ts +28 -1
  48. package/Components/VideoLive/PlayerLiveImageComponent.tsx +52 -24
  49. package/Components/VideoLive/__tests__/__snapshots__/PlayerLiveImageComponent.test.tsx.snap +26 -24
  50. package/Components/VideoModal/ModalAnimation/AnimatedPlayerModalWrapper.tsx +2 -14
  51. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +56 -43
  52. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.tsx +42 -32
  53. package/Components/VideoModal/ModalAnimation/AnimationComponent.tsx +104 -86
  54. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +32 -4
  55. package/Components/VideoModal/ModalAnimation/utils.ts +7 -4
  56. package/Components/VideoModal/PlayerDetails.tsx +3 -3
  57. package/Components/VideoModal/PlayerWrapper.tsx +67 -60
  58. package/Components/VideoModal/VideoModal.tsx +4 -4
  59. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +7 -0
  60. package/Components/VideoModal/__tests__/__snapshots__/PlayerWrapper.test.tsx.snap +32 -12
  61. package/Components/VideoModal/hooks/useModalSize.ts +10 -12
  62. package/Components/VideoModal/utils.ts +5 -2
  63. package/Components/Viewport/ViewportTracker/ViewportTracker.tsx +31 -40
  64. package/Components/default-cell-renderer/__tests__/__snapshots__/defaultCellRenderer.test.tsx.snap +4 -4
  65. package/Contexts/FocusableGroupContext/FocusableGroupContext.ts +3 -0
  66. package/Contexts/FocusableGroupContext/FocusableGroupContextProvider.tsx +16 -0
  67. package/Contexts/FocusableGroupContext/__tests__/FocusableGroupContextProvider.test.tsx +22 -0
  68. package/Contexts/FocusableGroupContext/__tests__/withFocusableContext.test.tsx +26 -0
  69. package/Contexts/FocusableGroupContext/index.ts +7 -0
  70. package/Contexts/FocusableGroupContext/withFocusableContext.tsx +17 -0
  71. package/Contexts/ScreenDataContext/index.tsx +20 -0
  72. package/Contexts/ZappPipesContext/ZappPipesEntryContext.ts +1 -1
  73. package/package.json +5 -5
  74. package/Components/Layout/TV/TransitionOverlay.tsx +0 -64
  75. /package/Components/FocusableGroup/{index.android.js → index.android.ts} +0 -0
  76. /package/Components/FocusableGroup/{index.ios.js → index.ios.ts} +0 -0
@@ -7,6 +7,10 @@ import type {
7
7
  ContainerProps,
8
8
  ContainerChildren,
9
9
  } from "./types";
10
+ import {
11
+ useIsRTL,
12
+ applyRTLStylesIfNeeded,
13
+ } from "@applicaster/zapp-react-native-utils/localizationUtils";
10
14
 
11
15
  const HorizontalSeparator = ({ width }: HorizontalSeparatorProps) => (
12
16
  <View style={{ width }} />
@@ -17,6 +21,7 @@ export function ButtonContainerView({
17
21
  children,
18
22
  ...otherProps
19
23
  }: ContainerProps) {
24
+ const isRTL = useIsRTL();
20
25
  const horizontalGutter = R.pathOr(0, ["horizontalGutter"], otherProps);
21
26
 
22
27
  const filteredChildren = useFilterChildren<ContainerChildren>(children);
@@ -26,10 +31,13 @@ export function ButtonContainerView({
26
31
  }
27
32
 
28
33
  return (
29
- <View style={style}>
34
+ <View style={applyRTLStylesIfNeeded(style, isRTL)}>
30
35
  {insertBetween(
31
36
  (index) => (
32
- <HorizontalSeparator key={index} width={horizontalGutter} />
37
+ <HorizontalSeparator
38
+ key={`separator_${index}`}
39
+ width={horizontalGutter}
40
+ />
33
41
  ),
34
42
  filteredChildren
35
43
  )}
@@ -37,14 +37,28 @@ function retrieveData(entry, func, args) {
37
37
  */
38
38
 
39
39
  export function configInflater(
40
- entry,
41
- { type, style, additionalProps = {}, data = [], elements }
40
+ entry: any,
41
+ {
42
+ type,
43
+ style,
44
+ additionalProps = {},
45
+ data = [],
46
+ elements,
47
+ }: {
48
+ type: any;
49
+ style: any;
50
+ additionalProps?: Record<string, any>;
51
+ data?: Array<{ propName: string; func: Function; args: any[] }>;
52
+ elements?: any[];
53
+ }
42
54
  ) {
43
- const props = R.reduce(
44
- (acc, curr) =>
45
- R.assoc(curr.propName, retrieveData(entry, curr.func, curr.args), acc),
46
- additionalProps,
47
- data
55
+ const props = data.reduce(
56
+ (acc, curr) => {
57
+ acc[curr.propName] = retrieveData(entry, curr.func, curr.args);
58
+
59
+ return acc;
60
+ },
61
+ { ...additionalProps }
48
62
  );
49
63
 
50
64
  let adjustedElements;
@@ -52,6 +52,8 @@ export function masterCellBuilder({
52
52
  const { View } = componentsMap;
53
53
  const elementsBuilder = dataAdapter(cellConfiguration);
54
54
 
55
+ // This is very expensive operation
56
+ // Keep calls to minimum
55
57
  const hasFocusableInside = hasFocusableInsideBuilder(elementsBuilder);
56
58
 
57
59
  /**
@@ -74,8 +76,8 @@ export function masterCellBuilder({
74
76
  const selectedState = screen?.selectedEntry;
75
77
 
76
78
  const entryIsSelected = React.useMemo(
77
- () => selectedState?.id === item?.id,
78
- [selectedState?.id]
79
+ () => !!(selectedState?.id && selectedState?.id === item?.id),
80
+ [selectedState?.id, item?.id]
79
81
  );
80
82
 
81
83
  const elements = React.useMemo(
@@ -84,7 +86,7 @@ export function masterCellBuilder({
84
86
  entry: item,
85
87
  state: getEntryState(state, entryIsSelected),
86
88
  }),
87
- [state, item, entryIsSelected]
89
+ [state, item?.id, entryIsSelected] // Assuming that item won't mutate
88
90
  );
89
91
 
90
92
  const wrapperRef = React.useRef(null);
@@ -9,4 +9,13 @@ describe("insertBetween", () => {
9
9
 
10
10
  expect(result).toEqual([1, "|", 2, "|", 3]);
11
11
  });
12
+
13
+ it("insertBetween", () => {
14
+ const separator = (index) => `separator:${index}`;
15
+ const items = [1, 2, 3];
16
+
17
+ const result = insertBetween(separator, items);
18
+
19
+ expect(result).toEqual([1, "separator:0", 2, "separator:1", 3]);
20
+ });
12
21
  });
@@ -1,5 +1,6 @@
1
1
  import React, { useEffect, useMemo } from "react";
2
2
  import * as R from "ramda";
3
+ import validateColor from "validate-color";
3
4
  import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
4
5
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
5
6
 
@@ -9,6 +10,13 @@ import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerM
9
10
  import { PushTopicManager } from "@applicaster/zapp-react-native-bridge/PushNotifications/PushTopicManager";
10
11
  import { StorageMultiSelectProvider } from "@applicaster/zapp-react-native-bridge/ZappStorage/StorageMultiSelectProvider";
11
12
  import { getCellState } from "../../Cell/utils";
13
+ import { getColorFromData } from "@applicaster/zapp-react-native-utils/cellUtils";
14
+ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
15
+
16
+ const log = createLogger({
17
+ category: "Debug",
18
+ subsystem: "MasterCellUtils",
19
+ });
12
20
 
13
21
  const hasElementSpecificViewType = (viewType) => (element) => {
14
22
  if (R.isNil(element)) {
@@ -47,44 +55,37 @@ export const hasElementsSpecificViewType = (viewType) => (elements) => {
47
55
  return R.any(hasElementSpecificViewType(viewType))(elements);
48
56
  };
49
57
 
50
- const isColorOrEmpty = (value) =>
51
- value === null ||
52
- value === undefined ||
53
- value === "" ||
54
- value.startsWith("#") ||
55
- value.startsWith("rgba") ||
56
- value === "transparent";
57
-
58
58
  function resolveColorForProp(entry, style, colorProp) {
59
- const colorValue = style[colorProp];
59
+ const colorFromProp = style[colorProp];
60
+ const nestedEntryValue = R.path(colorFromProp.split("."), entry);
61
+
62
+ log.log_error("resolvecolor resolveColorforProp", {
63
+ colorFromProp,
64
+ nestedEntryValue,
65
+ entry,
66
+ style,
67
+ colorProp,
68
+ });
60
69
 
61
- if (isColorOrEmpty(colorValue)) {
62
- return style;
70
+ if (
71
+ nestedEntryValue === undefined &&
72
+ !validateColor(colorFromProp.replace(".00", "")) // https://github.com/dreamyguy/validate-color/issues/44
73
+ ) {
74
+ logWarning(colorFromProp, style, entry, nestedEntryValue);
63
75
  }
64
76
 
65
- const pathParts = colorValue.split(".");
66
- let nestedEntryValue = entry;
67
-
68
- for (const part of pathParts) {
69
- if (nestedEntryValue === null || typeof nestedEntryValue !== "object") {
70
- logWarning(colorValue, style, entry, nestedEntryValue);
71
-
72
- return style;
73
- }
74
-
75
- nestedEntryValue = nestedEntryValue[part];
76
- }
77
+ const colorValue = getColorFromData({
78
+ data: entry,
79
+ valueFromLayout: colorFromProp,
80
+ });
77
81
 
78
- if (nestedEntryValue === undefined) {
79
- logWarning(colorValue, style, entry, nestedEntryValue);
82
+ if (!colorValue) {
83
+ logWarning(colorProp, style, entry, colorValue);
80
84
 
81
85
  return style;
82
86
  }
83
87
 
84
- // After successful traversal, set the value.
85
- style[colorProp] = nestedEntryValue;
86
-
87
- return style;
88
+ return { ...style, [colorProp]: colorValue };
88
89
  }
89
90
 
90
91
  export function resolveColor(entry, style) {
@@ -96,6 +97,8 @@ export function resolveColor(entry, style) {
96
97
  const styleKeys = Object.keys(style);
97
98
  const colorKeys = styleKeys.filter((key) => /color/i.test(key));
98
99
 
100
+ log.log_error("resolvecolor resolveColor", { colorKeys, entry, style });
101
+
99
102
  return colorKeys.reduce((acc, value) => {
100
103
  return { ...style, ...resolveColorForProp(entry, acc, value) };
101
104
  }, style);
@@ -0,0 +1,57 @@
1
+ import * as React from "react";
2
+ import { isEmpty } from "ramda";
3
+
4
+ import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks/navigation";
5
+ import { getPathAttributes } from "@applicaster/zapp-react-native-utils/navigationUtils";
6
+ import { FocusableGroup } from "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup";
7
+ import { ScreenDataContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenDataContext";
8
+ import { PathnameContext } from "@applicaster/zapp-react-native-ui-components/Contexts/PathnameContext";
9
+
10
+ import { Screen } from "../Screen/TV/index.web";
11
+ import { isLast } from "@applicaster/zapp-react-native-utils/arrayUtils";
12
+ import { ScreenContextProvider } from "../../Contexts/ScreenContext";
13
+
14
+ type Components = {
15
+ NavBar: React.ComponentType<any>;
16
+ Background: React.ComponentType<any>;
17
+ };
18
+
19
+ type Props = {
20
+ Components: Components;
21
+ };
22
+
23
+ export const StackNavigator = ({ Components }: Props) => {
24
+ const { mainStack } = useNavigation();
25
+
26
+ const renderScreen = ({ route, state }, index) => {
27
+ const pathAttributes = getPathAttributes({ pathname: route });
28
+
29
+ if (isEmpty(pathAttributes)) {
30
+ return null;
31
+ }
32
+
33
+ // HACK: Adding index to route to avoid conflicts in the navigator.
34
+ // Startup hooks are all using PUSH navigation action to navigate, but their routes are always like root routes.
35
+ // This means some hooks can have the same routes, e.g., ["hooks/context-setter", "/hooks/login-plug", "hooks/context-setter"].
36
+ const routeId = `${route}--${index}`;
37
+
38
+ return (
39
+ <FocusableGroup
40
+ id={route}
41
+ preferredFocus={isLast(index, mainStack.length)}
42
+ excludeFromFocusSearching
43
+ key={routeId}
44
+ >
45
+ <ScreenDataContext.Provider value={state}>
46
+ <PathnameContext.Provider value={route}>
47
+ <ScreenContextProvider pathname={route}>
48
+ <Screen route={route} Components={Components} />
49
+ </ScreenContextProvider>
50
+ </PathnameContext.Provider>
51
+ </ScreenDataContext.Provider>
52
+ </FocusableGroup>
53
+ );
54
+ };
55
+
56
+ return <>{mainStack.map(renderScreen)}</>;
57
+ };
@@ -0,0 +1 @@
1
+ export { StackNavigator } from "./StackNavigator";
@@ -68,6 +68,7 @@ type Props = {
68
68
  mode?: VideoModalMode;
69
69
  loadPipesData: (url: string, options: {}) => void;
70
70
  disableCastAction?: boolean;
71
+ groupId?: string;
71
72
  };
72
73
 
73
74
  type ScreenStyles = ZappRiver["styles"] & {
@@ -123,6 +124,7 @@ const nativeStyles = {
123
124
  playerScreen: {
124
125
  flex: 1,
125
126
  backgroundColor: "black",
127
+ overflow: "hidden",
126
128
  },
127
129
  playerWrapper: {
128
130
  flex: 1,
@@ -200,6 +202,7 @@ const PlayerContainerComponent = (props: Props) => {
200
202
  style: videoStyle,
201
203
  mode = VideoModalMode.MAXIMIZED,
202
204
  disableCastAction = false,
205
+ groupId,
203
206
  } = props;
204
207
 
205
208
  // Hooks
@@ -249,8 +252,8 @@ const PlayerContainerComponent = (props: Props) => {
249
252
  return;
250
253
  }
251
254
 
252
- navigator.goBack();
253
255
  showNavBar(true);
256
+ navigator.goBack();
254
257
  }, [isModal, navigator.goBack, showNavBar]);
255
258
 
256
259
  const playEntry = (entry) => navigator.replaceTop(entry, { mode });
@@ -629,6 +632,7 @@ const PlayerContainerComponent = (props: Props) => {
629
632
  style={styles.focusableGroup}
630
633
  preferredFocus
631
634
  shouldUsePreferredFocus
635
+ groupId={groupId}
632
636
  >
633
637
  {/* Video player and components */}
634
638
  <View
@@ -271,6 +271,12 @@ function ComponentsMapComponent(props: Props) {
271
271
 
272
272
  if (y >= 0 && y + height <= contentHeight) {
273
273
  scrollY.setValue(y);
274
+ } else {
275
+ if (y <= 0) {
276
+ scrollY.setValue(0);
277
+ } else if (y + height > contentHeight) {
278
+ scrollY.setValue(contentHeight - height);
279
+ }
274
280
  }
275
281
  }, []);
276
282
 
@@ -134,10 +134,10 @@ export const useGetProgressViewOffset = () => {
134
134
  switch (contentPosition) {
135
135
  case "on_bottom_status_bar":
136
136
  // navbar height
137
- return offset + height - top;
137
+ return offset + height;
138
138
  case "on_top_screen":
139
139
  // navbar height + status bar height
140
- return offset + height;
140
+ return offset + height + top;
141
141
  default:
142
142
  return offset;
143
143
  }
@@ -7,14 +7,10 @@ import * as R from "ramda";
7
7
  import {
8
8
  useFeedLoader,
9
9
  useLayoutVersion,
10
- useNavigationPluginData,
11
10
  } from "@applicaster/zapp-react-native-utils/reactHooks";
12
11
  import { GeneralContentScreen } from "../../GeneralContentScreen";
13
12
  import { ScreenResolver } from "@applicaster/zapp-react-native-ui-components/Components/ScreenResolver";
14
13
  import { utilsLogger } from "@applicaster/zapp-react-native-utils/logger";
15
- import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils";
16
- import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
17
- import { isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
18
14
  import {
19
15
  useNavbarState,
20
16
  useSetNavbarState,
@@ -51,8 +47,6 @@ export const River = (props: Props) => {
51
47
  const { setTitle: setScreenTitle, setSummary: setScreenSummary } =
52
48
  useSetNavbarState();
53
49
 
54
- const navigationPluginData = useNavigationPluginData();
55
-
56
50
  const rivers = useRivers();
57
51
  const isV2 = useLayoutVersion({ isV2: true });
58
52
 
@@ -99,23 +93,6 @@ export const River = (props: Props) => {
99
93
  }
100
94
  }, [feedData, screenData, screenTitle, screenSummary]);
101
95
 
102
- React.useEffect(() => {
103
- if (
104
- isWeb() &&
105
- toBooleanWithDefaultFalse(
106
- navigationPluginData?.rules?.focus_on_content
107
- ) &&
108
- river &&
109
- river.type !== "general_content"
110
- ) {
111
- const groupId = R.pathOr("river", ["groupId"], componentsMapExtraProps);
112
-
113
- focusManager.setupInitialFocus(groupId, true);
114
- focusManager.setInitialFocus();
115
- focusManager.enableAutomaticFocus();
116
- }
117
- }, [river, componentsMapExtraProps]);
118
-
119
96
  if (!river) {
120
97
  utilsLogger.error({
121
98
  message: `River ${screenData.id} not found!`,
@@ -14,7 +14,7 @@ type Props = { pathname: string; screenData: Record<string, any> };
14
14
  * @param {Props}: see props above
15
15
  */
16
16
  export function RouteManager(props: Props) {
17
- const { pathname, screenData: _screenData } = useRoute();
17
+ const { pathname, screenData } = useRoute();
18
18
  const pathAttributes = getPathAttributes({ pathname });
19
19
 
20
20
  const { screenType, screenId } = R.last(pathAttributes);
@@ -25,7 +25,7 @@ export function RouteManager(props: Props) {
25
25
  screenType={screenType}
26
26
  screenId={screenId}
27
27
  {...props}
28
- screenData={props.screenData}
28
+ screenData={screenData}
29
29
  />
30
30
  </TestId>
31
31
  );
@@ -0,0 +1,3 @@
1
+ export { useInitialFocus } from "./useInitialFocus";
2
+
3
+ export { useRestoreFocus } from "./useRestoreFocus";
@@ -0,0 +1,70 @@
1
+ import * as React from "react";
2
+ import {
3
+ useNavigationPluginData,
4
+ useIsNavBarVisible,
5
+ useRoute,
6
+ } from "@applicaster/zapp-react-native-utils/reactHooks";
7
+ import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
8
+ import {
9
+ setFocusOnContent,
10
+ setFocusOnMenu,
11
+ } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux";
12
+
13
+ type Return =
14
+ | {
15
+ onContent: true;
16
+ onMenu: false;
17
+ }
18
+ | {
19
+ onContent: false;
20
+ onMenu: true;
21
+ };
22
+
23
+ const getInitialFocus = (focusOnContent, isNavBarVisible): Return => {
24
+ if (focusOnContent) {
25
+ return {
26
+ onContent: true,
27
+ onMenu: false,
28
+ };
29
+ }
30
+
31
+ if (!focusOnContent && isNavBarVisible) {
32
+ // put focus on navbar
33
+ return {
34
+ onContent: false,
35
+ onMenu: true,
36
+ };
37
+ }
38
+
39
+ // we don't have navbar, put focus on content anyway
40
+ return {
41
+ onContent: true,
42
+ onMenu: false,
43
+ };
44
+ };
45
+
46
+ export const useInitialFocus = (): void => {
47
+ const navigationPluginData = useNavigationPluginData();
48
+
49
+ const focusOnContent = toBooleanWithDefaultFalse(
50
+ navigationPluginData?.rules?.focus_on_content
51
+ );
52
+
53
+ const isNavBarVisible = useIsNavBarVisible();
54
+
55
+ const { pathname: currentRoute } = useRoute();
56
+
57
+ React.useEffect(() => {
58
+ const initialFocus = getInitialFocus(focusOnContent, isNavBarVisible);
59
+
60
+ if (initialFocus.onContent) {
61
+ setFocusOnContent(currentRoute);
62
+
63
+ return;
64
+ }
65
+
66
+ if (initialFocus.onMenu) {
67
+ setFocusOnMenu(currentRoute);
68
+ }
69
+ }, []);
70
+ };
@@ -0,0 +1,22 @@
1
+ import * as React from "react";
2
+ import { isNil } from "ramda";
3
+ import { useIsScreenActive } from "@applicaster/zapp-react-native-utils/reactHooks/navigation";
4
+ import { usePrevious } from "@applicaster/zapp-react-native-utils/reactHooks/utils";
5
+ import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager";
6
+
7
+ export const useRestoreFocus = (): void => {
8
+ const isScreenActive = useIsScreenActive();
9
+ const previousIsScreenActive = usePrevious(isScreenActive);
10
+
11
+ React.useEffect(() => {
12
+ if (isNil(previousIsScreenActive)) {
13
+ // it's initial mounting of the screen, do nothing here
14
+ return;
15
+ }
16
+
17
+ if (!previousIsScreenActive && isScreenActive) {
18
+ // restore focus when we go back from next screen
19
+ focusManager.setInitialFocus();
20
+ }
21
+ }, [previousIsScreenActive, isScreenActive]);
22
+ };
@@ -0,0 +1,165 @@
1
+ import * as React from "react";
2
+ import { last } from "ramda";
3
+ import { View, StyleSheet } from "react-native";
4
+ import {
5
+ findMenuPlugin,
6
+ getNavigationProps,
7
+ getPathAttributes,
8
+ } from "@applicaster/zapp-react-native-utils/navigationUtils";
9
+ import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
10
+ import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
11
+ import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
12
+ import {
13
+ useNavbarState,
14
+ useScreenBackgroundColor,
15
+ } from "@applicaster/zapp-react-native-utils/reactHooks/screen";
16
+ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
17
+
18
+ import { NavBarContainer } from "../../Layout/TV/NavBarContainer";
19
+ import { ScreenResolver } from "../../ScreenResolver";
20
+ import { useInitialFocus, useRestoreFocus } from "./hooks";
21
+ import { isPlayerPlugin } from "@applicaster/zapp-react-native-utils/pluginUtils";
22
+
23
+ const styles = StyleSheet.create({
24
+ container: {
25
+ position: "absolute",
26
+ top: 0,
27
+ left: 0,
28
+ right: 0,
29
+ bottom: 0,
30
+ },
31
+ });
32
+
33
+ type Components = {
34
+ NavBar: React.ComponentType<any>;
35
+ Background: React.ComponentType<any>;
36
+ };
37
+
38
+ type Props = {
39
+ route: string;
40
+ Components: Components;
41
+ };
42
+
43
+ const getActiveRiver = ({
44
+ screenId,
45
+ screenType,
46
+ routeState,
47
+ rivers,
48
+ fallbackActiveRiver,
49
+ }) => {
50
+ if (rivers[screenId]) {
51
+ if (screenType === "hooks") {
52
+ const navigations = routeState?.state?.entry?.payload?.navigations;
53
+
54
+ if (navigations) {
55
+ return routeState.state.entry.payload;
56
+ }
57
+ }
58
+
59
+ return routeState?.state?.screen;
60
+ }
61
+
62
+ // screenId is not presented into rivers, use fallback instead
63
+ return fallbackActiveRiver;
64
+ };
65
+
66
+ const getNavigations = ({
67
+ screenId,
68
+ screenType,
69
+ routeState,
70
+ rivers,
71
+ fallbackNavigations,
72
+ }) => {
73
+ if (rivers[screenId]) {
74
+ // river presents
75
+ if (screenType === "hooks") {
76
+ const navigations = routeState?.state?.entry?.payload?.navigations;
77
+
78
+ if (navigations) {
79
+ return navigations;
80
+ }
81
+ }
82
+
83
+ return routeState?.state?.screen?.navigations;
84
+ }
85
+
86
+ // screenId is not presented into rivers, use fallback instead
87
+ return fallbackNavigations;
88
+ };
89
+
90
+ export const Screen = ({ route, Components }: Props) => {
91
+ if (isNilOrEmpty(route)) {
92
+ throw Error("Required props: route is missing");
93
+ }
94
+
95
+ const navigator = useNavigation();
96
+ const { plugins = [], rivers = [] } = usePickFromState(["plugins", "rivers"]);
97
+
98
+ const pathAttributes = getPathAttributes({ pathname: route });
99
+ const routeState = navigator.getStackForPathname(route);
100
+
101
+ const { screenType, screenId } = last(pathAttributes);
102
+
103
+ const backgroundColor = useScreenBackgroundColor(screenId);
104
+
105
+ useInitialFocus();
106
+ useRestoreFocus();
107
+
108
+ const activeRiver = getActiveRiver({
109
+ screenId,
110
+ screenType,
111
+ routeState,
112
+ rivers,
113
+ fallbackActiveRiver: navigator.activeRiver,
114
+ });
115
+
116
+ const navigationProps = getNavigationProps({
117
+ navigator: {
118
+ // @ts-ignore
119
+ activeRiver,
120
+ // @ts-ignore
121
+ screenTitle: routeState.state.entry?.title || routeState.screen?.name,
122
+ },
123
+ category: "menu",
124
+ });
125
+
126
+ const navigations = getNavigations({
127
+ screenId,
128
+ screenType,
129
+ routeState,
130
+ rivers,
131
+ fallbackNavigations: navigator.activeRiver.navigations,
132
+ });
133
+
134
+ const menuPlugin = findMenuPlugin(navigations, plugins);
135
+
136
+ const NavBar = (Components.NavBar ||
137
+ menuPlugin.module) as React.ComponentType<any>;
138
+
139
+ const isPlayer = isPlayerPlugin(routeState);
140
+
141
+ const { visible: isNavBarVisible } = useNavbarState();
142
+
143
+ const screenData =
144
+ screenType === "hooks" || isPlayer
145
+ ? routeState?.state?.entry
146
+ : routeState?.state?.screen;
147
+
148
+ return (
149
+ <View style={[styles.container, { backgroundColor }]}>
150
+ <NavBarContainer isVisible={isNavBarVisible} onReady={noop}>
151
+ <NavBar
152
+ isVisible={isNavBarVisible}
153
+ menuPlugin={menuPlugin}
154
+ navigationProps={navigationProps}
155
+ />
156
+ </NavBarContainer>
157
+ <ScreenResolver
158
+ screenType={screenType}
159
+ screenId={screenId}
160
+ screenData={screenData}
161
+ groupId={route}
162
+ />
163
+ </View>
164
+ );
165
+ };
@@ -10,6 +10,8 @@ const mock_navBarVisibleFlag = true;
10
10
 
11
11
  const mockIsNavBarVisible = jest.fn(() => mock_navBarVisibleFlag);
12
12
 
13
+ const mockIsOrientationCompatible = jest.fn(() => true);
14
+
13
15
  jest.mock("react-native-safe-area-context", () => ({
14
16
  useSafeAreaInsets: () => ({ top: 44 }),
15
17
  }));
@@ -21,6 +23,7 @@ jest.mock("../../RouteManager", () => ({
21
23
  jest.mock(
22
24
  "@applicaster/zapp-react-native-utils/appUtils/orientationHelper",
23
25
  () => ({
26
+ isOrientationCompatible: mockIsOrientationCompatible,
24
27
  allowedOrientationsForScreen: jest.fn(),
25
28
  getOrientation: jest.fn(),
26
29
  getScreenOrientation: jest.fn(),