@applicaster/zapp-react-native-ui-components 15.0.0-rc.66 → 15.0.0-rc.67

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.
@@ -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/search-screen/src/tv/utils";
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,9 +10,13 @@ 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 { emitNativeRegistered } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
14
13
  import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
15
14
 
15
+ import {
16
+ emitFocused,
17
+ emitNativeRegistered,
18
+ } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
19
+
16
20
  type Props = {
17
21
  id: string;
18
22
  groupId: string;
@@ -86,6 +90,9 @@ export class Focusable extends BaseFocusable<Props> {
86
90
  });
87
91
  }
88
92
 
93
+ const id: string = nativeEvent.itemID;
94
+ emitFocused(id);
95
+
89
96
  onFocus(nativeEvent);
90
97
  }
91
98
 
@@ -2,6 +2,9 @@ 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";
5
8
  import { emitNativeRegistered } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
6
9
 
7
10
  const { log_verbose } = createLogger({
@@ -34,7 +37,7 @@ type Props = {
34
37
  screenData: { screenId: string; parentScreenId: string };
35
38
  };
36
39
 
37
- export class FocusableGroup extends BaseFocusable<Props> {
40
+ class FocusableGroupComponent extends BaseFocusable<Props> {
38
41
  public readonly isGroup: boolean = true;
39
42
 
40
43
  onRegistered = ({ nativeEvent }) => {
@@ -83,3 +86,20 @@ export class FocusableGroup extends BaseFocusable<Props> {
83
86
  );
84
87
  }
85
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);
@@ -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,12 @@
1
- import { compose } from "ramda";
1
+ import { compose } from "@applicaster/zapp-react-native-utils/utils";
2
+
2
3
  import { River as RiverComponent } from "./River";
3
- import { withTvEventHandler } from "./withTVEventHandler";
4
4
  import { withComponentsMapOffsetContext } from "../../../Contexts/ComponentsMapOffsetContext";
5
5
  import { withRiverDataLoader } from "./withRiverDataLoader";
6
+ import { withFocusableGroupForContent } from "./withFocusableGroupForContent";
6
7
 
7
8
  export const River = compose(
8
- withTvEventHandler,
9
9
  withComponentsMapOffsetContext,
10
- withRiverDataLoader
10
+ withRiverDataLoader,
11
+ withFocusableGroupForContent
11
12
  )(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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "15.0.0-rc.66",
3
+ "version": "15.0.0-rc.67",
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-rc.66",
32
- "@applicaster/zapp-react-native-bridge": "15.0.0-rc.66",
33
- "@applicaster/zapp-react-native-redux": "15.0.0-rc.66",
34
- "@applicaster/zapp-react-native-utils": "15.0.0-rc.66",
31
+ "@applicaster/applicaster-types": "15.0.0-rc.67",
32
+ "@applicaster/zapp-react-native-bridge": "15.0.0-rc.67",
33
+ "@applicaster/zapp-react-native-redux": "15.0.0-rc.67",
34
+ "@applicaster/zapp-react-native-utils": "15.0.0-rc.67",
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
- };