@applicaster/zapp-react-native-utils 12.0.0-rc.69 → 12.0.0-rc.70

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.
@@ -3,9 +3,7 @@
3
3
  exports[`focusManager should be defined 1`] = `
4
4
  Object {
5
5
  "blur": [Function],
6
- "disableAutomaticFocus": [Function],
7
6
  "disableFocus": [Function],
8
- "enableAutomaticFocus": [Function],
9
7
  "enableFocus": [Function],
10
8
  "focusableTree": Tree {
11
9
  "loadingCounter": 0,
@@ -23,6 +21,7 @@ Object {
23
21
  "getGroupById": [Function],
24
22
  "getGroupRootById": [Function],
25
23
  "invokeHandler": [Function],
24
+ "isCurrentFocusOnTheTopScreen": [Function],
26
25
  "isFocusDisabled": [Function],
27
26
  "isGroupItemFocused": [Function],
28
27
  "longPress": [Function],
@@ -30,11 +29,11 @@ Object {
30
29
  "on": [Function],
31
30
  "press": [Function],
32
31
  "pressOut": [Function],
32
+ "recoverFocus": [Function],
33
33
  "register": [Function],
34
34
  "removeHandler": [Function],
35
35
  "setFocus": [Function],
36
36
  "setInitialFocus": [Function],
37
- "setupInitialFocus": [Function],
38
37
  "unregister": [Function],
39
38
  }
40
39
  `;
@@ -14,10 +14,8 @@ describe("focusManager", () => {
14
14
  expect(focusManager).toMatchSnapshot();
15
15
  });
16
16
 
17
- it("enableAutomaticFocus should not block setting focus", () => {
17
+ it("manages to land initial focus", () => {
18
18
  const mockSetFocus = jest.fn();
19
- focusManager.setupInitialFocus("1", true);
20
- focusManager.enableAutomaticFocus();
21
19
 
22
20
  focusManager.register({
23
21
  id: "1",
@@ -31,31 +29,13 @@ describe("focusManager", () => {
31
29
  },
32
30
  });
33
31
 
32
+ const { success } = focusManager.setInitialFocus();
33
+
34
+ expect(success).toBe(true);
34
35
  expect(mockSetFocus).toBeCalledTimes(1);
35
36
  expect(mockSetFocus).toBeCalledWith(null);
36
37
  });
37
38
 
38
- it("enableAutomaticFocus should block setting focus", () => {
39
- const mockSetFocus = jest.fn();
40
- focusManager.setupInitialFocus("1", true);
41
- focusManager.disableAutomaticFocus();
42
-
43
- focusManager.register({
44
- id: "1",
45
- component: {
46
- setFocus: mockSetFocus,
47
- willReceiveFocus: function () {},
48
- isMounted: function () {
49
- return true;
50
- },
51
- props: { groupId: "121" },
52
- },
53
- });
54
-
55
- expect(mockSetFocus).toBeCalledTimes(0);
56
- expect(mockSetFocus).not.toBeCalledWith(null);
57
- });
58
-
59
39
  describe("register", () => {});
60
40
  describe("unregister", () => {});
61
41
  describe("find", () => {});
@@ -18,7 +18,6 @@ describe("focusManager", () => {
18
18
  "on",
19
19
  "invokeHandler",
20
20
  "removeHandler",
21
- "setupInitialFocus",
22
21
  "longPress",
23
22
  "pressOut",
24
23
  "enableFocus",
@@ -27,10 +26,10 @@ describe("focusManager", () => {
27
26
  "isFocusDisabled",
28
27
  "getCurrentFocusNode",
29
28
  "focusableTree",
30
- "disableAutomaticFocus",
31
- "enableAutomaticFocus",
32
29
  "getGroupRootById",
33
30
  "isGroupItemFocused",
31
+ "recoverFocus",
32
+ "isCurrentFocusOnTheTopScreen",
34
33
  ];
35
34
 
36
35
  expectedMethods.forEach((method) => {
@@ -1,6 +1,15 @@
1
1
  import * as R from "ramda";
2
+ import {
3
+ isNilOrEmpty,
4
+ isNotNil,
5
+ } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
6
+
2
7
  import { Tree } from "./treeDataStructure/Tree";
3
- import { findFocusableNode, findPriorityItem } from "./treeDataStructure/Utils";
8
+ import {
9
+ findFocusableNode,
10
+ findPriorityItem,
11
+ haveSameParentBeforeRoot,
12
+ } from "./treeDataStructure/Utils";
4
13
  import { subscriber } from "../../functionUtils";
5
14
  import * as FOCUS_EVENTS from "./events";
6
15
  import { coreLogger } from "../../logger";
@@ -44,21 +53,9 @@ export const focusManager = (function () {
44
53
  // focusable node which currently holds the focus
45
54
  let currentFocusNode = null;
46
55
  let lastFocusedId = null;
47
- let initialFocusOnContent = null;
48
56
  let focusDisabled = false;
49
- let focusAutoEnabled = true;
50
57
  const eventHandler = subscriber();
51
58
 
52
- function disableAutomaticFocus() {
53
- logger.log({ message: "disableAutomaticFocus" });
54
- focusAutoEnabled = false;
55
- }
56
-
57
- function enableAutomaticFocus() {
58
- logger.log({ message: "focusManager: enableAutomaticFocus" });
59
- focusAutoEnabled = true;
60
- }
61
-
62
59
  function enableFocus() {
63
60
  focusDisabled = false;
64
61
  }
@@ -93,7 +90,7 @@ export const focusManager = (function () {
93
90
  !currentFocusable.isGroup &&
94
91
  currentFocusable.isMounted()
95
92
  ) {
96
- currentFocusable.blur(currentFocusable, direction);
93
+ currentFocusable.blur(direction);
97
94
  currentFocusable.hasLostFocus(getCurrentFocus(), direction);
98
95
  }
99
96
  }
@@ -226,32 +223,52 @@ export const focusManager = (function () {
226
223
 
227
224
  logger.log({ message: "setInitialFocus", data: { preferredFocus } });
228
225
 
229
- if (preferredFocus && preferredFocus.length > 0) {
230
- const focusCandidate = preferredFocus[0];
231
- const focusableItem = focusCandidate.component;
232
-
233
- if (
234
- focusableItem &&
235
- !focusableItem.isGroup &&
236
- isFocusEnabled(focusableItem)
237
- ) {
238
- if (initialFocusOnContent) {
239
- blur();
240
- initialFocusOnContent = false;
241
- }
226
+ if (isNilOrEmpty(preferredFocus)) {
227
+ return { success: false };
228
+ }
242
229
 
243
- logger.log({
244
- message: "setInitialFocus – currentFocusNode",
245
- data: {
246
- focusableItem,
247
- },
248
- });
230
+ const focusCandidate = preferredFocus[0];
231
+ const focusableItem = focusCandidate.component;
249
232
 
250
- currentFocusNode = focusCandidate;
251
- focusableItem.willReceiveFocus(focusableItem, null);
252
- focusableItem && setFocus(focusCandidate.id, null);
253
- }
233
+ const currentFocusedId = R.path(["props", "id"], getCurrentFocus());
234
+ const nextFocusedId = R.path(["props", "id"], focusableItem);
235
+
236
+ if (
237
+ isNotNil(currentFocusedId) &&
238
+ isNotNil(nextFocusedId) &&
239
+ currentFocusedId === nextFocusedId
240
+ ) {
241
+ // we are going to move focus to the same node(already focused) -> nothing to do
242
+ return { success: true };
243
+ }
244
+
245
+ if (focusableItem?.isGroup) {
246
+ // focusCandidate is a FocusableGroup, we are not able to land focus on group(only on Focusable)
247
+ return { success: false };
254
248
  }
249
+
250
+ if (
251
+ focusableItem &&
252
+ !focusableItem.isGroup &&
253
+ isFocusEnabled(focusableItem)
254
+ ) {
255
+ blur();
256
+
257
+ logger.log({
258
+ message: "setInitialFocus – currentFocusNode",
259
+ data: {
260
+ focusableItem,
261
+ },
262
+ });
263
+
264
+ currentFocusNode = focusCandidate;
265
+ focusableItem.willReceiveFocus(focusableItem, null);
266
+ focusableItem && setFocus(focusCandidate.id, null);
267
+
268
+ return { success: true };
269
+ }
270
+
271
+ return { success: false };
255
272
  }
256
273
 
257
274
  function treeLoaded(val, node) {
@@ -261,17 +278,8 @@ export const focusManager = (function () {
261
278
  val,
262
279
  node,
263
280
  currentFocusNode,
264
- initialFocusOnContent,
265
281
  },
266
282
  });
267
-
268
- if ((val && !currentFocusNode) || initialFocusOnContent) {
269
- logger.log({ message: `focusAutoEnabled ${focusAutoEnabled}` });
270
-
271
- if (focusAutoEnabled) {
272
- setInitialFocus(initialFocusOnContent ? node : undefined);
273
- }
274
- }
275
283
  }
276
284
 
277
285
  /**
@@ -402,11 +410,21 @@ export const focusManager = (function () {
402
410
  group ? unregisterGroup(id) : unregisterItem(id);
403
411
  }
404
412
 
405
- function setupInitialFocus(id: string, focusOnContent?: boolean) {
406
- if (focusOnContent) {
407
- initialFocusOnContent = focusOnContent;
408
- } else {
409
- setFocus(id);
413
+ function isCurrentFocusOnTheTopScreen() {
414
+ const routes = R.pathOr([], ["root", "children"], focusableTree);
415
+
416
+ if (routes.length <= 1) {
417
+ // there is only one or zero routes
418
+ return true;
419
+ }
420
+
421
+ return haveSameParentBeforeRoot(currentFocusNode, R.last(routes));
422
+ }
423
+
424
+ function recoverFocus() {
425
+ if (!isCurrentFocusOnTheTopScreen()) {
426
+ // We've failed to set focused node on the new screen => run focus recovery
427
+ setInitialFocus();
410
428
  }
411
429
  }
412
430
 
@@ -416,6 +434,11 @@ export const focusManager = (function () {
416
434
  * @param {Object} direction of the scroll
417
435
  */
418
436
  function moveFocus(direction) {
437
+ if (!isCurrentFocusOnTheTopScreen()) {
438
+ // We are opening new screen. We need to ignore movement of focus.
439
+ return false;
440
+ }
441
+
419
442
  if (focusDisabled) return false;
420
443
 
421
444
  if (currentFocusNode) {
@@ -444,8 +467,6 @@ export const focusManager = (function () {
444
467
  } else {
445
468
  currentFocusNode.component.failedLostFocus(currentFocusNode, direction);
446
469
  }
447
- } else {
448
- setInitialFocus();
449
470
  }
450
471
  }
451
472
 
@@ -505,7 +526,6 @@ export const focusManager = (function () {
505
526
  on,
506
527
  invokeHandler,
507
528
  removeHandler,
508
- setupInitialFocus,
509
529
  longPress,
510
530
  pressOut,
511
531
  enableFocus,
@@ -514,9 +534,9 @@ export const focusManager = (function () {
514
534
  isFocusDisabled,
515
535
  getCurrentFocusNode,
516
536
  focusableTree,
517
- disableAutomaticFocus,
518
- enableAutomaticFocus,
519
537
  getGroupRootById,
520
538
  isGroupItemFocused,
539
+ recoverFocus,
540
+ isCurrentFocusOnTheTopScreen,
521
541
  };
522
542
  })();
@@ -1,5 +1,7 @@
1
- import * as R from "ramda";
1
+ import { allPass, pathOr, startsWith, isNil } from "ramda";
2
2
  import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
3
+ import { isZero } from "@applicaster/zapp-react-native-utils/numberUtils";
4
+ import { QUICK_BRICK_NAVBAR } from "@applicaster/quick-brick-core/const";
3
5
 
4
6
  import { rejectItemByPath } from "../../../../objectUtils";
5
7
  import {
@@ -143,7 +145,47 @@ const hasChildren = (target) =>
143
145
 
144
146
  const hasComponent = (target) => !!target?.component;
145
147
 
146
- const hasComponentAndChildren = R.allPass([hasComponent, hasChildren]);
148
+ const hasComponentAndChildren = allPass([hasComponent, hasChildren]);
149
+
150
+ const positiveOrZero = (value) => value > 0 || isZero(value);
151
+
152
+ const excludeFromFocusSearching = pathOr(false, [
153
+ "component",
154
+ "props",
155
+ "excludeFromFocusSearching",
156
+ ]);
157
+
158
+ const isNavBar = (target) => startsWith(QUICK_BRICK_NAVBAR, target?.id);
159
+
160
+ const ignoreMenuIfMultipleTargets = (nodes) => {
161
+ if (nodes.length > 1) {
162
+ return nodes.filter((n) => !isNavBar(n?.target));
163
+ }
164
+
165
+ return nodes;
166
+ };
167
+
168
+ const toDistance = ({ target, direction, comparisonRect }) => {
169
+ const componentRect = target.component?.getRect();
170
+
171
+ const orientationDistance = measureDistance(
172
+ direction,
173
+ comparisonRect,
174
+ componentRect
175
+ );
176
+
177
+ if (orientationDistance === -1) {
178
+ // ignores offscreen targets when moving up/down
179
+ return null;
180
+ }
181
+
182
+ const euclidDistance = measureEuclidianDistanceToCenter(
183
+ comparisonRect,
184
+ componentRect
185
+ );
186
+
187
+ return orientationDistance > 0 ? euclidDistance : orientationDistance;
188
+ };
147
189
 
148
190
  /**
149
191
  * Get target in shortest distance in specified direction
@@ -173,52 +215,18 @@ export function getTargetInDirection(
173
215
 
174
216
  const distances = targets
175
217
  .filter(hasComponentAndChildren)
176
- .map((target) => {
177
- const componentRect = target.component?.getRect();
178
-
179
- const distance = measureDistance(
180
- direction,
181
- comparisonRect,
182
- componentRect
183
- );
184
-
185
- if (distance === -1) {
186
- // ignores offscreen targets when moving up/down
187
- return null;
188
- }
189
-
190
- const orientationDistance = measureDistance(
191
- direction,
192
- comparisonRect,
193
- componentRect
194
- );
195
-
196
- const euclidDistance = measureEuclidianDistanceToCenter(
197
- comparisonRect,
198
- componentRect
199
- );
200
-
201
- return {
202
- distance:
203
- orientationDistance > 0 ? euclidDistance : orientationDistance,
204
- target,
205
- };
206
- })
207
- .filter((obj) => obj && obj.distance >= 0);
218
+ .filter((target) => !excludeFromFocusSearching(target))
219
+ .map((target) => ({
220
+ distance: toDistance({ target, direction, comparisonRect }),
221
+ target,
222
+ }))
223
+ .filter(({ distance }) => positiveOrZero(distance));
208
224
 
209
225
  const match = distances
210
226
  .sort((a, b) => a.distance - b.distance) // sorts the distances
211
227
  .filter(ignoreMenuIfMultipleTargets)[0]; // filters out multiple targets if necessary // gets the first element
212
228
 
213
- return distances && distances.length > 0 ? match.target : null;
214
- }
215
-
216
- function ignoreMenuIfMultipleTargets(nodes) {
217
- if (nodes.length > 1 && nodes.map((n) => n?.target?.id).includes("navbar")) {
218
- return nodes.filter((n) => n?.target?.id !== "navbar");
219
- }
220
-
221
- return nodes;
229
+ return match?.target;
222
230
  }
223
231
 
224
232
  /**
@@ -309,3 +317,26 @@ export function hasPriorityInDirection(direction, component) {
309
317
 
310
318
  return (prioritiseFocusOn & mappedDirection) > 0;
311
319
  }
320
+
321
+ export const getNodeBeforeRoot = (node) => {
322
+ if (isNil(node)) {
323
+ return undefined;
324
+ }
325
+
326
+ if (isNil(node.parent)) {
327
+ return undefined;
328
+ }
329
+
330
+ if (node.parent.id === "root") {
331
+ return node;
332
+ }
333
+
334
+ return getNodeBeforeRoot(node.parent);
335
+ };
336
+
337
+ export const haveSameParentBeforeRoot = (currentNode, nextNode) => {
338
+ const routeForCurrentNode = getNodeBeforeRoot(currentNode);
339
+ const routeForNextNode = getNodeBeforeRoot(nextNode);
340
+
341
+ return routeForCurrentNode?.id === routeForNextNode?.id;
342
+ };
@@ -0,0 +1,100 @@
1
+ import { isNotNil } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
2
+ import { find, last, startsWith, pathOr } from "ramda";
3
+ import {
4
+ QUICK_BRICK_NAVBAR,
5
+ QUICK_BRICK_CONTENT,
6
+ } from "@applicaster/quick-brick-core/const";
7
+
8
+ // run check each 300 ms
9
+ // run this check too often could lead to performance penalty on low-end devices
10
+ const HOW_OFTEN_TO_CHECK_CONDITION = 300; // ms
11
+
12
+ type Props = {
13
+ maxTimeout: number;
14
+ conditionFn: () => boolean;
15
+ };
16
+
17
+ export const waitUntil = ({ maxTimeout, conditionFn }: Props) => {
18
+ let interval;
19
+ let timeout;
20
+
21
+ return new Promise((resolve, reject) => {
22
+ // run interval check
23
+ interval = setInterval(() => {
24
+ if (conditionFn()) {
25
+ clearInterval(interval);
26
+ clearTimeout(timeout);
27
+
28
+ resolve(undefined);
29
+ }
30
+ }, HOW_OFTEN_TO_CHECK_CONDITION);
31
+
32
+ // max timeout when we need to stop interval check
33
+ timeout = setTimeout(() => {
34
+ clearInterval(interval);
35
+ clearTimeout(timeout);
36
+
37
+ const error = Error(
38
+ "Failed to wait satisfy condition. Max timeout is reached."
39
+ );
40
+
41
+ reject(error);
42
+ }, maxTimeout);
43
+ });
44
+ };
45
+
46
+ export const waitForActiveScreen = (currentRoute: string, focusableTree) => {
47
+ const isActiveScreenAvailableToFocus = (): boolean => {
48
+ const routes = focusableTree.root.children;
49
+
50
+ const route = find((route) => route.id === currentRoute, routes);
51
+
52
+ return isNotNil(route);
53
+ };
54
+
55
+ return waitUntil({
56
+ maxTimeout: HOW_OFTEN_TO_CHECK_CONDITION * 10, // ms
57
+ conditionFn: isActiveScreenAvailableToFocus,
58
+ });
59
+ };
60
+
61
+ const getActiveScreenChildren = (focusableTree) => {
62
+ return pathOr([], ["children"], last(focusableTree.root.children));
63
+ };
64
+
65
+ export const getContentNode = (focusableTree) => {
66
+ const content = find(
67
+ (child) =>
68
+ startsWith(QUICK_BRICK_CONTENT, child.id) ||
69
+ startsWith("player", child.id),
70
+ getActiveScreenChildren(focusableTree)
71
+ );
72
+
73
+ return content;
74
+ };
75
+
76
+ export const getNavbarNode = (focusableTree) => {
77
+ const navbar = find(
78
+ (child) => startsWith(QUICK_BRICK_NAVBAR, child.id),
79
+ getActiveScreenChildren(focusableTree)
80
+ );
81
+
82
+ return navbar;
83
+ };
84
+
85
+ export const waitForContent = (focusableTree) => {
86
+ const contentHasAnyChildren = (): boolean => {
87
+ const countOfChildren = pathOr(
88
+ 0,
89
+ ["children", "length"],
90
+ getContentNode(focusableTree)
91
+ );
92
+
93
+ return countOfChildren > 0;
94
+ };
95
+
96
+ return waitUntil({
97
+ maxTimeout: HOW_OFTEN_TO_CHECK_CONDITION * 10, // ms
98
+ conditionFn: contentHasAnyChildren,
99
+ });
100
+ };
@@ -0,0 +1,76 @@
1
+ import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager";
2
+ import { find, path } from "ramda";
3
+
4
+ import {
5
+ waitForActiveScreen,
6
+ waitForContent,
7
+ getContentNode,
8
+ getNavbarNode,
9
+ } from "./utils";
10
+
11
+ import { coreLogger } from "../../logger";
12
+
13
+ const logger = coreLogger.addSubsystem("focusManagerAux");
14
+
15
+ export async function setFocusOnMenu(currentRoute: string) {
16
+ const { setFocus, focusableTree } = focusManager;
17
+
18
+ try {
19
+ await waitForActiveScreen(currentRoute, focusableTree);
20
+
21
+ const navbar = getNavbarNode(focusableTree);
22
+
23
+ const selectedMenuItemId = find(
24
+ (child) => child.component.props.selected,
25
+ navbar.children
26
+ )?.id;
27
+
28
+ if (selectedMenuItemId) {
29
+ // set focus on selected menu item
30
+ setFocus(selectedMenuItemId);
31
+
32
+ return;
33
+ }
34
+
35
+ const firstMenuItemId = path(["children", 0, "id"], navbar);
36
+
37
+ if (firstMenuItemId) {
38
+ // set focus on first menu item
39
+ setFocus(firstMenuItemId);
40
+ }
41
+ } catch (error) {
42
+ logger.log({
43
+ message: "setFocusOnMenu – failed to put focus on top-menu",
44
+ data: {
45
+ currentRoute,
46
+ error,
47
+ },
48
+ });
49
+ }
50
+ }
51
+
52
+ export async function setFocusOnContent(currentRoute: string) {
53
+ // we need to put focus on content here
54
+ // For doing it we have to wait registration of first focusable node into focusableTree
55
+ // we wait until content node starts having children
56
+
57
+ const { setInitialFocus, focusableTree } = focusManager;
58
+
59
+ try {
60
+ await waitForActiveScreen(currentRoute, focusableTree);
61
+ await waitForContent(focusableTree);
62
+
63
+ const content = getContentNode(focusableTree);
64
+
65
+ const { success } = setInitialFocus(content);
66
+
67
+ if (!success) {
68
+ // we've failed to put initial focus on content, re-run "setFocusOnContent" again as a fallback
69
+ throw Error(
70
+ "we've failed to put initial focus on content, re-run 'setFocusOnContent' again as a fallback"
71
+ );
72
+ }
73
+ } catch (error) {
74
+ setFocusOnContent(currentRoute);
75
+ }
76
+ }
@@ -0,0 +1,100 @@
1
+ import { isNotNil } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
2
+ import { find, last, startsWith, pathOr } from "ramda";
3
+ import {
4
+ QUICK_BRICK_NAVBAR,
5
+ QUICK_BRICK_CONTENT,
6
+ } from "@applicaster/quick-brick-core/const";
7
+
8
+ // run check each 300 ms
9
+ // run this check too often could lead to performance penalty on low-end devices
10
+ const HOW_OFTEN_TO_CHECK_CONDITION = 300; // ms
11
+
12
+ type Props = {
13
+ maxTimeout: number;
14
+ conditionFn: () => boolean;
15
+ };
16
+
17
+ export const waitUntil = ({ maxTimeout, conditionFn }: Props) => {
18
+ let interval;
19
+ let timeout;
20
+
21
+ return new Promise((resolve, reject) => {
22
+ // run interval check
23
+ interval = setInterval(() => {
24
+ if (conditionFn()) {
25
+ clearInterval(interval);
26
+ clearTimeout(timeout);
27
+
28
+ resolve(undefined);
29
+ }
30
+ }, HOW_OFTEN_TO_CHECK_CONDITION);
31
+
32
+ // max timeout when we need to stop interval check
33
+ timeout = setTimeout(() => {
34
+ clearInterval(interval);
35
+ clearTimeout(timeout);
36
+
37
+ const error = Error(
38
+ "Failed to wait satisfy condition. Max timeout is reached."
39
+ );
40
+
41
+ reject(error);
42
+ }, maxTimeout);
43
+ });
44
+ };
45
+
46
+ export const waitForActiveScreen = (currentRoute: string, focusableTree) => {
47
+ const isActiveScreenAvailableToFocus = (): boolean => {
48
+ const routes = focusableTree.root.children;
49
+
50
+ const route = find((route) => route.id === currentRoute, routes);
51
+
52
+ return isNotNil(route);
53
+ };
54
+
55
+ return waitUntil({
56
+ maxTimeout: HOW_OFTEN_TO_CHECK_CONDITION * 10, // ms
57
+ conditionFn: isActiveScreenAvailableToFocus,
58
+ });
59
+ };
60
+
61
+ const getActiveScreenChildren = (focusableTree) => {
62
+ return pathOr([], ["children"], last(focusableTree.root.children));
63
+ };
64
+
65
+ export const getContentNode = (focusableTree) => {
66
+ const content = find(
67
+ (child) =>
68
+ startsWith(QUICK_BRICK_CONTENT, child.id) ||
69
+ startsWith("player", child.id),
70
+ getActiveScreenChildren(focusableTree)
71
+ );
72
+
73
+ return content;
74
+ };
75
+
76
+ export const getNavbarNode = (focusableTree) => {
77
+ const navbar = find(
78
+ (child) => startsWith(QUICK_BRICK_NAVBAR, child.id),
79
+ getActiveScreenChildren(focusableTree)
80
+ );
81
+
82
+ return navbar;
83
+ };
84
+
85
+ export const waitForContent = (focusableTree) => {
86
+ const contentHasAnyChildren = (): boolean => {
87
+ const countOfChildren = pathOr(
88
+ 0,
89
+ ["children", "length"],
90
+ getContentNode(focusableTree)
91
+ );
92
+
93
+ return countOfChildren > 0;
94
+ };
95
+
96
+ return waitUntil({
97
+ maxTimeout: HOW_OFTEN_TO_CHECK_CONDITION * 10, // ms
98
+ conditionFn: contentHasAnyChildren,
99
+ });
100
+ };
@@ -0,0 +1,63 @@
1
+ import { isFilledArray } from "..";
2
+
3
+ describe("isFilledArray", () => {
4
+ it("non-empty array is not empty", () => {
5
+ const value = [1, 2, 3];
6
+
7
+ expect(isFilledArray(value)).toBe(true);
8
+ });
9
+
10
+ it("empty array is empty", () => {
11
+ const value = [];
12
+
13
+ expect(isFilledArray(value)).toBe(false);
14
+ });
15
+
16
+ it("number is not array", () => {
17
+ const value = 123;
18
+
19
+ expect(isFilledArray(value)).toBe(false);
20
+ });
21
+
22
+ it("string is not array", () => {
23
+ const value = "vfnjdk";
24
+
25
+ expect(isFilledArray(value)).toBe(false);
26
+ });
27
+
28
+ it("empty string is not array", () => {
29
+ const value = "";
30
+
31
+ expect(isFilledArray(value)).toBe(false);
32
+ });
33
+
34
+ it("NaN is not array", () => {
35
+ const value = "";
36
+
37
+ expect(isFilledArray(value)).toBe(false);
38
+ });
39
+
40
+ it("object is not array", () => {
41
+ const value = { test: 1 };
42
+
43
+ expect(isFilledArray(value)).toBe(false);
44
+ });
45
+
46
+ it("empty object is not array", () => {
47
+ const value = {};
48
+
49
+ expect(isFilledArray(value)).toBe(false);
50
+ });
51
+
52
+ it("undefined is not array", () => {
53
+ const value = undefined;
54
+
55
+ expect(isFilledArray(value)).toBe(false);
56
+ });
57
+
58
+ it("null is not array", () => {
59
+ const value = null;
60
+
61
+ expect(isFilledArray(value)).toBe(false);
62
+ });
63
+ });
@@ -95,3 +95,8 @@ export const makeListOfIndexes = (size: number): number[] =>
95
95
  export const makeListOf = (value: unknown, size: number): number[] => {
96
96
  return Array(size).fill(value);
97
97
  };
98
+
99
+ /** Checks if a value is a non-empty array */
100
+ export function isFilledArray(value: unknown): boolean {
101
+ return R.is(Array, value) && R.length(value) > 0;
102
+ }
@@ -7,12 +7,24 @@ describe("isLikeNumber", () => {
7
7
  expect(isLikeNumber(value)).toBe(true);
8
8
  });
9
9
 
10
+ it("value is a number", () => {
11
+ const value = 123.45;
12
+
13
+ expect(isLikeNumber(value)).toBe(true);
14
+ });
15
+
10
16
  it("value is a string as number", () => {
11
17
  const value = "123";
12
18
 
13
19
  expect(isLikeNumber(value)).toBe(true);
14
20
  });
15
21
 
22
+ it("value is a string as number", () => {
23
+ const value = "123.45";
24
+
25
+ expect(isLikeNumber(value)).toBe(true);
26
+ });
27
+
16
28
  it("value is empty string", () => {
17
29
  const value = "";
18
30
 
@@ -54,4 +66,10 @@ describe("isLikeNumber", () => {
54
66
 
55
67
  expect(isLikeNumber(value)).toBe(false);
56
68
  });
69
+
70
+ it("NaN is not a number", () => {
71
+ const value = NaN;
72
+
73
+ expect(isLikeNumber(value)).toBe(false);
74
+ });
57
75
  });
@@ -50,27 +50,14 @@ export const toFiniteNumberWithDefault = (
50
50
  return possibleNumber;
51
51
  };
52
52
 
53
- export const isLikeNumber = (value: any): boolean => {
54
- if (R.isEmpty(value)) {
55
- return false;
56
- }
57
-
58
- if (R.is(Number, value)) {
59
- return true;
60
- }
61
-
62
- if (R.is(String, value)) {
63
- const onlyDigit = value
64
- .split("")
65
- .filter((char) =>
66
- ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"].includes(char)
67
- )
68
- .join("");
53
+ export const isLikeNumber = (value: unknown): boolean => {
54
+ const possibleNumber = toNumber(value);
69
55
 
70
- return value === onlyDigit;
56
+ if (R.isNil(possibleNumber)) {
57
+ return false;
71
58
  }
72
59
 
73
- return false;
60
+ return true;
74
61
  };
75
62
 
76
63
  export const isZero = (value: unknown): boolean =>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "12.0.0-rc.69",
3
+ "version": "12.0.0-rc.70",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "12.0.0-rc.69",
31
+ "@applicaster/applicaster-types": "12.0.0-rc.70",
32
32
  "buffer": "^5.2.1",
33
33
  "camelize": "^1.0.0",
34
34
  "create-react-context": "^0.2.3",
@@ -1,6 +1,8 @@
1
1
  import * as React from "react";
2
2
  import { cleanup, renderHook } from "@testing-library/react-hooks";
3
3
  import { CellTapContext } from "@applicaster/zapp-react-native-ui-components/Contexts/CellTapContext";
4
+ import { ActionExecutorContext } from "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutorContext";
5
+
4
6
  import { waitFor } from "@testing-library/react-native";
5
7
 
6
8
  const navigator = {
@@ -29,10 +31,6 @@ jest.mock("@applicaster/zapp-react-native-ui-components/Contexts", () => ({
29
31
  },
30
32
  }));
31
33
 
32
- jest.mock("@applicaster/zapp-react-native-utils/reactHooks/screen", () => ({
33
- useScreenContext: jest.fn(() => navigator.data),
34
- }));
35
-
36
34
  const { useCellClick } = require("../");
37
35
 
38
36
  const mockProps = {
@@ -47,9 +45,15 @@ const mockProps = {
47
45
 
48
46
  const onCellTap = jest.fn();
49
47
 
48
+ const actionExecutor = {
49
+ handleEntryActions: jest.fn(() => Promise.resolve()),
50
+ };
51
+
50
52
  const wrapper = ({ children }) => (
51
53
  <CellTapContext.Provider value={onCellTap}>
52
- {children}
54
+ <ActionExecutorContext.Provider value={actionExecutor}>
55
+ {children}
56
+ </ActionExecutorContext.Provider>
53
57
  </CellTapContext.Provider>
54
58
  );
55
59
 
@@ -1,7 +1,6 @@
1
1
  /// <reference types="@applicaster/applicaster-types" />
2
2
 
3
3
  import * as React from "react";
4
- import { useContext } from "react";
5
4
  import * as R from "ramda";
6
5
  import { handleActionSchemeUrl } from "@applicaster/quick-brick-core/App/DeepLinking/URLSchemeHandler/SchemeHandlerHooks/useUrlSchemeHandler";
7
6
  import { CellTapContext } from "@applicaster/zapp-react-native-ui-components/Contexts/CellTapContext";
@@ -12,8 +11,9 @@ import {
12
11
  } from "@applicaster/zapp-react-native-utils/reactHooks/navigation";
13
12
  import { ZappPipesEntryContext } from "@applicaster/zapp-react-native-ui-components/Contexts";
14
13
  import { toBooleanWithDefaultTrue } from "@applicaster/zapp-react-native-utils/booleanUtils";
15
- import { noop } from "../../functionUtils";
16
- import { ActionExecutorContext } from "../../actionsExecutor/ActionExecutorContext";
14
+ import { ActionExecutorContext } from "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutorContext";
15
+
16
+ import { isFunction, noop } from "../../functionUtils";
17
17
  import { useSendAnalyticsOnPress } from "../analytics";
18
18
  import { logOnPress, warnEmptyContentType } from "./helpers";
19
19
 
@@ -30,12 +30,12 @@ type Props = {
30
30
  zappPipesData?: ZappPipesData;
31
31
  };
32
32
 
33
- export const useCellClick = (props: Props) => {
34
- const { component, zappPipesData } = props;
33
+ export const useCellClick = ({ component, zappPipesData, item }: Props) => {
35
34
  const { push, currentRoute } = useNavigation();
36
35
  const { pathname } = useRoute();
37
- const onCellTap = React.useContext(CellTapContext);
38
- const actionExecutor = useContext(ActionExecutorContext);
36
+
37
+ const onCellTap: Option<Function> = React.useContext(CellTapContext);
38
+ const actionExecutor = React.useContext(ActionExecutorContext);
39
39
 
40
40
  const cellSelectable = toBooleanWithDefaultTrue(
41
41
  component?.rules?.component_cells_selectable
@@ -47,7 +47,7 @@ export const useCellClick = (props: Props) => {
47
47
  const logTimestamp = useProfilerLogging();
48
48
 
49
49
  const sendAnalyticsOnPress = useSendAnalyticsOnPress(
50
- props.item,
50
+ item,
51
51
  component,
52
52
  zappPipesData
53
53
  );
@@ -56,7 +56,7 @@ export const useCellClick = (props: Props) => {
56
56
  async (_item?: ZappEntry | any, _index?: number) => {
57
57
  // Making sure we are not getting GestureResponderEvent in here.
58
58
  const isZappEntry = _item?.id && _item?.type;
59
- const selectedItem = isZappEntry ? _item : props.item;
59
+ const selectedItem = isZappEntry ? _item : item;
60
60
  setEntryContext?.(selectedItem);
61
61
 
62
62
  if (!selectedItem?.type?.value) {
@@ -80,8 +80,8 @@ export const useCellClick = (props: Props) => {
80
80
  });
81
81
  }
82
82
  } else {
83
- if (typeof onCellTap === "function") {
84
- onCellTap(selectedItem, _index);
83
+ if (isFunction(onCellTap)) {
84
+ onCellTap?.(selectedItem, _index);
85
85
  } else {
86
86
  if (currentRoute === pathname || pathname.includes("video-modal")) {
87
87
  const targetScreen = component?.data?.target;
@@ -1,77 +1,26 @@
1
- import React from "react";
2
1
  import { renderHook } from "@testing-library/react-hooks";
3
- import configureStore from "redux-mock-store";
4
- import thunk from "redux-thunk";
5
- import { Provider } from "react-redux";
6
-
7
2
  import { encodeParams } from "@applicaster/zapp-pipes-v2-client/src/utils";
8
3
 
9
- const mockStore = configureStore([thunk]);
10
-
11
- jest.useFakeTimers();
12
-
13
4
  const appData = {
14
5
  platform: "foo",
15
6
  version_name: "1.2.0",
16
7
  sdk_version: "2.0.0",
17
8
  };
18
9
 
19
- const pipesEndpoints = {
20
- "http://foo.com/show": {
21
- context_obj: [
22
- { type: "ctx", key: "platform" },
23
- { type: "query", mapper: "version", key: "appVersion" },
24
- { type: "header", mapper: "SDK", key: "sdkVersion" },
25
- ],
26
- },
27
- };
28
-
29
- const store_mock = { appData, pipesEndpoints };
30
-
31
- const defaultStore = mockStore({
32
- pipesEndpoints,
33
- appData,
34
- });
10
+ let INFLATED_URL;
35
11
 
36
- const wrapper: React.FC<any> = ({ children, store }) => (
37
- <Provider store={store}>{children}</Provider>
12
+ jest.mock(
13
+ "@applicaster/zapp-react-native-utils/reactHooks/feed/useInflatedUrl",
14
+ () => ({
15
+ useInflatedUrl: () => INFLATED_URL,
16
+ })
38
17
  );
39
18
 
40
- const navigator = {
41
- currentRoute: "/river/A1234",
42
- };
43
-
44
- jest.mock("../../navigation", () => ({
45
- useNavigation: jest.fn(() => navigator),
46
- useRoute: jest.fn(() => ({})),
47
- }));
48
-
49
- const contexts = {
50
- entry: { id: "show-1" },
51
- search: {},
52
- screen: {},
53
- };
54
-
55
- const contextMock = (type) => ({
56
- useZappPipesContext: () => [contexts[type] || {}],
57
- });
58
-
59
- jest.mock("@applicaster/zapp-react-native-ui-components/Contexts", () => ({
60
- ZappPipesEntryContext: contextMock("entry"),
61
- ZappPipesSearchContext: contextMock("search"),
62
- ZappPipesScreenContext: contextMock("screen"),
63
- }));
64
-
65
- jest.mock("@applicaster/zapp-react-native-redux/AppStore", () => ({
66
- appStore: {
67
- get: jest.fn((prop) => store_mock[prop]),
68
- },
69
- }));
70
-
71
19
  const { useBuildPipesUrl } = require("../useBuildPipesUrl");
72
20
 
73
21
  describe("useBuildPipesUrl", () => {
74
22
  it("returns the proper url", async () => {
23
+ INFLATED_URL = "http://foo.com/show/show-1";
75
24
  const url = "http://foo.com/show/{{showId}}";
76
25
 
77
26
  const mapping = {
@@ -81,12 +30,20 @@ describe("useBuildPipesUrl", () => {
81
30
  },
82
31
  };
83
32
 
84
- const { result, waitForNextUpdate } = renderHook(
85
- () => useBuildPipesUrl({ url, mapping }),
86
- {
87
- wrapper,
88
- initialProps: { store: defaultStore },
89
- }
33
+ const buildResult = {
34
+ params: {
35
+ ctx: encodeParams({ platform: appData.platform }),
36
+ version: appData.version_name,
37
+ },
38
+ headers: { SDK: appData.sdk_version },
39
+ };
40
+
41
+ const mockedRequestBuilder = {
42
+ build: jest.fn(() => Promise.resolve(buildResult)),
43
+ };
44
+
45
+ const { result, waitForNextUpdate } = renderHook(() =>
46
+ useBuildPipesUrl({ url, mapping, requestBuilder: mockedRequestBuilder })
90
47
  );
91
48
 
92
49
  expect(result.current.url).toBe("http://foo.com/show/show-1");
@@ -108,6 +65,7 @@ describe("useBuildPipesUrl", () => {
108
65
  });
109
66
 
110
67
  it("returns empty object if url is not in endpoints", async () => {
68
+ INFLATED_URL = "http://foobar.com/show-1";
111
69
  const url = "http://foobar.com/{{showId}}";
112
70
 
113
71
  const mapping = {
@@ -117,12 +75,14 @@ describe("useBuildPipesUrl", () => {
117
75
  },
118
76
  };
119
77
 
120
- const { result, waitForNextUpdate } = renderHook(
121
- () => useBuildPipesUrl({ url, mapping }),
122
- {
123
- wrapper,
124
- initialProps: { store: defaultStore },
125
- }
78
+ const buildResult = {};
79
+
80
+ const mockedRequestBuilder = {
81
+ build: jest.fn(() => Promise.resolve(buildResult)),
82
+ };
83
+
84
+ const { result, waitForNextUpdate } = renderHook(() =>
85
+ useBuildPipesUrl({ url, mapping, requestBuilder: mockedRequestBuilder })
126
86
  );
127
87
 
128
88
  expect(result.current.url).toBe("http://foobar.com/show-1");
@@ -1,7 +1,6 @@
1
- // @ts-ignore
2
1
  import { RequestBuilder } from "@applicaster/zapp-pipes-v2-client";
3
- import { useEffect, useState } from "react";
4
- import { useInflatedUrl } from "./useInflatedUrl";
2
+ import * as React from "react";
3
+ import { useInflatedUrl } from "@applicaster/zapp-react-native-utils/reactHooks/feed/useInflatedUrl";
5
4
  import { reactHooksLogger } from "../logger";
6
5
 
7
6
  export const logger = reactHooksLogger.addSubsystem("build-pipes-url");
@@ -15,18 +14,16 @@ type PipesBuildRequestObject = {
15
14
  export function useBuildPipesUrl({
16
15
  url,
17
16
  mapping = {},
17
+ requestBuilder = new RequestBuilder(),
18
18
  }): PipesBuildRequestObject {
19
- const [requestParams, setRequestParams] = useState<object | null>(null);
20
- const [error, setError] = useState(null);
19
+ const [requestParams, setRequestParams] = React.useState<object | null>(null);
21
20
 
21
+ const [error, setError] = React.useState(null);
22
22
  const inflatedUrl = useInflatedUrl({ feedUrl: url, mapping });
23
23
 
24
- const requestBuilder = new RequestBuilder();
25
-
26
24
  const buildRequest = async (_url) => {
27
25
  try {
28
26
  const result = await requestBuilder.build(_url);
29
-
30
27
  setRequestParams(result);
31
28
  } catch (e) {
32
29
  setError(e);
@@ -34,7 +31,7 @@ export function useBuildPipesUrl({
34
31
  }
35
32
  };
36
33
 
37
- useEffect(() => {
34
+ React.useEffect(() => {
38
35
  if (inflatedUrl && !requestParams) {
39
36
  buildRequest(inflatedUrl);
40
37
  }
@@ -42,7 +39,7 @@ export function useBuildPipesUrl({
42
39
 
43
40
  return {
44
41
  url: inflatedUrl || "",
45
- requestParams: requestParams, // TODO should it be: || {} ?
42
+ requestParams, // TODO should it be: || {} ?
46
43
  error: error || undefined,
47
44
  };
48
45
  }
@@ -13,7 +13,11 @@ import {
13
13
  import { HooksManager } from "@applicaster/zapp-react-native-utils/appUtils/HooksManager";
14
14
 
15
15
  import { legacyScreenData } from "@applicaster/quick-brick-core/App/NavigationProvider/utils";
16
- import { LONG_KEY_PRESS_TIMEOUT } from "@applicaster/quick-brick-core/const";
16
+ import {
17
+ LONG_KEY_PRESS_TIMEOUT,
18
+ QUICK_BRICK_NAVBAR,
19
+ QUICK_BRICK_CONTENT,
20
+ } from "@applicaster/quick-brick-core/const";
17
21
  import { useZappHooksModalState } from "@applicaster/zapp-react-dom-ui-components/Components/ZappHooksModal/ZappHooksModal";
18
22
  import { ZappHookModalContext } from "@applicaster/zapp-react-native-ui-components/Contexts";
19
23
  import { PathnameContext } from "@applicaster/zapp-react-native-ui-components/Contexts/PathnameContext";
@@ -452,3 +456,30 @@ export const useIsScreenActive = () => {
452
456
 
453
457
  return pathname === currentRoute;
454
458
  };
459
+
460
+ type Return = (prefix: string) => string;
461
+
462
+ export const useUniqueRouteSuffix = (): Return => {
463
+ const pathname = usePathname();
464
+ const { currentRoute } = useNavigation();
465
+
466
+ const suffix = `currentRoute:${currentRoute}_pathname:${pathname}`;
467
+
468
+ const withUniqueRouteSuffix = React.useCallback((prefix: string) => {
469
+ return `${prefix}___${suffix}`;
470
+ }, []);
471
+
472
+ return withUniqueRouteSuffix;
473
+ };
474
+
475
+ export const useNavbarId = () => {
476
+ const withUniqueRouteSuffix = useUniqueRouteSuffix();
477
+
478
+ return withUniqueRouteSuffix(QUICK_BRICK_NAVBAR);
479
+ };
480
+
481
+ export const useContentId = () => {
482
+ const withUniqueRouteSuffix = useUniqueRouteSuffix();
483
+
484
+ return withUniqueRouteSuffix(QUICK_BRICK_CONTENT);
485
+ };
@@ -3,23 +3,28 @@ import { Provider } from "react-redux";
3
3
  import { renderHook } from "@testing-library/react-hooks";
4
4
  import configureStore from "redux-mock-store";
5
5
  import thunk from "redux-thunk";
6
- import { NavigationContext } from "@applicaster/zapp-react-native-ui-components/Contexts/NavigationContext";
7
6
 
8
7
  const mockStore = configureStore([thunk]);
9
8
 
10
- import { useCurrentScreenData } from "../useCurrentScreenData";
11
-
12
- describe("useScreenData", function () {
13
- const river_id_2: ZappRiver = {
14
- name: "River 2",
15
- id: "A1234",
16
- home: false,
17
- home_offline: false,
18
- supports_offline: false,
19
- navigations: [],
20
- hooks: { preload_plugins: [] },
21
- };
22
-
9
+ const river_id_2: ZappRiver = {
10
+ name: "River 2",
11
+ id: "A1234",
12
+ home: false,
13
+ home_offline: false,
14
+ supports_offline: false,
15
+ navigations: [],
16
+ hooks: { preload_plugins: [] },
17
+ };
18
+
19
+ jest.mock("../useScreenContext", () => ({
20
+ useScreenContext: () => ({
21
+ screen: river_id_2,
22
+ }),
23
+ }));
24
+
25
+ const { useCurrentScreenData } = require("../useCurrentScreenData");
26
+
27
+ describe("useScreenData", () => {
23
28
  const river_id_1 = { id: "B5678" };
24
29
 
25
30
  const store = mockStore({
@@ -27,17 +32,10 @@ describe("useScreenData", function () {
27
32
  });
28
33
 
29
34
  const wrapper: React.FC<any> = ({ children }) => (
30
- <NavigationContext.Provider
31
- value={{
32
- currentRoute: "/river/river_id_1/river/river_id_2",
33
- data: { screen: river_id_2 },
34
- }}
35
- >
36
- <Provider store={store}>{children}</Provider>
37
- </NavigationContext.Provider>
35
+ <Provider store={store}>{children}</Provider>
38
36
  );
39
37
 
40
- it("should return river data for the current screen/route", function () {
38
+ it("should return river data for the current screen/route", () => {
41
39
  const { result } = renderHook(() => useCurrentScreenData(), { wrapper });
42
40
 
43
41
  expect(result.current).toBe(river_id_2);
@@ -10,3 +10,5 @@ export {
10
10
  useNavbarState,
11
11
  useSetNavbarState,
12
12
  } from "./useScreenContext";
13
+
14
+ export { useScreenBackgroundColor } from "./useScreenBackgroundColor";
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
3
+ import { useScreenConfiguration } from "@applicaster/zapp-react-native-ui-components/Components/River/useScreenConfiguration";
4
+ import { ifEmptyUseFallback } from "@applicaster/zapp-react-native-utils/cellUtils";
5
+
6
+ export const useScreenBackgroundColor = (screenId: string): string => {
7
+ const { backgroundColor: screenBackgroundColor } =
8
+ useScreenConfiguration(screenId);
9
+
10
+ const theme = useTheme();
11
+
12
+ const themeBackgroundColor = React.useMemo(
13
+ () => theme.app_background_color,
14
+ [theme.app_background_color]
15
+ );
16
+
17
+ const backgroundColor = ifEmptyUseFallback(
18
+ screenBackgroundColor,
19
+ themeBackgroundColor
20
+ );
21
+
22
+ return backgroundColor;
23
+ };
@@ -45,8 +45,7 @@ export function useSetNavbarState() {
45
45
 
46
46
  export function useScreenContext(): NavigationScreenData {
47
47
  const navigator = useNavigation();
48
- // Cover cases where screenContext is used before navigation is initialized
49
- const screenData = useContext(ScreenDataContext) || navigator.data;
48
+ const screenData = useContext(ScreenDataContext);
50
49
 
51
50
  const modalScreenData = navigator?.modalData;
52
51