@applicaster/zapp-react-native-utils 12.0.0-alpha.9004455836 → 12.0.0-alpha.9233494544

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 (66) hide show
  1. package/actionsExecutor/ActionExecutorContext.tsx +16 -8
  2. package/analyticsUtils/__tests__/analyticsManager.test.js +75 -73
  3. package/analyticsUtils/helpers/hooks.ts +1 -1
  4. package/appUtils/HooksManager/index.js +3 -1
  5. package/appUtils/focusManager/index.ts +147 -25
  6. package/appUtils/focusManager/treeDataStructure/Utils/index.js +83 -33
  7. package/appUtils/focusManager/utils/index.ts +77 -0
  8. package/appUtils/keyCodes/__tests__/__snapshots__/keyCodes.test.js.snap +1 -1
  9. package/appUtils/keyCodes/__tests__/keyCodes.test.js +80 -85
  10. package/appUtils/keyCodes/keys.web.js +1 -0
  11. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +53 -0
  12. package/appUtils/playerManager/conts.ts +5 -0
  13. package/appUtils/playerManager/player.ts +47 -4
  14. package/appUtils/playerManager/playerFactory.ts +12 -10
  15. package/appUtils/playerManager/playerNative.ts +35 -0
  16. package/appUtils/playerManager/usePlayerState.tsx +13 -1
  17. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +50 -0
  18. package/cellUtils/__tests__/cellUtils.test.ts +59 -0
  19. package/cellUtils/__tests__/fixtures.ts +13 -0
  20. package/cellUtils/index.ts +33 -2
  21. package/focusManager/index.ts +1 -1
  22. package/functionUtils/__tests__/__snapshots__/functionUtils.test.js.snap +2 -2
  23. package/functionUtils/__tests__/functionUtils.test.js +244 -248
  24. package/headersUtils/index.ts +1 -0
  25. package/localizationUtils/__tests__/localizationUtils.test.ts +17 -13
  26. package/localizationUtils/index.ts +6 -3
  27. package/logger/index.ts +1 -1
  28. package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
  29. package/manifestUtils/defaultManifestConfigurations/player.js +10 -1
  30. package/manifestUtils/index.js +5 -1
  31. package/manifestUtils/secondaryImage/index.js +18 -15
  32. package/modalState/index.ts +57 -0
  33. package/navigationUtils/index.ts +56 -7
  34. package/package.json +3 -2
  35. package/playerUtils/configurationGenerator.ts +11 -1
  36. package/pluginUtils/index.ts +2 -2
  37. package/reactHooks/autoscrolling/__tests__/useTrackCurrentAutoScrollingElement.test.ts +2 -0
  38. package/reactHooks/autoscrolling/__tests__/{useTrackedView.test.ts → useTrackedView.test.tsx} +18 -1
  39. package/reactHooks/cell-click/__tests__/index.test.js +6 -2
  40. package/reactHooks/cell-click/index.ts +2 -2
  41. package/reactHooks/flatList/useLoadNextPageIfNeeded.ts +16 -9
  42. package/reactHooks/index.ts +2 -0
  43. package/reactHooks/layout/__tests__/{index.test.js → index.test.tsx} +66 -59
  44. package/reactHooks/layout/getDeviceInfo/index.ts +4 -2
  45. package/reactHooks/layout/types/index.ts +2 -0
  46. package/reactHooks/layout/useDimensions/helpers/__tests__/getInitialDimensions.native.test.js +11 -16
  47. package/reactHooks/layout/useDimensions/helpers/__tests__/getInitialDimensions.test.js +12 -14
  48. package/reactHooks/layout/useDimensions/useDimensions.ts +13 -7
  49. package/reactHooks/navigation/__mocks__/index.ts +15 -0
  50. package/reactHooks/navigation/__mocks__/useNavigation.ts +7 -0
  51. package/reactHooks/navigation/__tests__/index.test.tsx +234 -78
  52. package/reactHooks/navigation/index.ts +262 -81
  53. package/reactHooks/resolvers/__tests__/useCellResolver.test.tsx +3 -1
  54. package/reactHooks/screen/__mocks__/useScreenContext.ts +3 -1
  55. package/reactHooks/screen/index.ts +2 -0
  56. package/reactHooks/screen/useScreenContext.ts +5 -4
  57. package/reactHooks/screen/useScreenData.ts +1 -1
  58. package/reactHooks/screen/useTargetScreenData.ts +7 -0
  59. package/reactHooks/state/useRivers.ts +3 -1
  60. package/reactHooks/useListenEventBusEvent.ts +1 -4
  61. package/reactHooks/videoModal/hooks/useVideoModalScreenData.tsx +10 -9
  62. package/reactUtils/helpers.tsx +1 -1
  63. package/riverComponetsMeasurementProvider/index.tsx +2 -2
  64. package/stringUtils/index.ts +5 -0
  65. package/testUtils/getByTestId.ts +14 -0
  66. package/testUtils/index.tsx +38 -23
@@ -11,11 +11,9 @@ import * as QuickBrickManager from "@applicaster/zapp-react-native-bridge/QuickB
11
11
  import { showConfirmationDialog } from "../alertUtils";
12
12
  import { createCloudEvent, sendCloudEvent } from "../cloudEventsUtils";
13
13
  import { createLogger } from "../logger";
14
- import {
15
- StorageMultiSelectProvider
16
- } from "@applicaster/zapp-react-native-bridge/ZappStorage/StorageMultiSelectProvider";
14
+ import { StorageMultiSelectProvider } from "@applicaster/zapp-react-native-bridge/ZappStorage/StorageMultiSelectProvider";
17
15
 
18
- const { log_error, log_debug, log_info } = createLogger({
16
+ const { log_error, log_info } = createLogger({
19
17
  subsystem: "ActionExecutorContext",
20
18
  category: "General",
21
19
  });
@@ -110,11 +108,17 @@ const prepareDefaultActions = (actionExecutor) => {
110
108
  const tag = entry?.extensions?.tag;
111
109
 
112
110
  if (keyNamespace && tag) {
113
- const multiSelectProvider = StorageMultiSelectProvider.getProvider(keyNamespace);
111
+ const multiSelectProvider =
112
+ StorageMultiSelectProvider.getProvider(keyNamespace);
114
113
 
115
114
  const selectedItems = await multiSelectProvider.getSelectedAsync();
116
115
  const isTagInSelectedItems = selectedItems.includes(tag);
117
- log_info(`handleAction: localStorageToggleFlag event will ${isTagInSelectedItems ? "remove" : "add"} tag: ${tag} for keyNamespace: ${keyNamespace}, current selectedItems: ${selectedItems}`);
116
+
117
+ log_info(
118
+ `handleAction: localStorageToggleFlag event will ${
119
+ isTagInSelectedItems ? "remove" : "add"
120
+ } tag: ${tag} for keyNamespace: ${keyNamespace}, current selectedItems: ${selectedItems}`
121
+ );
118
122
 
119
123
  if (selectedItems.includes(tag)) {
120
124
  await multiSelectProvider.removeItem(tag);
@@ -122,13 +126,17 @@ const prepareDefaultActions = (actionExecutor) => {
122
126
  await multiSelectProvider.addItem(tag);
123
127
  }
124
128
  } else {
125
- log_error("handleAction: localStorageToggleFlag event missing keyNamespace or tag", { keyNamespace, tag });
129
+ log_error(
130
+ "handleAction: localStorageToggleFlag event missing keyNamespace or tag",
131
+ { keyNamespace, tag }
132
+ );
126
133
 
127
134
  return ActionResult.Error;
128
135
  }
129
136
 
130
137
  return ActionResult.Success;
131
- });
138
+ }
139
+ );
132
140
  };
133
141
 
134
142
  export const ActionExecutorContext =
@@ -1,18 +1,18 @@
1
+ import * as ReactNative from "react-native";
2
+
1
3
  const mock_postEvent = jest.fn();
2
4
  const mock_setPlugins = jest.fn();
3
5
  const mock_postTimedEvent = jest.fn();
4
6
  const mock_endTimedEvent = jest.fn();
5
7
 
6
- jest.mock("react-native", () => ({
7
- NativeModules: {
8
- AnalyticsBridge: {
9
- postEvent: mock_postEvent,
10
- setPlugins: mock_setPlugins,
11
- postTimedEvent: mock_postTimedEvent,
12
- endTimedEvent: mock_endTimedEvent,
13
- },
14
- },
15
- }));
8
+ const AnalyticsBridge = {
9
+ postEvent: mock_postEvent,
10
+ setPlugins: mock_setPlugins,
11
+ postTimedEvent: mock_postTimedEvent,
12
+ endTimedEvent: mock_endTimedEvent,
13
+ };
14
+
15
+ ReactNative.NativeModules.AnalyticsBridge = AnalyticsBridge;
16
16
 
17
17
  const eventName = "event";
18
18
  const payload = {};
@@ -26,90 +26,92 @@ function clearAllMocks() {
26
26
  mock_endTimedEvent.mockClear();
27
27
  }
28
28
 
29
- describe("isValidAnalyticsData", () => {
30
- beforeEach(clearAllMocks);
29
+ describe("AnalyticsManager", () => {
30
+ describe("isValidAnalyticsData", () => {
31
+ beforeEach(clearAllMocks);
31
32
 
32
- it("returns false if eventName is not a string", () => {
33
- expect(analyticsManager.isValidAnalyticsData(42)).toBe(false);
34
- expect(analyticsManager.isValidAnalyticsData({})).toBe(false);
35
- expect(analyticsManager.isValidAnalyticsData([])).toBe(false);
36
- expect(analyticsManager.isValidAnalyticsData(() => ({}))).toBe(false);
37
- });
33
+ it("returns false if eventName is not a string", () => {
34
+ expect(analyticsManager.isValidAnalyticsData(42)).toBe(false);
35
+ expect(analyticsManager.isValidAnalyticsData({})).toBe(false);
36
+ expect(analyticsManager.isValidAnalyticsData([])).toBe(false);
37
+ expect(analyticsManager.isValidAnalyticsData(() => ({}))).toBe(false);
38
+ });
38
39
 
39
- it("returns false if payload is not an object", () => {
40
- expect(analyticsManager.isValidAnalyticsData(eventName, "string")).toBe(
41
- false
42
- );
40
+ it("returns false if payload is not an object", () => {
41
+ expect(analyticsManager.isValidAnalyticsData(eventName, "string")).toBe(
42
+ false
43
+ );
43
44
 
44
- expect(analyticsManager.isValidAnalyticsData(eventName, 42)).toBe(false);
45
+ expect(analyticsManager.isValidAnalyticsData(eventName, 42)).toBe(false);
45
46
 
46
- expect(analyticsManager.isValidAnalyticsData(eventName, () => ({}))).toBe(
47
- false
48
- );
49
- });
47
+ expect(analyticsManager.isValidAnalyticsData(eventName, () => ({}))).toBe(
48
+ false
49
+ );
50
+ });
50
51
 
51
- it("returns true if eventName is a string and payload is null or an object", () => {
52
- expect(analyticsManager.isValidAnalyticsData(eventName)).toBe(true);
52
+ it("returns true if eventName is a string and payload is null or an object", () => {
53
+ expect(analyticsManager.isValidAnalyticsData(eventName)).toBe(true);
53
54
 
54
- expect(analyticsManager.isValidAnalyticsData(eventName, payload)).toBe(
55
- true
56
- );
55
+ expect(analyticsManager.isValidAnalyticsData(eventName, payload)).toBe(
56
+ true
57
+ );
58
+ });
57
59
  });
58
- });
59
60
 
60
- describe("postAnalyticEvent", () => {
61
- beforeEach(clearAllMocks);
61
+ describe("postAnalyticEvent", () => {
62
+ beforeEach(clearAllMocks);
62
63
 
63
- it("calls the postEvent method of the analytic bridge", () => {
64
- analyticsManager.postAnalyticEvent(eventName, payload);
65
- expect(mock_postEvent).toHaveBeenCalledWith(eventName, payload);
66
- });
64
+ it("calls the postEvent method of the analytic bridge", () => {
65
+ analyticsManager.postAnalyticEvent(eventName, payload);
66
+ expect(mock_postEvent).toHaveBeenCalledWith(eventName, payload);
67
+ });
67
68
 
68
- it("skips the event if it is not valid", () => {
69
- analyticsManager.postAnalyticEvent(42);
70
- expect(mock_postTimedEvent).not.toHaveBeenCalled();
69
+ it("skips the event if it is not valid", () => {
70
+ analyticsManager.postAnalyticEvent(42);
71
+ expect(mock_postTimedEvent).not.toHaveBeenCalled();
72
+ });
71
73
  });
72
- });
73
74
 
74
- describe("providePlugins", () => {
75
- beforeEach(clearAllMocks);
75
+ describe("providePlugins", () => {
76
+ beforeEach(clearAllMocks);
76
77
 
77
- it("calls the setPlugins method of the analytic bridge", () => {
78
- const plugins = [];
79
- const plugin_configurations = {};
80
- const callback = jest.fn();
78
+ it("calls the setPlugins method of the analytic bridge", () => {
79
+ const plugins = [];
80
+ const plugin_configurations = {};
81
+ const callback = jest.fn();
81
82
 
82
- analyticsManager.providePlugins(plugins, plugin_configurations, callback);
83
+ analyticsManager.providePlugins(plugins, plugin_configurations, callback);
83
84
 
84
- expect(mock_setPlugins).toHaveBeenCalledWith(
85
- plugins,
86
- plugin_configurations,
87
- callback
88
- );
85
+ expect(mock_setPlugins).toHaveBeenCalledWith(
86
+ plugins,
87
+ plugin_configurations,
88
+ callback
89
+ );
90
+ });
89
91
  });
90
- });
91
92
 
92
- describe("startAnalyticsTimedEvent", () => {
93
- beforeEach(clearAllMocks);
93
+ describe("startAnalyticsTimedEvent", () => {
94
+ beforeEach(clearAllMocks);
94
95
 
95
- it("skips the event if it is not valid", () => {
96
- analyticsManager.startAnalyticsTimedEvent(42);
97
- expect(mock_postTimedEvent).not.toHaveBeenCalled();
96
+ it("skips the event if it is not valid", () => {
97
+ analyticsManager.startAnalyticsTimedEvent(42);
98
+ expect(mock_postTimedEvent).not.toHaveBeenCalled();
99
+ });
98
100
  });
99
- });
100
101
 
101
- describe("endAnalyticsTimedEvent", () => {
102
- beforeEach(clearAllMocks);
102
+ describe("endAnalyticsTimedEvent", () => {
103
+ beforeEach(clearAllMocks);
103
104
 
104
- it("emits a time_on_screen event and adds duration to payload", () => {
105
- analyticsManager.endAnalyticsTimedEvent(eventName, payload);
106
- const newEventName = "time_on_screen";
107
- payload.duration = "N/A";
108
- expect(mock_postEvent).toHaveBeenCalledWith(newEventName, payload);
109
- });
105
+ it("emits a time_on_screen event and adds duration to payload", () => {
106
+ analyticsManager.endAnalyticsTimedEvent(eventName, payload);
107
+ const newEventName = "time_on_screen";
108
+ payload.duration = "N/A";
109
+ expect(mock_postEvent).toHaveBeenCalledWith(newEventName, payload);
110
+ });
110
111
 
111
- it("skips the event if it is not valid", () => {
112
- analyticsManager.endAnalyticsTimedEvent(42);
113
- expect(mock_postEvent).not.toHaveBeenCalled();
112
+ it("skips the event if it is not valid", () => {
113
+ analyticsManager.endAnalyticsTimedEvent(42);
114
+ expect(mock_postEvent).not.toHaveBeenCalled();
115
+ });
114
116
  });
115
117
  });
@@ -26,7 +26,7 @@ export const useHookAnalytics = (props): void => {
26
26
  if (screenData?.hookPlugin?.module?.runInBackground) {
27
27
  sendScreenEvent({ analyticsScreenData });
28
28
  }
29
- }, [hookPlugin.screen_id]);
29
+ }, [sendScreenEvent, screenData, hookPlugin.screen_id]);
30
30
  };
31
31
 
32
32
  export const useScreenAnalytics = (props) => {
@@ -270,7 +270,9 @@ export function HooksManager({
270
270
  });
271
271
 
272
272
  const isHookInHomescreen = payload?.home;
273
- const isHookFlowBlocker = hookPlugin?.module?.isFlowBlocker();
273
+
274
+ const isHookFlowBlocker =
275
+ hookPlugin?.module?.isFlowBlocker?.() ?? false;
274
276
 
275
277
  // TODO: We technically need to also support the case where there is more than one hook in the stack,
276
278
  // if needed we could add differentiation explicit user cancellation (when we should go to background) and other types of cancellation
@@ -1,9 +1,24 @@
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
+ isOnTheSameScreen,
12
+ } from "./treeDataStructure/Utils";
4
13
  import { subscriber } from "../../functionUtils";
5
14
  import * as FOCUS_EVENTS from "./events";
6
15
  import { coreLogger } from "../../logger";
16
+ import {
17
+ waitForActiveScreen,
18
+ waitForContent,
19
+ getContentNode,
20
+ getActiveScreen,
21
+ } from "./utils";
7
22
 
8
23
  const logger = coreLogger.addSubsystem("focusManager");
9
24
 
@@ -46,7 +61,7 @@ export const focusManager = (function () {
46
61
  let lastFocusedId = null;
47
62
  let initialFocusOnContent = null;
48
63
  let focusDisabled = false;
49
- let focusAutoEnabled = true;
64
+ let focusAutoEnabled = false;
50
65
  const eventHandler = subscriber();
51
66
 
52
67
  function disableAutomaticFocus() {
@@ -93,7 +108,7 @@ export const focusManager = (function () {
93
108
  !currentFocusable.isGroup &&
94
109
  currentFocusable.isMounted()
95
110
  ) {
96
- currentFocusable.blur(currentFocusable, direction);
111
+ currentFocusable.blur(direction);
97
112
  currentFocusable.hasLostFocus(getCurrentFocus(), direction);
98
113
  }
99
114
  }
@@ -226,32 +241,51 @@ export const focusManager = (function () {
226
241
 
227
242
  logger.log({ message: "setInitialFocus", data: { preferredFocus } });
228
243
 
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
- }
244
+ if (isNilOrEmpty(preferredFocus)) {
245
+ return { success: false };
246
+ }
242
247
 
243
- logger.log({
244
- message: "setInitialFocus – currentFocusNode",
245
- data: {
246
- focusableItem,
247
- },
248
- });
248
+ const focusCandidate = preferredFocus[0];
249
+ const focusableItem = focusCandidate.component;
250
+
251
+ const currentFocusedId = R.path(["props", "id"], getCurrentFocus());
252
+ const nextFocusedId = R.path(["props", "id"], focusableItem);
253
+
254
+ if (currentFocusedId === nextFocusedId) {
255
+ // we are going to move focus to the same node(already focused) -> nothing to do
256
+ return { success: true };
257
+ }
249
258
 
250
- currentFocusNode = focusCandidate;
251
- focusableItem.willReceiveFocus(focusableItem, null);
252
- focusableItem && setFocus(focusCandidate.id, null);
259
+ if (preferredFocus.length === 1 && focusableItem && focusableItem.isGroup) {
260
+ // we've found only one FocusableGroup as a candidate to focus, try to use fallback
261
+ return { success: false };
262
+ }
263
+
264
+ if (
265
+ focusableItem &&
266
+ !focusableItem.isGroup &&
267
+ isFocusEnabled(focusableItem)
268
+ ) {
269
+ if (initialFocusOnContent) {
270
+ blur();
271
+ initialFocusOnContent = false;
253
272
  }
273
+
274
+ logger.log({
275
+ message: "setInitialFocus – currentFocusNode",
276
+ data: {
277
+ focusableItem,
278
+ },
279
+ });
280
+
281
+ currentFocusNode = focusCandidate;
282
+ focusableItem.willReceiveFocus(focusableItem, null);
283
+ focusableItem && setFocus(focusCandidate.id, null);
284
+
285
+ return { success: true };
254
286
  }
287
+
288
+ return { success: false };
255
289
  }
256
290
 
257
291
  function treeLoaded(val, node) {
@@ -323,6 +357,8 @@ export const focusManager = (function () {
323
357
  * @returns {React.Component} the registered component
324
358
  */
325
359
  function registerGroup(id, component) {
360
+ // console.log("debug_2", "FM registerGroup", { id, component });
361
+
326
362
  focusableTree.addNode({ id, component });
327
363
 
328
364
  return component;
@@ -338,6 +374,8 @@ export const focusManager = (function () {
338
374
  * @returns {React.Component} the registered component
339
375
  */
340
376
  function registerItem(id, component) {
377
+ // console.log("debug_2", "FM registerItem", { id, component });
378
+
341
379
  focusableTree.addNode({ id, component });
342
380
 
343
381
  if (!currentFocusNode && id === lastFocusedId && !id.includes("menu")) {
@@ -354,6 +392,8 @@ export const focusManager = (function () {
354
392
  * @param {String} id of the group to unregister
355
393
  */
356
394
  function unregisterGroup(id) {
395
+ // console.log("debug_2", "FM unregisterGroup", { id });
396
+
357
397
  focusableTree.findAndRemoveNode(id);
358
398
  }
359
399
 
@@ -365,6 +405,8 @@ export const focusManager = (function () {
365
405
  * @param {String} id of the Focusable to unregister
366
406
  */
367
407
  function unregisterItem(id) {
408
+ // console.log("debug_2", "FM unregisterItem", { id });
409
+
368
410
  focusableTree.findAndRemoveNode(id);
369
411
 
370
412
  if (currentFocusNode && currentFocusNode.id === id) {
@@ -403,6 +445,8 @@ export const focusManager = (function () {
403
445
  }
404
446
 
405
447
  function setupInitialFocus(id: string, focusOnContent?: boolean) {
448
+ console.log("debug_2", "FM - setupInitialFocus", { id, focusOnContent });
449
+
406
450
  if (focusOnContent) {
407
451
  initialFocusOnContent = focusOnContent;
408
452
  } else {
@@ -410,6 +454,74 @@ export const focusManager = (function () {
410
454
  }
411
455
  }
412
456
 
457
+ async function setFocusOnMenu(currentRoute: string) {
458
+ console.log("debug_2", "FM - setFocusOnMenu", { currentRoute });
459
+
460
+ try {
461
+ await waitForActiveScreen(currentRoute, focusableTree);
462
+
463
+ const navbar = R.find(
464
+ (child) => R.startsWith("navbar", child.id),
465
+ getActiveScreen(focusableTree).children
466
+ );
467
+
468
+ const selectedMenuItemId = R.find(
469
+ (child) => child.component.props.selected,
470
+ navbar?.children || []
471
+ )?.id;
472
+
473
+ if (selectedMenuItemId) {
474
+ // set focus on selected menu item
475
+ setFocus(selectedMenuItemId);
476
+ return;
477
+ }
478
+
479
+ const firstMenuItemId = R.path(["children", 0, "id"], navbar);
480
+
481
+ if (firstMenuItemId) {
482
+ // set focus on first menu item
483
+ setFocus(firstMenuItemId);
484
+ }
485
+ } catch (error) {
486
+ // failed to put focus on top-menu => log it
487
+ }
488
+ }
489
+
490
+ async function setFocusOnContent(currentRoute: string) {
491
+ console.log("debug_2", "FM - setFocusOnContent", { currentRoute });
492
+
493
+ // we need to put focus on content here
494
+ // For doing it we have to wait registration of first focusable node into focusableTree
495
+ // this is the temporary hack with waiting until content node starts having children
496
+ try {
497
+ await waitForActiveScreen(currentRoute, focusableTree);
498
+ await waitForContent(focusableTree);
499
+
500
+ const content = getContentNode(focusableTree);
501
+
502
+ const { success } = setInitialFocus(content);
503
+
504
+ if (!success) {
505
+ // we've failed to put initial focus on content, re-run "setFocusOnContent" again
506
+ throw Error(
507
+ "we've failed to put initial focus on content, re-run 'setFocusOnContent' again"
508
+ );
509
+ }
510
+ } catch (e) {
511
+ setFocusOnContent(currentRoute);
512
+ }
513
+ }
514
+
515
+ function recoverFocus() {
516
+ const lastRoute = R.last(focusableTree.root.children);
517
+ const onSameScreen = isOnTheSameScreen(currentFocusNode, lastRoute);
518
+
519
+ if (!onSameScreen) {
520
+ // currentFocusedNode is not from the last route. We need to run recover focus.
521
+ setInitialFocus();
522
+ }
523
+ }
524
+
413
525
  /**
414
526
  * moves the focus in a provided direction
415
527
  * @public
@@ -422,6 +534,13 @@ export const focusManager = (function () {
422
534
  currentFocusNode.component.willLoseFocus(currentFocusNode, direction);
423
535
  const nextFocusable = findFocusableNode(direction, currentFocusNode);
424
536
 
537
+ // console.log("debug_2", "FM moveFocus", {
538
+ // direction: direction?.value,
539
+ // currentFocusNode,
540
+ // focusableTree,
541
+ // nextFocusable,
542
+ // });
543
+
425
544
  if (nextFocusable?.component?.isMounted?.()) {
426
545
  const nextRect = nextFocusable.component.getRect();
427
546
 
@@ -518,5 +637,8 @@ export const focusManager = (function () {
518
637
  enableAutomaticFocus,
519
638
  getGroupRootById,
520
639
  isGroupItemFocused,
640
+ setFocusOnMenu,
641
+ setFocusOnContent,
642
+ recoverFocus,
521
643
  };
522
644
  })();
@@ -1,4 +1,4 @@
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
3
 
4
4
  import { rejectItemByPath } from "../../../../objectUtils";
@@ -143,7 +143,49 @@ const hasChildren = (target) =>
143
143
 
144
144
  const hasComponent = (target) => !!target?.component;
145
145
 
146
- const hasComponentAndChildren = R.allPass([hasComponent, hasChildren]);
146
+ const hasComponentAndChildren = allPass([hasComponent, hasChildren]);
147
+
148
+ const positiveOrZero = (value) => value >= 0;
149
+
150
+ const excludeFromFocusSearching = pathOr(false, [
151
+ "component",
152
+ "props",
153
+ "excludeFromFocusSearching",
154
+ ]);
155
+
156
+ // const isAllScreens = all(excludeFromFocusSearching);
157
+
158
+ const isNavBar = (target) => startsWith("navbar", target?.id);
159
+
160
+ const ignoreMenuIfMultipleTargets = (nodes) => {
161
+ if (nodes.length > 1 && nodes.map((n) => n?.target?.id).includes("navbar")) {
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,43 +215,32 @@ 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
- }
218
+ .filter((target) => !excludeFromFocusSearching(target))
219
+ .map((target) => ({
220
+ distance: toDistance({ target, direction, comparisonRect }),
221
+ target,
222
+ }))
223
+ .filter(({ distance }) => positiveOrZero(distance));
189
224
 
190
- return {
191
- distance:
192
- measureDistance(direction, comparisonRect, componentRect) +
193
- measureEuclidianDistanceToCenter(comparisonRect, componentRect) /
194
- 10000,
195
- target,
196
- };
197
- })
198
- .filter((obj) => obj && obj.distance >= 0);
225
+ // if (isAllScreens(targets)) {
226
+ // return last(targets);
227
+ // }
199
228
 
200
229
  const match = distances
201
230
  .sort((a, b) => a.distance - b.distance) // sorts the distances
202
231
  .filter(ignoreMenuIfMultipleTargets)[0]; // filters out multiple targets if necessary // gets the first element
203
232
 
204
- return distances && distances.length > 0 ? match.target : null;
205
- }
206
-
207
- function ignoreMenuIfMultipleTargets(nodes) {
208
- if (nodes.length > 1 && nodes.map((n) => n?.target?.id).includes("navbar")) {
209
- return nodes.filter((n) => n?.target?.id !== "navbar");
210
- }
211
-
212
- return nodes;
233
+ // console.log("debug_2", "FM getTargetInDirection", {
234
+ // direction: direction?.value,
235
+ // currentNode,
236
+ // targetNodes,
237
+ // targets,
238
+ // comparisonRect,
239
+ // distances,
240
+ // match,
241
+ // });
242
+
243
+ return match?.target;
213
244
  }
214
245
 
215
246
  /**
@@ -300,3 +331,22 @@ export function hasPriorityInDirection(direction, component) {
300
331
 
301
332
  return (prioritiseFocusOn & mappedDirection) > 0;
302
333
  }
334
+
335
+ export const getNodeBeforeRoot = (node) => {
336
+ if (isNil(node)) {
337
+ return undefined;
338
+ }
339
+
340
+ if (node.parent.id === "root") {
341
+ return node;
342
+ }
343
+
344
+ return getNodeBeforeRoot(node.parent);
345
+ };
346
+
347
+ export const isOnTheSameScreen = (currentNode, nextNode) => {
348
+ const routeForCurrentNode = getNodeBeforeRoot(currentNode);
349
+ const routeForNextNode = getNodeBeforeRoot(nextNode);
350
+
351
+ return routeForCurrentNode.id === routeForNextNode.id;
352
+ };