@applicaster/zapp-react-native-utils 12.0.0-rc.7 → 12.0.0-rc.71

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 (98) hide show
  1. package/actionsExecutor/ActionExecutorContext.tsx +16 -8
  2. package/analyticsUtils/AnalyticsEvents/index.ts +6 -0
  3. package/analyticsUtils/AnalyticsEvents/searchEvents.ts +21 -0
  4. package/analyticsUtils/__tests__/analyticsManager.test.js +75 -73
  5. package/analyticsUtils/analyticsPlugins.ts +25 -0
  6. package/analyticsUtils/events.ts +6 -0
  7. package/analyticsUtils/helpers/hooks.ts +1 -1
  8. package/analyticsUtils/index.tsx +6 -0
  9. package/appUtils/HooksManager/index.js +3 -1
  10. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +2 -3
  11. package/appUtils/focusManager/__tests__/focusManager.test.js +4 -24
  12. package/appUtils/focusManager/__tests__/index.test.ts +2 -3
  13. package/appUtils/focusManager/index.ts +76 -56
  14. package/appUtils/focusManager/treeDataStructure/Utils/index.js +74 -34
  15. package/appUtils/focusManager/utils/index.ts +100 -0
  16. package/appUtils/focusManagerAux/index.ts +76 -0
  17. package/appUtils/focusManagerAux/utils/index.ts +100 -0
  18. package/appUtils/keyCodes/__tests__/__snapshots__/keyCodes.test.js.snap +1 -1
  19. package/appUtils/keyCodes/__tests__/keyCodes.test.js +80 -85
  20. package/appUtils/keyCodes/keys.web.js +1 -0
  21. package/appUtils/orientationHelper.ts +1 -1
  22. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +53 -0
  23. package/appUtils/playerManager/conts.ts +5 -0
  24. package/appUtils/playerManager/player.ts +47 -4
  25. package/appUtils/playerManager/playerFactory.ts +15 -1
  26. package/appUtils/playerManager/playerNative.ts +36 -1
  27. package/appUtils/playerManager/usePlayerControllerSetup.tsx +6 -1
  28. package/appUtils/playerManager/usePlayerState.tsx +13 -1
  29. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +50 -0
  30. package/arrayUtils/__tests__/isFilledArray.test.ts +63 -0
  31. package/arrayUtils/index.ts +5 -0
  32. package/cellUtils/__tests__/cellUtils.test.ts +48 -0
  33. package/cellUtils/__tests__/fixtures.ts +13 -0
  34. package/cellUtils/index.ts +40 -2
  35. package/dateUtils/index.ts +11 -0
  36. package/focusManager/index.ts +1 -1
  37. package/functionUtils/__tests__/__snapshots__/functionUtils.test.js.snap +2 -2
  38. package/functionUtils/__tests__/functionUtils.test.js +244 -248
  39. package/headersUtils/index.ts +1 -0
  40. package/inflatedUrlUtils/index.ts +4 -4
  41. package/localizationUtils/__tests__/localizationUtils.test.ts +78 -55
  42. package/localizationUtils/index.ts +13 -19
  43. package/logger/index.ts +1 -1
  44. package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
  45. package/manifestUtils/defaultManifestConfigurations/player.js +10 -1
  46. package/manifestUtils/index.js +5 -1
  47. package/manifestUtils/secondaryImage/index.js +18 -15
  48. package/modalState/index.ts +57 -0
  49. package/navigationUtils/index.ts +56 -7
  50. package/numberUtils/__tests__/isLikeNumber.test.ts +18 -0
  51. package/numberUtils/index.ts +9 -18
  52. package/package.json +3 -2
  53. package/playerUtils/configurationGenerator.ts +11 -1
  54. package/pluginUtils/index.ts +11 -7
  55. package/reactHooks/autoscrolling/__tests__/useTrackCurrentAutoScrollingElement.test.ts +2 -0
  56. package/reactHooks/autoscrolling/__tests__/{useTrackedView.test.ts → useTrackedView.test.tsx} +18 -1
  57. package/reactHooks/cell-click/__tests__/index.test.js +16 -3
  58. package/reactHooks/cell-click/index.ts +14 -14
  59. package/reactHooks/device/index.ts +0 -1
  60. package/reactHooks/device/useIsTablet.tsx +28 -3
  61. package/reactHooks/device/useMemoizedIsTablet.ts +1 -1
  62. package/reactHooks/device/useOrientation.tsx +3 -1
  63. package/reactHooks/feed/__tests__/useBuildPipesUrl.test.tsx +30 -70
  64. package/reactHooks/feed/__tests__/useInflatedUrl.test.ts +18 -3
  65. package/reactHooks/feed/useBuildPipesUrl.ts +7 -10
  66. package/reactHooks/feed/useInflatedUrl.ts +44 -15
  67. package/reactHooks/flatList/useLoadNextPageIfNeeded.ts +16 -9
  68. package/reactHooks/index.ts +2 -0
  69. package/reactHooks/layout/__tests__/{index.test.js → index.test.tsx} +66 -59
  70. package/reactHooks/layout/getDeviceInfo/index.ts +4 -2
  71. package/reactHooks/layout/index.ts +2 -1
  72. package/reactHooks/layout/types/index.ts +2 -0
  73. package/reactHooks/layout/useDimensions/helpers/__tests__/getInitialDimensions.native.test.js +11 -16
  74. package/reactHooks/layout/useDimensions/helpers/__tests__/getInitialDimensions.test.js +12 -14
  75. package/reactHooks/layout/useDimensions/useDimensions.ts +13 -7
  76. package/reactHooks/navigation/__mocks__/index.ts +15 -0
  77. package/reactHooks/navigation/__mocks__/useNavigation.ts +7 -0
  78. package/reactHooks/navigation/__tests__/index.test.tsx +240 -81
  79. package/reactHooks/navigation/index.ts +234 -81
  80. package/reactHooks/resolvers/__tests__/useCellResolver.test.tsx +3 -1
  81. package/reactHooks/screen/__mocks__/useScreenContext.ts +3 -1
  82. package/reactHooks/screen/__tests__/useCurrentScreenData.test.tsx +21 -23
  83. package/reactHooks/screen/index.ts +4 -0
  84. package/reactHooks/screen/useScreenBackgroundColor.ts +23 -0
  85. package/reactHooks/screen/useScreenContext.ts +4 -4
  86. package/reactHooks/screen/useScreenData.ts +1 -1
  87. package/reactHooks/screen/useTargetScreenData.ts +7 -0
  88. package/reactHooks/state/useRivers.ts +3 -1
  89. package/reactHooks/useListenEventBusEvent.ts +1 -4
  90. package/reactHooks/videoModal/hooks/useVideoModalScreenData.tsx +10 -9
  91. package/riverComponetsMeasurementProvider/index.tsx +2 -2
  92. package/stringUtils/index.ts +5 -0
  93. package/testUtils/getByTestId.ts +14 -0
  94. package/testUtils/index.tsx +38 -23
  95. package/types.ts +4 -0
  96. package/reactHooks/device/useIsTablet.android.tsx +0 -14
  97. package/reactHooks/device/useIsTablet.ios.tsx +0 -7
  98. package/reactHooks/device/useIsTablet.web.tsx +0 -3
@@ -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 =
@@ -7,6 +7,10 @@ import { sendHeaderClickEvent } from "./sendHeaderClickEvent";
7
7
  import { sendHardwareBackButtonClickEvent } from "./sendHardwareBackButtonClickEvent";
8
8
  import { sendDownloadsEvent } from "./sendDownloadsEvent";
9
9
  import { sendScreenEvent } from "./sendScreenEvent";
10
+ import {
11
+ sendSearchCanceledEvent,
12
+ sendSearchExecutedEvent,
13
+ } from "./searchEvents";
10
14
 
11
15
  export {
12
16
  sendScreenEvent,
@@ -18,4 +22,6 @@ export {
18
22
  sendHeaderClickEvent,
19
23
  sendHardwareBackButtonClickEvent,
20
24
  sendDownloadsEvent,
25
+ sendSearchCanceledEvent,
26
+ sendSearchExecutedEvent,
21
27
  };
@@ -0,0 +1,21 @@
1
+ import { log_debug } from "../logger";
2
+ import { postAnalyticEvent } from "../manager";
3
+ import { ANALYTICS_CORE_EVENTS } from "../events";
4
+
5
+ export const sendSearchCanceledEvent = (analyticsProps) => {
6
+ const eventName = ANALYTICS_CORE_EVENTS.SEARCH_CANCELED;
7
+ postAnalyticEvent(eventName, analyticsProps);
8
+
9
+ log_debug(`sendSearchCanceledEvent: send event: ${eventName}`, {
10
+ analyticsProps,
11
+ });
12
+ };
13
+
14
+ export const sendSearchExecutedEvent = (analyticsProps) => {
15
+ const eventName = ANALYTICS_CORE_EVENTS.SEARCH_EXECUTED;
16
+ postAnalyticEvent(eventName, analyticsProps);
17
+
18
+ log_debug(`sendSearchExecutedEvent: send event: ${eventName}`, {
19
+ analyticsProps,
20
+ });
21
+ };
@@ -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
  });
@@ -0,0 +1,25 @@
1
+ import * as R from "ramda";
2
+ import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
3
+
4
+ export const getVideojsAnalyticPluginsConfig = async (entry: ZappEntry) => {
5
+ let videojsConfiguration = {};
6
+
7
+ const plugins = appStore.get("plugins");
8
+ const analyticPlugins = R.filter(R.propEq("type", "analytics"), plugins);
9
+
10
+ const configurations = await Promise.all(
11
+ analyticPlugins.map(async (plugin) => {
12
+ const { configuration, module } = plugin;
13
+
14
+ return module?.getPlayerConfiguration?.(configuration, entry);
15
+ })
16
+ );
17
+
18
+ configurations.forEach((analyticConfig) => {
19
+ if (typeof analyticConfig === "object" && analyticConfig !== null) {
20
+ videojsConfiguration = { ...videojsConfiguration, ...analyticConfig };
21
+ }
22
+ });
23
+
24
+ return videojsConfiguration;
25
+ };
@@ -23,10 +23,16 @@ export const TAPPING_EVENTS = {
23
23
  TAP_NAVBAR_BACK_BUTTON: "tap_navbar_back_button",
24
24
  };
25
25
 
26
+ export const SEARCH_EVENTS = {
27
+ SEARCH_EXECUTED: "search_executed",
28
+ SEARCH_CANCELED: "search_canceled",
29
+ };
30
+
26
31
  export const ANALYTICS_CORE_EVENTS = {
27
32
  ...LEGACY_EVENTS,
28
33
  ...SCREEN_VIEW_EVENTS,
29
34
  ...TAPPING_EVENTS,
35
+ ...SEARCH_EVENTS,
30
36
  };
31
37
 
32
38
  export const WEBVIEW_EVENTS = {
@@ -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) => {
@@ -18,6 +18,8 @@ import {
18
18
  sendHardwareBackButtonClickEvent,
19
19
  sendDownloadsEvent,
20
20
  sendScreenEvent,
21
+ sendSearchCanceledEvent,
22
+ sendSearchExecutedEvent,
21
23
  } from "./AnalyticsEvents";
22
24
 
23
25
  import { postAnalyticEvent } from "./manager";
@@ -66,6 +68,8 @@ type AnalyticsFunctions = {
66
68
  item: ZappEntry;
67
69
  state: OfflineItemState;
68
70
  }) => void;
71
+ sendSearchCanceledEvent: (props: AnalyticsDefaultProperties) => void;
72
+ sendSearchExecutedEvent: (props: AnalyticsDefaultProperties) => void;
69
73
  };
70
74
 
71
75
  type GetAnalyticsFunctions = (
@@ -102,6 +106,8 @@ export function getAnalyticsFunctions({
102
106
  sendHardwareBackButtonClickEvent,
103
107
  sendScreenEvent,
104
108
  sendDownloadsEvent,
109
+ sendSearchCanceledEvent,
110
+ sendSearchExecutedEvent,
105
111
  };
106
112
  }
107
113
 
@@ -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
@@ -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
  })();