@applicaster/zapp-react-native-utils 14.0.0-alpha.5114565431 → 14.0.0-alpha.5219335081

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.
@@ -6,6 +6,7 @@ exports[`focusManager should be defined 1`] = `
6
6
  "disableFocus": [Function],
7
7
  "enableFocus": [Function],
8
8
  "findPreferredFocusChild": [Function],
9
+ "focusTopNavigation": [Function],
9
10
  "focusableTree": Tree {
10
11
  "loadingCounter": 0,
11
12
  "root": {
@@ -24,6 +25,8 @@ exports[`focusManager should be defined 1`] = `
24
25
  "invokeHandler": [Function],
25
26
  "isCurrentFocusOnTheTopScreen": [Function],
26
27
  "isFocusDisabled": [Function],
28
+ "isFocusOnContent": [Function],
29
+ "isFocusOnMenu": [Function],
27
30
  "isGroupItemFocused": [Function],
28
31
  "longPress": [Function],
29
32
  "moveFocus": [Function],
@@ -33,7 +33,7 @@ describe("focusManager", () => {
33
33
 
34
34
  expect(success).toBe(true);
35
35
  expect(mockSetFocus).toBeCalledTimes(1);
36
- expect(mockSetFocus).toBeCalledWith(null);
36
+ expect(mockSetFocus).toBeCalledWith(null, undefined);
37
37
  });
38
38
 
39
39
  describe("register", () => {});
@@ -14,6 +14,14 @@ import { subscriber } from "../../functionUtils";
14
14
  import { coreLogger } from "../../logger";
15
15
  import { ACTION } from "./utils/enums";
16
16
 
17
+ import {
18
+ findSelectedTabId,
19
+ findSelectedMenuId,
20
+ isTabsScreenContentFocused,
21
+ isCurrentFocusOnContent,
22
+ isCurrentFocusOnMenu,
23
+ } from "../focusManagerAux/utils";
24
+
17
25
  const logger = coreLogger.addSubsystem("focusManager");
18
26
 
19
27
  const isFocusEnabled = (focusableItem): boolean => {
@@ -100,7 +108,7 @@ export const focusManager = (function () {
100
108
  * @private
101
109
  * @param {Object} direction of the navigation which led to this action
102
110
  */
103
- function focus(direction) {
111
+ function focus(direction, context?: FocusManager.FocusContext) {
104
112
  const currentFocusable = getCurrentFocus();
105
113
 
106
114
  if (
@@ -108,7 +116,7 @@ export const focusManager = (function () {
108
116
  !currentFocusable.isGroup &&
109
117
  currentFocusable.isMounted()
110
118
  ) {
111
- currentFocusable.setFocus(direction);
119
+ currentFocusable.setFocus(direction, context);
112
120
  }
113
121
  }
114
122
 
@@ -205,7 +213,7 @@ export const focusManager = (function () {
205
213
  * @param {Array<string>} ids - An array of node IDs to update.
206
214
  * @param {boolean} setFocus - A flag indicating whether to set focus (true) or blur (false) on the nodes.
207
215
  */
208
- const updateNodeFocus = (ids, action) => {
216
+ const updateNodeFocus = (ids, action, context: FocusManager.FocusContext) => {
209
217
  if (!ids || ids.length === 0) {
210
218
  return; // Nothing to do
211
219
  }
@@ -222,11 +230,13 @@ export const focusManager = (function () {
222
230
 
223
231
  // Function to apply the action (focus or blur)
224
232
  const applyAction = (node) => {
233
+ const direction = undefined;
234
+
225
235
  if (node && node.component) {
226
236
  if (action === "focus") {
227
- node.component.setFocus();
237
+ node.component.setFocus(direction, context);
228
238
  } else if (action === "blur") {
229
- node.component.setBlur();
239
+ node.component.setBlur(direction, context);
230
240
  }
231
241
  }
232
242
  };
@@ -253,7 +263,11 @@ export const focusManager = (function () {
253
263
  * @param {Object} direction of the navigation, which led to this focus change
254
264
  * to another group or not. defaults to false
255
265
  */
256
- function setFocus(id: string, direction?: FocusManager.Web.Direction) {
266
+ function setFocus(
267
+ id: string,
268
+ direction?: FocusManager.Web.Direction,
269
+ context?: FocusManager.FocusContext
270
+ ) {
257
271
  if (focusDisabled) return false;
258
272
 
259
273
  // due to optimisiation it's recommanded to set currentFocusNode before setFocus
@@ -266,21 +280,65 @@ export const focusManager = (function () {
266
280
  );
267
281
 
268
282
  // Set focus on current node parents and blur on previous node parents
269
- updateNodeFocus(currentNodeParentsIDs, ACTION.FOCUS);
270
- updateNodeFocus(previousNodeParentsIDs, ACTION.BLUR);
283
+ updateNodeFocus(currentNodeParentsIDs, ACTION.FOCUS, context);
284
+ updateNodeFocus(previousNodeParentsIDs, ACTION.BLUR, context);
271
285
 
272
286
  currentFocusNode = focusableTree.findInTree(id);
273
287
  }
274
288
 
275
289
  setLastFocusOnParentNode(currentFocusNode);
276
290
 
277
- focus(direction);
291
+ focus(direction, context);
292
+ }
293
+
294
+ function isFocusOnContent() {
295
+ return isCurrentFocusOnContent(currentFocusNode);
296
+ }
297
+
298
+ function isFocusOnMenu() {
299
+ return isCurrentFocusOnMenu(currentFocusNode);
300
+ }
301
+
302
+ function landFocusTo(id) {
303
+ if (id) {
304
+ // set focus on selected menu item
305
+ const direction = undefined;
306
+
307
+ const context: FocusManager.FocusContext = {
308
+ source: "back",
309
+ preserveScroll: true,
310
+ };
311
+
312
+ logger.log({ message: "landFocusTo", data: { id } });
313
+
314
+ blur(direction);
315
+ setFocus(id, direction, context);
316
+ }
317
+ }
318
+
319
+ // Move focus to appropriate top navigation tab with context
320
+ function focusTopNavigation(isTabsScreen: boolean, item: ZappEntry) {
321
+ if (isTabsScreen && isTabsScreenContentFocused(currentFocusNode)) {
322
+ const selectedTabId = findSelectedTabId(item);
323
+
324
+ // Set focus with back button context to tabs-menu
325
+ landFocusTo(selectedTabId);
326
+
327
+ return;
328
+ }
329
+
330
+ const selectedMenuItemId = findSelectedMenuId(focusableTree);
331
+ // Set focus with back button context to top-menu
332
+ landFocusTo(selectedMenuItemId);
278
333
  }
279
334
 
280
335
  /**
281
336
  * sets the initial focus when the screen loads, or when focus is lost
282
337
  */
283
- function setInitialFocus(lastAddedParentNode?: any) {
338
+ function setInitialFocus(
339
+ lastAddedParentNode?: any,
340
+ context?: FocusManager.FocusContext
341
+ ) {
284
342
  const preferredFocus = findPriorityItem(
285
343
  lastAddedParentNode?.children || focusableTree.root.children
286
344
  );
@@ -326,7 +384,7 @@ export const focusManager = (function () {
326
384
  },
327
385
  });
328
386
 
329
- focusableItem && setFocus(focusCandidate.id, null);
387
+ focusableItem && setFocus(focusCandidate.id, null, context);
330
388
 
331
389
  return { success: true };
332
390
  }
@@ -576,5 +634,9 @@ export const focusManager = (function () {
576
634
  recoverFocus,
577
635
  isCurrentFocusOnTheTopScreen,
578
636
  findPreferredFocusChild,
637
+
638
+ focusTopNavigation,
639
+ isFocusOnContent,
640
+ isFocusOnMenu,
579
641
  };
580
642
  })();
@@ -1,14 +1,31 @@
1
- import { isNotNil } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
2
- import { find, last, pathOr, startsWith } from "ramda";
1
+ import {
2
+ isNil,
3
+ last,
4
+ startsWith,
5
+ find,
6
+ pathOr,
7
+ } from "@applicaster/zapp-react-native-utils/utils";
8
+
3
9
  import {
4
10
  QUICK_BRICK_CONTENT,
5
11
  QUICK_BRICK_NAVBAR,
6
12
  } from "@applicaster/quick-brick-core/const";
7
13
 
14
+ import {
15
+ generateFocusableId,
16
+ SCREEN_PICKER_CONTAINER,
17
+ } from "@applicaster/zapp-react-native-utils/screenPickerUtils";
18
+
8
19
  // run check each 300 ms
9
20
  // run this check too often could lead to performance penalty on low-end devices
10
21
  const HOW_OFTEN_TO_CHECK_CONDITION = 300; // ms
11
22
 
23
+ const isTopMenu = (node) => startsWith(QUICK_BRICK_NAVBAR, node?.id);
24
+ const isContent = (node) => startsWith(QUICK_BRICK_CONTENT, node?.id);
25
+ const isRoot = (node) => node?.id === "root";
26
+
27
+ const isScrenPicker = (node) => startsWith(SCREEN_PICKER_CONTAINER, node?.id);
28
+
12
29
  type Props = {
13
30
  maxTimeout: number;
14
31
  conditionFn: () => boolean;
@@ -49,7 +66,7 @@ export const waitForActiveScreen = (currentRoute: string, focusableTree) => {
49
66
 
50
67
  const route = find((route) => route.id === currentRoute, routes);
51
68
 
52
- return isNotNil(route);
69
+ return !isNil(route);
53
70
  };
54
71
 
55
72
  return waitUntil({
@@ -99,3 +116,73 @@ export const waitForContent = (focusableTree) => {
99
116
  conditionFn: contentHasAnyChildren,
100
117
  });
101
118
  };
119
+
120
+ export const findSelectedTabId = (item: ZappEntry): string => {
121
+ const selectedTabId = generateFocusableId(item.id);
122
+
123
+ return selectedTabId;
124
+ };
125
+
126
+ export const findSelectedMenuId = (focusableTree) => {
127
+ // Set focus with back button context
128
+ const navbar = getNavbarNode(focusableTree);
129
+
130
+ const selectedMenuItemId = find(
131
+ (child) => child.component.props.selected,
132
+ navbar.children
133
+ )?.id;
134
+
135
+ return selectedMenuItemId;
136
+ };
137
+
138
+ export const isTabsScreenContentFocused = (node) => {
139
+ if (isRoot(node)) {
140
+ return false;
141
+ }
142
+
143
+ if (isTopMenu(node)) {
144
+ return false;
145
+ }
146
+
147
+ if (isContent(node)) {
148
+ return false;
149
+ }
150
+
151
+ if (isScrenPicker(node)) {
152
+ return true;
153
+ }
154
+
155
+ return isTabsScreenContentFocused(node.parent);
156
+ };
157
+
158
+ export const isCurrentFocusOnMenu = (node) => {
159
+ if (isRoot(node)) {
160
+ return false;
161
+ }
162
+
163
+ if (isTopMenu(node)) {
164
+ return true;
165
+ }
166
+
167
+ if (isContent(node)) {
168
+ return false;
169
+ }
170
+
171
+ return isCurrentFocusOnMenu(node.parent);
172
+ };
173
+
174
+ export const isCurrentFocusOnContent = (node) => {
175
+ if (isRoot(node)) {
176
+ return false;
177
+ }
178
+
179
+ if (isTopMenu(node)) {
180
+ return false;
181
+ }
182
+
183
+ if (isContent(node)) {
184
+ return true;
185
+ }
186
+
187
+ return isCurrentFocusOnContent(node.parent);
188
+ };
@@ -0,0 +1,130 @@
1
+ import { mapContentTypesToRivers } from "../index";
2
+
3
+ describe("mapContentTypesToRivers", () => {
4
+ it("should return the correct content types mapped to rivers", () => {
5
+ const state = {
6
+ rivers: {
7
+ "river-1": {
8
+ plugin_type: "river",
9
+ },
10
+ },
11
+ contentTypes: {
12
+ "content-type-1": {
13
+ screen_id: "river-1",
14
+ },
15
+ },
16
+ };
17
+
18
+ const result = mapContentTypesToRivers(state);
19
+
20
+ expect(result).toEqual({
21
+ "content-type-1": {
22
+ screenType: "river",
23
+ screen_id: "river-1",
24
+ },
25
+ });
26
+ });
27
+
28
+ it("should return null if contentTypes is undefined", () => {
29
+ const state = {
30
+ rivers: {
31
+ "river-1": {
32
+ plugin_type: "river",
33
+ },
34
+ },
35
+ // contentTypes is missing
36
+ };
37
+
38
+ const result = mapContentTypesToRivers(state);
39
+
40
+ expect(result).toBeNull();
41
+ });
42
+
43
+ it("should skip content types whose screen does not exist in rivers", () => {
44
+ const state = {
45
+ rivers: {
46
+ "river-1": {
47
+ plugin_type: "river",
48
+ },
49
+ },
50
+ contentTypes: {
51
+ "content-type-1": {
52
+ screen_id: "river-1",
53
+ },
54
+ "content-type-2": {
55
+ screen_id: "river-2", // river-2 does not exist
56
+ },
57
+ },
58
+ };
59
+
60
+ const result = mapContentTypesToRivers(state);
61
+
62
+ expect(result).toEqual({
63
+ "content-type-1": {
64
+ screenType: "river",
65
+ screen_id: "river-1",
66
+ },
67
+ });
68
+
69
+ // result is not null, but may be undefined for missing keys
70
+ expect(result && result["content-type-2"]).toBeUndefined();
71
+ });
72
+
73
+ it("should use 'type' if 'plugin_type' is not present in river", () => {
74
+ const state = {
75
+ rivers: {
76
+ "river-1": {
77
+ type: "custom-type",
78
+ },
79
+ },
80
+ contentTypes: {
81
+ "content-type-1": {
82
+ screen_id: "river-1",
83
+ },
84
+ },
85
+ };
86
+
87
+ const result = mapContentTypesToRivers(state);
88
+
89
+ expect(result).toEqual({
90
+ "content-type-1": {
91
+ screenType: "custom-type",
92
+ screen_id: "river-1",
93
+ },
94
+ });
95
+ });
96
+
97
+ it("should skip content types if neither plugin_type nor type is present in river", () => {
98
+ const state = {
99
+ rivers: {
100
+ "river-1": {
101
+ // no plugin_type or type
102
+ },
103
+ },
104
+ contentTypes: {
105
+ "content-type-1": {
106
+ screen_id: "river-1",
107
+ },
108
+ },
109
+ };
110
+
111
+ const result = mapContentTypesToRivers(state);
112
+
113
+ expect(result).toEqual({});
114
+ });
115
+
116
+ it("should handle empty contentTypes object", () => {
117
+ const state = {
118
+ rivers: {
119
+ "river-1": {
120
+ plugin_type: "river",
121
+ },
122
+ },
123
+ contentTypes: {},
124
+ };
125
+
126
+ const result = mapContentTypesToRivers(state);
127
+
128
+ expect(result).toEqual({});
129
+ });
130
+ });
@@ -13,6 +13,7 @@ import {
13
13
  isPlayable,
14
14
  isV2River,
15
15
  } from "./itemTypeMatchers";
16
+ import { RootState } from "@applicaster/zapp-react-native-redux/store";
16
17
 
17
18
  type PathAttribute = {
18
19
  screenType: string;
@@ -377,10 +378,11 @@ export const usesVideoModal = (
377
378
  return targetScreenConfiguration?.styles?.use_video_modal;
378
379
  };
379
380
 
380
- export const mapContentTypesToRivers = ({
381
- rivers,
382
- contentTypes,
383
- }): ZappContentTypesMapped | null => {
381
+ export const mapContentTypesToRivers = (
382
+ state: Partial<RootState>
383
+ ): ZappContentTypesMapped | null => {
384
+ const { rivers, contentTypes } = state;
385
+
384
386
  if (!contentTypes) {
385
387
  return null;
386
388
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "14.0.0-alpha.5114565431",
3
+ "version": "14.0.0-alpha.5219335081",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "homepage": "https://github.com/applicaster/quickbrick#readme",
29
29
  "dependencies": {
30
- "@applicaster/applicaster-types": "14.0.0-alpha.5114565431",
30
+ "@applicaster/applicaster-types": "14.0.0-alpha.5219335081",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -38,7 +38,6 @@
38
38
  "peerDependencies": {
39
39
  "@applicaster/zapp-pipes-v2-client": "*",
40
40
  "@react-native-community/netinfo": "*",
41
- "immer": "*",
42
41
  "react": "*",
43
42
  "react-native": "*",
44
43
  "uglify-js": "*",
@@ -23,7 +23,9 @@ jest.mock(
23
23
 
24
24
  jest.useFakeTimers();
25
25
 
26
- jest.mock("@applicaster/zapp-react-native-utils/reactHooks/navigation");
26
+ jest.mock(
27
+ "@applicaster/zapp-react-native-utils/reactHooks/navigation/useNavigation"
28
+ );
27
29
 
28
30
  const mockStore = configureStore();
29
31
 
@@ -1,8 +1,10 @@
1
1
  import { complement, compose, isNil, map, min, prop, take, uniq } from "ramda";
2
- import { useDispatch } from "react-redux";
3
2
  import * as React from "react";
4
- import { useZappPipesFeeds } from "@applicaster/zapp-react-native-redux/hooks";
5
- import { loadPipesData } from "@applicaster/zapp-react-native-redux/ZappPipes";
3
+ import {
4
+ ZappPipes,
5
+ useAppDispatch,
6
+ useZappPipesFeed,
7
+ } from "@applicaster/zapp-react-native-redux";
6
8
  import { isNilOrEmpty } from "../../reactUtils/helpers";
7
9
  import { ZappPipesSearchContext } from "@applicaster/zapp-react-native-ui-components/Contexts";
8
10
  import {
@@ -63,7 +65,7 @@ export const useBatchLoading = (
63
65
  componentsToRender: { data?: ZappDataSource; component_type: string }[],
64
66
  options: Options
65
67
  ) => {
66
- const dispatch = useDispatch();
68
+ const dispatch = useAppDispatch();
67
69
  const { screen: screenContext, entry: entryContext } = useScreenContext();
68
70
  const [searchContext] = ZappPipesSearchContext.useZappPipesContext();
69
71
  const [hasEverBeenReady, setHasEverBeenReady] = React.useState(false);
@@ -118,7 +120,7 @@ export const useBatchLoading = (
118
120
  []
119
121
  );
120
122
 
121
- const feeds = useZappPipesFeeds(feedUrls);
123
+ const feeds = useZappPipesFeed(feedUrls);
122
124
 
123
125
  // dispatch loadPipesData for each feed that is not loaded
124
126
  const runBatchLoading = React.useCallback(() => {
@@ -139,7 +141,7 @@ export const useBatchLoading = (
139
141
  if (mappedFeedUrl) {
140
142
  // 4. load data
141
143
  return dispatch(
142
- loadPipesData(mappedFeedUrl, { riverId: options.riverId })
144
+ ZappPipes.loadPipesData(mappedFeedUrl, { riverId: options.riverId })
143
145
  );
144
146
  }
145
147
  }
@@ -1,8 +1,10 @@
1
1
  import React, { useEffect } from "react";
2
- import { useDispatch } from "react-redux";
3
2
 
4
- import { loadPipesData } from "@applicaster/zapp-react-native-redux/ZappPipes";
5
- import { useZappPipesFeed } from "@applicaster/zapp-react-native-redux/hooks";
3
+ import {
4
+ ZappPipes,
5
+ useAppDispatch,
6
+ useZappPipesFeed,
7
+ } from "@applicaster/zapp-react-native-redux";
6
8
 
7
9
  import { reactHooksLogger } from "../logger";
8
10
  import { shouldDispatchData, useIsInitialRender } from "../utils";
@@ -49,7 +51,7 @@ export const useFeedLoader = ({
49
51
  }, []);
50
52
 
51
53
  const isInitialRender = useIsInitialRender();
52
- const dispatch = useDispatch();
54
+ const dispatch = useAppDispatch();
53
55
  const { screenData } = useRoute();
54
56
 
55
57
  const callableFeedUrl = useInflatedUrl({ feedUrl, mapping });
@@ -64,7 +66,7 @@ export const useFeedLoader = ({
64
66
  (silentRefresh = true, callback) => {
65
67
  if (callableFeedUrl) {
66
68
  dispatch(
67
- loadPipesData(callableFeedUrl, {
69
+ ZappPipes.loadPipesData(callableFeedUrl, {
68
70
  clearCache: true,
69
71
  silentRefresh,
70
72
  callback,
@@ -82,7 +84,7 @@ export const useFeedLoader = ({
82
84
 
83
85
  if (nextFeed) {
84
86
  dispatch(
85
- loadPipesData(nextFeed, {
87
+ ZappPipes.loadPipesData(nextFeed, {
86
88
  silentRefresh: true,
87
89
  parentFeed: callableFeedUrl,
88
90
  riverId,
@@ -98,7 +100,7 @@ export const useFeedLoader = ({
98
100
  ) {
99
101
  if (callableFeedUrl && !pipesOptions.skipLoading) {
100
102
  dispatch(
101
- loadPipesData(callableFeedUrl, {
103
+ ZappPipes.loadPipesData(callableFeedUrl, {
102
104
  ...pipesOptions,
103
105
  clearCache: true,
104
106
  riverId,
@@ -131,7 +133,9 @@ export const useFeedLoader = ({
131
133
  // Reload feed when feedUrl changes, unless skipLoading is true
132
134
  useEffect(() => {
133
135
  if (!isInitialRender && callableFeedUrl && !pipesOptions.skipLoading) {
134
- dispatch(loadPipesData(callableFeedUrl, { ...pipesOptions, riverId }));
136
+ dispatch(
137
+ ZappPipes.loadPipesData(callableFeedUrl, { ...pipesOptions, riverId })
138
+ );
135
139
  }
136
140
  }, [callableFeedUrl]);
137
141
 
@@ -1,11 +1,11 @@
1
1
  import React from "react";
2
- import { useDispatch } from "react-redux";
3
2
 
4
3
  import { getDatasourceUrl } from "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/getDatasourceUrl";
5
4
  import { usePipesContexts } from "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/usePipesContexts";
6
5
  import { clearPipesData } from "@applicaster/zapp-react-native-redux/ZappPipes";
7
6
 
8
7
  import { useRoute } from "../navigation";
8
+ import { useAppDispatch } from "@applicaster/zapp-react-native-redux";
9
9
 
10
10
  /**
11
11
  * reset river components cache when screen is unmounted
@@ -13,7 +13,7 @@ import { useRoute } from "../navigation";
13
13
  * @param {Array} riverComponents list of UI components
14
14
  */
15
15
  export const usePipesCacheReset = (riverId, riverComponents) => {
16
- const dispatch = useDispatch();
16
+ const dispatch = useAppDispatch();
17
17
  const { screenData, pathname } = useRoute();
18
18
  const pipesContexts = usePipesContexts(riverId, pathname);
19
19
 
@@ -42,15 +42,17 @@ jest.mock("react-native-safe-area-context", () => ({
42
42
  }));
43
43
 
44
44
  jest.mock("../../../reactUtils", () => ({
45
+ ...jest.requireActual("../../../reactUtils"),
45
46
  platformSelect: jest.fn((specs) => specs[platform] || specs.default),
46
47
  isTV: jest.fn(() => mock_tv_flag),
47
48
  }));
48
49
 
49
50
  jest.mock("../../navigation", () => ({
50
- useNavigation: () => null,
51
51
  useIsScreenActive: () => true,
52
52
  }));
53
53
 
54
+ jest.mock("../../navigation/useNavigation");
55
+
54
56
  const { Dimensions } = require("react-native");
55
57
  const { useDimensions } = require("..");
56
58
 
@@ -1,46 +1,48 @@
1
1
  import { renderHook } from "@testing-library/react-hooks";
2
2
  import { Dimensions, StatusBar } from "react-native";
3
+ import { useDimensions } from "../useDimensions";
4
+ import { usePickFromState } from "@applicaster/zapp-react-native-redux";
3
5
 
4
- const mockUsePickFromState = jest.fn();
5
- const mockUseIsScreenActive = jest.fn();
6
- const mockGetInitialDimensions = jest.fn();
7
- const mockGetDeviceInfo = jest.fn();
6
+ import { useIsScreenActive } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useIsScreenActive";
8
7
 
9
- jest.mock("@applicaster/zapp-react-native-redux/hooks", () => ({
10
- ...(jest.requireActual("@applicaster/zapp-react-native-redux/hooks") as {}),
11
- usePickFromState: mockUsePickFromState,
12
- }));
13
-
14
- jest.mock("../../../navigation", () => ({
15
- useIsScreenActive: mockUseIsScreenActive,
16
- }));
8
+ jest.mock("@applicaster/zapp-react-native-redux/hooks", () => {
9
+ return {
10
+ ...jest.requireActual("@applicaster/zapp-react-native-redux/hooks"),
11
+ usePickFromState: jest.fn(),
12
+ };
13
+ });
17
14
 
18
- jest.mock("../helpers", () => ({
19
- getInitialDimensions: mockGetInitialDimensions,
15
+ jest.mock(
16
+ "@applicaster/zapp-react-native-utils/reactHooks/navigation/useIsScreenActive",
17
+ () => ({
18
+ useIsScreenActive: jest.fn().mockReturnValue(true),
19
+ })
20
+ );
21
+
22
+ jest.doMock("../helpers", () => ({
23
+ getInitialDimensions: jest
24
+ .fn()
25
+ .mockReturnValue({ width: 100, height: 200, scale: 1, fontScale: 1 }),
20
26
  }));
21
27
 
22
28
  jest.mock("../../getDeviceInfo", () => ({
23
- getDeviceInfo: mockGetDeviceInfo,
29
+ getDeviceInfo: jest.fn().mockReturnValue({ deviceInfo: "testDeviceInfo" }),
24
30
  }));
25
31
 
26
- const { useDimensions } = require("../useDimensions");
32
+ const mockDimensions = { width: 100, height: 200, scale: 1, fontScale: 1 };
33
+
34
+ Dimensions.get = jest.fn().mockReturnValue(mockDimensions);
35
+
36
+ Dimensions.addEventListener = jest.fn().mockReturnValue({
37
+ remove: jest.fn(),
38
+ });
27
39
 
28
40
  describe("useDimensions", () => {
29
- const mockDimensions = { width: 100, height: 200, scale: 1, fontScale: 1 };
30
41
  const mockAppData = { someData: "test" };
31
42
 
32
43
  beforeEach(() => {
33
- jest.clearAllMocks();
34
- Dimensions.get = jest.fn().mockReturnValue(mockDimensions);
35
-
36
- Dimensions.addEventListener = jest.fn().mockReturnValue({
37
- remove: jest.fn(),
38
- });
39
-
40
- mockUsePickFromState.mockReturnValue({ appData: mockAppData });
41
- mockUseIsScreenActive.mockReturnValue(true);
42
- mockGetInitialDimensions.mockReturnValue(mockDimensions);
43
- mockGetDeviceInfo.mockReturnValue({ deviceInfo: "testDeviceInfo" });
44
+ StatusBar.currentHeight = 20;
45
+ (usePickFromState as jest.Mock).mockReturnValue({ appData: mockAppData });
44
46
  });
45
47
 
46
48
  it("returns correct initial dimensions", () => {
@@ -48,12 +50,9 @@ describe("useDimensions", () => {
48
50
  useDimensions("window", { fullDimensions: false })
49
51
  );
50
52
 
51
- expect(result.current).toEqual({
52
- ...mockDimensions,
53
+ expect(result.current).toMatchObject({
53
54
  statusBarHeight: StatusBar.currentHeight,
54
55
  });
55
-
56
- expect(mockGetInitialDimensions).toHaveBeenCalledWith("window");
57
56
  });
58
57
 
59
58
  it("calls handler on mount", () => {
@@ -70,7 +69,7 @@ describe("useDimensions", () => {
70
69
  useDimensions("window", { fullDimensions: false })
71
70
  );
72
71
 
73
- mockUseIsScreenActive.mockReturnValue(false);
72
+ (useIsScreenActive as jest.Mock).mockReturnValue(false);
74
73
  rerender();
75
74
 
76
75
  expect(Dimensions.addEventListener).toHaveBeenCalledWith(
@@ -84,8 +83,7 @@ describe("useDimensions", () => {
84
83
  useDimensions("window", { fullDimensions: true })
85
84
  );
86
85
 
87
- expect(result.current).toEqual({
88
- ...mockDimensions,
86
+ expect(result.current).toMatchObject({
89
87
  scale: 1,
90
88
  fontScale: 1,
91
89
  statusBarHeight: StatusBar.currentHeight,
@@ -98,7 +96,7 @@ describe("useDimensions", () => {
98
96
  );
99
97
 
100
98
  expect(result.current.height).toBe(
101
- mockDimensions.height - StatusBar.currentHeight ?? 0
99
+ mockDimensions.height - (StatusBar?.currentHeight ?? 0)
102
100
  );
103
101
  });
104
102
 
@@ -10,7 +10,7 @@ import { isTV } from "../../../reactUtils";
10
10
  import { Options, UseDimensions } from "../types";
11
11
  import { getDeviceInfo } from "../getDeviceInfo";
12
12
  import { getInitialDimensions } from "./helpers";
13
- import { useIsScreenActive } from "../../navigation";
13
+ import { useIsScreenActive } from "../../navigation/useIsScreenActive";
14
14
 
15
15
  function compensateForScaleIfNeeded(context) {
16
16
  return function () {
@@ -24,8 +24,6 @@ const applyScaleToDimensions = R.unless(R.propEq("scale", 1), (dimensions) => ({
24
24
  scale: 1,
25
25
  }));
26
26
 
27
- const statusBarHeight = StatusBar?.currentHeight;
28
-
29
27
  /**
30
28
  * Returns React-native Dimensions object and updates it on any dimension change
31
29
  * @param {('screen'|'window')} [context=window] - Dimensions context passed to Dimensions.get method
@@ -37,6 +35,7 @@ export const useDimensions: UseDimensions = (
37
35
  context = "window",
38
36
  fullDimensions = { fullDimensions: false, updateForInactiveScreens: true }
39
37
  ) => {
38
+ const statusBarHeight = StatusBar?.currentHeight;
40
39
  const isActive = useIsScreenActive();
41
40
  const { appData } = usePickFromState(["appData"]);
42
41
 
@@ -1,6 +1,8 @@
1
1
  /* eslint-disable no-redeclare */
2
- import { useSelector } from "react-redux";
3
- import * as R from "ramda";
2
+ import {
3
+ useAppSelector,
4
+ selectLayoutVersion,
5
+ } from "@applicaster/zapp-react-native-redux";
4
6
 
5
7
  export function useLayoutVersion(): ZappLayoutVersions;
6
8
 
@@ -23,9 +25,7 @@ export function useLayoutVersion({
23
25
  isV2?: boolean;
24
26
  isV1?: boolean;
25
27
  } = {}): boolean | ZappLayoutVersions {
26
- const layoutVersion = useSelector<any, ZappLayoutVersions>(
27
- R.path(["appData", "layoutVersion"])
28
- );
28
+ const layoutVersion = useAppSelector(selectLayoutVersion);
29
29
 
30
30
  if (isV2) {
31
31
  return layoutVersion === "v2";
@@ -14,6 +14,10 @@ jest.mock("@applicaster/zapp-react-native-utils/localizationUtils", () => ({
14
14
 
15
15
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/navigation");
16
16
 
17
+ jest.mock(
18
+ "@applicaster/zapp-react-native-utils/reactHooks/navigation/useNavigation"
19
+ );
20
+
17
21
  const { useCellResolver } = require("../useCellResolver");
18
22
 
19
23
  describe("cellResolver", () => {
@@ -1,5 +1,11 @@
1
1
  export { useRivers } from "./useRivers";
2
2
 
3
- export { useHomeRiver } from "./useHomeRiver";
3
+ export {
4
+ useHomeRiver,
5
+ getHomeRiver,
6
+ useIsHomeScreen,
7
+ useIsRootScreen,
8
+ useIsTabsScreen,
9
+ } from "./useHomeRiver";
4
10
 
5
11
  export { ZStoreProvider, useZStore } from "./ZStoreProvider";
@@ -1,8 +1,41 @@
1
- import * as R from "ramda";
1
+ import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
2
+ import { last } from "@applicaster/zapp-react-native-utils/utils";
3
+
2
4
  import { useRivers } from "./useRivers";
3
5
 
6
+ export const getHomeRiver = (rivers: Record<string, ZappRiver>) =>
7
+ Object.values(rivers).find((river: ZappRiver) => river.home);
8
+
4
9
  export const useHomeRiver = () => {
5
10
  const rivers = useRivers();
6
11
 
7
- return R.compose(R.find(R.propEq("home", true)), R.values)(rivers);
12
+ return getHomeRiver(rivers);
13
+ };
14
+
15
+ export const useIsHomeScreen = (): boolean => {
16
+ const navigator = useNavigation();
17
+ const homeRiver = useHomeRiver();
18
+
19
+ const homePath = `/river/${homeRiver.id}`;
20
+
21
+ return homePath === navigator.currentRoute;
22
+ };
23
+
24
+ export const useIsRootScreen = (): boolean => {
25
+ const { mainStack = [] } = useNavigation();
26
+
27
+ // root screen is the bottom(first pushed, deepest) element of the stack
28
+ return mainStack.length <= 1;
29
+ };
30
+
31
+ export const useIsTabsScreen = (): boolean => {
32
+ const navigator = useNavigation();
33
+
34
+ const riverId = last(navigator.currentRoute.split("/"));
35
+
36
+ const rivers = useRivers();
37
+
38
+ const river = rivers[riverId];
39
+
40
+ return river?.type === "tabs_screen";
8
41
  };
@@ -1,9 +1,8 @@
1
- import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
1
+ import {
2
+ useAppSelector,
3
+ selectRivers,
4
+ } from "@applicaster/zapp-react-native-redux";
2
5
 
3
- const riversSelector = ["rivers"];
4
-
5
- export const useRivers = () => {
6
- const { rivers } = usePickFromState(riversSelector as any);
7
-
8
- return rivers;
9
- };
6
+ export function useRivers(): Record<string, ZappRiver> {
7
+ return useAppSelector(selectRivers);
8
+ }
@@ -0,0 +1,10 @@
1
+ export const generateFocusableId = (id) => `PickerItem.${id}`;
2
+
3
+ export const TABS_MENU_ID = "PickerSelector";
4
+
5
+ export const generatePickerSelectorId = (id) => `${TABS_MENU_ID}.${id}`;
6
+
7
+ export const SCREEN_PICKER_CONTAINER = "ScreenPickerContainer";
8
+
9
+ export const generateScreenPickerId = (id) =>
10
+ `${SCREEN_PICKER_CONTAINER}.${id}`;
@@ -0,0 +1,36 @@
1
+ import { find } from "../find";
2
+
3
+ test("example 1", () => {
4
+ const predicate = <T>(_: T, index: number): boolean => index === 0;
5
+ const xs = ["1", "2", "2", "3", "4"];
6
+
7
+ expect(find(predicate, xs)).toBe("1");
8
+ });
9
+
10
+ test("example 2", () => {
11
+ const predicate = <T>(_: T, index: number): boolean => index === 0;
12
+ const xs: string[] = [];
13
+
14
+ expect(find(predicate, xs)).toBe(undefined);
15
+ });
16
+
17
+ test("example 3", () => {
18
+ const predicate = () => false;
19
+ const xs = ["1", "2", "2", "3"];
20
+
21
+ expect(find(predicate, xs)).toBe(undefined);
22
+ });
23
+
24
+ test("example 4", () => {
25
+ const predicate = <T>(_: T, index: number): boolean => index === 1;
26
+ const xs = ["1", "2", "2", "3"];
27
+
28
+ expect(find(predicate, xs)).toBe("2");
29
+ });
30
+
31
+ test("example 5", () => {
32
+ const predicate = <T>(_: T, index: number): boolean => index === 2;
33
+ const xs = ["1", "2.1", "2", "3", "2", "4"];
34
+
35
+ expect(find(predicate, xs)).toBe("2");
36
+ });
@@ -0,0 +1,37 @@
1
+ import { pathOr } from "../pathOr";
2
+
3
+ test("example 1", () => {
4
+ const defaultValue = "defaultValue";
5
+ const path = ["a", "b", "c"];
6
+ const xs = { a: { b: { c: 1 } } };
7
+
8
+ const output = 1;
9
+
10
+ expect(pathOr(defaultValue, path, xs)).toEqual(output);
11
+ });
12
+
13
+ test("example 2", () => {
14
+ const defaultValue = "defaultValue";
15
+ const path = ["a", "b"];
16
+ const xs = { a: { b: { c: 1 } } };
17
+
18
+ const output = { c: 1 };
19
+
20
+ expect(pathOr(defaultValue, path, xs)).toEqual(output);
21
+ });
22
+
23
+ test("example 3", () => {
24
+ const defaultValue = "defaultValue";
25
+ const path = ["a", "b", "x"];
26
+ const xs = { a: { b: { c: 1 } } };
27
+
28
+ expect(pathOr(defaultValue, path, xs)).toBe(defaultValue);
29
+ });
30
+
31
+ test("example 4", () => {
32
+ const defaultValue = "defaultValue";
33
+ const path = ["a", "b", "c"];
34
+ const xs = undefined;
35
+
36
+ expect(pathOr(defaultValue, path, xs)).toBe(defaultValue);
37
+ });
@@ -0,0 +1,30 @@
1
+ import { startsWith } from "../startsWith";
2
+
3
+ describe("startsWith", () => {
4
+ it("returns false when str is null", () => {
5
+ expect(startsWith("a", null)).toBe(false);
6
+ });
7
+
8
+ it("returns false when str is undefined", () => {
9
+ expect(startsWith("a", undefined)).toBe(false);
10
+ });
11
+
12
+ it("returns true when string starts with target", () => {
13
+ expect(startsWith("he", "hello")).toBe(true);
14
+ expect(startsWith("", "hello")).toBe(true); // empty target always matches
15
+ });
16
+
17
+ it("returns false when string does not start with target", () => {
18
+ expect(startsWith("yo", "hello")).toBe(false);
19
+ });
20
+
21
+ it("works with single character target", () => {
22
+ expect(startsWith("h", "hello")).toBe(true);
23
+ expect(startsWith("x", "hello")).toBe(false);
24
+ });
25
+
26
+ it("is case-sensitive", () => {
27
+ expect(startsWith("He", "hello")).toBe(false);
28
+ expect(startsWith("he", "hello")).toBe(true);
29
+ });
30
+ });
package/utils/find.ts ADDED
@@ -0,0 +1,3 @@
1
+ export const find = (predicate, xs) => {
2
+ return (xs || []).find((x, index) => predicate(x, index));
3
+ };
package/utils/index.ts CHANGED
@@ -2,6 +2,12 @@ export { chunk } from "./chunk";
2
2
 
3
3
  export { times } from "./times";
4
4
 
5
+ export { startsWith } from "./startsWith";
6
+
7
+ export { find } from "./find";
8
+
9
+ export { pathOr } from "./pathOr";
10
+
5
11
  export {
6
12
  cloneDeep as clone,
7
13
  flatten,
@@ -14,7 +20,9 @@ export {
14
20
  flatMap,
15
21
  difference,
16
22
  take,
23
+ pick,
17
24
  map,
18
25
  trim,
19
26
  toString,
27
+ last,
20
28
  } from "lodash";
@@ -0,0 +1,5 @@
1
+ import { get } from "lodash";
2
+
3
+ export const pathOr = (defaultValue, path, record) => {
4
+ return get(record, path, defaultValue);
5
+ };
@@ -0,0 +1,9 @@
1
+ import { isNil } from "lodash";
2
+
3
+ export const startsWith = (target, str) => {
4
+ if (isNil(str)) {
5
+ return false;
6
+ }
7
+
8
+ return str.startsWith(target);
9
+ };