@applicaster/zapp-react-native-utils 12.0.0-alpha.4300857220 → 12.0.0-alpha.4355141911
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.
- package/analyticsUtils/AnalyticsEvents/index.ts +6 -0
- package/analyticsUtils/AnalyticsEvents/searchEvents.ts +21 -0
- package/analyticsUtils/events.ts +6 -0
- package/analyticsUtils/index.tsx +6 -0
- package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +3 -3
- package/appUtils/focusManager/__tests__/focusManager.test.js +4 -24
- package/appUtils/focusManager/__tests__/index.test.ts +3 -3
- package/appUtils/focusManager/index.ts +141 -56
- package/appUtils/focusManager/treeDataStructure/Utils/index.js +74 -43
- package/appUtils/focusManager/utils/index.ts +100 -0
- package/appUtils/playerManager/usePlayerControllerSetup.tsx +6 -1
- package/arrayUtils/__tests__/isFilledArray.test.ts +63 -0
- package/arrayUtils/index.ts +5 -0
- package/cellUtils/__tests__/cellUtils.test.ts +9 -20
- package/cellUtils/index.ts +6 -12
- package/dateUtils/index.ts +11 -0
- package/inflatedUrlUtils/index.ts +4 -4
- package/localizationUtils/__tests__/localizationUtils.test.ts +72 -53
- package/localizationUtils/index.ts +13 -22
- package/numberUtils/__tests__/isLikeNumber.test.ts +18 -0
- package/numberUtils/index.ts +9 -18
- package/package.json +2 -2
- package/reactHooks/cell-click/__tests__/index.test.js +10 -1
- package/reactHooks/cell-click/index.ts +12 -12
- package/reactHooks/feed/__tests__/useBuildPipesUrl.test.tsx +30 -70
- package/reactHooks/feed/__tests__/useInflatedUrl.test.ts +18 -3
- package/reactHooks/feed/useBuildPipesUrl.ts +7 -10
- package/reactHooks/feed/useInflatedUrl.ts +44 -15
- package/reactHooks/navigation/__tests__/index.test.tsx +11 -8
- package/reactHooks/navigation/index.ts +49 -66
- package/reactHooks/screen/__tests__/useCurrentScreenData.test.tsx +21 -23
- package/reactHooks/screen/index.ts +2 -0
- package/reactHooks/screen/useScreenBackgroundColor.ts +23 -0
- package/reactHooks/screen/useScreenContext.ts +2 -2
- package/types.ts +4 -0
|
@@ -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
|
+
};
|
package/analyticsUtils/events.ts
CHANGED
|
@@ -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 = {
|
package/analyticsUtils/index.tsx
CHANGED
|
@@ -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
|
|
|
@@ -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,
|
|
@@ -30,11 +28,13 @@ Object {
|
|
|
30
28
|
"on": [Function],
|
|
31
29
|
"press": [Function],
|
|
32
30
|
"pressOut": [Function],
|
|
31
|
+
"recoverFocus": [Function],
|
|
33
32
|
"register": [Function],
|
|
34
33
|
"removeHandler": [Function],
|
|
35
34
|
"setFocus": [Function],
|
|
35
|
+
"setFocusOnContent": [Function],
|
|
36
|
+
"setFocusOnMenu": [Function],
|
|
36
37
|
"setInitialFocus": [Function],
|
|
37
|
-
"setupInitialFocus": [Function],
|
|
38
38
|
"unregister": [Function],
|
|
39
39
|
}
|
|
40
40
|
`;
|
|
@@ -14,10 +14,8 @@ describe("focusManager", () => {
|
|
|
14
14
|
expect(focusManager).toMatchSnapshot();
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
it("
|
|
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,11 @@ describe("focusManager", () => {
|
|
|
27
26
|
"isFocusDisabled",
|
|
28
27
|
"getCurrentFocusNode",
|
|
29
28
|
"focusableTree",
|
|
30
|
-
"disableAutomaticFocus",
|
|
31
|
-
"enableAutomaticFocus",
|
|
32
29
|
"getGroupRootById",
|
|
33
30
|
"isGroupItemFocused",
|
|
31
|
+
"setFocusOnMenu",
|
|
32
|
+
"setFocusOnContent",
|
|
33
|
+
"recoverFocus",
|
|
34
34
|
];
|
|
35
35
|
|
|
36
36
|
expectedMethods.forEach((method) => {
|
|
@@ -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 {
|
|
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";
|
|
16
|
+
import {
|
|
17
|
+
waitForActiveScreen,
|
|
18
|
+
waitForContent,
|
|
19
|
+
getContentNode,
|
|
20
|
+
getNavbarNode,
|
|
21
|
+
} from "./utils";
|
|
7
22
|
|
|
8
23
|
const logger = coreLogger.addSubsystem("focusManager");
|
|
9
24
|
|
|
@@ -44,21 +59,9 @@ export const focusManager = (function () {
|
|
|
44
59
|
// focusable node which currently holds the focus
|
|
45
60
|
let currentFocusNode = null;
|
|
46
61
|
let lastFocusedId = null;
|
|
47
|
-
let initialFocusOnContent = null;
|
|
48
62
|
let focusDisabled = false;
|
|
49
|
-
let focusAutoEnabled = true;
|
|
50
63
|
const eventHandler = subscriber();
|
|
51
64
|
|
|
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
65
|
function enableFocus() {
|
|
63
66
|
focusDisabled = false;
|
|
64
67
|
}
|
|
@@ -93,7 +96,7 @@ export const focusManager = (function () {
|
|
|
93
96
|
!currentFocusable.isGroup &&
|
|
94
97
|
currentFocusable.isMounted()
|
|
95
98
|
) {
|
|
96
|
-
currentFocusable.blur(
|
|
99
|
+
currentFocusable.blur(direction);
|
|
97
100
|
currentFocusable.hasLostFocus(getCurrentFocus(), direction);
|
|
98
101
|
}
|
|
99
102
|
}
|
|
@@ -226,32 +229,52 @@ export const focusManager = (function () {
|
|
|
226
229
|
|
|
227
230
|
logger.log({ message: "setInitialFocus", data: { preferredFocus } });
|
|
228
231
|
|
|
229
|
-
if (preferredFocus
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
focusableItem &&
|
|
235
|
-
!focusableItem.isGroup &&
|
|
236
|
-
isFocusEnabled(focusableItem)
|
|
237
|
-
) {
|
|
238
|
-
if (initialFocusOnContent) {
|
|
239
|
-
blur();
|
|
240
|
-
initialFocusOnContent = false;
|
|
241
|
-
}
|
|
232
|
+
if (isNilOrEmpty(preferredFocus)) {
|
|
233
|
+
return { success: false };
|
|
234
|
+
}
|
|
242
235
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
data: {
|
|
246
|
-
focusableItem,
|
|
247
|
-
},
|
|
248
|
-
});
|
|
236
|
+
const focusCandidate = preferredFocus[0];
|
|
237
|
+
const focusableItem = focusCandidate.component;
|
|
249
238
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
239
|
+
const currentFocusedId = R.path(["props", "id"], getCurrentFocus());
|
|
240
|
+
const nextFocusedId = R.path(["props", "id"], focusableItem);
|
|
241
|
+
|
|
242
|
+
if (
|
|
243
|
+
isNotNil(currentFocusedId) &&
|
|
244
|
+
isNotNil(nextFocusedId) &&
|
|
245
|
+
currentFocusedId === nextFocusedId
|
|
246
|
+
) {
|
|
247
|
+
// we are going to move focus to the same node(already focused) -> nothing to do
|
|
248
|
+
return { success: true };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (focusableItem?.isGroup) {
|
|
252
|
+
// focusCandidate is a FocusableGroup, we are not able to land focus on group(only on Focusable)
|
|
253
|
+
return { success: false };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (
|
|
257
|
+
focusableItem &&
|
|
258
|
+
!focusableItem.isGroup &&
|
|
259
|
+
isFocusEnabled(focusableItem)
|
|
260
|
+
) {
|
|
261
|
+
blur();
|
|
262
|
+
|
|
263
|
+
logger.log({
|
|
264
|
+
message: "setInitialFocus – currentFocusNode",
|
|
265
|
+
data: {
|
|
266
|
+
focusableItem,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
currentFocusNode = focusCandidate;
|
|
271
|
+
focusableItem.willReceiveFocus(focusableItem, null);
|
|
272
|
+
focusableItem && setFocus(focusCandidate.id, null);
|
|
273
|
+
|
|
274
|
+
return { success: true };
|
|
254
275
|
}
|
|
276
|
+
|
|
277
|
+
return { success: false };
|
|
255
278
|
}
|
|
256
279
|
|
|
257
280
|
function treeLoaded(val, node) {
|
|
@@ -261,17 +284,8 @@ export const focusManager = (function () {
|
|
|
261
284
|
val,
|
|
262
285
|
node,
|
|
263
286
|
currentFocusNode,
|
|
264
|
-
initialFocusOnContent,
|
|
265
287
|
},
|
|
266
288
|
});
|
|
267
|
-
|
|
268
|
-
if ((val && !currentFocusNode) || initialFocusOnContent) {
|
|
269
|
-
logger.log({ message: `focusAutoEnabled ${focusAutoEnabled}` });
|
|
270
|
-
|
|
271
|
-
if (focusAutoEnabled) {
|
|
272
|
-
setInitialFocus(initialFocusOnContent ? node : undefined);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
289
|
}
|
|
276
290
|
|
|
277
291
|
/**
|
|
@@ -402,11 +416,79 @@ export const focusManager = (function () {
|
|
|
402
416
|
group ? unregisterGroup(id) : unregisterItem(id);
|
|
403
417
|
}
|
|
404
418
|
|
|
405
|
-
function
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
419
|
+
async function setFocusOnMenu(currentRoute: string) {
|
|
420
|
+
try {
|
|
421
|
+
await waitForActiveScreen(currentRoute, focusableTree);
|
|
422
|
+
|
|
423
|
+
const navbar = getNavbarNode(focusableTree);
|
|
424
|
+
|
|
425
|
+
const selectedMenuItemId = R.find(
|
|
426
|
+
(child) => child.component.props.selected,
|
|
427
|
+
navbar.children
|
|
428
|
+
)?.id;
|
|
429
|
+
|
|
430
|
+
if (selectedMenuItemId) {
|
|
431
|
+
// set focus on selected menu item
|
|
432
|
+
setFocus(selectedMenuItemId);
|
|
433
|
+
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const firstMenuItemId = R.path(["children", 0, "id"], navbar);
|
|
438
|
+
|
|
439
|
+
if (firstMenuItemId) {
|
|
440
|
+
// set focus on first menu item
|
|
441
|
+
setFocus(firstMenuItemId);
|
|
442
|
+
}
|
|
443
|
+
} catch (error) {
|
|
444
|
+
logger.log({
|
|
445
|
+
message: "setFocusOnMenu – failed to put focus on top-menu",
|
|
446
|
+
data: {
|
|
447
|
+
currentRoute,
|
|
448
|
+
error,
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function setFocusOnContent(currentRoute: string) {
|
|
455
|
+
// we need to put focus on content here
|
|
456
|
+
// For doing it we have to wait registration of first focusable node into focusableTree
|
|
457
|
+
// we wait until content node starts having children
|
|
458
|
+
try {
|
|
459
|
+
await waitForActiveScreen(currentRoute, focusableTree);
|
|
460
|
+
await waitForContent(focusableTree);
|
|
461
|
+
|
|
462
|
+
const content = getContentNode(focusableTree);
|
|
463
|
+
|
|
464
|
+
const { success } = setInitialFocus(content);
|
|
465
|
+
|
|
466
|
+
if (!success) {
|
|
467
|
+
// we've failed to put initial focus on content, re-run "setFocusOnContent" again as a fallback
|
|
468
|
+
throw Error(
|
|
469
|
+
"we've failed to put initial focus on content, re-run 'setFocusOnContent' again as a fallback"
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
} catch (error) {
|
|
473
|
+
setFocusOnContent(currentRoute);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function isCurrentFocusOnTheTopScreen() {
|
|
478
|
+
const routes = R.pathOr([], ["root", "children"], focusableTree);
|
|
479
|
+
|
|
480
|
+
if (routes.length <= 1) {
|
|
481
|
+
// there is only one or zero routes
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return haveSameParentBeforeRoot(currentFocusNode, R.last(routes));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function recoverFocus() {
|
|
489
|
+
if (!isCurrentFocusOnTheTopScreen()) {
|
|
490
|
+
// We've failed to set focused node on the new screen => run focus recovery
|
|
491
|
+
setInitialFocus();
|
|
410
492
|
}
|
|
411
493
|
}
|
|
412
494
|
|
|
@@ -416,6 +498,11 @@ export const focusManager = (function () {
|
|
|
416
498
|
* @param {Object} direction of the scroll
|
|
417
499
|
*/
|
|
418
500
|
function moveFocus(direction) {
|
|
501
|
+
if (!isCurrentFocusOnTheTopScreen()) {
|
|
502
|
+
// We are opening new screen. We need to ignore movement of focus.
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
|
|
419
506
|
if (focusDisabled) return false;
|
|
420
507
|
|
|
421
508
|
if (currentFocusNode) {
|
|
@@ -444,8 +531,6 @@ export const focusManager = (function () {
|
|
|
444
531
|
} else {
|
|
445
532
|
currentFocusNode.component.failedLostFocus(currentFocusNode, direction);
|
|
446
533
|
}
|
|
447
|
-
} else {
|
|
448
|
-
setInitialFocus();
|
|
449
534
|
}
|
|
450
535
|
}
|
|
451
536
|
|
|
@@ -505,7 +590,6 @@ export const focusManager = (function () {
|
|
|
505
590
|
on,
|
|
506
591
|
invokeHandler,
|
|
507
592
|
removeHandler,
|
|
508
|
-
setupInitialFocus,
|
|
509
593
|
longPress,
|
|
510
594
|
pressOut,
|
|
511
595
|
enableFocus,
|
|
@@ -514,9 +598,10 @@ export const focusManager = (function () {
|
|
|
514
598
|
isFocusDisabled,
|
|
515
599
|
getCurrentFocusNode,
|
|
516
600
|
focusableTree,
|
|
517
|
-
disableAutomaticFocus,
|
|
518
|
-
enableAutomaticFocus,
|
|
519
601
|
getGroupRootById,
|
|
520
602
|
isGroupItemFocused,
|
|
603
|
+
setFocusOnMenu,
|
|
604
|
+
setFocusOnContent,
|
|
605
|
+
recoverFocus,
|
|
521
606
|
};
|
|
522
607
|
})();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import
|
|
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 =
|
|
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
|
-
.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
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
|
+
};
|