@applicaster/zapp-react-native-ui-components 12.0.0-alpha.7496053101 → 12.0.0-alpha.7517336525

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 (100) 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/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +13 -6
  8. package/Components/GeneralContentScreen/utils/useCurationAPI.ts +1 -1
  9. package/Components/HandlePlayable/HandlePlayable.tsx +3 -0
  10. package/Components/Layout/TV/ScreenContainer.tsx +79 -5
  11. package/Components/Layout/TV/ScreenLayoutContextProvider.tsx +2 -2
  12. package/Components/Layout/TV/__tests__/ScreenContainer.test.tsx +38 -28
  13. package/Components/Layout/TV/__tests__/__snapshots__/ScreenContainer.test.tsx.snap +25 -13
  14. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +10 -50
  15. package/Components/Layout/TV/__tests__/index.test.tsx +20 -75
  16. package/Components/Layout/TV/index.tsx +14 -67
  17. package/Components/Layout/TV/index.web.tsx +33 -0
  18. package/Components/Layout/index.tsx +15 -11
  19. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +4 -1
  20. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +88 -112
  21. package/Components/MasterCell/DefaultComponents/DynamicBadge.tsx +4 -2
  22. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -1
  23. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +5 -3
  24. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -1
  25. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +34 -1
  26. package/Components/MasterCell/DefaultComponents/Text/index.tsx +14 -2
  27. package/Components/MasterCell/DefaultComponents/__tests__/image.test.js +1 -1
  28. package/Components/MasterCell/DefaultComponents/__tests__/text.test.tsx +24 -9
  29. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/index.android.tsx +4 -1
  30. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/index.tsx +10 -2
  31. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +1 -3
  32. package/Components/MasterCell/dataAdapter.ts +21 -7
  33. package/Components/MasterCell/hooks/useAsyncRendering/index.ts +1 -1
  34. package/Components/MasterCell/index.tsx +3 -1
  35. package/Components/MasterCell/utils/__tests__/insertBetween.test.js +9 -0
  36. package/Components/MasterCell/utils/index.ts +33 -35
  37. package/Components/MeasurmentsPortal/BasicMeasurementsPortal.tsx +11 -7
  38. package/Components/MeasurmentsPortal/MeasurementsPortal.tsx +10 -1
  39. package/Components/Navigator/StackNavigator.tsx +57 -0
  40. package/Components/Navigator/index.ts +1 -0
  41. package/Components/PlayerContainer/PlayerContainer.tsx +20 -3
  42. package/Components/PlayerImageBackground/index.tsx +34 -8
  43. package/Components/River/ComponentsMap/ComponentsMap.tsx +92 -22
  44. package/Components/River/RefreshControl.tsx +55 -1
  45. package/Components/River/TV/River.tsx +0 -23
  46. package/Components/River/__tests__/componentsMap.test.js +6 -0
  47. package/Components/RouteManager/index.tsx +2 -2
  48. package/Components/Screen/TV/hooks/index.ts +3 -0
  49. package/Components/Screen/TV/hooks/useInitialFocus.ts +70 -0
  50. package/Components/Screen/TV/hooks/useRestoreFocus.ts +22 -0
  51. package/Components/Screen/TV/index.web.tsx +165 -0
  52. package/Components/Screen/__tests__/Screen.test.tsx +3 -0
  53. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +4 -0
  54. package/Components/Screen/index.tsx +62 -54
  55. package/Components/Screen/navigationHandler.ts +5 -0
  56. package/Components/ScreenResolver/index.tsx +9 -12
  57. package/Components/Transitioner/Scene.tsx +32 -26
  58. package/Components/Transitioner/Transitioner.tsx +38 -43
  59. package/Components/Transitioner/__tests__/__snapshots__/Scene.test.js.snap +12 -4
  60. package/Components/Transitioner/__tests__/__snapshots__/transitioner.test.js.snap +1 -0
  61. package/Components/VideoLive/LiveImageManager.ts +28 -1
  62. package/Components/VideoLive/PlayerLiveImageComponent.tsx +52 -24
  63. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +20 -5
  64. package/Components/VideoLive/__tests__/__snapshots__/PlayerLiveImageComponent.test.tsx.snap +25 -28
  65. package/Components/VideoModal/ModalAnimation/AnimatedPlayerModalWrapper.tsx +66 -0
  66. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +355 -0
  67. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.tsx +161 -0
  68. package/Components/VideoModal/ModalAnimation/AnimationComponent.tsx +456 -0
  69. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +182 -0
  70. package/Components/VideoModal/ModalAnimation/index.ts +15 -0
  71. package/Components/VideoModal/ModalAnimation/utils.ts +302 -0
  72. package/Components/VideoModal/PlayerDetails.tsx +3 -3
  73. package/Components/VideoModal/PlayerWrapper.tsx +77 -47
  74. package/Components/VideoModal/VideoModal.tsx +23 -40
  75. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +13 -1
  76. package/Components/VideoModal/__tests__/__snapshots__/PlayerWrapper.test.tsx.snap +157 -105
  77. package/Components/VideoModal/hooks/useModalSize.ts +10 -12
  78. package/Components/VideoModal/utils.ts +8 -5
  79. package/Components/Viewport/ViewportTracker/ViewportTracker.tsx +31 -40
  80. package/Components/Viewport/ViewportTracker/index.tsx +1 -0
  81. package/Components/Viewport/utils.js +1 -1
  82. package/Contexts/FocusableGroupContext/FocusableGroupContext.ts +3 -0
  83. package/Contexts/FocusableGroupContext/FocusableGroupContextProvider.tsx +16 -0
  84. package/Contexts/FocusableGroupContext/__tests__/FocusableGroupContextProvider.test.tsx +22 -0
  85. package/Contexts/FocusableGroupContext/__tests__/withFocusableContext.test.tsx +26 -0
  86. package/Contexts/FocusableGroupContext/index.ts +7 -0
  87. package/Contexts/FocusableGroupContext/withFocusableContext.tsx +17 -0
  88. package/Contexts/MenuToggleContext/{index.js → index.ts} +16 -0
  89. package/Contexts/ScreenContext/__tests__/__snapshots__/index.test.tsx.snap +3 -0
  90. package/Contexts/ScreenContext/__tests__/index.test.tsx +27 -0
  91. package/Contexts/ScreenContext/index.tsx +94 -25
  92. package/Contexts/ScreenDataContext/index.tsx +20 -0
  93. package/Contexts/ZappPipesContext/ZappPipesEntryContext.ts +1 -1
  94. package/events/index.ts +1 -0
  95. package/package.json +5 -5
  96. package/Components/Layout/TV/TransitionOverlay.tsx +0 -64
  97. package/Components/VideoModal/hooks/usePosition.ts +0 -50
  98. /package/Components/FocusableGroup/{index.android.js → index.android.ts} +0 -0
  99. /package/Components/FocusableGroup/{index.ios.js → index.ios.ts} +0 -0
  100. /package/Contexts/{index.js → index.ts} +0 -0
@@ -66,14 +66,14 @@ export class BaseFocusable<
66
66
  const { initialFocus } = this.props;
67
67
 
68
68
  if (initialFocus) {
69
- focusManager.setupInitialFocus(this.getId());
69
+ focusManager.setFocus(this.getId());
70
70
  }
71
71
  }
72
72
 
73
73
  getId(props?: { id: string; groupId: string }) {
74
- const { id, groupId = null } = props || this.props;
74
+ const { id } = props || this.props;
75
75
 
76
- return groupId ? `${groupId}-${id}` : id;
76
+ return id;
77
77
  }
78
78
 
79
79
  getRect() {
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import * as R from "ramda";
2
+ import { join, merge, path } from "ramda";
3
3
  import { View } from "react-native";
4
4
 
5
5
  import { Focusable } from "@applicaster/zapp-react-native-ui-components/Components/Focusable";
@@ -11,6 +11,7 @@ import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
11
11
 
12
12
  import { CellWithFocusable } from "./CellWithFocusable";
13
13
  import { BaseFocusable } from "../BaseFocusable";
14
+ import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager";
14
15
 
15
16
  type Props = {
16
17
  item: {
@@ -66,6 +67,10 @@ type Props = {
66
67
  behavior: Behavior;
67
68
  };
68
69
 
70
+ type State = {
71
+ hasFocusableInside: boolean;
72
+ };
73
+
69
74
  const baseCellStyles = {
70
75
  height: "100%",
71
76
  display: "flex",
@@ -77,7 +82,7 @@ const touchableStyles = {
77
82
  height: "100%",
78
83
  } as const;
79
84
 
80
- export class CellComponent extends React.Component<Props> {
85
+ export class CellComponent extends React.Component<Props, State> {
81
86
  constructor(props) {
82
87
  super(props);
83
88
  this.onPress = this.onPress.bind(this);
@@ -87,6 +92,10 @@ export class CellComponent extends React.Component<Props> {
87
92
  this.hasReceivedFocus = this.hasReceivedFocus.bind(this);
88
93
  this.scrollVertically = this.scrollVertically.bind(this);
89
94
  this.scrollToIndex = this.scrollToIndex.bind(this);
95
+
96
+ this.state = {
97
+ hasFocusableInside: props.CellRenderer.hasFocusableInside?.(props.item),
98
+ };
90
99
  }
91
100
 
92
101
  setScreenLayout(componentAnchorPointY, screenLayout) {
@@ -105,8 +114,8 @@ export class CellComponent extends React.Component<Props> {
105
114
  onPress(item, index);
106
115
  } else {
107
116
  const { item, navigator, component, appData } = this.props;
108
- const itemTargetScreen = R.path(["screen_type"], item);
109
- const componentTargetScreen = R.path(["data", "target"], component);
117
+ const itemTargetScreen = path(["screen_type"], item);
118
+ const componentTargetScreen = path(["data", "target"], component);
110
119
  const itemType = getItemType(item, appData?.layoutVersion);
111
120
 
112
121
  sendSelectCellEvent(item, component, item.title, index);
@@ -116,7 +125,7 @@ export class CellComponent extends React.Component<Props> {
116
125
  componentTargetScreen &&
117
126
  !itemTargetScreen
118
127
  ) {
119
- navigator.push(R.merge(item, { screen_type: componentTargetScreen }));
128
+ navigator.push(merge(item, { screen_type: componentTargetScreen }));
120
129
 
121
130
  return;
122
131
  }
@@ -139,6 +148,10 @@ export class CellComponent extends React.Component<Props> {
139
148
  shouldScrollVertically?.(mouse, focusable, id, title)
140
149
  ) {
141
150
  this.scrollVertically(focusable);
151
+
152
+ const accessibilityManager = AccessibilityManager.getInstance();
153
+ // Text from DSP should be localized already
154
+ accessibilityManager.readText({ text: title });
142
155
  }
143
156
  }
144
157
  }
@@ -187,6 +200,16 @@ export class CellComponent extends React.Component<Props> {
187
200
  return !isFocusable ? false : focused || focusableFocused;
188
201
  }
189
202
 
203
+ componentDidUpdate(prevProps: Readonly<Props>) {
204
+ if (prevProps.item !== this.props.item) {
205
+ this.setState({
206
+ hasFocusableInside: this.props.CellRenderer.hasFocusableInside?.(
207
+ this.props.item
208
+ ),
209
+ });
210
+ }
211
+ }
212
+
190
213
  render() {
191
214
  const {
192
215
  item,
@@ -207,7 +230,7 @@ export class CellComponent extends React.Component<Props> {
207
230
 
208
231
  const { id } = item;
209
232
 
210
- const focusableId = R.join("-", [component?.id, id, index]);
233
+ const focusableId = join("-", [component?.id, id, index]);
211
234
 
212
235
  const handleFocus = (focusable, mouse) => {
213
236
  const focusFn = onFocus || noop;
@@ -215,9 +238,7 @@ export class CellComponent extends React.Component<Props> {
215
238
  this.onFocus(focusable, mouse);
216
239
  };
217
240
 
218
- const hasFocusableInside = CellRenderer.hasFocusableInside?.(item);
219
-
220
- if (hasFocusableInside) {
241
+ if (this.state.hasFocusableInside) {
221
242
  return (
222
243
  <CellWithFocusable
223
244
  CellRenderer={CellRenderer}
@@ -3,15 +3,25 @@ import * as React from "react";
3
3
  import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager";
4
4
  import * as FOCUS_EVENTS from "@applicaster/zapp-react-native-utils/appUtils/focusManager/events";
5
5
  import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
6
+ import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
7
+
6
8
  import { isAppleTV } from "../../Helpers/Platform";
7
9
  import { useCellState } from "../MasterCell/utils";
8
10
 
9
- const useCellFocusedState = (groupId: string, id: string) => {
11
+ const useCellFocusedState = (
12
+ skipFocusManagerRegistration: boolean,
13
+ groupId: string,
14
+ id: string
15
+ ) => {
10
16
  const [currentCellFocused, setCurrentCellFocused] = React.useState(false);
11
17
 
12
18
  React.useEffect(() => {
13
- const isGroupItemFocused = () =>
14
- setCurrentCellFocused(focusManager.isGroupItemFocused(groupId, id));
19
+ const isGroupItemFocused = () => {
20
+ if (!skipFocusManagerRegistration) {
21
+ const isFocused = focusManager.isGroupItemFocused(groupId, id);
22
+ setCurrentCellFocused(isFocused);
23
+ }
24
+ };
15
25
 
16
26
  const handler = () => {
17
27
  // tvOS hack for properly checking focus
@@ -29,7 +39,7 @@ const useCellFocusedState = (groupId: string, id: string) => {
29
39
  return () => {
30
40
  focusManager.removeHandler(FOCUS_EVENTS.FOCUS, handler);
31
41
  };
32
- }, [groupId]);
42
+ }, [groupId, skipFocusManagerRegistration]);
33
43
 
34
44
  return currentCellFocused;
35
45
  };
@@ -46,7 +56,7 @@ type Props = {
46
56
  skipFocusManagerRegistration?: boolean;
47
57
  isFocusable?: boolean;
48
58
  behavior: Behavior;
49
- selected?: boolean;
59
+ focused?: boolean;
50
60
  };
51
61
 
52
62
  export function CellWithFocusable(props: Props) {
@@ -62,16 +72,19 @@ export function CellWithFocusable(props: Props) {
62
72
  skipFocusManagerRegistration,
63
73
  isFocusable,
64
74
  behavior,
65
- selected,
75
+ focused,
66
76
  } = props;
67
77
 
68
- const isFocused = useCellFocusedState(groupId, id);
78
+ const isFocused = useCellFocusedState(
79
+ skipFocusManagerRegistration,
80
+ groupId,
81
+ id
82
+ );
69
83
 
70
84
  const state = useCellState({
71
85
  id: item.id,
72
86
  behavior,
73
- focused: isFocused,
74
- selected,
87
+ focused: isFocused || toBooleanWithDefaultFalse(focused),
75
88
  });
76
89
 
77
90
  const [focusedButtonId, setFocusedButtonId] = React.useState(undefined);
@@ -102,6 +115,7 @@ export function CellWithFocusable(props: Props) {
102
115
  preferredFocus={preferredFocus}
103
116
  skipFocusManagerRegistration={skipFocusManagerRegistration}
104
117
  isFocusable={isFocusable}
118
+ focused={focused}
105
119
  />
106
120
  );
107
121
  }
@@ -226,13 +226,7 @@ export class TvOSCellComponent extends React.Component<Props, State> {
226
226
 
227
227
  hasReceivedFocus() {}
228
228
 
229
- scrollTo(index) {
230
- // Workaround for HL autoscroll/autoloop feature, we scroll manually on tvOS.
231
- // Should be removed after refactoring autoscroll/autoloop feature
232
- if (this.props.component?.component_type !== "horizontal_list_qb") {
233
- this.props?.scrollToIndex?.(index);
234
- }
235
- }
229
+ scrollTo() {}
236
230
 
237
231
  render() {
238
232
  const {
@@ -279,7 +273,9 @@ export class TvOSCellComponent extends React.Component<Props, State> {
279
273
  index={index}
280
274
  scrollTo={this.scrollTo}
281
275
  preferredFocus={preferredFocus}
276
+ focused={this.props.focused}
282
277
  behavior={behavior}
278
+ isFocusable={isFocusable}
283
279
  />
284
280
  </View>
285
281
  );
@@ -4,6 +4,7 @@ import * as R from "ramda";
4
4
  import { BaseFocusable } from "../BaseFocusable";
5
5
  import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager";
6
6
  import { LONG_KEY_PRESS_TIMEOUT } from "@applicaster/quick-brick-core/const";
7
+ import { withFocusableContext } from "../../Contexts/FocusableGroupContext/withFocusableContext";
7
8
 
8
9
  type Props = {
9
10
  initialFocus?: boolean;
@@ -23,7 +24,7 @@ type Props = {
23
24
  style?: React.CSSProperties;
24
25
  };
25
26
 
26
- export class Focusable extends BaseFocusable<Props> {
27
+ class Focusable extends BaseFocusable<Props> {
27
28
  isGroup: boolean;
28
29
  mouse: boolean;
29
30
  longPressTimeout = null;
@@ -198,3 +199,7 @@ export class Focusable extends BaseFocusable<Props> {
198
199
  );
199
200
  }
200
201
  }
202
+
203
+ const FocusableWithFocusableContext = withFocusableContext(Focusable);
204
+
205
+ export { FocusableWithFocusableContext as Focusable };
@@ -1,6 +1,8 @@
1
1
  import * as React from "react";
2
2
 
3
3
  import { BaseFocusable } from "../BaseFocusable";
4
+ import { FocusableGroupContextProvider } from "../../Contexts/FocusableGroupContext";
5
+ import { withFocusableContext } from "../../Contexts/FocusableGroupContext/withFocusableContext";
4
6
 
5
7
  type Props = {
6
8
  testID?: string;
@@ -26,9 +28,10 @@ type Props = {
26
28
  nextFocusLeft?: any;
27
29
  nextFocusRight?: any;
28
30
  onLayout?: any;
31
+ excludeFromFocusSearching?: boolean;
29
32
  };
30
33
 
31
- export class FocusableGroup extends BaseFocusable<Props> {
34
+ class FocusableGroup extends BaseFocusable<Props> {
32
35
  constructor(props) {
33
36
  super(props);
34
37
  this.isGroup = true;
@@ -81,8 +84,14 @@ export class FocusableGroup extends BaseFocusable<Props> {
81
84
  style={style}
82
85
  ref={this.ref}
83
86
  >
84
- {children}
87
+ <FocusableGroupContextProvider id={id}>
88
+ {children}
89
+ </FocusableGroupContextProvider>
85
90
  </div>
86
91
  );
87
92
  }
88
93
  }
94
+
95
+ const FocusableGroupWithFocusableContext = withFocusableContext(FocusableGroup);
96
+
97
+ export { FocusableGroupWithFocusableContext as FocusableGroup };
@@ -50,7 +50,11 @@ describe("getTransformedPreset should return the passed components if smartCompo
50
50
  );
51
51
 
52
52
  expect(transformed_preset).toBeDefined();
53
- expect(transformed_preset.id).toEqual("comp10"); // id was transformed
53
+
54
+ expect(transformed_preset.id).toEqual(
55
+ `${mockComp.id}-${mockPreset.feed_url}-0`
56
+ ); // id was transformed
57
+
54
58
  expect(transformed_preset.data.source).toEqual(mockPreset.feed_url); // source was replaced with preset feed_url.
55
59
  });
56
60
  });
@@ -117,7 +121,10 @@ describe("getTransformedPreset should return the passed components if smartCompo
117
121
 
118
122
  expect(Array.isArray(result)).toBe(true);
119
123
  expect(result).toHaveLength(2);
120
- expect(result[0].id).toEqual("smart10");
124
+
125
+ expect(result[0].id).toEqual(
126
+ `${mockSmartComp.id}-${mockFeeds.preset_source.data.entry[0].feed_url}-0`
127
+ );
121
128
 
122
129
  expect(result[0].data.source).toEqual(
123
130
  mockFeeds.preset_source.data.entry[0].feed_url
@@ -287,7 +294,7 @@ describe("getTransformedPreset should return the passed components if smartCompo
287
294
  mockComponents[0],
288
295
  {
289
296
  ...mockLayoutPresets.preset_one,
290
- id: mockComponents[1].id + 1,
297
+ id: `${mockComponents[1].id}-${mockPresetEntry[0].feed_url}-1`,
291
298
  data: { source: mockPresetEntry[0].feed_url },
292
299
  },
293
300
  ];
@@ -344,12 +351,12 @@ describe("getTransformedPreset should return the passed components if smartCompo
344
351
  const mockTransformedComponents = [
345
352
  {
346
353
  ...mockLayoutPresets.preset_one,
347
- id: mockComponents[0].id + 0,
354
+ id: `${mockComponents[0].id}-${mockPresetEntry1[0].feed_url}-0`,
348
355
  data: { source: mockPresetEntry1[0].feed_url },
349
356
  },
350
357
  {
351
358
  ...mockLayoutPresets.preset_one,
352
- id: mockComponents[1].id + 1,
359
+ id: `${mockComponents[1].id}-${mockPresetEntry2[0].feed_url}-1`,
353
360
  data: { source: mockPresetEntry2[0].feed_url },
354
361
  },
355
362
  ];
@@ -412,7 +419,7 @@ describe("getTransformedPreset should return the passed components if smartCompo
412
419
  const mockTransformedComponents = mockPresetEntry.map((preset, i) => {
413
420
  return {
414
421
  ...mockLayoutPresets[preset.preset_name],
415
- id: mockComponents[0].id + i,
422
+ id: `${mockComponents[0].id}-${preset.feed_url}-${i}`,
416
423
  data: { source: preset.feed_url },
417
424
  };
418
425
  });
@@ -41,7 +41,7 @@ export const getTransformedPreset = (
41
41
  }
42
42
 
43
43
  const updatedPresetComponent = produce(presetComponent, (draft) => {
44
- draft.id = `${componentId}${index}`;
44
+ draft.id = `${componentId}-${preset?.feed_url}-${index}`;
45
45
  draft.data.source = preset?.feed_url;
46
46
 
47
47
  if (draft.ui_components) {
@@ -19,6 +19,7 @@ type Props = {
19
19
  modalHeight?: number;
20
20
  modalWidth?: number;
21
21
  mode: VideoModalMode;
22
+ groupId?: string;
22
23
  };
23
24
 
24
25
  const YOUTUBE_PLUGIN_ID = "youtube-player-qb";
@@ -93,6 +94,7 @@ export function HandlePlayable({
93
94
  modalHeight,
94
95
  modalWidth,
95
96
  mode,
97
+ groupId,
96
98
  }: Props): React.ReactElement | null {
97
99
  const state = usePickFromState();
98
100
 
@@ -167,6 +169,7 @@ export function HandlePlayable({
167
169
  isModal={isModal}
168
170
  mode={mode}
169
171
  disableCastAction={playerModuleProperties?.disableCastAction}
172
+ groupId={groupId}
170
173
  />
171
174
  );
172
175
  }
@@ -5,13 +5,30 @@ import * as R from "ramda";
5
5
  import { ScreenLayoutContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenLayoutContext";
6
6
  import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks/navigation";
7
7
  import { ifEmptyUseFallback } from "@applicaster/zapp-react-native-utils/cellUtils";
8
- import { useCurrentScreenData } from "@applicaster/zapp-react-native-utils/reactHooks/screen";
8
+ import {
9
+ useCurrentScreenData,
10
+ useNavbarState,
11
+ } from "@applicaster/zapp-react-native-utils/reactHooks/screen";
9
12
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
10
13
 
11
14
  import { useScreenConfiguration } from "../../River/useScreenConfiguration";
12
- import { routeIsPlayerScreen } from "@applicaster/zapp-react-native-utils/navigationUtils";
15
+ import {
16
+ findMenuPlugin,
17
+ getNavigationProps,
18
+ routeIsPlayerScreen,
19
+ } from "@applicaster/zapp-react-native-utils/navigationUtils";
20
+ import { isApplePlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
21
+ import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
22
+ import { NavBarContainer } from "./NavBarContainer";
23
+
24
+ type ComponentsExtraProps = {
25
+ Background?: Record<string, any>;
26
+ NavBar?: Record<string, any>;
27
+ };
13
28
 
14
29
  type Props = {
30
+ ComponentsExtraProps?: ComponentsExtraProps;
31
+ NavBar: React.ComponentType<any>;
15
32
  children: React.ReactNode;
16
33
  };
17
34
 
@@ -30,9 +47,13 @@ const styles = StyleSheet.create({
30
47
  container: { flex: 1, height: "100%", width: "100%" },
31
48
  });
32
49
 
50
+ const isTVOS = isApplePlatform();
51
+
33
52
  // eslint-disable-next-line react/display-name
34
53
  export const ScreenContainer = React.memo(function ScreenContainer({
35
54
  children,
55
+ ComponentsExtraProps,
56
+ NavBar,
36
57
  }: Props) {
37
58
  const { screenMarginBottom } = React.useContext(ScreenLayoutContext);
38
59
  const { currentRoute, screenData } = useNavigation();
@@ -80,9 +101,62 @@ export const ScreenContainer = React.memo(function ScreenContainer({
80
101
  [themeStyles]
81
102
  );
82
103
 
104
+ const navigator = useNavigation();
105
+ const { activeRiver } = navigator;
106
+ const { title, visible } = useNavbarState();
107
+
108
+ const { plugins = [] } = usePickFromState([
109
+ "appState",
110
+ "remoteConfigurations",
111
+ "plugins",
112
+ ]);
113
+
114
+ const navigationProps = getNavigationProps({
115
+ navigator,
116
+ title,
117
+ category: "menu",
118
+ });
119
+
120
+ const menuPlugin = React.useMemo(
121
+ () => findMenuPlugin(activeRiver?.navigations, plugins),
122
+ [activeRiver?.navigations]
123
+ );
124
+
125
+ const NavBarComponent = React.useMemo(
126
+ () => NavBar || (menuPlugin.module as React.ComponentType<any>),
127
+ []
128
+ );
129
+
130
+ const [navBarReady, setNavBarReady] = React.useState(false);
131
+
132
+ const navBarContainer = (
133
+ <NavBarContainer isVisible={visible} onReady={setNavBarReady}>
134
+ <NavBarComponent
135
+ ref={ComponentsExtraProps?.NavBar?.ref}
136
+ isVisible={visible}
137
+ menuPlugin={menuPlugin}
138
+ navigationProps={navigationProps}
139
+ {...R.omit(["ref"])(ComponentsExtraProps?.NavBar)}
140
+ />
141
+ </NavBarContainer>
142
+ );
143
+
83
144
  return (
84
- <View testID="screen-container" style={viewStyles}>
85
- {children}
86
- </View>
145
+ <>
146
+ {!isTVOS ? navBarContainer : null}
147
+ {navBarReady ? (
148
+ /**
149
+ Need to re-mount this component on each navigation event
150
+ It's a hack to work around the fact that we don't support
151
+ correct screen rendering in our navigator (shared currentRoute state)
152
+ **/
153
+ <View testID="screen-container" style={viewStyles}>
154
+ {children}
155
+ </View>
156
+ ) : null}
157
+ {/** Render NavBarContainer over ScreenContainer to let tvOS native
158
+ focus manager works correctly to focus nav bar items first **/}
159
+ {isTVOS ? navBarContainer : null}
160
+ </>
87
161
  );
88
162
  });
@@ -60,7 +60,7 @@ export function ScreenLayoutContextProvider(props: {
60
60
  initialState
61
61
  );
62
62
 
63
- const screeenLayoutValue = React.useMemo(
63
+ const screenLayoutValue = React.useMemo(
64
64
  () => ({
65
65
  ...screenLayout,
66
66
  setScreenLayout,
@@ -69,7 +69,7 @@ export function ScreenLayoutContextProvider(props: {
69
69
  );
70
70
 
71
71
  return (
72
- <ScreenLayoutContext.Provider value={screeenLayoutValue}>
72
+ <ScreenLayoutContext.Provider value={screenLayoutValue}>
73
73
  {props.children}
74
74
  </ScreenLayoutContext.Provider>
75
75
  );
@@ -1,14 +1,27 @@
1
1
  import * as React from "react";
2
2
  import { View } from "react-native";
3
- import { render } from "@testing-library/react-native";
3
+ import { render, waitFor } from "@testing-library/react-native";
4
+ import { WrappedWithProviders } from "@applicaster/zapp-react-native-utils/testUtils";
4
5
 
5
6
  import { ScreenLayoutContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenLayoutContext";
6
7
 
7
8
  const navigator_mock = {
8
9
  currentRoute: "/river/A1234",
9
10
  screenData: {},
11
+ activeRiver: {
12
+ navigations: {},
13
+ id: "A1234",
14
+ home: false,
15
+ },
10
16
  };
11
17
 
18
+ jest.mock("@applicaster/zapp-react-native-utils/navigationUtils", () => ({
19
+ ...jest.requireActual<any>(
20
+ "@applicaster/zapp-react-native-utils/navigationUtils"
21
+ ),
22
+ findMenuPlugin: () => ({}),
23
+ }));
24
+
12
25
  jest.mock("@applicaster/quick-brick-core/App/ActionSetters", () => ({
13
26
  useRiverInitialState: () => ({}),
14
27
  }));
@@ -17,13 +30,13 @@ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/navigation", () => ({
17
30
  useNavigation: () => navigator_mock,
18
31
  }));
19
32
 
20
- jest.mock("../../../River/useScreenConfiguration.ts", () => ({
33
+ jest.mock("../../../River/useScreenConfiguration", () => ({
21
34
  useScreenConfiguration: () => ({}),
22
35
  }));
23
36
 
24
- jest.mock("@applicaster/zapp-react-native-utils/reactHooks/screen", () => ({
25
- useCurrentScreenData: jest.fn(() => ({})),
26
- }));
37
+ jest.mock(
38
+ "@applicaster/zapp-react-native-utils/reactHooks/screen/useCurrentScreenData"
39
+ );
27
40
 
28
41
  jest.mock("@applicaster/zapp-react-native-utils/theme", () => ({
29
42
  useTheme: () => ({}),
@@ -44,42 +57,39 @@ describe("ScreenContainer", () => {
44
57
  const setScreenLayout = () => {};
45
58
 
46
59
  const wrapper = ({ children }) => (
47
- <ScreenLayoutContext.Provider value={{ ...screenLayout, setScreenLayout }}>
48
- {children}
49
- </ScreenLayoutContext.Provider>
60
+ <WrappedWithProviders>
61
+ <ScreenLayoutContext.Provider
62
+ value={{ ...screenLayout, setScreenLayout }}
63
+ >
64
+ <ScreenContainer NavBar={View}>{children}</ScreenContainer>
65
+ </ScreenLayoutContext.Provider>
66
+ </WrappedWithProviders>
50
67
  );
51
68
 
52
69
  const TestComp = () => <View testID="test-component" />;
53
70
 
54
71
  it("renders", () => {
55
- const { toJSON } = render(
56
- <ScreenContainer>
57
- <TestComp />
58
- </ScreenContainer>,
59
- { wrapper }
60
- );
72
+ const { toJSON } = render(<TestComp />, { wrapper });
61
73
 
62
74
  expect(toJSON()).toMatchSnapshot();
63
75
  });
64
76
 
65
- it("renders passed component", () => {
66
- const { getByTestId } = render(
67
- <ScreenContainer>
68
- <TestComp />
69
- </ScreenContainer>,
70
- { wrapper }
71
- );
77
+ it("renders passed component", async () => {
78
+ const { getByTestId } = render(<TestComp />, { wrapper });
79
+
80
+ await waitFor(() => {
81
+ return expect(getByTestId("screen-container")).toBeDefined();
82
+ });
72
83
 
73
84
  expect(getByTestId("test-component")).toBeDefined();
74
85
  });
75
86
 
76
- it("sets margins provided by the theme", () => {
77
- const { getByTestId } = render(
78
- <ScreenContainer>
79
- <TestComp />
80
- </ScreenContainer>,
81
- { wrapper }
82
- );
87
+ it("sets margins provided by the theme", async () => {
88
+ const { getByTestId } = render(<TestComp />, { wrapper });
89
+
90
+ await waitFor(() => {
91
+ return expect(getByTestId("screen-container")).toBeDefined();
92
+ });
83
93
 
84
94
  const screen = getByTestId("screen-container");
85
95
  expect(screen.props.style).toMatchSnapshot();
@@ -1,28 +1,40 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
3
  exports[`ScreenContainer renders 1`] = `
4
- <View
4
+ <RNCSafeAreaProvider
5
+ onInsetsChange={[Function]}
5
6
  style={
6
7
  Array [
7
8
  Object {
8
9
  "flex": 1,
9
- "height": "100%",
10
- "width": "100%",
11
- },
12
- Object {
13
- "backgroundColor": "transparent",
14
- "marginBottom": 10,
15
- "paddingBottom": 0,
16
- "paddingTop": 0,
17
10
  },
11
+ undefined,
18
12
  ]
19
13
  }
20
- testID="screen-container"
21
14
  >
22
15
  <View
23
- testID="test-component"
24
- />
25
- </View>
16
+ style={
17
+ Array [
18
+ Object {
19
+ "position": "absolute",
20
+ "top": 0,
21
+ "width": 1920,
22
+ "zIndex": 10,
23
+ },
24
+ Object {
25
+ "maxHeight": 10,
26
+ },
27
+ ]
28
+ }
29
+ testID="nav-bar-container"
30
+ >
31
+ <View
32
+ isVisible={true}
33
+ menuPlugin={Object {}}
34
+ navigationProps={null}
35
+ />
36
+ </View>
37
+ </RNCSafeAreaProvider>
26
38
  `;
27
39
 
28
40
  exports[`ScreenContainer sets margins provided by the theme 1`] = `