@applicaster/zapp-react-native-ui-components 15.0.0-alpha.4429053208 → 15.0.0-alpha.4920595583

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 (23) hide show
  1. package/Components/Cell/CellWithFocusable.tsx +9 -0
  2. package/Components/Focusable/FocusableTvOS.tsx +2 -2
  3. package/Components/FocusableGroup/FocusableTvOS.tsx +4 -27
  4. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  5. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +7 -6
  6. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +6 -2
  7. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +107 -10
  8. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +21 -15
  9. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  10. package/Components/Screen/orientationHandler.ts +7 -10
  11. package/Components/Tabs/TabContent.tsx +7 -4
  12. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +15 -7
  13. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  14. package/Contexts/ScreenContext/index.tsx +25 -18
  15. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  16. package/Contexts/ZappHookModalContext/index.tsx +37 -61
  17. package/Contexts/index.ts +0 -2
  18. package/events/index.ts +1 -0
  19. package/package.json +5 -5
  20. package/Components/FocusableGroup/hooks/__tests__/useIsFocusEnabled.test.ts +0 -113
  21. package/Components/FocusableGroup/hooks/index.ts +0 -1
  22. package/Components/FocusableGroup/hooks/useIsFocusEnabled.ts +0 -68
  23. package/Contexts/AboveTabsScreenContext/index.tsx +0 -33
@@ -2,6 +2,7 @@ import * as React from "react";
2
2
 
3
3
  import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
4
4
  import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
5
+ import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
5
6
 
6
7
  import { useCellState } from "../MasterCell/utils";
7
8
  import { FocusableGroup } from "../FocusableGroup";
@@ -26,6 +27,13 @@ type Props = {
26
27
 
27
28
  const addPrefix = (id: string) => `focusable-cell-wrapper-${id}`;
28
29
 
30
+ const wrapperStyles = {
31
+ flex: platformSelect({
32
+ tvos: 1,
33
+ default: undefined,
34
+ }),
35
+ };
36
+
29
37
  export function CellWithFocusable(props: Props) {
30
38
  const {
31
39
  index,
@@ -94,6 +102,7 @@ export function CellWithFocusable(props: Props) {
94
102
  onFocus={onGroupFocus}
95
103
  onBlur={onGroupBlur}
96
104
  skipFocusManagerRegistration={skipFocusManagerRegistration}
105
+ style={wrapperStyles}
97
106
  >
98
107
  <CellWrapper style={styles.cellWrapper}>
99
108
  <CellRenderer
@@ -13,7 +13,7 @@ import { findNodeHandle, ViewStyle } from "react-native";
13
13
  import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
14
14
 
15
15
  import {
16
- emitDidFocused,
16
+ emitFocused,
17
17
  emitNativeRegistered,
18
18
  } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
19
19
 
@@ -91,7 +91,7 @@ export class Focusable extends BaseFocusable<Props> {
91
91
  }
92
92
 
93
93
  const id: string = nativeEvent.itemID;
94
- emitDidFocused(id);
94
+ emitFocused(id);
95
95
 
96
96
  onFocus(nativeEvent);
97
97
  }
@@ -1,5 +1,4 @@
1
1
  import * as React from "react";
2
- import { compose } from "@applicaster/zapp-react-native-utils/utils";
3
2
  import { FocusableGroupNative } from "@applicaster/zapp-react-native-ui-components/Components/NativeFocusables";
4
3
  import { BaseFocusable } from "@applicaster/zapp-react-native-ui-components/Components/BaseFocusable";
5
4
  import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
@@ -7,9 +6,6 @@ import { LayoutContext } from "@applicaster/zapp-react-native-tvos-app/Context/L
7
6
  import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useRoute";
8
7
  import { isScreenPlayable } from "@applicaster/zapp-react-native-utils/navigationUtils/itemTypes";
9
8
  import { emitNativeRegistered } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
10
- import { withAboveTabsScreenContextConsumer } from "@applicaster/zapp-react-native-ui-components/Contexts/AboveTabsScreenContext";
11
-
12
- import { useIsFocusEnabled } from "./hooks";
13
9
 
14
10
  const { log_verbose } = createLogger({
15
11
  subsystem: "General",
@@ -91,8 +87,8 @@ class FocusableGroupComponent extends BaseFocusable<Props> {
91
87
  }
92
88
  }
93
89
 
94
- export const withFocusDisabledHOC = (Component) => {
95
- return function WithFocusDisabledHOC(props) {
90
+ export const withFocusDisabled = (Component) => {
91
+ return function WithFocusDisabled(props) {
96
92
  // @ts-ignore
97
93
  const { screenFocusBlocked } = React.useContext(LayoutContext.ReactContext);
98
94
 
@@ -102,27 +98,8 @@ export const withFocusDisabledHOC = (Component) => {
102
98
 
103
99
  const blockScreenFocus = isPlayerPresented === false && screenFocusBlocked;
104
100
 
105
- return (
106
- <Component
107
- {...props}
108
- isFocusDisabled={blockScreenFocus || props.isFocusDisabled}
109
- />
110
- );
111
- };
112
- };
113
-
114
- const withAboveTabsScreenHOC = (Component) => {
115
- return function WithAboveTabsScreenHOC(props) {
116
- const { aboveTabsScreen } = props;
117
-
118
- const isFocusEnabled = useIsFocusEnabled(aboveTabsScreen);
119
-
120
- return <Component {...props} isFocusDisabled={!isFocusEnabled} />;
101
+ return <Component {...props} isFocusDisabled={blockScreenFocus} />;
121
102
  };
122
103
  };
123
104
 
124
- export const FocusableGroup = compose(
125
- withAboveTabsScreenContextConsumer,
126
- withAboveTabsScreenHOC,
127
- withFocusDisabledHOC
128
- )(FocusableGroupComponent);
105
+ export const FocusableGroup = withFocusDisabled(FocusableGroupComponent);
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { renderHook } from "@testing-library/react-hooks";
2
+ import { renderHook } from "@testing-library/react-native";
3
3
 
4
4
  import {
5
5
  getTransformedPreset,
@@ -1,4 +1,4 @@
1
- import { renderHook } from "@testing-library/react-hooks";
1
+ import { renderHook, waitFor } from "@testing-library/react-native";
2
2
  import { Image } from "react-native";
3
3
 
4
4
  import { useGetImageDimensions } from "../useGetImageDimensions";
@@ -13,15 +13,16 @@ jest.spyOn(Image, "getSize").mockImplementation((_uri, success) => {
13
13
 
14
14
  describe("useGetImageDimensions", () => {
15
15
  it("should return aspect ration initially when known dimensions", async () => {
16
- const { result, waitForNextUpdate } = renderHook(() =>
16
+ const { result } = renderHook(() =>
17
17
  useGetImageDimensions("https://some_url.com", WIDTH, undefined)
18
18
  );
19
19
 
20
20
  expect(result.current).toBeUndefined();
21
- await waitForNextUpdate();
22
21
 
23
- expect(result.current).toEqual(
24
- getDimension({ width: WIDTH, height: HEIGTH })
25
- );
22
+ await waitFor(() => {
23
+ expect(result.current).toEqual(
24
+ getDimension({ width: WIDTH, height: HEIGTH })
25
+ );
26
+ });
26
27
  });
27
28
  });
@@ -4,7 +4,7 @@ import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/nu
4
4
  import { Button } from "./Button";
5
5
  import {
6
6
  getButtonsCount,
7
- getPluginIdentifier,
7
+ memoizedGetPluginIdentifier,
8
8
  mapSelfAlignment,
9
9
  } from "./utils";
10
10
 
@@ -80,7 +80,11 @@ export const TvActionButtons = ({
80
80
  prefix: independentStyles ? prefixSpecificButton : buttonId(1),
81
81
  value,
82
82
  platformValue,
83
- pluginIdentifier: getPluginIdentifier(configuration, PREFIX, index),
83
+ pluginIdentifier: memoizedGetPluginIdentifier(
84
+ configuration,
85
+ PREFIX,
86
+ index
87
+ ),
84
88
  suffixId: prefixSpecificButton,
85
89
  preferredFocus: index === 0,
86
90
  });
@@ -3,7 +3,7 @@ import { getPluginIdentifier } from "..";
3
3
  describe("getPluginIdentifier", () => {
4
4
  const prefix = "tv_buttons";
5
5
 
6
- it("get first plugin identifier", () => {
6
+ it("returns the first valid plugin identifier", () => {
7
7
  const configuration = {
8
8
  tv_buttons_button_1_other: "value",
9
9
  tv_buttons_button_1_assign_action:
@@ -13,13 +13,12 @@ describe("getPluginIdentifier", () => {
13
13
  };
14
14
 
15
15
  const index = 0;
16
-
17
16
  const result = getPluginIdentifier(configuration, prefix, index);
18
17
 
19
18
  expect(result).toEqual(configuration.tv_buttons_button_1_assign_action);
20
19
  });
21
20
 
22
- it("get second plugin identifier", () => {
21
+ it("returns the second valid plugin identifier", () => {
23
22
  const configuration = {
24
23
  tv_buttons_button_1_other: "value_1",
25
24
  tv_buttons_button_1_assign_action:
@@ -30,24 +29,124 @@ describe("getPluginIdentifier", () => {
30
29
  };
31
30
 
32
31
  const index = 1;
33
-
34
32
  const result = getPluginIdentifier(configuration, prefix, index);
35
33
 
36
34
  expect(result).toEqual(configuration.tv_buttons_button_2_assign_action);
37
35
  });
38
36
 
39
- it("get undefined if no assign_actions at all", () => {
37
+ it("returns undefined when there are no assign_action keys", () => {
40
38
  const configuration = {};
39
+ const index = 0;
40
+
41
+ const result = getPluginIdentifier(configuration, prefix, index);
42
+
43
+ expect(result).toBeUndefined();
44
+ });
45
+
46
+ it("skips undefined values and returns the correct plugin identifier", () => {
47
+ const configuration = {
48
+ tv_buttons_button_1_assign_action: "tv_buttons_button_1_assign_action",
49
+ tv_buttons_button_2_assign_action: undefined,
50
+ tv_buttons_button_3_assign_action: "tv_buttons_button_3_assign_action",
51
+ };
52
+
53
+ const index = 1;
54
+ const result = getPluginIdentifier(configuration, prefix, index);
55
+
56
+ expect(result).toEqual(configuration.tv_buttons_button_3_assign_action);
57
+ });
58
+
59
+ it("handles missing intermediate keys and returns the correct plugin identifier", () => {
60
+ const configuration = {
61
+ tv_buttons_button_1_assign_action: "tv_buttons_button_1_assign_action",
62
+ tv_buttons_button_3_assign_action: "tv_buttons_button_3_assign_action",
63
+ };
64
+
65
+ const index = 1;
66
+ const result = getPluginIdentifier(configuration, prefix, index);
67
+
68
+ expect(result).toEqual(configuration.tv_buttons_button_3_assign_action);
69
+ });
70
+
71
+ it("skips empty string values and returns the first non-empty assign_action", () => {
72
+ const configuration = {
73
+ tv_buttons_button_1_assign_action: "",
74
+ tv_buttons_button_2_assign_action: "tv_buttons_button_2_assign_action",
75
+ };
41
76
 
42
77
  const index = 0;
78
+ const result = getPluginIdentifier(configuration, prefix, index);
79
+
80
+ expect(result).toEqual(configuration.tv_buttons_button_2_assign_action);
81
+ });
82
+
83
+ it("returns undefined for negative index", () => {
84
+ const configuration = {
85
+ tv_buttons_button_1_assign_action: "a",
86
+ tv_buttons_button_2_assign_action: "b",
87
+ };
43
88
 
89
+ const index = -1;
44
90
  const result = getPluginIdentifier(configuration, prefix, index);
45
91
 
46
92
  expect(result).toBeUndefined();
47
93
  });
94
+
95
+ it("returns undefined for out-of-bounds index", () => {
96
+ const configuration = {
97
+ tv_buttons_button_1_assign_action: "a",
98
+ tv_buttons_button_2_assign_action: "b",
99
+ };
100
+
101
+ const index = 5;
102
+ const result = getPluginIdentifier(configuration, prefix, index);
103
+
104
+ expect(result).toBeUndefined();
105
+ });
106
+
107
+ it("ignores keys with wrong prefix or format", () => {
108
+ const configuration = {
109
+ tv_buttons_button_1_assign_action: "a",
110
+ tv_buttons_wrongprefix_2_assign_action: "b",
111
+ tv_buttons_button_x_assign_action: "c",
112
+ };
113
+
114
+ const index = 1;
115
+ const result = getPluginIdentifier(configuration, prefix, index);
116
+
117
+ expect(result).toBeUndefined();
118
+ });
119
+
120
+ it("returns undefined if all assign_actions are null or empty", () => {
121
+ const configuration = {
122
+ tv_buttons_button_1_assign_action: null,
123
+ tv_buttons_button_2_assign_action: undefined,
124
+ tv_buttons_button_3_assign_action: "",
125
+ };
126
+
127
+ const index = 0;
128
+ const result = getPluginIdentifier(configuration, prefix, index);
129
+
130
+ expect(result).toBeUndefined();
131
+ });
132
+
133
+ it("handles non-string values correctly", () => {
134
+ const configuration = {
135
+ tv_buttons_button_1_assign_action: 123,
136
+ tv_buttons_button_2_assign_action: true,
137
+ tv_buttons_button_3_assign_action: { nested: "value" },
138
+ };
139
+
140
+ expect(getPluginIdentifier(configuration, prefix, 0)).toEqual(123);
141
+ expect(getPluginIdentifier(configuration, prefix, 1)).toEqual(true);
142
+
143
+ expect(getPluginIdentifier(configuration, prefix, 2)).toEqual({
144
+ nested: "value",
145
+ });
146
+ });
48
147
  });
49
148
 
50
- describe("getPluginIdentifier - when configuration has same values for different keys", () => {
149
+ describe("getPluginIdentifier - when configuration has duplicate values", () => {
51
150
  const prefix = "tv_buttons";
52
151
 
53
152
  const configuration = {
@@ -55,17 +154,15 @@ describe("getPluginIdentifier - when configuration has same values for different
55
154
  tv_buttons_button_2_assign_action: "navigation_action",
56
155
  };
57
156
 
58
- it("get first plugin identifier", () => {
157
+ it("returns the first plugin identifier when values are identical", () => {
59
158
  const index = 0;
60
-
61
159
  const result = getPluginIdentifier(configuration, prefix, index);
62
160
 
63
161
  expect(result).toEqual(configuration.tv_buttons_button_1_assign_action);
64
162
  });
65
163
 
66
- it("get second plugin identifier", () => {
164
+ it("returns the second plugin identifier when values are identical", () => {
67
165
  const index = 1;
68
-
69
166
  const result = getPluginIdentifier(configuration, prefix, index);
70
167
 
71
168
  expect(result).toEqual(configuration.tv_buttons_button_2_assign_action);
@@ -1,7 +1,7 @@
1
1
  import * as R from "ramda";
2
+ import memoizee from "memoizee";
2
3
 
3
4
  import { isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
4
- import { isNotNil } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
5
5
 
6
6
  export const getButtonsCount = (
7
7
  configuration: Record<string, unknown>,
@@ -21,23 +21,29 @@ export const getPluginIdentifier = (
21
21
  configuration: Record<string, unknown>,
22
22
  prefix: string,
23
23
  index: number
24
- ): string => {
25
- const match = `${prefix}_button_\\d_assign_action`;
26
- const re = new RegExp(match);
27
-
28
- const rejectNils = R.compose(R.path([index]), R.filter(isNotNil));
29
-
30
- return rejectNils(
31
- R.toPairs(configuration)
32
- .filter(([key]) => R.startsWith(`${prefix}_button`, key))
33
- .map(([key, value]) => {
34
- const matched = key.match(re);
24
+ ): string | undefined => {
25
+ const re = new RegExp(`${prefix}_button_\\d_assign_action`);
26
+ let count = 0;
27
+
28
+ for (const [key, value] of Object.entries(configuration)) {
29
+ if (
30
+ key.startsWith(`${prefix}_button`) &&
31
+ re.test(key) &&
32
+ value != null &&
33
+ value !== ""
34
+ ) {
35
+ if (count === index) return value as string;
36
+ count++;
37
+ }
38
+ }
35
39
 
36
- return matched ? value : undefined;
37
- })
38
- );
40
+ return undefined;
39
41
  };
40
42
 
43
+ export const memoizedGetPluginIdentifier = memoizee(getPluginIdentifier, {
44
+ primitive: true,
45
+ });
46
+
41
47
  type Label = {
42
48
  name: string;
43
49
  };
@@ -1,4 +1,4 @@
1
- import { renderHook, act } from "@testing-library/react-hooks";
1
+ import { renderHook, act } from "@testing-library/react-native";
2
2
  import { BehaviorSubject } from "rxjs";
3
3
  import { useLoadingState } from "../useLoadingState";
4
4
 
@@ -1,18 +1,16 @@
1
- import React, { useLayoutEffect } from "react";
1
+ import { useLayoutEffect } from "react";
2
2
 
3
3
  import {
4
4
  allowedOrientationsForScreen,
5
- ORIENTATIONS,
6
5
  getOrientation,
6
+ ORIENTATIONS,
7
7
  useGetScreenOrientation,
8
8
  } from "@applicaster/zapp-react-native-utils/appUtils/orientationHelper";
9
9
  import { usePrevious } from "@applicaster/zapp-react-native-utils/reactHooks/utils";
10
10
  import { usePlugins, useAppData } from "@applicaster/zapp-react-native-redux";
11
11
  import { findPluginByType } from "@applicaster/zapp-react-native-utils/pluginUtils";
12
12
  import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
13
-
14
- import { ZappHookModalContext } from "../../Contexts";
15
- import { HookModalContextT } from "../../Contexts/ZappHookModalContext";
13
+ import { zappHookModalStore } from "../../Contexts/ZappHookModalContext";
16
14
 
17
15
  /**
18
16
  * This function calls the native module needed to set orientation
@@ -89,10 +87,6 @@ type Props = {
89
87
  };
90
88
 
91
89
  export function useScreenOrientationHandler({ screenData, isActive }: Props) {
92
- const { isHooksExecutionInProgress } = React.useContext<HookModalContextT>(
93
- ZappHookModalContext.ReactContext
94
- );
95
-
96
90
  const prevIsActive = usePrevious(isActive);
97
91
 
98
92
  const newOrientation = useNewOrientationForScreenData({
@@ -105,6 +99,9 @@ export function useScreenOrientationHandler({ screenData, isActive }: Props) {
105
99
  return;
106
100
  }
107
101
 
102
+ // TODO: make sure it can be static getter and subscription is not needed
103
+ const { isHooksExecutionInProgress } = zappHookModalStore.getState();
104
+
108
105
  // If modal hook presented we need to skip
109
106
  // Change orientation for presenter screen
110
107
  if (isHooksExecutionInProgress) {
@@ -116,7 +113,7 @@ export function useScreenOrientationHandler({ screenData, isActive }: Props) {
116
113
 
117
114
  setOrientation(newOrientation);
118
115
  }
119
- }, [newOrientation, isHooksExecutionInProgress, prevIsActive, isActive]);
116
+ }, [newOrientation, prevIsActive, isActive]);
120
117
  }
121
118
 
122
119
  export function useOrientationHandler({ screenData }: OrientationHookArgs) {
@@ -1,11 +1,10 @@
1
1
  import React from "react";
2
- import { View, ViewStyle } from "react-native";
2
+ import { View, StyleSheet } from "react-native";
3
3
 
4
4
  import { River } from "@applicaster/zapp-react-native-ui-components/Components";
5
5
  import { withNestedNavigationContextProvider } from "@applicaster/zapp-react-native-ui-components/Contexts/NestedNavigationContext";
6
6
 
7
7
  type Props = {
8
- styles: Record<string, ViewStyle>;
9
8
  minHeight: number;
10
9
  changingTab: boolean;
11
10
  feedUrl: string;
@@ -14,9 +13,14 @@ type Props = {
14
13
  backgroundColor: string;
15
14
  };
16
15
 
16
+ const styles = StyleSheet.create({
17
+ riverWrapper: {
18
+ flex: 1,
19
+ },
20
+ });
21
+
17
22
  function TabContentComponent(props: Props) {
18
23
  const {
19
- styles,
20
24
  minHeight,
21
25
  backgroundColor,
22
26
  changingTab,
@@ -29,7 +33,6 @@ function TabContentComponent(props: Props) {
29
33
  <View
30
34
  style={[
31
35
  styles.riverWrapper,
32
-
33
36
  {
34
37
  backgroundColor,
35
38
  minHeight,
@@ -1,4 +1,4 @@
1
- import { renderHook } from "@testing-library/react-hooks";
1
+ import { act, renderHook, waitFor } from "@testing-library/react-native";
2
2
  import { useDelayedPlayerDetails } from "../useDelayedPlayerDetails";
3
3
  import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
4
4
 
@@ -13,16 +13,20 @@ describe("useDelayedPlayerDetails", () => {
13
13
  jest.clearAllMocks();
14
14
  });
15
15
 
16
- it("should return false initially, that changes after 1 second", () => {
16
+ it("should return false initially, that changes after 1 second", async () => {
17
17
  const { result } = renderHook(() =>
18
18
  useDelayedPlayerDetails({ isInline: true, isDocked: false, isPip: false })
19
19
  );
20
20
 
21
21
  expect(result.current).toBe(false);
22
22
 
23
- jest.advanceTimersByTime(1000);
23
+ act(() => {
24
+ jest.advanceTimersByTime(1000);
25
+ });
24
26
 
25
- expect(result.current).toBe(true);
27
+ await waitFor(() => {
28
+ expect(result.current).toBe(true);
29
+ });
26
30
  });
27
31
 
28
32
  it("should return false when isPip is true", () => {
@@ -41,7 +45,7 @@ describe("useDelayedPlayerDetails", () => {
41
45
  expect(result.current).toBe(false);
42
46
  });
43
47
 
44
- it("should return true for tablet regardless of other flags", () => {
48
+ it("should return true for tablet regardless of other flags", async () => {
45
49
  (useIsTablet as jest.Mock).mockReturnValue(true);
46
50
 
47
51
  const { result } = renderHook(() =>
@@ -52,8 +56,12 @@ describe("useDelayedPlayerDetails", () => {
52
56
  })
53
57
  );
54
58
 
55
- jest.advanceTimersByTime(1000);
59
+ act(() => {
60
+ jest.advanceTimersByTime(1000);
61
+ });
56
62
 
57
- expect(result.current).toBe(true);
63
+ await waitFor(() => {
64
+ expect(result.current).toBe(true);
65
+ });
58
66
  });
59
67
  });
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { renderHook } from "@testing-library/react-hooks";
2
+ import { renderHook } from "@testing-library/react-native";
3
3
  import {
4
4
  ViewportEvents,
5
5
  useViewportEventsContext,
@@ -16,6 +16,7 @@ import { useNestedNavigationContext } from "@applicaster/zapp-react-native-ui-co
16
16
  import { create } from "zustand";
17
17
  import { subscribeWithSelector } from "zustand/middleware";
18
18
  import { useShallow } from "zustand/react/shallow";
19
+
19
20
  import { Animated } from "react-native";
20
21
 
21
22
  interface NavBarStoreState {
@@ -29,6 +30,8 @@ interface NavBarStoreState {
29
30
  scrollState: number;
30
31
  contentPosition: string;
31
32
  scrollYAnimated: Animated.Value;
33
+ hideOnScrollAnimated: Animated.Value;
34
+ hideOnScroll: boolean;
32
35
  }
33
36
 
34
37
  interface NavBarState {
@@ -62,24 +65,28 @@ const createStateStore = () =>
62
65
  );
63
66
 
64
67
  const createStore = () =>
65
- create<NavBarStoreState>((set) => ({
66
- title: "",
67
- summary: "",
68
- visible: true,
69
- height: 0,
70
- scrollState: 0,
71
- contentPosition: "",
72
- scrollYAnimated: new Animated.Value(0),
73
- setTitle(title) {
74
- set({ title });
75
- },
76
- setSummary(summary) {
77
- set({ summary });
78
- },
79
- setVisible(visible) {
80
- set({ visible });
81
- },
82
- }));
68
+ create(
69
+ subscribeWithSelector<NavBarStoreState>((set) => ({
70
+ title: "",
71
+ summary: "",
72
+ visible: true,
73
+ height: 0,
74
+ scrollState: 0,
75
+ contentPosition: "",
76
+ scrollYAnimated: new Animated.Value(0),
77
+ hideOnScrollAnimated: new Animated.Value(0),
78
+ hideOnScroll: false,
79
+ setTitle(title) {
80
+ set({ title });
81
+ },
82
+ setSummary(summary) {
83
+ set({ summary });
84
+ },
85
+ setVisible(visible) {
86
+ set({ visible });
87
+ },
88
+ }))
89
+ );
83
90
 
84
91
  type ScreenContextType = {
85
92
  _navBarStore: ReturnType<typeof createStore>;
@@ -1,4 +1,4 @@
1
- import { renderHook } from "@testing-library/react-hooks";
1
+ import { renderHook } from "@testing-library/react-native";
2
2
  import {
3
3
  useScreenTrackedViewPositionsContext,
4
4
  ScreenTrackedViewPositionsContext,
@@ -1,4 +1,4 @@
1
- import React, { useCallback } from "react";
1
+ import { create } from "zustand";
2
2
 
3
3
  type HookModalState = {
4
4
  path: string;
@@ -10,11 +10,12 @@ export type HookModalContextT = {
10
10
  setIsHooksExecutionInProgress: (hookExecutionState?: boolean) => void;
11
11
  isRunningInBackground: boolean;
12
12
  isPresentationFullScreen: boolean;
13
- setIsRunningInBackground: (hookBackgroundProcessState?: boolean) => void;
14
- setIsPresentingFullScreen: (hookBackgroundProcessState?: boolean) => void;
13
+ setIsRunningInBackground: () => void;
14
+ setIsPresentingFullScreen: () => void;
15
15
  state: HookModalState;
16
16
  setState: (state: HookModalState) => void;
17
17
  resetState: () => void;
18
+ hookPresentationMode: HookPresentationMode;
18
19
  };
19
20
 
20
21
  const initialState = {
@@ -24,68 +25,43 @@ const initialState = {
24
25
 
25
26
  type HookPresentationMode = "background" | "fullScreen";
26
27
 
27
- const ReactContext = React.createContext<HookModalContextT>({
28
- isHooksExecutionInProgress: false,
29
- setIsHooksExecutionInProgress: () => {},
30
- isRunningInBackground: null,
31
- setIsRunningInBackground: () => {},
32
- setIsPresentingFullScreen: () => {},
33
- isPresentationFullScreen: null,
28
+ // Use useZappHookModalStore() in React components (hooks)
29
+ // Use zappHookModalStore.getState() in non-React functions (utility functions, etc.)
30
+ export const useZappHookModalStore = create<HookModalContextT>()((set) => ({
34
31
  state: initialState,
35
- setState: () => null,
36
- resetState: () => null,
37
- });
38
-
39
- const Provider = ({ children }: { children: React.ReactNode }) => {
40
- const [state, setState] = React.useState<HookModalState>(initialState);
41
-
42
- const [hookPresentationMode, setHookPresentationMode] =
43
- React.useState<HookPresentationMode>(null);
44
-
45
- const resetState = useCallback(() => {
46
- setState(initialState);
47
- }, [setState]);
48
-
49
- const [isHooksExecutionInProgress, setIsHooksExecutionInProgress] =
50
- React.useState(false);
32
+ isHooksExecutionInProgress: false,
33
+ hookPresentationMode: null as HookPresentationMode,
34
+ isRunningInBackground: false,
35
+ isPresentationFullScreen: false,
51
36
 
52
- const setIsRunningInBackground = () => {
53
- setHookPresentationMode("background");
54
- };
37
+ setState: (newState: HookModalState) => {
38
+ set({ state: newState });
39
+ },
55
40
 
56
- const setIsPresentingFullScreen = () => {
57
- setHookPresentationMode("fullScreen");
58
- };
41
+ setIsHooksExecutionInProgress: (hookExecutionState?: boolean) => {
42
+ set({ isHooksExecutionInProgress: hookExecutionState ?? false });
43
+ },
59
44
 
60
- return (
61
- <ReactContext.Provider
62
- value={{
63
- isRunningInBackground: hookPresentationMode === "background",
64
- setIsRunningInBackground,
65
- setIsPresentingFullScreen,
66
- isPresentationFullScreen: hookPresentationMode === "fullScreen",
67
- isHooksExecutionInProgress,
68
- setIsHooksExecutionInProgress,
69
- state,
70
- setState,
71
- resetState,
72
- }}
73
- >
74
- {children}
75
- </ReactContext.Provider>
76
- );
77
- };
45
+ setIsRunningInBackground: () => {
46
+ set({
47
+ hookPresentationMode: "background",
48
+ isRunningInBackground: true,
49
+ isPresentationFullScreen: false,
50
+ });
51
+ },
78
52
 
79
- const withProvider = (Component) => {
80
- const WithProvider = (props) => {
81
- return (
82
- <Provider>
83
- <Component {...props} />
84
- </Provider>
85
- );
86
- };
53
+ setIsPresentingFullScreen: () => {
54
+ set({
55
+ hookPresentationMode: "fullScreen",
56
+ isRunningInBackground: false,
57
+ isPresentationFullScreen: true,
58
+ });
59
+ },
87
60
 
88
- return WithProvider;
89
- };
61
+ resetState: () => {
62
+ set({ state: initialState });
63
+ },
64
+ }));
90
65
 
91
- export const ZappHookModalContext = { withProvider, ReactContext };
66
+ // Export an alias for clearer non-React usage (utility functions, etc.)
67
+ export const zappHookModalStore = useZappHookModalStore;
package/Contexts/index.ts CHANGED
@@ -12,8 +12,6 @@ export { HorizontalScrollContext } from "./HorizontalScrollContext";
12
12
 
13
13
  export { RiverOffsetContext } from "./RiverOffsetContext";
14
14
 
15
- export { ZappHookModalContext } from "./ZappHookModalContext";
16
-
17
15
  export { MeasurementContext } from "./MeasurementContext";
18
16
 
19
17
  export * from "./ZappPipesContext";
package/events/index.ts CHANGED
@@ -5,4 +5,5 @@ export enum QBUIComponentEvents {
5
5
  scrollVerticallyToInitialOffset = "scrollVerticallyToInitialOffset",
6
6
  focusOnSelectedTab = "focusOnSelectedTab",
7
7
  focusOnSelectedTopMenuItem = "focusOnSelectedTopMenuItem",
8
+ scrollToTopForScreenWrappedInContainer = "scrollToTopForScreenWrappedInContainer",
8
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.4429053208",
3
+ "version": "15.0.0-alpha.4920595583",
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.4429053208",
32
- "@applicaster/zapp-react-native-bridge": "15.0.0-alpha.4429053208",
33
- "@applicaster/zapp-react-native-redux": "15.0.0-alpha.4429053208",
34
- "@applicaster/zapp-react-native-utils": "15.0.0-alpha.4429053208",
31
+ "@applicaster/applicaster-types": "15.0.0-alpha.4920595583",
32
+ "@applicaster/zapp-react-native-bridge": "15.0.0-alpha.4920595583",
33
+ "@applicaster/zapp-react-native-redux": "15.0.0-alpha.4920595583",
34
+ "@applicaster/zapp-react-native-utils": "15.0.0-alpha.4920595583",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "url": "^0.11.0",
@@ -1,113 +0,0 @@
1
- import { act, renderHook } from "@testing-library/react-native";
2
-
3
- import { useIsFocusEnabled } from "../useIsFocusEnabled";
4
-
5
- // ----------------- MOCKS -----------------
6
- jest.mock(
7
- "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios",
8
- () => ({
9
- focusManager: {
10
- isFocusOnMenu: jest.fn(),
11
- isFocusOnTabsScreen: jest.fn(),
12
- },
13
- })
14
- );
15
-
16
- jest.mock(
17
- "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios",
18
- () => {
19
- const { Subject } = require("rxjs");
20
-
21
- return {
22
- willFocused$: new Subject<void>(),
23
- didFocused$: new Subject<void>(),
24
- TabsScreenScreenSelectorContainerRegistry: {
25
- observable$: new Subject<string>(),
26
- },
27
- };
28
- }
29
- );
30
-
31
- import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
32
- import {
33
- willFocused$,
34
- didFocused$,
35
- TabsScreenScreenSelectorContainerRegistry,
36
- } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
37
-
38
- // ----------------- TESTS -----------------
39
- describe("useIsFocusEnabled", () => {
40
- beforeEach(() => {
41
- jest.clearAllMocks();
42
- });
43
-
44
- it("returns true by default", () => {
45
- const { result } = renderHook(() => useIsFocusEnabled(true));
46
-
47
- expect(result.current).toBe(true);
48
- });
49
-
50
- it("disables focus when focus moves to menu", () => {
51
- (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(true);
52
-
53
- const { result } = renderHook(() => useIsFocusEnabled(true));
54
-
55
- act(() => {
56
- willFocused$.next();
57
- didFocused$.next();
58
- });
59
-
60
- expect(result.current).toBe(false);
61
- });
62
-
63
- it("re-enables focus when focus lands on tabs screen", () => {
64
- (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(true);
65
- (focusManager.isFocusOnTabsScreen as jest.Mock).mockReturnValue(true);
66
-
67
- const { result } = renderHook(() => useIsFocusEnabled(true));
68
-
69
- // Disable focus first
70
- act(() => {
71
- willFocused$.next();
72
- didFocused$.next();
73
- });
74
-
75
- expect(result.current).toBe(false);
76
-
77
- // Enable focus again
78
- act(() => {
79
- didFocused$.next();
80
- TabsScreenScreenSelectorContainerRegistry.observable$.next("tabs-id");
81
- });
82
-
83
- expect(result.current).toBe(true);
84
- });
85
-
86
- it("does nothing when not above tabs screen", () => {
87
- const { result } = renderHook(() => useIsFocusEnabled(false));
88
-
89
- act(() => {
90
- willFocused$.next();
91
- didFocused$.next();
92
- TabsScreenScreenSelectorContainerRegistry.observable$.next("id");
93
- });
94
-
95
- expect(result.current).toBe(true);
96
- });
97
-
98
- it("cleans up subscriptions on unmount", () => {
99
- (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(true);
100
-
101
- const { unmount, result } = renderHook(() => useIsFocusEnabled(true));
102
-
103
- unmount();
104
-
105
- act(() => {
106
- willFocused$.next();
107
- didFocused$.next();
108
- });
109
-
110
- // State should not change after unmount
111
- expect(result.current).toBe(true);
112
- });
113
- });
@@ -1 +0,0 @@
1
- export { useIsFocusEnabled } from "./useIsFocusEnabled";
@@ -1,68 +0,0 @@
1
- import * as React from "react";
2
- import { switchMap, first, filter, repeat } from "rxjs/operators";
3
- import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
4
-
5
- import {
6
- willFocused$,
7
- didFocused$,
8
- TabsScreenScreenSelectorContainerRegistry,
9
- } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
10
-
11
- export const useIsFocusEnabled = (isAboveTabsScreen: boolean): boolean => {
12
- const [isFocusEnabled, setIsFocusedEnabled] = React.useState(true);
13
-
14
- const enableFocus = React.useCallback(() => {
15
- setIsFocusedEnabled(true);
16
- }, []);
17
-
18
- const disableFocus = React.useCallback(() => {
19
- setIsFocusedEnabled(false);
20
- }, []);
21
-
22
- React.useEffect(() => {
23
- const subscription = didFocused$
24
- .pipe(
25
- filter(() => isAboveTabsScreen && !isFocusEnabled),
26
-
27
- switchMap(() => TabsScreenScreenSelectorContainerRegistry.observable$),
28
- filter((id) => id && focusManager.isFocusOnTabsScreen(id)),
29
-
30
- // run only once, then re-subscribe on didFocused$ again
31
- first(),
32
- repeat()
33
- )
34
- .subscribe(() => {
35
- enableFocus();
36
- });
37
-
38
- return () => {
39
- subscription.unsubscribe();
40
- };
41
- }, [enableFocus, isFocusEnabled, isAboveTabsScreen]);
42
-
43
- React.useEffect(() => {
44
- const subscription = willFocused$
45
- .pipe(
46
- filter(() => isAboveTabsScreen && isFocusEnabled),
47
-
48
- // start waiting onFocus event
49
- switchMap(() => didFocused$),
50
-
51
- // focus is landed on top-menu
52
- filter(() => focusManager.isFocusOnMenu()),
53
-
54
- // run only once, then re-subscribe on willFocused$ again
55
- first(),
56
- repeat()
57
- )
58
- .subscribe(() => {
59
- disableFocus();
60
- });
61
-
62
- return () => {
63
- subscription.unsubscribe();
64
- };
65
- }, [disableFocus, isFocusEnabled, isAboveTabsScreen]);
66
-
67
- return isFocusEnabled;
68
- };
@@ -1,33 +0,0 @@
1
- import React, { useMemo } from "react";
2
- import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
3
-
4
- export const AboveTabsScreenContext = React.createContext({
5
- aboveTabsScreen: false,
6
- });
7
-
8
- export const withAboveTabsScreenContext = (Component) =>
9
- function AbovetabsScreenContextProviderWrapper(props) {
10
- const aboveTabsScreen = toBooleanWithDefaultFalse(
11
- props?.item?.above_tabs_screen
12
- );
13
-
14
- const contextValue = useMemo(
15
- () => ({
16
- aboveTabsScreen,
17
- }),
18
- [aboveTabsScreen]
19
- );
20
-
21
- return (
22
- <AboveTabsScreenContext.Provider value={contextValue}>
23
- <Component {...props} />
24
- </AboveTabsScreenContext.Provider>
25
- );
26
- };
27
-
28
- export const withAboveTabsScreenContextConsumer = (Component) =>
29
- function AboveTabsScreenContextConsumerWrapper(props) {
30
- const { aboveTabsScreen } = React.useContext(AboveTabsScreenContext);
31
-
32
- return <Component {...props} aboveTabsScreen={aboveTabsScreen} />;
33
- };