@applicaster/zapp-react-native-utils 13.0.0-alpha.5281161050 → 13.0.0-alpha.5284645460
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/README.md +0 -10
- package/actionsExecutor/ActionExecutor.ts +16 -9
- package/actionsExecutor/ActionExecutorContext.tsx +59 -8
- package/alertUtils/alert.ts +148 -0
- package/alertUtils/index.ts +1 -148
- package/analyticsUtils/AnalyticPlayerListener.ts +18 -1
- package/analyticsUtils/__tests__/__snapshots__/analyticsManagerNoModule.test.js.snap +9 -9
- package/analyticsUtils/currentScreenProperities.ts +6 -4
- package/analyticsUtils/helpers/hooks.ts +1 -1
- package/appUtils/HooksManager/Hook.ts +2 -2
- package/appUtils/HooksManager/__tests__/__snapshots__/hook.test.js.snap +7 -7
- package/appUtils/HooksManager/__tests__/__snapshots__/hooksManager.test.js.snap +186 -186
- package/appUtils/HooksManager/types.ts +5 -2
- package/appUtils/accessibilityManager/index.ts +42 -1
- package/appUtils/contextKeysManager/contextResolver.ts +77 -0
- package/appUtils/contextKeysManager/index.ts +2 -2
- package/appUtils/contextKeysManager/utils/index.ts +2 -1
- package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +8 -6
- package/appUtils/focusManager/__tests__/focusManager.test.js +82 -0
- package/appUtils/focusManager/__tests__/index.test.ts +1 -0
- package/appUtils/focusManager/index.ts +116 -68
- package/appUtils/focusManager/treeDataStructure/Tree/index.js +23 -0
- package/appUtils/focusManager/utils/enums.ts +4 -0
- package/appUtils/focusManager/utils/index.ts +14 -0
- package/appUtils/focusManagerAux/index.ts +2 -2
- package/appUtils/focusManagerAux/utils/index.ts +4 -3
- package/appUtils/keyCodes/__tests__/__snapshots__/keyCodes.test.js.snap +2 -2
- package/appUtils/keyCodes/__tests__/keyCodes.test.js +1 -1
- package/appUtils/keyCodes/keys/keysByPlatform.ts +4 -0
- package/appUtils/keyInputHandler/KeyInputHandler.ts +68 -0
- package/appUtils/orientationHelper.ts +1 -1
- package/appUtils/platform/const.ts +14 -0
- package/appUtils/platform/platformUtils.ts +47 -4
- package/appUtils/playerManager/index.ts +1 -1
- package/appUtils/playerManager/player.ts +19 -7
- package/appUtils/playerManager/playerNative.ts +3 -2
- package/appUtils/playerManager/usePlayerControllerSetup.tsx +3 -3
- package/arrayUtils/__tests__/__snapshots__/arrayUtils.test.ts.snap +4 -4
- package/equalUtils/__tests__/isEqual/combination.test.ts +263 -0
- package/equalUtils/index.ts +49 -30
- package/focusManager/FocusManager.ts +11 -1
- package/focusManager/index.ts +41 -4
- package/functionUtils/__tests__/__snapshots__/functionUtils.test.js.snap +4 -4
- package/idleUtils/__tests__/withTimeout$.test.ts +37 -0
- package/idleUtils/index.ts +1 -1
- package/idleUtils/withTimeout.ts +15 -2
- package/logger/const.ts +9 -67
- package/manifestUtils/__tests__/createConfig.test.js +16 -16
- package/manifestUtils/sharedConfiguration/screenPicker/__tests__/utils.test.js +304 -0
- package/manifestUtils/sharedConfiguration/screenPicker/stylesFields.js +875 -0
- package/manifestUtils/sharedConfiguration/screenPicker/utils.js +130 -0
- package/navigationUtils/__tests__/__snapshots__/navigationUtils.test.js.snap +6 -6
- package/navigationUtils/__tests__/itemTypes.test.js +2 -2
- package/navigationUtils/__tests__/navigationUtils.test.js +2 -2
- package/navigationUtils/index.ts +3 -3
- package/numberUtils/index.ts +5 -7
- package/package.json +5 -6
- package/reactHooks/autoscrolling/__tests__/useTrackCurrentAutoScrollingElement.test.ts +1 -1
- package/reactHooks/autoscrolling/__tests__/useTrackedView.test.tsx +1 -1
- package/reactHooks/cell-click/__tests__/index.test.js +10 -2
- package/reactHooks/cell-click/index.ts +7 -1
- package/reactHooks/feed/__tests__/useBuildPipesUrl.test.tsx +4 -4
- package/reactHooks/feed/__tests__/useFeedLoader.test.tsx +10 -11
- package/reactHooks/feed/useFeedLoader.tsx +1 -1
- package/reactHooks/feed/usePipesCacheReset.ts +5 -5
- package/reactHooks/flatList/__tests__/index.test.tsx +30 -21
- package/reactHooks/layout/__tests__/index.test.tsx +10 -10
- package/reactHooks/layout/useDimensions/helpers/__tests__/getInitialDimensions.native.test.js +14 -28
- package/reactHooks/layout/useDimensions/helpers/__tests__/getInitialDimensions.test.js +1 -1
- package/reactHooks/layout/useDimensions/useDimensions.ts +8 -0
- package/reactHooks/navigation/__mocks__/useNavigation.ts +3 -0
- package/reactHooks/navigation/__tests__/index.test.tsx +0 -4
- package/reactHooks/navigation/index.ts +28 -232
- package/reactHooks/navigation/useGetBottomTabBarHeight.ts +12 -0
- package/reactHooks/navigation/useGetNavBarTopBorderWidth.ts +14 -0
- package/reactHooks/navigation/useGetTabBarHeight.ts +10 -0
- package/reactHooks/navigation/useIsScreenActive.ts +17 -0
- package/reactHooks/navigation/useNavigationPluginData.ts +15 -0
- package/reactHooks/navigation/useNavigationType.ts +15 -0
- package/reactHooks/navigation/usePathname.ts +4 -0
- package/reactHooks/navigation/useProfilerLogging.ts +15 -0
- package/reactHooks/navigation/useRoute.ts +100 -0
- package/reactHooks/navigation/useUniqueRouteSuffix.ts +41 -0
- package/reactHooks/player/TVSeekControlller/ISeekController.ts +32 -0
- package/reactHooks/player/TVSeekControlller/LongPressSeekController.ts +71 -0
- package/reactHooks/player/TVSeekControlller/TVSeekController.ts +258 -0
- package/reactHooks/player/TVSeekControlller/TapSeekController.ts +53 -0
- package/reactHooks/player/__tests__/{useAutoSeek.test.tsx → useAutoSeek._test.tsx} +2 -3
- package/reactHooks/player/const.ts +0 -4
- package/reactHooks/player/index.ts +0 -6
- package/reactHooks/state/useRefWithInitialValue.ts +14 -0
- package/reactHooks/state/useStateFromSubscribe.ts +23 -0
- package/reactHooks/usePluginConfiguration.ts +1 -1
- package/reactHooks/useSubscriberFor.ts +4 -3
- package/reactUtils/__tests__/__snapshots__/reactUtilsHelpers.test.js.snap +16 -4
- package/reactUtils/__tests__/reactUtils.test.js +1 -37
- package/reactUtils/__tests__/reactUtilsHelpers.test.js +72 -71
- package/reactUtils/createContext/__tests__/__snapshots__/helpers.test.js.snap +13 -79
- package/reactUtils/createContext/__tests__/__snapshots__/{index.test.js.snap → index.test.ts.snap} +2 -2
- package/reactUtils/createContext/__tests__/__snapshots__/provider.test.js.snap +3 -28
- package/reactUtils/createContext/__tests__/__snapshots__/withConsumer.test.js.snap +3 -4
- package/reactUtils/createContext/__tests__/__snapshots__/withProvider.test.js.snap +7 -3
- package/reactUtils/createContext/__tests__/helpers.test.js +107 -121
- package/reactUtils/createContext/__tests__/{index.test.js → index.test.ts} +2 -2
- package/reactUtils/createContext/__tests__/provider.test.js +4 -6
- package/reactUtils/createContext/__tests__/withConsumer.test.js +12 -12
- package/reactUtils/createContext/__tests__/withProvider.test.js +4 -4
- package/reactUtils/createContext/helpers.ts +6 -1
- package/reactUtils/createContext/index.ts +11 -25
- package/reactUtils/index.ts +0 -11
- package/screenState/index.ts +24 -10
- package/testUtils/index.tsx +0 -10
- package/theme/__mocks__/index.ts +4 -0
- package/theme/index.ts +1 -1
- package/time/Timer.ts +5 -2
- package/types.ts +1 -0
- package/zappFrameworkUtils/presentPluginOneTime.ts +2 -3
- package/reactHooks/player/useAutoSeek.tsx +0 -87
- package/reactHooks/player/useSeek.tsx +0 -66
- package/reactHooks/player/useTapSeek.tsx +0 -58
- package/reactUtils/createContext/__tests__/index-no-react.test.js +0 -27
- /package/reactHooks/player/__tests__/{useTapSeek.test.ts → useTapSeek._test.ts} +0 -0
|
@@ -4,6 +4,8 @@ import { HOOKS_EVENTS } from "./constants";
|
|
|
4
4
|
export interface HookConfiguration {}
|
|
5
5
|
|
|
6
6
|
/* existing Hook Plugn props: sample */
|
|
7
|
+
|
|
8
|
+
/*
|
|
7
9
|
interface _HookModulePlugin {
|
|
8
10
|
Component:
|
|
9
11
|
| React.FC<HookPluginProps>
|
|
@@ -23,6 +25,7 @@ interface _HookModulePlugin {
|
|
|
23
25
|
presentUI: (payload?: HookPluginProps["payload"]) => void
|
|
24
26
|
) => any;
|
|
25
27
|
}
|
|
28
|
+
*/
|
|
26
29
|
|
|
27
30
|
//* Derived from usage */
|
|
28
31
|
export interface HookModule {
|
|
@@ -100,7 +103,7 @@ export interface Hook<T extends HookModule = HookModule> {
|
|
|
100
103
|
identifier: string;
|
|
101
104
|
configuration: T["configuration"];
|
|
102
105
|
setStateAndNotify(
|
|
103
|
-
event: typeof HOOKS_EVENTS[keyof typeof HOOKS_EVENTS],
|
|
106
|
+
event: (typeof HOOKS_EVENTS)[keyof typeof HOOKS_EVENTS],
|
|
104
107
|
opts: {
|
|
105
108
|
error?: typeof Error;
|
|
106
109
|
route?: string;
|
|
@@ -109,7 +112,7 @@ export interface Hook<T extends HookModule = HookModule> {
|
|
|
109
112
|
}
|
|
110
113
|
);
|
|
111
114
|
general?: {
|
|
112
|
-
transition_type: typeof HOOKS_EVENTS[keyof typeof HOOKS_EVENTS];
|
|
115
|
+
transition_type: (typeof HOOKS_EVENTS)[keyof typeof HOOKS_EVENTS];
|
|
113
116
|
allow_screen_plugin_presentation: boolean;
|
|
114
117
|
is_flow_blocker: boolean;
|
|
115
118
|
};
|
|
@@ -6,6 +6,9 @@ import { AccessibilityRole } from "react-native";
|
|
|
6
6
|
|
|
7
7
|
export class AccessibilityManager {
|
|
8
8
|
private static _instance: AccessibilityManager | null = null;
|
|
9
|
+
private headingTimeout: NodeJS.Timeout | null = null;
|
|
10
|
+
private WORDS_PER_MINUTE = 160;
|
|
11
|
+
private MINIMUM_PAUSE = 500;
|
|
9
12
|
private state$ = new BehaviorSubject<AccessibilityState>({
|
|
10
13
|
screenReaderEnabled: false,
|
|
11
14
|
reduceMotionEnabled: false,
|
|
@@ -20,6 +23,7 @@ export class AccessibilityManager {
|
|
|
20
23
|
|
|
21
24
|
private ttsManager = TTSManager.getInstance();
|
|
22
25
|
private localizations: { [key: string]: string } = {};
|
|
26
|
+
private headingQueue: string[] = [];
|
|
23
27
|
|
|
24
28
|
private constructor() {}
|
|
25
29
|
|
|
@@ -52,6 +56,28 @@ export class AccessibilityManager {
|
|
|
52
56
|
return this.state$.asObservable();
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
/** Calculates the reading time for a given text
|
|
60
|
+
* This method is a bit of a hack because we don't have a callback, or promise from VIZIO API
|
|
61
|
+
* @param text - The text to calculate the reading time for
|
|
62
|
+
* @returns The reading time in milliseconds
|
|
63
|
+
*/
|
|
64
|
+
private calculateReadingTime(text: string): number {
|
|
65
|
+
const words = text.trim().split(/\s+/).length;
|
|
66
|
+
|
|
67
|
+
return Math.max(
|
|
68
|
+
this.MINIMUM_PAUSE,
|
|
69
|
+
(words / this.WORDS_PER_MINUTE) * 60 * 1000
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Adds a heading to the queue, headings will be read before the next text
|
|
75
|
+
* Each heading will be read once and removed from the queue
|
|
76
|
+
*/
|
|
77
|
+
public addHeading(heading: string) {
|
|
78
|
+
this.headingQueue.push(heading);
|
|
79
|
+
}
|
|
80
|
+
|
|
55
81
|
/**
|
|
56
82
|
* text you want to be read, if you want to use localized text pass keyOfLocalizedText instead
|
|
57
83
|
* keyOfLocalizedText is the key to the localized text
|
|
@@ -85,7 +111,22 @@ export class AccessibilityManager {
|
|
|
85
111
|
textToRead = localizedMessage;
|
|
86
112
|
}
|
|
87
113
|
|
|
88
|
-
this.
|
|
114
|
+
if (this.headingQueue.length > 0) {
|
|
115
|
+
const heading = this.headingQueue.shift()!;
|
|
116
|
+
this.ttsManager?.readText(heading);
|
|
117
|
+
|
|
118
|
+
if (this.headingTimeout) {
|
|
119
|
+
clearTimeout(this.headingTimeout);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const pauseTime = this.calculateReadingTime(heading);
|
|
123
|
+
|
|
124
|
+
this.headingTimeout = setTimeout(() => {
|
|
125
|
+
this.ttsManager?.readText(textToRead);
|
|
126
|
+
}, pauseTime);
|
|
127
|
+
} else {
|
|
128
|
+
this.ttsManager?.readText(textToRead);
|
|
129
|
+
}
|
|
89
130
|
|
|
90
131
|
this.announcements$.next({
|
|
91
132
|
message: textToRead,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createLogger } from "../../logger";
|
|
2
|
+
import { ContextKeysManager } from "./index";
|
|
3
|
+
import * as R from "ramda";
|
|
4
|
+
|
|
5
|
+
const { log_error } = createLogger({
|
|
6
|
+
subsystem: "contextResolver",
|
|
7
|
+
category: "General",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
interface IResolver {
|
|
11
|
+
resolve: (string) => Promise<string | number | object>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class EntryResolver implements IResolver {
|
|
15
|
+
entry: ZappEntry;
|
|
16
|
+
|
|
17
|
+
constructor(entry: ZappEntry) {
|
|
18
|
+
this.entry = entry;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async resolve(key: string) {
|
|
22
|
+
if (!key || key.length === 0) {
|
|
23
|
+
return this.entry;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return R.view(R.lensPath(key.split(".")), this.entry);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class ContextResolver implements IResolver {
|
|
31
|
+
resolve = async (compositeKey: string) =>
|
|
32
|
+
ContextKeysManager.instance.getKey(compositeKey);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const resolveObjectValues = async (
|
|
36
|
+
data: Record<string, any>,
|
|
37
|
+
resolvers: Record<string, IResolver> = null
|
|
38
|
+
) => {
|
|
39
|
+
const entries = Object.entries(data);
|
|
40
|
+
|
|
41
|
+
const exResolvers = { ctx: new ContextResolver(), ...resolvers };
|
|
42
|
+
|
|
43
|
+
const resolvedEntries = await Promise.all(
|
|
44
|
+
entries.map(async ([key, value]) => {
|
|
45
|
+
if (typeof value !== "string") {
|
|
46
|
+
return [key, value];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!value.startsWith("@")) {
|
|
50
|
+
return [key, value];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const regex = /@{([^/]+)\/([^}]*)}/;
|
|
54
|
+
|
|
55
|
+
const match = (value as string).match(regex);
|
|
56
|
+
|
|
57
|
+
if (match) {
|
|
58
|
+
const contextResolverName = match[1];
|
|
59
|
+
const compositeKey = match[2];
|
|
60
|
+
|
|
61
|
+
const resolver = exResolvers[contextResolverName] || exResolvers.ctx;
|
|
62
|
+
const resolvedValue = await resolver.resolve(compositeKey);
|
|
63
|
+
|
|
64
|
+
return [key, resolvedValue];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
log_error("resolveObjectValues: invalid context string", {
|
|
68
|
+
key,
|
|
69
|
+
value,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return [key, null];
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return Object.fromEntries(resolvedEntries);
|
|
77
|
+
};
|
|
@@ -16,12 +16,12 @@ import {
|
|
|
16
16
|
|
|
17
17
|
export { StorageLevel };
|
|
18
18
|
|
|
19
|
-
type KeyNameObj = {
|
|
19
|
+
export type KeyNameObj = {
|
|
20
20
|
namespace: string;
|
|
21
21
|
key: string;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
type KeyName = string | KeyNameObj;
|
|
24
|
+
export type KeyName = string | KeyNameObj;
|
|
25
25
|
|
|
26
26
|
type ParsedKeyAndNamespace = {
|
|
27
27
|
namespace?: string;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as R from "ramda";
|
|
2
2
|
import { DEFAULT_NAMESPACE } from "../consts";
|
|
3
|
+
import { KeyName, KeyNameObj } from "../index";
|
|
3
4
|
|
|
4
5
|
const lastDotRegex = /\.([^.]+)$/;
|
|
5
6
|
const splitByLastDot = R.compose(R.init, R.split(lastDotRegex));
|
|
6
7
|
|
|
7
|
-
export function getNamespaceAndKey(namespacedKey) {
|
|
8
|
+
export function getNamespaceAndKey(namespacedKey: KeyName): KeyNameObj {
|
|
8
9
|
if (typeof namespacedKey !== "string") return namespacedKey;
|
|
9
10
|
|
|
10
11
|
const namespacedKeyProps = namespacedKey.includes(".")
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
3
|
exports[`focusManager should be defined 1`] = `
|
|
4
|
-
|
|
4
|
+
{
|
|
5
5
|
"blur": [Function],
|
|
6
6
|
"disableFocus": [Function],
|
|
7
7
|
"enableFocus": [Function],
|
|
8
8
|
"focusableTree": Tree {
|
|
9
9
|
"loadingCounter": 0,
|
|
10
|
-
"root":
|
|
11
|
-
"children":
|
|
10
|
+
"root": {
|
|
11
|
+
"children": [],
|
|
12
12
|
"component": null,
|
|
13
13
|
"id": "root",
|
|
14
14
|
"lastFocusedItem": null,
|
|
@@ -28,10 +28,12 @@ Object {
|
|
|
28
28
|
"moveFocus": [Function],
|
|
29
29
|
"on": [Function],
|
|
30
30
|
"press": [Function],
|
|
31
|
+
"pressIn": [Function],
|
|
31
32
|
"pressOut": [Function],
|
|
32
33
|
"recoverFocus": [Function],
|
|
33
34
|
"register": [Function],
|
|
34
35
|
"removeHandler": [Function],
|
|
36
|
+
"resetFocus": [Function],
|
|
35
37
|
"setFocus": [Function],
|
|
36
38
|
"setInitialFocus": [Function],
|
|
37
39
|
"unregister": [Function],
|
|
@@ -39,12 +41,12 @@ Object {
|
|
|
39
41
|
`;
|
|
40
42
|
|
|
41
43
|
exports[`focusManagerIOS should be defined 1`] = `
|
|
42
|
-
|
|
44
|
+
{
|
|
43
45
|
"focus": [Function],
|
|
44
46
|
"focusableTree": Tree {
|
|
45
47
|
"loadingCounter": 0,
|
|
46
|
-
"root":
|
|
47
|
-
"children":
|
|
48
|
+
"root": {
|
|
49
|
+
"children": [],
|
|
48
50
|
"component": null,
|
|
49
51
|
"id": "root",
|
|
50
52
|
"lastFocusedItem": null,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { focusManager } from "../index.ts";
|
|
2
2
|
import { focusManager as focusManagerIOS } from "../index.ios.ts";
|
|
3
|
+
import { findFirstFocusableItem } from "../utils";
|
|
3
4
|
|
|
4
5
|
describe("focusManagerIOS", () => {
|
|
5
6
|
it("should be defined", () => {
|
|
@@ -8,6 +9,67 @@ describe("focusManagerIOS", () => {
|
|
|
8
9
|
});
|
|
9
10
|
});
|
|
10
11
|
|
|
12
|
+
const focusableTree = {
|
|
13
|
+
root: {
|
|
14
|
+
children: [
|
|
15
|
+
{
|
|
16
|
+
children: [
|
|
17
|
+
{
|
|
18
|
+
children: [
|
|
19
|
+
{
|
|
20
|
+
children: [],
|
|
21
|
+
component: {
|
|
22
|
+
isGroup: true,
|
|
23
|
+
},
|
|
24
|
+
id: "route/1/1",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
children: [],
|
|
28
|
+
component: {
|
|
29
|
+
isGroup: true,
|
|
30
|
+
},
|
|
31
|
+
id: "route/1/2",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
component: {
|
|
35
|
+
isGroup: true,
|
|
36
|
+
},
|
|
37
|
+
id: "route/1",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
children: [
|
|
41
|
+
{
|
|
42
|
+
children: [],
|
|
43
|
+
component: {
|
|
44
|
+
isGroup: true,
|
|
45
|
+
},
|
|
46
|
+
id: "route/2/1",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
children: [],
|
|
50
|
+
component: {
|
|
51
|
+
isGroup: false,
|
|
52
|
+
},
|
|
53
|
+
id: "route/2/2",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
component: {
|
|
57
|
+
isGroup: true,
|
|
58
|
+
},
|
|
59
|
+
id: "route/2",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
component: {
|
|
63
|
+
isGroup: true,
|
|
64
|
+
},
|
|
65
|
+
id: "/river/1234",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
component: null,
|
|
69
|
+
id: "root",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
11
73
|
describe("focusManager", () => {
|
|
12
74
|
it("should be defined", () => {
|
|
13
75
|
expect(focusManager).toBeDefined();
|
|
@@ -45,4 +107,24 @@ describe("focusManager", () => {
|
|
|
45
107
|
it("placeholder test", () => {
|
|
46
108
|
expect(true).toBe(true);
|
|
47
109
|
});
|
|
110
|
+
|
|
111
|
+
it("findFirstFocusableItem returns focusableItem", () => {
|
|
112
|
+
const expectedItemId =
|
|
113
|
+
focusableTree.root.children[0].children[1].children[1].id;
|
|
114
|
+
|
|
115
|
+
const focusableItem = findFirstFocusableItem(focusableTree.root.children);
|
|
116
|
+
|
|
117
|
+
expect(
|
|
118
|
+
focusableItem !== undefined && expectedItemId === focusableItem.id
|
|
119
|
+
).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("findFirstFocusableItem does not return focusableItem", () => {
|
|
123
|
+
focusableTree.root.children[0].children[1].children[1].component.isGroup =
|
|
124
|
+
true;
|
|
125
|
+
|
|
126
|
+
const focusableItem = findFirstFocusableItem(focusableTree.root.children);
|
|
127
|
+
|
|
128
|
+
expect(focusableItem === undefined).toBe(true);
|
|
129
|
+
});
|
|
48
130
|
});
|
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
haveSameParentBeforeRoot,
|
|
12
12
|
} from "./treeDataStructure/Utils";
|
|
13
13
|
import { subscriber } from "../../functionUtils";
|
|
14
|
-
import * as FOCUS_EVENTS from "./events";
|
|
15
14
|
import { coreLogger } from "../../logger";
|
|
15
|
+
import { ACTION } from "./utils/enums";
|
|
16
|
+
import { findFirstFocusableItem } from "./utils";
|
|
17
|
+
|
|
16
18
|
const logger = coreLogger.addSubsystem("focusManager");
|
|
17
19
|
|
|
18
20
|
const isFocusEnabled = (focusableItem): boolean => {
|
|
@@ -167,13 +169,82 @@ export const focusManager = (function () {
|
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
172
|
+
* Computes the difference in parent groups between two nodes.
|
|
173
|
+
* It returns the IDs of parents that are present in the current node but not in the previous node,
|
|
174
|
+
* as well as the IDs of parents that are present in the previous node but not in the current node.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} currentNodeID - The ID of the current node.
|
|
177
|
+
* @param {string} previousNodeID - The ID of the previous node.
|
|
178
|
+
* @returns {Object} An object containing the differences in parent IDs:
|
|
179
|
+
* - {Array<string>} currentNodeParentsIDs: Parent IDs present in the current node but not in the previous node.
|
|
180
|
+
* - {Array<string>} previousNodeParentsIDs: Parent IDs present in the previous node but not in the current node.
|
|
173
181
|
*/
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
182
|
+
const getParentDiffs = (currentNodeID, previousNodeID) => {
|
|
183
|
+
const currentNodeParentsIDs =
|
|
184
|
+
focusableTree.getParentGroupIds(currentNodeID);
|
|
185
|
+
|
|
186
|
+
const previousNodeParentsIDs =
|
|
187
|
+
focusableTree.getParentGroupIds(previousNodeID);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
currentNodeParentsIDs: R.difference(
|
|
191
|
+
currentNodeParentsIDs,
|
|
192
|
+
previousNodeParentsIDs
|
|
193
|
+
),
|
|
194
|
+
previousNodeParentsIDs: R.difference(
|
|
195
|
+
previousNodeParentsIDs,
|
|
196
|
+
currentNodeParentsIDs
|
|
197
|
+
),
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Updates the focus state of nodes identified by their IDs.
|
|
203
|
+
* For each ID, if `setFocus` is true, the node will receive focus;
|
|
204
|
+
* otherwise, it will lose focus.
|
|
205
|
+
*
|
|
206
|
+
* @param {Array<string>} ids - An array of node IDs to update.
|
|
207
|
+
* @param {boolean} setFocus - A flag indicating whether to set focus (true) or blur (false) on the nodes.
|
|
208
|
+
*/
|
|
209
|
+
const updateNodeFocus = (ids, action) => {
|
|
210
|
+
if (!ids || ids.length === 0) {
|
|
211
|
+
return; // Nothing to do
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const startNode = focusableTree.findInTree(ids[0]);
|
|
215
|
+
|
|
216
|
+
if (!startNode) {
|
|
217
|
+
return; // Start node not found
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const endNodeId = ids[ids.length - 1]; // Get the ID of the end node
|
|
221
|
+
|
|
222
|
+
let currentNode = startNode;
|
|
223
|
+
|
|
224
|
+
// Function to apply the action (focus or blur)
|
|
225
|
+
const applyAction = (node) => {
|
|
226
|
+
if (node && node.component) {
|
|
227
|
+
if (action === "focus") {
|
|
228
|
+
node.component.setFocus();
|
|
229
|
+
} else if (action === "blur") {
|
|
230
|
+
node.component.setBlur();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// Apply action to the starting node
|
|
236
|
+
applyAction(currentNode);
|
|
237
|
+
|
|
238
|
+
// Traverse upwards
|
|
239
|
+
while (currentNode && currentNode.id !== endNodeId) {
|
|
240
|
+
currentNode = currentNode.parent;
|
|
241
|
+
|
|
242
|
+
if (currentNode) {
|
|
243
|
+
// check if current node exists, as parent might be root
|
|
244
|
+
applyAction(currentNode);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
};
|
|
177
248
|
|
|
178
249
|
/**
|
|
179
250
|
* Sets the current focus to the provided id. If there is an existing focus, it will
|
|
@@ -181,34 +252,29 @@ export const focusManager = (function () {
|
|
|
181
252
|
* @private
|
|
182
253
|
* @param {String} id of the focusable to set as currentFocus
|
|
183
254
|
* @param {Object} direction of the navigation, which led to this focus change
|
|
184
|
-
* @param {Object} options
|
|
185
|
-
* @param {Boolean} options.groupFocusedChanged tells wether the focus has moved
|
|
186
255
|
* to another group or not. defaults to false
|
|
187
256
|
*/
|
|
188
|
-
function setFocus(
|
|
189
|
-
id: string,
|
|
190
|
-
direction?: FocusManager.Web.Direction,
|
|
191
|
-
options?: Partial<{ groupFocusedChanged: boolean }>
|
|
192
|
-
) {
|
|
257
|
+
function setFocus(id: string, direction?: FocusManager.Web.Direction) {
|
|
193
258
|
if (focusDisabled) return false;
|
|
194
259
|
|
|
195
|
-
const { groupFocusedChanged = false } = options || {};
|
|
196
|
-
|
|
197
260
|
// due to optimisiation it's recommanded to set currentFocusNode before setFocus
|
|
198
261
|
// is called, so there will be no delays on tree search.
|
|
199
262
|
// If currentFocusNode is null or undefined we still want it to findInTree
|
|
200
263
|
if (currentFocusNode?.id !== id) {
|
|
264
|
+
const { currentNodeParentsIDs, previousNodeParentsIDs } = getParentDiffs(
|
|
265
|
+
id,
|
|
266
|
+
currentFocusNode?.id
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Set focus on current node parents and blur on previous node parents
|
|
270
|
+
updateNodeFocus(currentNodeParentsIDs, ACTION.FOCUS);
|
|
271
|
+
updateNodeFocus(previousNodeParentsIDs, ACTION.BLUR);
|
|
272
|
+
|
|
201
273
|
currentFocusNode = focusableTree.findInTree(id);
|
|
202
274
|
}
|
|
203
275
|
|
|
204
276
|
setLastFocusOnParentNode(currentFocusNode);
|
|
205
277
|
|
|
206
|
-
const currentGroup = getCurrentGroup();
|
|
207
|
-
|
|
208
|
-
if (groupFocusedChanged && currentGroup) {
|
|
209
|
-
currentGroup.hasLostFocus(currentGroup, direction);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
278
|
focus(direction);
|
|
213
279
|
}
|
|
214
280
|
|
|
@@ -251,6 +317,7 @@ export const focusManager = (function () {
|
|
|
251
317
|
!focusableItem.isGroup &&
|
|
252
318
|
isFocusEnabled(focusableItem)
|
|
253
319
|
) {
|
|
320
|
+
// TODO: Move this to setFocus method
|
|
254
321
|
blur();
|
|
255
322
|
|
|
256
323
|
logger.log({
|
|
@@ -260,8 +327,6 @@ export const focusManager = (function () {
|
|
|
260
327
|
},
|
|
261
328
|
});
|
|
262
329
|
|
|
263
|
-
currentFocusNode = focusCandidate;
|
|
264
|
-
focusableItem.willReceiveFocus(focusableItem, null);
|
|
265
330
|
focusableItem && setFocus(focusCandidate.id, null);
|
|
266
331
|
|
|
267
332
|
return { success: true };
|
|
@@ -281,6 +346,14 @@ export const focusManager = (function () {
|
|
|
281
346
|
});
|
|
282
347
|
}
|
|
283
348
|
|
|
349
|
+
function pressIn() {
|
|
350
|
+
const currentFocusable = getCurrentFocus();
|
|
351
|
+
|
|
352
|
+
if (currentFocusable) {
|
|
353
|
+
currentFocusable.pressIn(null);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
284
357
|
/**
|
|
285
358
|
* calls the `press` method on the current focus component
|
|
286
359
|
* @public
|
|
@@ -412,7 +485,7 @@ export const focusManager = (function () {
|
|
|
412
485
|
function isCurrentFocusOnTheTopScreen() {
|
|
413
486
|
const routes = R.pathOr([], ["root", "children"], focusableTree);
|
|
414
487
|
|
|
415
|
-
if (routes.length <= 1) {
|
|
488
|
+
if (currentFocusNode && routes.length <= 1) {
|
|
416
489
|
// there is only one or zero routes
|
|
417
490
|
return true;
|
|
418
491
|
}
|
|
@@ -420,20 +493,29 @@ export const focusManager = (function () {
|
|
|
420
493
|
return haveSameParentBeforeRoot(currentFocusNode, R.last(routes));
|
|
421
494
|
}
|
|
422
495
|
|
|
423
|
-
function
|
|
496
|
+
function resetFocus() {
|
|
424
497
|
if (!isCurrentFocusOnTheTopScreen()) {
|
|
425
|
-
// We've failed to set focused node on the new screen => run focus
|
|
498
|
+
// We've failed to set focused node on the new screen => run focus reset
|
|
426
499
|
setInitialFocus();
|
|
427
500
|
}
|
|
428
501
|
}
|
|
429
502
|
|
|
503
|
+
/**
|
|
504
|
+
* sets the focus on first focusable item
|
|
505
|
+
* @public
|
|
506
|
+
*/
|
|
507
|
+
function recoverFocus() {
|
|
508
|
+
const focusableItem = findFirstFocusableItem(focusableTree.root.children);
|
|
509
|
+
focusableItem && setFocus(focusableItem.id);
|
|
510
|
+
}
|
|
511
|
+
|
|
430
512
|
/**
|
|
431
513
|
* moves the focus in a provided direction
|
|
432
514
|
* @public
|
|
433
515
|
* @param {Object} direction of the scroll
|
|
434
516
|
*/
|
|
435
517
|
function moveFocus(direction) {
|
|
436
|
-
if (!isCurrentFocusOnTheTopScreen()) {
|
|
518
|
+
if (!isCurrentFocusOnTheTopScreen() && currentFocusNode) {
|
|
437
519
|
// We are opening new screen. We need to ignore movement of focus.
|
|
438
520
|
return false;
|
|
439
521
|
}
|
|
@@ -448,24 +530,16 @@ export const focusManager = (function () {
|
|
|
448
530
|
const nextRect = nextFocusable.component.getRect();
|
|
449
531
|
|
|
450
532
|
if (nextRect && nextRect.width > 0) {
|
|
451
|
-
|
|
452
|
-
nextFocusable.component,
|
|
453
|
-
direction
|
|
454
|
-
);
|
|
455
|
-
|
|
533
|
+
// TODO: Move this to setFocus method
|
|
456
534
|
blur(direction);
|
|
457
535
|
|
|
458
|
-
|
|
459
|
-
nextFocusable.component.props.groupId !==
|
|
460
|
-
currentFocusNode?.component?.props?.groupId;
|
|
461
|
-
|
|
462
|
-
setFocus(nextFocusable.id, direction, {
|
|
463
|
-
groupFocusedChanged: groupChanged,
|
|
464
|
-
});
|
|
536
|
+
setFocus(nextFocusable.id, direction);
|
|
465
537
|
}
|
|
466
538
|
} else {
|
|
467
539
|
currentFocusNode.component.failedLostFocus(currentFocusNode, direction);
|
|
468
540
|
}
|
|
541
|
+
} else {
|
|
542
|
+
recoverFocus();
|
|
469
543
|
}
|
|
470
544
|
}
|
|
471
545
|
|
|
@@ -481,34 +555,6 @@ export const focusManager = (function () {
|
|
|
481
555
|
|
|
482
556
|
const { on, invokeHandler, removeHandler } = eventHandler;
|
|
483
557
|
|
|
484
|
-
on(FOCUS_EVENTS.FOCUS, (focusable, direction) => {
|
|
485
|
-
if (focusable.props.groupId) {
|
|
486
|
-
const parent = focusableTree.findInTree(focusable.props.groupId);
|
|
487
|
-
parent?.component?.onFocus(focusable.component, direction);
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
on(FOCUS_EVENTS.BLUR, (focusable, direction) => {
|
|
492
|
-
if (focusable.props.groupId) {
|
|
493
|
-
const parent = focusableTree.findInTree(focusable.props.groupId);
|
|
494
|
-
parent?.component?.onBlur(focusable.component, direction);
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
on(FOCUS_EVENTS.HAS_LOST_FOCUS, (focusable, direction) => {
|
|
499
|
-
if (focusable.props.groupId) {
|
|
500
|
-
const parent = focusableTree.findInTree(focusable.props.groupId);
|
|
501
|
-
parent?.component?.hasLostFocus(parent.component, direction);
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
on(FOCUS_EVENTS.WILL_RECEIVE_FOCUS, (focusable, direction) => {
|
|
506
|
-
if (focusable.props.groupId) {
|
|
507
|
-
const parent = focusableTree.findInTree(focusable.props.groupId);
|
|
508
|
-
parent?.component?.willReceiveFocus(focusable.component, direction);
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
|
|
512
558
|
/**
|
|
513
559
|
* this is the list of the functions available externally
|
|
514
560
|
* when importing the focus manager
|
|
@@ -526,6 +572,7 @@ export const focusManager = (function () {
|
|
|
526
572
|
invokeHandler,
|
|
527
573
|
removeHandler,
|
|
528
574
|
longPress,
|
|
575
|
+
pressIn,
|
|
529
576
|
pressOut,
|
|
530
577
|
enableFocus,
|
|
531
578
|
setInitialFocus,
|
|
@@ -537,5 +584,6 @@ export const focusManager = (function () {
|
|
|
537
584
|
isGroupItemFocused,
|
|
538
585
|
recoverFocus,
|
|
539
586
|
isCurrentFocusOnTheTopScreen,
|
|
587
|
+
resetFocus,
|
|
540
588
|
};
|
|
541
589
|
})();
|
|
@@ -231,4 +231,27 @@ export class Tree {
|
|
|
231
231
|
|
|
232
232
|
return retVal;
|
|
233
233
|
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Gets the IDs of all parent groups for a given node id.
|
|
237
|
+
* @public
|
|
238
|
+
* @param {String} nodeId - The id of the node to get parent IDs for.
|
|
239
|
+
* @returns {Array} An array of parent group IDs or an empty array if the node is not found.
|
|
240
|
+
*/
|
|
241
|
+
getParentGroupIds(nodeId) {
|
|
242
|
+
const parentIds = [];
|
|
243
|
+
let currentNode = this.findInTree(nodeId);
|
|
244
|
+
|
|
245
|
+
if (!currentNode) {
|
|
246
|
+
return []; // Node not found, return an empty array
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Traverse up the tree, collecting parent IDs
|
|
250
|
+
while (currentNode.parent && currentNode.parent.id !== "root") {
|
|
251
|
+
parentIds.push(currentNode.parent.id);
|
|
252
|
+
currentNode = currentNode.parent;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return parentIds;
|
|
256
|
+
}
|
|
234
257
|
}
|