@applicaster/zapp-react-native-utils 12.0.0-alpha.4840257570 → 12.0.0-alpha.4885205607
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 +7 -0
- package/analyticsUtils/AnalyticsEvents/sendShareEvent.ts +21 -0
- package/analyticsUtils/analyticsPlugins.ts +25 -0
- package/analyticsUtils/events.ts +6 -0
- package/analyticsUtils/index.tsx +6 -0
- package/appUtils/accessibilityManager/const.ts +50 -0
- package/appUtils/accessibilityManager/hooks.ts +40 -0
- package/appUtils/accessibilityManager/index.ts +155 -0
- package/appUtils/accessibilityManager/logger.ts +5 -0
- package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +2 -3
- package/appUtils/focusManager/__tests__/focusManager.test.js +4 -24
- package/appUtils/focusManager/__tests__/index.test.ts +2 -3
- package/appUtils/focusManager/index.ts +76 -57
- package/appUtils/focusManager/treeDataStructure/Utils/index.js +75 -44
- package/appUtils/focusManager/utils/index.ts +100 -0
- package/appUtils/focusManagerAux/index.ts +76 -0
- package/appUtils/focusManagerAux/utils/index.ts +100 -0
- package/appUtils/index.js +2 -0
- package/appUtils/keyCodes/keys.web.js +1 -1
- package/appUtils/platform/const.ts +25 -0
- package/appUtils/platform/index.ts +3 -0
- package/appUtils/platform/platformUtils.ts +185 -0
- package/appUtils/playerManager/conts.ts +1 -0
- package/appUtils/playerManager/index.ts +11 -5
- package/appUtils/playerManager/nativePlayerManager.ts +12 -0
- package/appUtils/playerManager/player.ts +9 -3
- package/appUtils/playerManager/playerNative.ts +5 -14
- package/appUtils/playerManager/usePlayerControllerSetup.tsx +7 -1
- package/arrayUtils/__tests__/isFilledArray.test.ts +63 -0
- package/arrayUtils/index.ts +5 -0
- package/booleanUtils/index.ts +4 -4
- package/cellUtils/index.ts +5 -5
- package/index.d.ts +15 -0
- package/navigationUtils/index.ts +48 -0
- package/numberUtils/__tests__/isLikeNumber.test.ts +18 -0
- package/numberUtils/__tests__/toNumber.test.ts +2 -2
- package/numberUtils/index.ts +5 -18
- package/package.json +2 -2
- package/playerUtils/imaScriptDom.ts +59 -0
- package/pluginUtils/index.ts +9 -0
- 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__/useFeedRefresh.test.tsx +6 -2
- package/reactHooks/feed/useBuildPipesUrl.ts +7 -10
- package/reactHooks/feed/useFeedRefresh.tsx +9 -7
- package/reactHooks/navigation/__tests__/index.test.tsx +11 -8
- package/reactHooks/navigation/index.ts +60 -67
- 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/reactUtils/index.ts +104 -0
- package/appUtils/focusManager/FocusableEvents.ts +0 -8
- package/appUtils/focusManager/treeDataStructure/Utils/__tests__/Utils_android.test.js +0 -1094
- package/appUtils/focusManager/treeDataStructure/Utils/index.android.js +0 -261
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
sendSearchExecutedEvent,
|
|
13
13
|
} from "./searchEvents";
|
|
14
14
|
|
|
15
|
+
import {
|
|
16
|
+
sendShareInitiatedEvent,
|
|
17
|
+
sendShareSuccessEvent,
|
|
18
|
+
} from "./sendShareEvent";
|
|
19
|
+
|
|
15
20
|
export {
|
|
16
21
|
sendScreenEvent,
|
|
17
22
|
sendOnClickEvent,
|
|
@@ -24,4 +29,6 @@ export {
|
|
|
24
29
|
sendDownloadsEvent,
|
|
25
30
|
sendSearchCanceledEvent,
|
|
26
31
|
sendSearchExecutedEvent,
|
|
32
|
+
sendShareInitiatedEvent,
|
|
33
|
+
sendShareSuccessEvent,
|
|
27
34
|
};
|
|
@@ -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 sendShareInitiatedEvent = (analyticsProps) => {
|
|
6
|
+
const eventName = ANALYTICS_CORE_EVENTS.SHARE_INITIATED;
|
|
7
|
+
postAnalyticEvent(eventName, analyticsProps);
|
|
8
|
+
|
|
9
|
+
log_debug(`sendShareInitiatedEvent: send event: ${eventName}`, {
|
|
10
|
+
analyticsProps,
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const sendShareSuccessEvent = (analyticsProps) => {
|
|
15
|
+
const eventName = ANALYTICS_CORE_EVENTS.SHARE_SUCCESS;
|
|
16
|
+
postAnalyticEvent(eventName, analyticsProps);
|
|
17
|
+
|
|
18
|
+
log_debug(`sendShareSuccessEvent: send event: ${eventName}`, {
|
|
19
|
+
analyticsProps,
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -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
|
+
};
|
package/analyticsUtils/events.ts
CHANGED
|
@@ -28,11 +28,17 @@ export const SEARCH_EVENTS = {
|
|
|
28
28
|
SEARCH_CANCELED: "search_canceled",
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
export const SHARE_EVENTS = {
|
|
32
|
+
SHARE_INITIATED: "share_initiated",
|
|
33
|
+
SHARE_SUCCESS: "share_success",
|
|
34
|
+
};
|
|
35
|
+
|
|
31
36
|
export const ANALYTICS_CORE_EVENTS = {
|
|
32
37
|
...LEGACY_EVENTS,
|
|
33
38
|
...SCREEN_VIEW_EVENTS,
|
|
34
39
|
...TAPPING_EVENTS,
|
|
35
40
|
...SEARCH_EVENTS,
|
|
41
|
+
...SHARE_EVENTS,
|
|
36
42
|
};
|
|
37
43
|
|
|
38
44
|
export const WEBVIEW_EVENTS = {
|
package/analyticsUtils/index.tsx
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
sendScreenEvent,
|
|
21
21
|
sendSearchCanceledEvent,
|
|
22
22
|
sendSearchExecutedEvent,
|
|
23
|
+
sendShareInitiatedEvent,
|
|
24
|
+
sendShareSuccessEvent,
|
|
23
25
|
} from "./AnalyticsEvents";
|
|
24
26
|
|
|
25
27
|
import { postAnalyticEvent } from "./manager";
|
|
@@ -70,6 +72,8 @@ type AnalyticsFunctions = {
|
|
|
70
72
|
}) => void;
|
|
71
73
|
sendSearchCanceledEvent: (props: AnalyticsDefaultProperties) => void;
|
|
72
74
|
sendSearchExecutedEvent: (props: AnalyticsDefaultProperties) => void;
|
|
75
|
+
sendShareInitiatedEvent: (props: AnalyticsDefaultProperties) => void;
|
|
76
|
+
sendShareSuccessEvent: (props: AnalyticsDefaultProperties) => void;
|
|
73
77
|
};
|
|
74
78
|
|
|
75
79
|
type GetAnalyticsFunctions = (
|
|
@@ -108,6 +112,8 @@ export function getAnalyticsFunctions({
|
|
|
108
112
|
sendDownloadsEvent,
|
|
109
113
|
sendSearchCanceledEvent,
|
|
110
114
|
sendSearchExecutedEvent,
|
|
115
|
+
sendShareInitiatedEvent,
|
|
116
|
+
sendShareSuccessEvent,
|
|
111
117
|
};
|
|
112
118
|
}
|
|
113
119
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const BUTTON_ACCESSIBILITY_KEYS = {
|
|
2
|
+
play: {
|
|
3
|
+
label: "accessibility_play_label",
|
|
4
|
+
hint: "accessibility_play_hint",
|
|
5
|
+
mini: {
|
|
6
|
+
label: "accessibility_play_mini_label",
|
|
7
|
+
hint: "accessibility_play_mini_hint",
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
pause: {
|
|
11
|
+
label: "accessibility_pause_label",
|
|
12
|
+
hint: "accessibility_pause_hint",
|
|
13
|
+
mini: {
|
|
14
|
+
label: "accessibility_pause_mini_label",
|
|
15
|
+
hint: "accessibility_pause_mini_hint",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
rewind: {
|
|
19
|
+
label: "accessibility_rewind_label",
|
|
20
|
+
hint: "accessibility_rewind_hint",
|
|
21
|
+
},
|
|
22
|
+
forward: {
|
|
23
|
+
label: "accessibility_forward_label",
|
|
24
|
+
hint: "accessibility_forward_hint",
|
|
25
|
+
},
|
|
26
|
+
close: {
|
|
27
|
+
label: "accessibility_close_label",
|
|
28
|
+
hint: "accessibility_close_hint",
|
|
29
|
+
mini: {
|
|
30
|
+
label: "accessibility_close_mini_label",
|
|
31
|
+
hint: "accessibility_close_mini_hint",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
maximize: {
|
|
35
|
+
label: "accessibility_maximize_label",
|
|
36
|
+
hint: "accessibility_maximize_hint",
|
|
37
|
+
},
|
|
38
|
+
subtitles: {
|
|
39
|
+
label: "accessibility_subtitle_label",
|
|
40
|
+
hint: "accessibility_subtitle_hint",
|
|
41
|
+
},
|
|
42
|
+
back: {
|
|
43
|
+
label: "accessibility_back_label",
|
|
44
|
+
hint: "accessibility_back_hint",
|
|
45
|
+
},
|
|
46
|
+
fullscreen: {
|
|
47
|
+
label: "accessibility_fullscreen_label",
|
|
48
|
+
hint: "accessibility_fullscreen_hint",
|
|
49
|
+
},
|
|
50
|
+
} as const;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect, useMemo } from "react";
|
|
2
|
+
import { AccessibilityManager } from "./index";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This hook is used to instantiate the accessibility manager and update localizations used by the class.
|
|
6
|
+
* If the plugin config is not provided or it does not contain any localizations, the accessibility manager will not be updated.
|
|
7
|
+
* We use the plugin configuration because it offers a flattened structure where we can easily access the accessibility labels and hints
|
|
8
|
+
* i.e. pluginConfiguration = {
|
|
9
|
+
* accessibility_close_label: "Close",
|
|
10
|
+
* accessibility_close_hint: "Press here to close",
|
|
11
|
+
* }
|
|
12
|
+
*/
|
|
13
|
+
export const useAccessibilityManager = (
|
|
14
|
+
pluginConfiguration: Record<string, any> = {}
|
|
15
|
+
) => {
|
|
16
|
+
const accessibilityManager = useMemo(() => {
|
|
17
|
+
return AccessibilityManager.getInstance();
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (pluginConfiguration) {
|
|
22
|
+
accessibilityManager.updateLocalizations(pluginConfiguration);
|
|
23
|
+
}
|
|
24
|
+
}, [pluginConfiguration, accessibilityManager]);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const subscription = accessibilityManager.getStateAsObservable().subscribe({
|
|
28
|
+
next: () => {
|
|
29
|
+
// TODO: handle accessibility states
|
|
30
|
+
// screenReaderEnabled: false
|
|
31
|
+
// reduceMotionEnabled: false
|
|
32
|
+
// boldTextEnabled: false
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return () => subscription.unsubscribe();
|
|
37
|
+
}, [accessibilityManager]);
|
|
38
|
+
|
|
39
|
+
return accessibilityManager;
|
|
40
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { BehaviorSubject } from "rxjs";
|
|
2
|
+
import { accessibilityManagerLogger as logger } from "./logger";
|
|
3
|
+
import { TTSManager } from "../platform/platformUtils";
|
|
4
|
+
import { BUTTON_ACCESSIBILITY_KEYS } from "./const";
|
|
5
|
+
import { AccessibilityRole } from "react-native";
|
|
6
|
+
|
|
7
|
+
export class AccessibilityManager {
|
|
8
|
+
private static _instance: AccessibilityManager | null = null;
|
|
9
|
+
private state$ = new BehaviorSubject<AccessibilityState>({
|
|
10
|
+
screenReaderEnabled: false,
|
|
11
|
+
reduceMotionEnabled: false,
|
|
12
|
+
boldTextEnabled: false,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
private announcements$ = new BehaviorSubject<{
|
|
16
|
+
message: string;
|
|
17
|
+
localizedMessage?: string | undefined;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
} | null>(null);
|
|
20
|
+
|
|
21
|
+
private ttsManager = TTSManager.getInstance();
|
|
22
|
+
private localizations: { [key: string]: string } = {};
|
|
23
|
+
|
|
24
|
+
private constructor() {}
|
|
25
|
+
|
|
26
|
+
public static getInstance(): AccessibilityManager {
|
|
27
|
+
if (!AccessibilityManager._instance) {
|
|
28
|
+
AccessibilityManager._instance = new AccessibilityManager();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return AccessibilityManager._instance;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The method now accepts any object with localizations using a flattened structure
|
|
36
|
+
*
|
|
37
|
+
* i.e. { accessibility_close_label: "Close", accessibility_close_hint: "Press here to close" }
|
|
38
|
+
*
|
|
39
|
+
* No longer accepts:
|
|
40
|
+
*
|
|
41
|
+
* i.e. localizations: [{ en: { accessibility_close_label: "Close", accessibility_close_hint: "Press here to close" } }]
|
|
42
|
+
*/
|
|
43
|
+
public updateLocalizations(localizations: { [key: string]: string }) {
|
|
44
|
+
this.localizations = localizations;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public getState(): AccessibilityState {
|
|
48
|
+
return this.state$.getValue();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public getStateAsObservable() {
|
|
52
|
+
return this.state$.asObservable();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* text you want to be read, if you want to use localized text pass keyOfLocalizedText instead
|
|
57
|
+
* keyOfLocalizedText is the key to the localized text
|
|
58
|
+
*/
|
|
59
|
+
public readText({
|
|
60
|
+
text,
|
|
61
|
+
keyOfLocalizedText,
|
|
62
|
+
}: {
|
|
63
|
+
text: string;
|
|
64
|
+
keyOfLocalizedText?: string;
|
|
65
|
+
}) {
|
|
66
|
+
let textToRead = text;
|
|
67
|
+
|
|
68
|
+
if (keyOfLocalizedText) {
|
|
69
|
+
if (!this.localizations) {
|
|
70
|
+
logger.error(
|
|
71
|
+
"Attempting to use localized key without initialized localizations"
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const localizedMessage = this.getLocalizedMessage(keyOfLocalizedText);
|
|
78
|
+
|
|
79
|
+
if (!localizedMessage) {
|
|
80
|
+
logger.warn(`No localization found for key: ${keyOfLocalizedText}`);
|
|
81
|
+
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
textToRead = localizedMessage;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this.ttsManager) {
|
|
89
|
+
logger.debug({
|
|
90
|
+
message: `Reading text: ${textToRead}`,
|
|
91
|
+
data: {
|
|
92
|
+
textToRead,
|
|
93
|
+
text,
|
|
94
|
+
keyOfLocalizedText,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
this.ttsManager.readText(textToRead);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.announcements$.next({
|
|
102
|
+
message: textToRead,
|
|
103
|
+
localizedMessage: keyOfLocalizedText ? textToRead : undefined,
|
|
104
|
+
timestamp: Date.now(),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public getButtonAccessibilityProps(buttonName: string): AccessibilityProps {
|
|
109
|
+
const buttonConfig = BUTTON_ACCESSIBILITY_KEYS[buttonName];
|
|
110
|
+
|
|
111
|
+
if (!buttonConfig) {
|
|
112
|
+
return {
|
|
113
|
+
accessibilityLabel: buttonName,
|
|
114
|
+
accessibilityHint: `Press button to perform action on ${buttonName}`,
|
|
115
|
+
"aria-label": buttonName,
|
|
116
|
+
"aria-description": `Press button to perform action on ${buttonName}`,
|
|
117
|
+
accessibilityRole: "button" as AccessibilityRole,
|
|
118
|
+
"aria-role": "button",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const labelKey = buttonConfig.label;
|
|
123
|
+
const hintKey = buttonConfig.hint;
|
|
124
|
+
|
|
125
|
+
const label = this.getLocalizedMessage(labelKey) || buttonName;
|
|
126
|
+
|
|
127
|
+
const hint =
|
|
128
|
+
this.getLocalizedMessage(hintKey) ||
|
|
129
|
+
`Press button to perform action on ${buttonName}`;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
accessibilityLabel: label,
|
|
133
|
+
accessibilityHint: hint,
|
|
134
|
+
"aria-label": label,
|
|
135
|
+
"aria-description": hint,
|
|
136
|
+
accessibilityRole: "button" as AccessibilityRole,
|
|
137
|
+
"aria-role": "button",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public getLocalizedMessage(key: string): string | void {
|
|
142
|
+
if (!key) return;
|
|
143
|
+
|
|
144
|
+
if (!this.localizations) {
|
|
145
|
+
logger.error(
|
|
146
|
+
"Attempting to use localized key without initialized localizations",
|
|
147
|
+
{ key }
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return this.localizations[key];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -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("
|
|
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,10 +1,18 @@
|
|
|
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";
|
|
7
|
-
|
|
8
16
|
const logger = coreLogger.addSubsystem("focusManager");
|
|
9
17
|
|
|
10
18
|
const isFocusEnabled = (focusableItem): boolean => {
|
|
@@ -44,21 +52,9 @@ export const focusManager = (function () {
|
|
|
44
52
|
// focusable node which currently holds the focus
|
|
45
53
|
let currentFocusNode = null;
|
|
46
54
|
let lastFocusedId = null;
|
|
47
|
-
let initialFocusOnContent = null;
|
|
48
55
|
let focusDisabled = false;
|
|
49
|
-
let focusAutoEnabled = true;
|
|
50
56
|
const eventHandler = subscriber();
|
|
51
57
|
|
|
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
58
|
function enableFocus() {
|
|
63
59
|
focusDisabled = false;
|
|
64
60
|
}
|
|
@@ -93,7 +89,7 @@ export const focusManager = (function () {
|
|
|
93
89
|
!currentFocusable.isGroup &&
|
|
94
90
|
currentFocusable.isMounted()
|
|
95
91
|
) {
|
|
96
|
-
currentFocusable.blur(
|
|
92
|
+
currentFocusable.blur(direction);
|
|
97
93
|
currentFocusable.hasLostFocus(getCurrentFocus(), direction);
|
|
98
94
|
}
|
|
99
95
|
}
|
|
@@ -226,32 +222,52 @@ export const focusManager = (function () {
|
|
|
226
222
|
|
|
227
223
|
logger.log({ message: "setInitialFocus", data: { preferredFocus } });
|
|
228
224
|
|
|
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
|
-
}
|
|
225
|
+
if (isNilOrEmpty(preferredFocus)) {
|
|
226
|
+
return { success: false };
|
|
227
|
+
}
|
|
242
228
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
data: {
|
|
246
|
-
focusableItem,
|
|
247
|
-
},
|
|
248
|
-
});
|
|
229
|
+
const focusCandidate = preferredFocus[0];
|
|
230
|
+
const focusableItem = focusCandidate.component;
|
|
249
231
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
232
|
+
const currentFocusedId = R.path(["props", "id"], getCurrentFocus());
|
|
233
|
+
const nextFocusedId = R.path(["props", "id"], focusableItem);
|
|
234
|
+
|
|
235
|
+
if (
|
|
236
|
+
isNotNil(currentFocusedId) &&
|
|
237
|
+
isNotNil(nextFocusedId) &&
|
|
238
|
+
currentFocusedId === nextFocusedId
|
|
239
|
+
) {
|
|
240
|
+
// we are going to move focus to the same node(already focused) -> nothing to do
|
|
241
|
+
return { success: true };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (focusableItem?.isGroup) {
|
|
245
|
+
// focusCandidate is a FocusableGroup, we are not able to land focus on group(only on Focusable)
|
|
246
|
+
return { success: false };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (
|
|
250
|
+
focusableItem &&
|
|
251
|
+
!focusableItem.isGroup &&
|
|
252
|
+
isFocusEnabled(focusableItem)
|
|
253
|
+
) {
|
|
254
|
+
blur();
|
|
255
|
+
|
|
256
|
+
logger.log({
|
|
257
|
+
message: "setInitialFocus – currentFocusNode",
|
|
258
|
+
data: {
|
|
259
|
+
focusableItem,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
currentFocusNode = focusCandidate;
|
|
264
|
+
focusableItem.willReceiveFocus(focusableItem, null);
|
|
265
|
+
focusableItem && setFocus(focusCandidate.id, null);
|
|
266
|
+
|
|
267
|
+
return { success: true };
|
|
254
268
|
}
|
|
269
|
+
|
|
270
|
+
return { success: false };
|
|
255
271
|
}
|
|
256
272
|
|
|
257
273
|
function treeLoaded(val, node) {
|
|
@@ -261,17 +277,8 @@ export const focusManager = (function () {
|
|
|
261
277
|
val,
|
|
262
278
|
node,
|
|
263
279
|
currentFocusNode,
|
|
264
|
-
initialFocusOnContent,
|
|
265
280
|
},
|
|
266
281
|
});
|
|
267
|
-
|
|
268
|
-
if ((val && !currentFocusNode) || initialFocusOnContent) {
|
|
269
|
-
logger.log({ message: `focusAutoEnabled ${focusAutoEnabled}` });
|
|
270
|
-
|
|
271
|
-
if (focusAutoEnabled) {
|
|
272
|
-
setInitialFocus(initialFocusOnContent ? node : undefined);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
282
|
}
|
|
276
283
|
|
|
277
284
|
/**
|
|
@@ -402,11 +409,21 @@ export const focusManager = (function () {
|
|
|
402
409
|
group ? unregisterGroup(id) : unregisterItem(id);
|
|
403
410
|
}
|
|
404
411
|
|
|
405
|
-
function
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
412
|
+
function isCurrentFocusOnTheTopScreen() {
|
|
413
|
+
const routes = R.pathOr([], ["root", "children"], focusableTree);
|
|
414
|
+
|
|
415
|
+
if (routes.length <= 1) {
|
|
416
|
+
// there is only one or zero routes
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return haveSameParentBeforeRoot(currentFocusNode, R.last(routes));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function recoverFocus() {
|
|
424
|
+
if (!isCurrentFocusOnTheTopScreen()) {
|
|
425
|
+
// We've failed to set focused node on the new screen => run focus recovery
|
|
426
|
+
setInitialFocus();
|
|
410
427
|
}
|
|
411
428
|
}
|
|
412
429
|
|
|
@@ -416,6 +433,11 @@ export const focusManager = (function () {
|
|
|
416
433
|
* @param {Object} direction of the scroll
|
|
417
434
|
*/
|
|
418
435
|
function moveFocus(direction) {
|
|
436
|
+
if (!isCurrentFocusOnTheTopScreen()) {
|
|
437
|
+
// We are opening new screen. We need to ignore movement of focus.
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
419
441
|
if (focusDisabled) return false;
|
|
420
442
|
|
|
421
443
|
if (currentFocusNode) {
|
|
@@ -444,8 +466,6 @@ export const focusManager = (function () {
|
|
|
444
466
|
} else {
|
|
445
467
|
currentFocusNode.component.failedLostFocus(currentFocusNode, direction);
|
|
446
468
|
}
|
|
447
|
-
} else {
|
|
448
|
-
setInitialFocus();
|
|
449
469
|
}
|
|
450
470
|
}
|
|
451
471
|
|
|
@@ -505,7 +525,6 @@ export const focusManager = (function () {
|
|
|
505
525
|
on,
|
|
506
526
|
invokeHandler,
|
|
507
527
|
removeHandler,
|
|
508
|
-
setupInitialFocus,
|
|
509
528
|
longPress,
|
|
510
529
|
pressOut,
|
|
511
530
|
enableFocus,
|
|
@@ -514,9 +533,9 @@ export const focusManager = (function () {
|
|
|
514
533
|
isFocusDisabled,
|
|
515
534
|
getCurrentFocusNode,
|
|
516
535
|
focusableTree,
|
|
517
|
-
disableAutomaticFocus,
|
|
518
|
-
enableAutomaticFocus,
|
|
519
536
|
getGroupRootById,
|
|
520
537
|
isGroupItemFocused,
|
|
538
|
+
recoverFocus,
|
|
539
|
+
isCurrentFocusOnTheTopScreen,
|
|
521
540
|
};
|
|
522
541
|
})();
|