@applicaster/zapp-react-native-ui-components 15.0.0-alpha.8680244503 → 15.0.0-alpha.9300744523

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.
@@ -22,6 +22,7 @@ type Props = {
22
22
  onFocus?: FocusManager.FocusEventCB;
23
23
  onBlur?: FocusManager.FocusEventCB;
24
24
  selected?: boolean;
25
+ skipFocusManagerRegistration?: boolean;
25
26
  };
26
27
 
27
28
  export class BaseFocusable<
@@ -61,10 +62,14 @@ export class BaseFocusable<
61
62
  }
62
63
 
63
64
  componentDidMount() {
64
- const { id } = this.props;
65
+ const { id, skipFocusManagerRegistration } = this.props;
65
66
  const component = this;
66
67
  this.node = this.ref.current;
67
68
 
69
+ if (skipFocusManagerRegistration) {
70
+ return;
71
+ }
72
+
68
73
  focusManager.register({
69
74
  id,
70
75
  component: component,
@@ -118,7 +123,12 @@ export class BaseFocusable<
118
123
 
119
124
  componentWillUnmount() {
120
125
  this._isMounted = false;
121
- const { id } = this.props;
126
+ const { id, skipFocusManagerRegistration } = this.props;
127
+
128
+ if (skipFocusManagerRegistration) {
129
+ return;
130
+ }
131
+
122
132
  focusManager.unregister(id, { group: this.isGroup || false });
123
133
  }
124
134
 
@@ -10,6 +10,7 @@ type Props = {
10
10
  children: (focused: boolean) => React.ReactNode;
11
11
  onFocus: (arg1: any, index?: number) => void;
12
12
  onBlur: Callback;
13
+ skipFocusManagerRegistration?: boolean;
13
14
  };
14
15
 
15
16
  export const FocusableWrapper = ({
@@ -20,6 +21,7 @@ export const FocusableWrapper = ({
20
21
  applyWrapper,
21
22
  onFocus,
22
23
  onBlur,
24
+ skipFocusManagerRegistration,
23
25
  }: Props) => {
24
26
  if (applyWrapper) {
25
27
  return (
@@ -34,6 +36,7 @@ export const FocusableWrapper = ({
34
36
  // @ts-ignore
35
37
  offsetUpdater={noop}
36
38
  isFocusable
39
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
37
40
  >
38
41
  {(focused) => children(focused)}
39
42
  </Focusable>
@@ -80,6 +80,7 @@ type Props = {
80
80
  componentsMapOffset: number;
81
81
  applyFocusableWrapper: boolean;
82
82
  hasFocusableInside: boolean;
83
+ skipFocusManagerRegistration?: boolean;
83
84
  };
84
85
 
85
86
  type State = {
@@ -266,6 +267,7 @@ class TvOSCell extends React.Component<Props, State> {
266
267
  behavior,
267
268
  applyFocusableWrapper,
268
269
  hasFocusableInside,
270
+ skipFocusManagerRegistration,
269
271
  } = this.props;
270
272
 
271
273
  const { id } = item;
@@ -291,6 +293,7 @@ class TvOSCell extends React.Component<Props, State> {
291
293
  onFocus={handleFocus}
292
294
  onBlur={onBlur || this.onBlur}
293
295
  applyWrapper={applyFocusableWrapper}
296
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
294
297
  >
295
298
  {(focused) => (
296
299
  <CellWithFocusable
@@ -305,6 +308,7 @@ class TvOSCell extends React.Component<Props, State> {
305
308
  focused={focused || this.props.focused}
306
309
  behavior={behavior}
307
310
  isFocusable={isFocusable}
311
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
308
312
  />
309
313
  )}
310
314
  </FocusableWrapper>
@@ -327,6 +331,7 @@ class TvOSCell extends React.Component<Props, State> {
327
331
  offsetUpdater={offsetUpdater}
328
332
  style={baseCellStyles}
329
333
  isFocusable={isFocusable}
334
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
330
335
  >
331
336
  {(focused) => (
332
337
  <FocusableCell
@@ -8,6 +8,8 @@ import { withFocusableContext } from "../../Contexts/FocusableGroupContext/withF
8
8
  import { StyleSheet, ViewStyle } from "react-native";
9
9
  import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager";
10
10
 
11
+ import { isSearchInputId } from "@applicaster/zapp-react-native-utils/searchUtils";
12
+
11
13
  type Props = {
12
14
  initialFocus?: boolean;
13
15
  id: string;
@@ -106,7 +108,7 @@ class Focusable extends BaseFocusable<Props> {
106
108
  onMouseEnter() {
107
109
  const { id } = this.props;
108
110
 
109
- if (id !== "search_input_group_id") {
111
+ if (!isSearchInputId(id)) {
110
112
  this.mouse = true;
111
113
  this.props?.handleFocus?.({ mouse: true });
112
114
 
@@ -120,7 +122,7 @@ class Focusable extends BaseFocusable<Props> {
120
122
  onMouseLeave() {
121
123
  const { id } = this.props;
122
124
 
123
- if (id !== "search_input_group_id") {
125
+ if (!isSearchInputId(id)) {
124
126
  this.mouse = false;
125
127
  this.blur(null);
126
128
  }
@@ -10,8 +10,12 @@ import {
10
10
  forceFocusableFocus,
11
11
  } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
12
12
  import { findNodeHandle, ViewStyle } from "react-native";
13
+ import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
13
14
 
14
- function noop() {}
15
+ import {
16
+ emitFocused,
17
+ emitNativeRegistered,
18
+ } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
15
19
 
16
20
  type Props = {
17
21
  id: string;
@@ -39,6 +43,7 @@ type Props = {
39
43
  hasReceivedFocus: () => void;
40
44
  offsetUpdater: (arg1: string, arg2: number) => number;
41
45
  style: ViewStyle;
46
+ skipFocusManagerRegistration?: boolean;
42
47
  };
43
48
 
44
49
  export class Focusable extends BaseFocusable<Props> {
@@ -53,6 +58,7 @@ export class Focusable extends BaseFocusable<Props> {
53
58
  this.nextFocusableReactTags = {};
54
59
  this.preferredFocus = this.preferredFocus.bind(this);
55
60
  this.measureView = this.measureView.bind(this);
61
+ this.onRegistered = this.onRegistered.bind(this);
56
62
  }
57
63
 
58
64
  /**
@@ -84,6 +90,9 @@ export class Focusable extends BaseFocusable<Props> {
84
90
  });
85
91
  }
86
92
 
93
+ const id: string = nativeEvent.itemID;
94
+ emitFocused(id);
95
+
87
96
  onFocus(nativeEvent);
88
97
  }
89
98
 
@@ -169,6 +178,13 @@ export class Focusable extends BaseFocusable<Props> {
169
178
  });
170
179
  }
171
180
 
181
+ onRegistered({ nativeEvent }) {
182
+ const groupId = nativeEvent?.groupId;
183
+ const id = nativeEvent?.itemId;
184
+
185
+ emitNativeRegistered({ id, groupId, isGroup: false });
186
+ }
187
+
172
188
  render() {
173
189
  const {
174
190
  children,
@@ -203,6 +219,7 @@ export class Focusable extends BaseFocusable<Props> {
203
219
  focusable={isFocusable}
204
220
  {...this.nextFocusableReactTags}
205
221
  {...otherProps}
222
+ onRegistered={this.onRegistered}
206
223
  >
207
224
  {typeof children === "function" ? children(focused) : children}
208
225
  </FocusableItemNative>
@@ -5,6 +5,7 @@ exports[`FocusableTvOS should render correctly 1`] = `
5
5
  groupId={null}
6
6
  itemId={null}
7
7
  onLayout={[Function]}
8
+ onRegistered={[Function]}
8
9
  onViewBlur={[Function]}
9
10
  onViewFocus={[Function]}
10
11
  onViewPress={[Function]}
@@ -2,6 +2,10 @@ import * as React from "react";
2
2
  import { FocusableGroupNative } from "@applicaster/zapp-react-native-ui-components/Components/NativeFocusables";
3
3
  import { BaseFocusable } from "@applicaster/zapp-react-native-ui-components/Components/BaseFocusable";
4
4
  import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
5
+ import { LayoutContext } from "@applicaster/zapp-react-native-tvos-app/Context/LayoutContext";
6
+ import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useRoute";
7
+ import { isScreenPlayable } from "@applicaster/zapp-react-native-utils/navigationUtils/itemTypes";
8
+ import { emitNativeRegistered } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
5
9
 
6
10
  const { log_verbose } = createLogger({
7
11
  subsystem: "General",
@@ -33,7 +37,16 @@ type Props = {
33
37
  screenData: { screenId: string; parentScreenId: string };
34
38
  };
35
39
 
36
- export class FocusableGroup extends BaseFocusable<Props> {
40
+ class FocusableGroupComponent extends BaseFocusable<Props> {
41
+ public readonly isGroup: boolean = true;
42
+
43
+ onRegistered = ({ nativeEvent }) => {
44
+ const groupId = nativeEvent?.groupId;
45
+ const id = nativeEvent?.itemId;
46
+
47
+ emitNativeRegistered({ id, groupId, isGroup: true });
48
+ };
49
+
37
50
  render() {
38
51
  const {
39
52
  children,
@@ -66,9 +79,27 @@ export class FocusableGroup extends BaseFocusable<Props> {
66
79
  onGroupBlur={onGroupBlur}
67
80
  style={style}
68
81
  {...otherProps}
82
+ onRegistered={this.onRegistered}
69
83
  >
70
84
  {children}
71
85
  </FocusableGroupNative>
72
86
  );
73
87
  }
74
88
  }
89
+
90
+ export const withFocusDisabled = (Component) => {
91
+ return function WithFocusDisabled(props) {
92
+ // @ts-ignore
93
+ const { screenFocusBlocked } = React.useContext(LayoutContext.ReactContext);
94
+
95
+ const { pathname } = useRoute();
96
+
97
+ const isPlayerPresented = isScreenPlayable(pathname);
98
+
99
+ const blockScreenFocus = isPlayerPresented === false && screenFocusBlocked;
100
+
101
+ return <Component {...props} isFocusDisabled={blockScreenFocus} />;
102
+ };
103
+ };
104
+
105
+ export const FocusableGroup = withFocusDisabled(FocusableGroupComponent);
@@ -1,4 +1,4 @@
1
- import { all, equals, path, prop, isEmpty, pluck, values } from "ramda";
1
+ import { all, equals, isEmpty, path, pluck, prop, values } from "ramda";
2
2
 
3
3
  import { useEffect, useMemo } from "react";
4
4
 
@@ -24,6 +24,7 @@ import {
24
24
 
25
25
  import { produce } from "immer";
26
26
  import { useLoadPipesDataDispatch } from "@applicaster/zapp-react-native-utils/reactHooks";
27
+
27
28
  // types reference
28
29
 
29
30
  declare interface CurationEntry {
@@ -35,6 +36,7 @@ type Feeds = Record<string, ZappPipesData>;
35
36
 
36
37
  type LayoutPresets = PresetsMapping["presets_mappings"];
37
38
 
39
+ const TABS_SCREEN_TYPE = "tabs_screen";
38
40
  const SMART_COMPONENT_TYPE = "quick-brick-smart-component";
39
41
  const SOURCE_PATH = ["data", "source"];
40
42
  const MAPPING_PATH = ["data", "mapping"];
@@ -53,7 +55,10 @@ export const getTransformedPreset = (
53
55
  const presetComponent = layoutPresets?.[preset?.preset_name];
54
56
 
55
57
  if (!presetComponent) {
56
- logger.log_error("Preset missing or wrong data format", { entry: preset });
58
+ logger.log_error(
59
+ `Preset "${preset?.preset_name}" missing or wrong data format`,
60
+ { entry: preset }
61
+ );
57
62
 
58
63
  return;
59
64
  }
@@ -131,10 +136,16 @@ export const useCurationAPI = (
131
136
  );
132
137
 
133
138
  const { pathname } = useRoute();
134
- const [entryContext] = ZappPipesEntryContext.useZappPipesContext(pathname);
135
139
  const [searchContext] = ZappPipesSearchContext.useZappPipesContext();
136
140
  const [screenContext] = ZappPipesScreenContext.useZappPipesContext();
137
141
 
142
+ const isNestedScreen = screenContext?.type === TABS_SCREEN_TYPE;
143
+
144
+ const [entryContext] = ZappPipesEntryContext.useZappPipesContext(
145
+ pathname,
146
+ isNestedScreen
147
+ );
148
+
138
149
  const urlsMap = useMemo<{ [key: string]: string }>(() => {
139
150
  const map = {};
140
151
 
@@ -61,6 +61,7 @@ import {
61
61
  PlayerNativeSendCommand,
62
62
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
63
63
  import { useAppData } from "@applicaster/zapp-react-native-redux";
64
+ import { useRestrictMobilePlayback } from "./useRestrictMobilePlayback";
64
65
 
65
66
  type Props = {
66
67
  Player: React.ComponentType<any>;
@@ -264,7 +265,21 @@ const PlayerContainerComponent = (props: Props) => {
264
265
 
265
266
  showNavBar(true);
266
267
  navigator.goBack();
267
- }, [isModal, navigator.goBack, state.playerId, showNavBar]);
268
+ }, [isModal, state.playerId, showNavBar, navigator]);
269
+
270
+ const pluginConfiguration = React.useMemo(() => {
271
+ return (
272
+ playerManager.getPluginConfiguration() ||
273
+ R.prop("__plugin_configuration", Player)
274
+ );
275
+ }, [playerManager.isRegistered()]);
276
+
277
+ const { isRestricted } = useRestrictMobilePlayback({
278
+ player,
279
+ entry: item,
280
+ pluginConfiguration,
281
+ close,
282
+ });
268
283
 
269
284
  const playEntry = (entry) => navigator.replaceTop(entry, { mode });
270
285
 
@@ -456,13 +471,6 @@ const PlayerContainerComponent = (props: Props) => {
456
471
  }
457
472
  }, []);
458
473
 
459
- const pluginConfiguration = React.useMemo(() => {
460
- return (
461
- playerManager.getPluginConfiguration() ||
462
- R.prop("__plugin_configuration", Player)
463
- );
464
- }, [playerManager.isRegistered()]);
465
-
466
474
  const disableMiniPlayer = React.useMemo(() => {
467
475
  return pluginConfiguration?.disable_mini_player_when_inline;
468
476
  }, [pluginConfiguration]);
@@ -662,36 +670,38 @@ const PlayerContainerComponent = (props: Props) => {
662
670
  <PlayerFocusableWrapperView
663
671
  nextFocusDown={context.bottomFocusableId}
664
672
  >
665
- <Player
666
- source={{
667
- uri,
668
- entry: item,
669
- }}
670
- focused={isInlineTV ? true : undefined}
671
- autoplay={true}
672
- controls={false}
673
- disableCastAction={disableCastAction}
674
- docked={navigator.isVideoModalDocked()}
675
- entry={item}
676
- fullscreen={mode === VideoModalMode.FULLSCREEN}
677
- inline={inline}
678
- isModal={isModal}
679
- isTabletPortrait={isTabletPortrait}
680
- muted={false}
681
- playableItem={item}
682
- playerEvent={playerEvent}
683
- playerId={state.playerId}
684
- pluginConfiguration={pluginConfiguration}
685
- ref={playerRef}
686
- toggleFullscreen={toggleFullscreen}
687
- style={videoStyle}
688
- playNextData={playNextData}
689
- setNextVideoPreloadThresholdPercentage={
690
- setNextVideoPreloadThresholdPercentage
691
- }
692
- >
693
- {renderApplePlayer(applePlayerProps)}
694
- </Player>
673
+ {isRestricted ? null : (
674
+ <Player
675
+ source={{
676
+ uri,
677
+ entry: item,
678
+ }}
679
+ focused={isInlineTV ? true : undefined}
680
+ autoplay={true}
681
+ controls={false}
682
+ disableCastAction={disableCastAction}
683
+ docked={navigator.isVideoModalDocked()}
684
+ entry={item}
685
+ fullscreen={mode === VideoModalMode.FULLSCREEN}
686
+ inline={inline}
687
+ isModal={isModal}
688
+ isTabletPortrait={isTabletPortrait}
689
+ muted={false}
690
+ playableItem={item}
691
+ playerEvent={playerEvent}
692
+ playerId={state.playerId}
693
+ pluginConfiguration={pluginConfiguration}
694
+ ref={playerRef}
695
+ toggleFullscreen={toggleFullscreen}
696
+ style={videoStyle}
697
+ playNextData={playNextData}
698
+ setNextVideoPreloadThresholdPercentage={
699
+ setNextVideoPreloadThresholdPercentage
700
+ }
701
+ >
702
+ {renderApplePlayer(applePlayerProps)}
703
+ </Player>
704
+ )}
695
705
  </PlayerFocusableWrapperView>
696
706
 
697
707
  {state.error ? <ErrorDisplay error={state.error} /> : null}
@@ -0,0 +1,101 @@
1
+ import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
2
+ import NetInfo from "@react-native-community/netinfo";
3
+
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import { showAlertDialog } from "@applicaster/zapp-react-native-utils/alertUtils";
6
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
7
+ import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
8
+ import { log_info } from "./logger";
9
+
10
+ type RestrictMobilePlaybackProps = {
11
+ player?: Player;
12
+ entry?: ZappEntry;
13
+ pluginConfiguration?: Record<string, string>;
14
+ close: () => void;
15
+ };
16
+
17
+ export const useRestrictMobilePlayback = ({
18
+ player,
19
+ entry,
20
+ pluginConfiguration,
21
+ close,
22
+ }: RestrictMobilePlaybackProps): { isRestricted: boolean } => {
23
+ const dialogVisibleRef = useRef<boolean>(false);
24
+ const theme = useTheme();
25
+
26
+ useEffect(() => {
27
+ return () => {
28
+ if (isTV()) {
29
+ return;
30
+ }
31
+
32
+ dialogVisibleRef.current = false;
33
+ };
34
+ }, []);
35
+
36
+ const isConnectionRestricted = useMemo(() => {
37
+ if (isTV()) {
38
+ return false;
39
+ }
40
+
41
+ return player && entry?.extensions?.connection_restricted;
42
+ }, [player, entry]);
43
+
44
+ const [isRestricted, setIsRestricted] = useState<boolean>(
45
+ isConnectionRestricted
46
+ );
47
+
48
+ useEffect(() => {
49
+ if (!isConnectionRestricted) {
50
+ return;
51
+ }
52
+
53
+ const stopPlayer = () => {
54
+ log_info(
55
+ "Stopping player due to mobile restriction, connection_restricted: true"
56
+ );
57
+
58
+ player?.close();
59
+
60
+ dialogVisibleRef.current = true;
61
+
62
+ showAlertDialog({
63
+ title:
64
+ pluginConfiguration?.mobile_connection_restricted_alert_title ||
65
+ "Restricted Connection Type",
66
+ message:
67
+ pluginConfiguration?.mobile_connection_restricted_alert_message ||
68
+ "This content can only be viewed over a Wi-Fi or LAN network.",
69
+ okButtonText: theme.ok_button || "OK",
70
+ completion: () => {
71
+ dialogVisibleRef.current = false;
72
+
73
+ close();
74
+ },
75
+ });
76
+ };
77
+
78
+ return NetInfo.addEventListener((state) => {
79
+ if (state.type === "cellular") {
80
+ setIsRestricted(true);
81
+
82
+ if (dialogVisibleRef.current) {
83
+ return;
84
+ }
85
+
86
+ stopPlayer();
87
+ } else {
88
+ setIsRestricted(false);
89
+ }
90
+ });
91
+ }, [
92
+ close,
93
+ entry?.extensions?.connection_restricted,
94
+ pluginConfiguration,
95
+ player,
96
+ theme.ok_button,
97
+ isConnectionRestricted,
98
+ ]);
99
+
100
+ return { isRestricted };
101
+ };
@@ -2,7 +2,8 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import { Text } from "react-native";
5
- import * as R from "ramda";
5
+
6
+ import { mergeRight } from "@applicaster/zapp-react-native-utils/utils";
6
7
 
7
8
  import { GeneralContentScreen } from "../../GeneralContentScreen";
8
9
  import { ScreenResolver } from "@applicaster/zapp-react-native-ui-components/Components/ScreenResolver";
@@ -13,6 +14,8 @@ import {
13
14
  } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
14
15
  import { useRivers } from "@applicaster/zapp-react-native-utils/reactHooks/state";
15
16
 
17
+ import { toStringOrEmpty } from "./utils";
18
+
16
19
  type Props = {
17
20
  screenId: string;
18
21
  screenData: ZappRiver | ZappEntry;
@@ -24,6 +27,7 @@ type Props = {
24
27
  isInsideContainer?: boolean;
25
28
  extraAnchorPointYOffset: number;
26
29
  river?: ZappRiver | ZappEntry;
30
+ groupId: string;
27
31
  };
28
32
 
29
33
  export const River = (props: Props) => {
@@ -35,6 +39,7 @@ export const River = (props: Props) => {
35
39
  componentsMapExtraProps,
36
40
  isInsideContainer,
37
41
  extraAnchorPointYOffset,
42
+ groupId,
38
43
  } = props;
39
44
 
40
45
  const { title: screenTitle, summary: screenSummary } = useNavbarState();
@@ -51,28 +56,41 @@ export const River = (props: Props) => {
51
56
  [screenId]
52
57
  );
53
58
 
54
- const stringOrEmpty = (value: string | number | undefined): string =>
55
- R.isNil(value) ? "" : String(value);
59
+ const screenResolverData = React.useMemo(() => {
60
+ const extraData = mergeRight(extraProps, screenResolverExtraProps);
61
+
62
+ return {
63
+ extraData,
64
+ screenData: mergeRight(river, { groupId: extraData?.groupId }),
65
+ componentsMapExtraProps: mergeRight(componentsMapExtraProps, { groupId }),
66
+ };
67
+ }, [
68
+ extraProps,
69
+ screenResolverExtraProps,
70
+ river,
71
+ componentsMapExtraProps,
72
+ groupId,
73
+ ]);
56
74
 
57
75
  React.useEffect(() => {
58
76
  if (!isInsideContainer) {
59
- setScreenTitle(stringOrEmpty(screenData?.title));
60
- setScreenSummary(stringOrEmpty(screenData?.summary));
77
+ setScreenTitle(toStringOrEmpty(screenData?.title));
78
+ setScreenSummary(toStringOrEmpty(screenData?.summary));
61
79
  }
62
80
  }, [screenData.id]);
63
81
 
64
82
  React.useEffect(() => {
65
83
  if (feedData && !isInsideContainer) {
66
84
  if (feedData.title && feedData.title !== screenTitle) {
67
- setScreenTitle(stringOrEmpty(feedData.title));
85
+ setScreenTitle(toStringOrEmpty(feedData.title));
68
86
  }
69
87
 
70
88
  if (feedData.summary && feedData.summary !== screenSummary) {
71
- setScreenSummary(stringOrEmpty(feedData.summary));
89
+ setScreenSummary(toStringOrEmpty(feedData.summary));
72
90
  }
73
91
  } else {
74
- setScreenTitle(stringOrEmpty(screenData?.title));
75
- setScreenSummary(stringOrEmpty(screenData?.summary));
92
+ setScreenTitle(toStringOrEmpty(screenData?.title));
93
+ setScreenSummary(toStringOrEmpty(screenData?.summary));
76
94
  }
77
95
  }, [feedData, screenData, screenTitle, screenSummary]);
78
96
 
@@ -86,15 +104,13 @@ export const River = (props: Props) => {
86
104
  }
87
105
 
88
106
  if (river.type !== "general_content") {
89
- const extraData = { ...R.mergeRight(extraProps, screenResolverExtraProps) };
90
-
91
107
  return (
92
108
  <ScreenResolver
93
109
  screenType={river.type}
94
110
  screenId={screenId}
95
- screenData={R.merge(river, { groupId: extraData?.groupId })}
96
- componentsMapExtraProps={componentsMapExtraProps}
97
- {...extraData}
111
+ screenData={screenResolverData.screenData}
112
+ componentsMapExtraProps={screenResolverData.componentsMapExtraProps}
113
+ {...screenResolverData.extraData}
98
114
  />
99
115
  );
100
116
  }
@@ -106,6 +122,7 @@ export const River = (props: Props) => {
106
122
  isScreenWrappedInContainer={isInsideContainer}
107
123
  extraAnchorPointYOffset={extraAnchorPointYOffset}
108
124
  componentsMapExtraProps={componentsMapExtraProps}
125
+ groupId={groupId}
109
126
  />
110
127
  );
111
128
  };
@@ -1,11 +1,15 @@
1
- import { compose } from "ramda";
1
+ import { compose, identity } from "@applicaster/zapp-react-native-utils/utils";
2
+ import { isTvOSPlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
3
+
2
4
  import { River as RiverComponent } from "./River";
3
- import { withTvEventHandler } from "./withTVEventHandler";
4
5
  import { withComponentsMapOffsetContext } from "../../../Contexts/ComponentsMapOffsetContext";
5
6
  import { withRiverDataLoader } from "./withRiverDataLoader";
7
+ import { withFocusableGroupForContent } from "./withFocusableGroupForContent";
8
+
9
+ const isTVOS = isTvOSPlatform();
6
10
 
7
11
  export const River = compose(
8
- withTvEventHandler,
9
12
  withComponentsMapOffsetContext,
10
- withRiverDataLoader
13
+ withRiverDataLoader,
14
+ isTVOS ? withFocusableGroupForContent : identity
11
15
  )(RiverComponent);
@@ -0,0 +1,30 @@
1
+ import { toStringOrEmpty } from "..";
2
+
3
+ describe("toStringOrEmpty", () => {
4
+ test("returns empty string for undefined", () => {
5
+ expect(toStringOrEmpty(undefined)).toBe("");
6
+ });
7
+
8
+ test("returns empty string for null", () => {
9
+ expect(toStringOrEmpty(null)).toBe("");
10
+ });
11
+
12
+ test("converts number to string", () => {
13
+ expect(toStringOrEmpty(0)).toBe("0");
14
+ expect(toStringOrEmpty(123)).toBe("123");
15
+ expect(toStringOrEmpty(-42)).toBe("-42");
16
+ });
17
+
18
+ test("returns string as is", () => {
19
+ expect(toStringOrEmpty("hello")).toBe("hello");
20
+ expect(toStringOrEmpty("")).toBe("");
21
+ });
22
+
23
+ test("works with numeric strings", () => {
24
+ expect(toStringOrEmpty("123")).toBe("123");
25
+ });
26
+
27
+ test("does not throw on falsy values like 0", () => {
28
+ expect(toStringOrEmpty(0)).toBe("0");
29
+ });
30
+ });
@@ -0,0 +1,4 @@
1
+ import { isNil } from "@applicaster/zapp-react-native-utils/utils";
2
+
3
+ export const toStringOrEmpty = (value: unknown): string =>
4
+ isNil(value) ? "" : String(value);
@@ -0,0 +1,71 @@
1
+ import * as React from "react";
2
+ import { View, StyleSheet } from "react-native";
3
+
4
+ import { FocusableGroup } from "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup";
5
+ import { riverFocusManager } from "@applicaster/zapp-react-native-utils/appUtils/RiverFocusManager";
6
+
7
+ import { topMenuLayoutChange$ } from "@applicaster/zapp-react-native-tvos-app/Layout/topMenu";
8
+
9
+ const styles = StyleSheet.create({
10
+ flexOne: {
11
+ flex: 1,
12
+ },
13
+ });
14
+
15
+ export const withFocusableGroupForContent = (Component) => {
16
+ return function WithFocusableGroupForContent(props) {
17
+ const { screenId, isInsideContainer } = props;
18
+
19
+ const [topMenuHeight, setTopMenuHeight] = React.useState(0);
20
+
21
+ React.useEffect(() => {
22
+ const subscription = topMenuLayoutChange$.subscribe((layout) => {
23
+ setTopMenuHeight(layout.height);
24
+ });
25
+
26
+ return () => {
27
+ subscription.unsubscribe();
28
+ };
29
+ }, []);
30
+
31
+ const focusableId = React.useMemo(
32
+ () =>
33
+ riverFocusManager.screenFocusableGroupId({
34
+ screenId,
35
+ isInsideContainer,
36
+ }),
37
+ [screenId, isInsideContainer]
38
+ );
39
+
40
+ if (isInsideContainer) {
41
+ return <Component {...props} />;
42
+ }
43
+
44
+ return (
45
+ <FocusableGroup
46
+ key={focusableId}
47
+ id={focusableId}
48
+ // The top menu is rendered in its own FocusableGroup, anchored at the top of the screen.
49
+ // When the "content" FocusableGroup starts at y = 0 as well, the two groups visually overlap.
50
+ // On TvOS platform this overlap can confuse the focus engine, because the focusable bounds of
51
+ // the top-menu group and the content group intersect, leading to erratic navigation between
52
+ // the menu and the content (e.g. unexpected jumps or focus getting "stuck").
53
+ //
54
+ // To avoid this, we shift the entire content FocusableGroup down by the dynamic top menu
55
+ // height (marginTop: topMenuHeight). This separates the focus regions of the two groups in
56
+ // focus space, so they no longer intersect.
57
+ //
58
+ // The inner <View> below then applies the inverse margin (marginTop: -topMenuHeight) so that
59
+ // the actual visual position of the content on screen does not change; only the focusable
60
+ // bounds of the outer group are offset.
61
+ style={[styles.flexOne, { marginTop: topMenuHeight }]}
62
+ // this group does not have parent
63
+ groupId={undefined}
64
+ >
65
+ <View style={[styles.flexOne, { marginTop: -1 * topMenuHeight }]}>
66
+ <Component {...props} groupId={focusableId} />
67
+ </View>
68
+ </FocusableGroup>
69
+ );
70
+ };
71
+ };
@@ -88,6 +88,7 @@ exports[`withConfigurationProvider correctly passes all the configuration keys c
88
88
  tab_cell_padding_right={10}
89
89
  tab_cell_padding_top={14}
90
90
  tablet_theme={false}
91
+ tabs_screen_background_color="transparent"
91
92
  target={false}
92
93
  target_screen_switch={false}
93
94
  text_label_active_font_color="rgba(239, 239, 239, 0.5)"
@@ -208,4 +208,5 @@ export const keysMap: Record<string, Function> = {
208
208
  tab_bar_item_margin_right: castOrDefaultValue(Number, 0),
209
209
  tab_bar_item_margin_bottom: castOrDefaultValue(Number, 0),
210
210
  tab_bar_item_margin_left: castOrDefaultValue(Number, 0),
211
+ tabs_screen_background_color: castOrDefaultValue(R.identity, "transparent"),
211
212
  };
@@ -3,7 +3,7 @@ import { renderWithProviders } from "@applicaster/zapp-react-native-utils/testUt
3
3
 
4
4
  import * as zappPipesRedux from "@applicaster/zapp-react-native-redux/ZappPipes";
5
5
  import configureStore from "redux-mock-store";
6
- import thunk from "redux-thunk";
6
+ import { thunk } from "redux-thunk";
7
7
 
8
8
  import { zappPipesDataConnector } from "../index";
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "15.0.0-alpha.8680244503",
3
+ "version": "15.0.0-alpha.9300744523",
4
4
  "description": "Applicaster Zapp React Native ui components for the Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,10 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "15.0.0-alpha.8680244503",
32
- "@applicaster/zapp-react-native-bridge": "15.0.0-alpha.8680244503",
33
- "@applicaster/zapp-react-native-redux": "15.0.0-alpha.8680244503",
34
- "@applicaster/zapp-react-native-utils": "15.0.0-alpha.8680244503",
31
+ "@applicaster/applicaster-types": "15.0.0-alpha.9300744523",
32
+ "@applicaster/zapp-react-native-bridge": "15.0.0-alpha.9300744523",
33
+ "@applicaster/zapp-react-native-redux": "15.0.0-alpha.9300744523",
34
+ "@applicaster/zapp-react-native-utils": "15.0.0-alpha.9300744523",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "url": "^0.11.0",
@@ -1,27 +0,0 @@
1
- /* eslint max-len: off */
2
-
3
- import React from "react";
4
- import { TVEventHandlerComponent } from "@applicaster/zapp-react-native-tvos-ui-components/Components/TVEventHandlerComponent";
5
- import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
6
-
7
- export const withTvEventHandler = (Component) => {
8
- return function WithTVEventHandler(props) {
9
- const navigator = useNavigation();
10
-
11
- const remoteHandler = (event) => {
12
- const { eventType } = event;
13
-
14
- const canGoBack = navigator.canGoBack();
15
-
16
- if (eventType === "menu" && canGoBack) {
17
- navigator.goBack();
18
- }
19
- };
20
-
21
- return (
22
- <TVEventHandlerComponent tvEventHandler={remoteHandler}>
23
- <Component {...props} />
24
- </TVEventHandlerComponent>
25
- );
26
- };
27
- };