@applicaster/zapp-react-native-utils 15.0.0-alpha.3514407021 → 15.0.0-alpha.3564837831
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/actionsExecutor/ActionExecutorContext.tsx +3 -6
- package/actionsExecutor/feedDecorator.ts +6 -6
- package/adsUtils/index.ts +2 -2
- package/analyticsUtils/README.md +1 -1
- package/appUtils/HooksManager/index.ts +10 -10
- package/appUtils/RiverFocusManager/{index.js → index.ts} +25 -18
- package/appUtils/accessibilityManager/__tests__/utils.test.ts +360 -0
- package/appUtils/accessibilityManager/const.ts +4 -0
- package/appUtils/accessibilityManager/hooks.ts +20 -13
- package/appUtils/accessibilityManager/index.ts +28 -1
- package/appUtils/accessibilityManager/utils.ts +59 -8
- package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +3 -0
- package/appUtils/focusManager/index.ios.ts +44 -4
- package/appUtils/focusManagerAux/utils/index.ios.ts +122 -0
- package/appUtils/focusManagerAux/utils/index.ts +1 -1
- package/appUtils/focusManagerAux/utils/utils.ios.ts +210 -3
- package/appUtils/keyCodes/keys/keys.web.ts +1 -4
- package/appUtils/orientationHelper.ts +2 -4
- package/appUtils/platform/platformUtils.ts +117 -18
- package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +94 -4
- package/appUtils/playerManager/OverlayObserver/utils.ts +32 -20
- package/appUtils/playerManager/player.ts +4 -0
- package/appUtils/playerManager/playerNative.ts +29 -16
- package/appUtils/playerManager/usePlayerState.tsx +14 -2
- package/cellUtils/index.ts +32 -0
- package/configurationUtils/__tests__/manifestKeyParser.test.ts +26 -26
- package/focusManager/aux/index.ts +1 -1
- package/manifestUtils/defaultManifestConfigurations/player.js +96 -11
- package/manifestUtils/keys.js +21 -0
- package/manifestUtils/sharedConfiguration/screenPicker/utils.js +1 -0
- package/manifestUtils/tvAction/container/index.js +1 -1
- package/package.json +2 -2
- package/playerUtils/usePlayerTTS.ts +8 -3
- package/pluginUtils/index.ts +4 -0
- package/reactHooks/advertising/index.ts +2 -2
- package/reactHooks/debugging/__tests__/index.test.js +4 -4
- package/reactHooks/device/useMemoizedIsTablet.ts +3 -3
- package/reactHooks/feed/__tests__/useEntryScreenId.test.tsx +3 -0
- package/reactHooks/feed/__tests__/{useInflatedUrl.test.ts → useInflatedUrl.test.tsx} +62 -7
- package/reactHooks/feed/useEntryScreenId.ts +2 -2
- package/reactHooks/feed/useInflatedUrl.ts +43 -17
- package/reactHooks/flatList/useLoadNextPageIfNeeded.ts +13 -16
- package/reactHooks/layout/index.ts +1 -1
- package/reactHooks/layout/useDimensions/__tests__/{useDimensions.test.ts → useDimensions.test.tsx} +105 -25
- package/reactHooks/layout/useDimensions/useDimensions.ts +2 -2
- package/reactHooks/navigation/index.ts +11 -6
- package/reactHooks/navigation/useRoute.ts +8 -6
- package/reactHooks/player/TVSeekControlller/TVSeekController.ts +27 -10
- package/reactHooks/resolvers/useCellResolver.ts +6 -2
- package/reactHooks/resolvers/useComponentResolver.ts +8 -2
- package/reactHooks/screen/__tests__/useCurrentScreenData.test.tsx +1 -1
- package/reactHooks/screen/__tests__/useTargetScreenData.test.tsx +11 -3
- package/reactHooks/screen/useTargetScreenData.ts +4 -2
- package/reactHooks/state/useRivers.ts +1 -1
- package/reactHooks/usePluginConfiguration.ts +2 -2
- package/searchUtils/const.ts +7 -0
- package/searchUtils/index.ts +3 -0
- package/testUtils/index.tsx +30 -21
- package/utils/__tests__/mapAccum.test.ts +73 -0
- package/utils/__tests__/mergeRight.test.ts +48 -0
- package/utils/__tests__/selectors.test.ts +124 -0
- package/utils/index.ts +17 -0
- package/utils/mapAccum.ts +23 -0
- package/utils/mergeRight.ts +5 -0
- package/utils/path.ts +6 -3
- package/utils/pathOr.ts +5 -1
- package/utils/selectors.ts +46 -0
- package/zappFrameworkUtils/HookCallback/callbackNavigationAction.ts +34 -11
- package/zappFrameworkUtils/HookCallback/hookCallbackManifestExtensions.config.js +1 -1
|
@@ -36,7 +36,20 @@ export class AccessibilityManager {
|
|
|
36
36
|
false
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
-
private constructor() {
|
|
39
|
+
private constructor() {
|
|
40
|
+
this.ttsManager
|
|
41
|
+
.getScreenReaderEnabledAsObservable()
|
|
42
|
+
.subscribe((enabled) => {
|
|
43
|
+
const state = this.state$.getValue();
|
|
44
|
+
|
|
45
|
+
if (state.screenReaderEnabled !== enabled) {
|
|
46
|
+
this.state$.next({
|
|
47
|
+
...state,
|
|
48
|
+
screenReaderEnabled: enabled,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
40
53
|
|
|
41
54
|
public static getInstance(): AccessibilityManager {
|
|
42
55
|
if (!AccessibilityManager._instance) {
|
|
@@ -92,8 +105,15 @@ export class AccessibilityManager {
|
|
|
92
105
|
/**
|
|
93
106
|
* Adds a heading to the queue, headings will be read before the next text
|
|
94
107
|
* Each heading will be read once and removed from the queue
|
|
108
|
+
* Does nothing if screen reader is not enabled
|
|
95
109
|
*/
|
|
96
110
|
public addHeading(heading: string) {
|
|
111
|
+
const state = this.state$.getValue();
|
|
112
|
+
|
|
113
|
+
if (!state.screenReaderEnabled) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
97
117
|
if (!this.pendingFocusId) {
|
|
98
118
|
this.pendingFocusId = Date.now().toString();
|
|
99
119
|
}
|
|
@@ -108,6 +128,7 @@ export class AccessibilityManager {
|
|
|
108
128
|
*
|
|
109
129
|
* Implements a delay mechanism to reduce noise during rapid navigation.
|
|
110
130
|
* Only the most recent announcement will be read after the delay period.
|
|
131
|
+
* Does nothing if screen reader is not enabled
|
|
111
132
|
*/
|
|
112
133
|
public readText({
|
|
113
134
|
text,
|
|
@@ -116,6 +137,12 @@ export class AccessibilityManager {
|
|
|
116
137
|
text: string;
|
|
117
138
|
keyOfLocalizedText?: string;
|
|
118
139
|
}) {
|
|
140
|
+
const state = this.state$.getValue();
|
|
141
|
+
|
|
142
|
+
if (!state.screenReaderEnabled) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
119
146
|
let textToRead = text;
|
|
120
147
|
|
|
121
148
|
if (keyOfLocalizedText) {
|
|
@@ -1,24 +1,75 @@
|
|
|
1
|
+
import { createLogger } from "../../logger";
|
|
2
|
+
|
|
3
|
+
const { log_error } = createLogger({
|
|
4
|
+
category: "AccessibilityManager",
|
|
5
|
+
subsystem: "AppUtils",
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
/**
|
|
2
9
|
* Calculates the reading time for a given text based on word count
|
|
3
|
-
* @param text - The text to calculate the reading time for
|
|
4
|
-
* @param wordsPerMinute - Words per minute reading speed (default:
|
|
10
|
+
* @param text - The text to calculate the reading time for (string or number)
|
|
11
|
+
* @param wordsPerMinute - Words per minute reading speed (default: 140)
|
|
5
12
|
* @param minimumPause - Minimum pause time in milliseconds (default: 500)
|
|
6
13
|
* @param announcementDelay - Additional delay for announcement in milliseconds (default: 700)
|
|
7
14
|
* @returns The reading time in milliseconds
|
|
8
15
|
*/
|
|
9
16
|
export function calculateReadingTime(
|
|
10
|
-
text: string,
|
|
17
|
+
text: string | number,
|
|
11
18
|
wordsPerMinute: number = 140,
|
|
12
19
|
minimumPause: number = 500,
|
|
13
20
|
announcementDelay: number = 700
|
|
14
21
|
): number {
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
if (typeof text !== "string" && typeof text !== "number") {
|
|
23
|
+
log_error(
|
|
24
|
+
`Invalid text input for reading time calculation got: ${
|
|
25
|
+
typeof text === "symbol" ? String(text) : text
|
|
26
|
+
}`
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const trimmed = typeof text === "number" ? String(text) : text.trim();
|
|
33
|
+
|
|
34
|
+
if (!trimmed) {
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const words = trimmed
|
|
17
39
|
.split(/(?<=\d)(?=[a-zA-Z])|(?<=[a-zA-Z])(?=\d)|[^\w\s]+|\s+/)
|
|
18
40
|
.filter(Boolean).length;
|
|
19
41
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
42
|
+
// Count spaces - multiple consecutive spaces add extra pause time
|
|
43
|
+
const spaceMatches: string[] = trimmed.match(/\s+/g) || [];
|
|
44
|
+
|
|
45
|
+
const totalSpaces = spaceMatches.reduce(
|
|
46
|
+
(sum: number, match: string) => sum + match.length,
|
|
47
|
+
0
|
|
23
48
|
);
|
|
49
|
+
|
|
50
|
+
const extraSpaces = Math.max(0, totalSpaces - (words - 1)); // words-1 is normal spacing
|
|
51
|
+
|
|
52
|
+
// Heuristic: punctuation increases TTS duration beyond word-based WPM.
|
|
53
|
+
// Commas typically introduce short pauses, sentence terminators longer ones.
|
|
54
|
+
const commaCount = (trimmed.match(/,/g) || []).length;
|
|
55
|
+
const semicolonCount = (trimmed.match(/;/g) || []).length;
|
|
56
|
+
const colonCount = (trimmed.match(/:/g) || []).length;
|
|
57
|
+
const dashCount = (trimmed.match(/\u2013|\u2014|-/g) || []).length; // – — -
|
|
58
|
+
const sentenceEndCount = (trimmed.match(/[.!?](?!\d)/g) || []).length;
|
|
59
|
+
|
|
60
|
+
const commaPauseMs = 220; // short prosody pause for ","
|
|
61
|
+
const midPauseMs = 260; // for ";", ":", dashes
|
|
62
|
+
const sentenceEndPauseMs = 420; // for ".", "!", "?"
|
|
63
|
+
const extraSpacePauseMs = 50; // per extra space beyond normal spacing
|
|
64
|
+
|
|
65
|
+
const punctuationPause =
|
|
66
|
+
commaCount * commaPauseMs +
|
|
67
|
+
(semicolonCount + colonCount + dashCount) * midPauseMs +
|
|
68
|
+
sentenceEndCount * sentenceEndPauseMs +
|
|
69
|
+
extraSpaces * extraSpacePauseMs;
|
|
70
|
+
|
|
71
|
+
const baseByWordsMs = (words / wordsPerMinute) * 60 * 1000;
|
|
72
|
+
const estimatedMs = Math.max(minimumPause, baseByWordsMs + punctuationPause);
|
|
73
|
+
|
|
74
|
+
return estimatedMs + announcementDelay;
|
|
24
75
|
}
|
|
@@ -71,6 +71,9 @@ exports[`focusManagerIOS should be defined 1`] = `
|
|
|
71
71
|
"invokeHandler": [Function],
|
|
72
72
|
"isChildOf": [Function],
|
|
73
73
|
"isFocusOn": [Function],
|
|
74
|
+
"isFocusOnContent": [Function],
|
|
75
|
+
"isFocusOnMenu": [Function],
|
|
76
|
+
"isFocusOnTabsScreen": [Function],
|
|
74
77
|
"isGroupItemFocused": [Function],
|
|
75
78
|
"moveFocus": [Function],
|
|
76
79
|
"on": [Function],
|
|
@@ -4,7 +4,11 @@ import * as R from "ramda";
|
|
|
4
4
|
import {
|
|
5
5
|
isCurrentFocusOn,
|
|
6
6
|
isChildOf as isChildOfUtils,
|
|
7
|
-
|
|
7
|
+
isPartOfMenu,
|
|
8
|
+
isPartOfContent,
|
|
9
|
+
isPartOfTabsScreen,
|
|
10
|
+
} from "../focusManagerAux/utils/index.ios";
|
|
11
|
+
|
|
8
12
|
import { Tree } from "./treeDataStructure/Tree";
|
|
9
13
|
import { findFocusableNode } from "./treeDataStructure/Utils";
|
|
10
14
|
import { subscriber } from "../../functionUtils";
|
|
@@ -13,6 +17,7 @@ import { findChild } from "./utils";
|
|
|
13
17
|
import {
|
|
14
18
|
emitRegistered,
|
|
15
19
|
emitUnregistered,
|
|
20
|
+
emitWillFocused,
|
|
16
21
|
} from "../focusManagerAux/utils/utils.ios";
|
|
17
22
|
|
|
18
23
|
const { FocusableManagerModule } = NativeModules;
|
|
@@ -188,9 +193,15 @@ export const focusManager = (function () {
|
|
|
188
193
|
function register({ id, component }) {
|
|
189
194
|
const { isGroup = false } = component;
|
|
190
195
|
|
|
191
|
-
|
|
196
|
+
if (isGroup) {
|
|
197
|
+
registerGroup(id, component);
|
|
198
|
+
} else {
|
|
199
|
+
registerItem(id, component);
|
|
200
|
+
}
|
|
192
201
|
|
|
193
|
-
|
|
202
|
+
const groupId = component?.props?.groupId;
|
|
203
|
+
|
|
204
|
+
emitRegistered({ id, groupId, isGroup });
|
|
194
205
|
}
|
|
195
206
|
|
|
196
207
|
function unregister(id, { group = false } = {}) {
|
|
@@ -273,12 +284,16 @@ export const focusManager = (function () {
|
|
|
273
284
|
function setFocus(
|
|
274
285
|
id: string,
|
|
275
286
|
direction?: FocusManager.IOS.Direction,
|
|
276
|
-
options?: Partial<{
|
|
287
|
+
options?: Partial<{
|
|
288
|
+
groupFocusedChanged: boolean;
|
|
289
|
+
}>,
|
|
277
290
|
callback?: any
|
|
278
291
|
) {
|
|
279
292
|
blur(direction);
|
|
280
293
|
currentFocus = id;
|
|
281
294
|
|
|
295
|
+
emitWillFocused(id);
|
|
296
|
+
|
|
282
297
|
// Extract groupFocusedChanged with a default value of false
|
|
283
298
|
let groupFocusedChanged = false;
|
|
284
299
|
|
|
@@ -412,6 +427,28 @@ export const focusManager = (function () {
|
|
|
412
427
|
return id && isCurrentFocusOn(id, currentFocusNode);
|
|
413
428
|
}
|
|
414
429
|
|
|
430
|
+
function isFocusOnMenu(): boolean {
|
|
431
|
+
const currentFocusable = getCurrentFocus();
|
|
432
|
+
|
|
433
|
+
return isPartOfMenu(focusableTree, currentFocusable?.props?.id);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function isFocusOnContent(): boolean {
|
|
437
|
+
const currentFocusable = getCurrentFocus();
|
|
438
|
+
|
|
439
|
+
return isPartOfContent(focusableTree, currentFocusable?.props?.id);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function isFocusOnTabsScreen(tabsScreenContainerId: string): boolean {
|
|
443
|
+
const currentFocusable = getCurrentFocus();
|
|
444
|
+
|
|
445
|
+
return isPartOfTabsScreen(
|
|
446
|
+
focusableTree,
|
|
447
|
+
tabsScreenContainerId,
|
|
448
|
+
currentFocusable?.props?.id
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
415
452
|
function isChildOf(childId, parentId): boolean {
|
|
416
453
|
return isChildOfUtils(focusableTree, childId, parentId);
|
|
417
454
|
}
|
|
@@ -438,6 +475,9 @@ export const focusManager = (function () {
|
|
|
438
475
|
isGroupItemFocused,
|
|
439
476
|
getPreferredFocusChild,
|
|
440
477
|
isFocusOn,
|
|
478
|
+
isFocusOnMenu,
|
|
479
|
+
isFocusOnContent,
|
|
480
|
+
isFocusOnTabsScreen,
|
|
441
481
|
isChildOf,
|
|
442
482
|
};
|
|
443
483
|
})();
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { isNil, startsWith } from "@applicaster/zapp-react-native-utils/utils";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
QUICK_BRICK_CONTENT,
|
|
5
|
+
QUICK_BRICK_NAVBAR,
|
|
6
|
+
} from "@applicaster/quick-brick-core/const";
|
|
7
|
+
|
|
8
|
+
const isNavBar = (node) => startsWith(QUICK_BRICK_NAVBAR, node?.id);
|
|
9
|
+
const isContent = (node) => startsWith(QUICK_BRICK_CONTENT, node?.id);
|
|
10
|
+
const isRoot = (node) => node?.id === "root";
|
|
11
|
+
|
|
12
|
+
export const isPartOfTabsScreen = (
|
|
13
|
+
focusableTree,
|
|
14
|
+
tabsScreenContainerId,
|
|
15
|
+
id
|
|
16
|
+
) => {
|
|
17
|
+
const node = focusableTree.findInTree(id);
|
|
18
|
+
|
|
19
|
+
if (isNil(node)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (isRoot(node)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (isNavBar(node)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isContent(node)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (node?.id === tabsScreenContainerId) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return isPartOfTabsScreen(
|
|
40
|
+
focusableTree,
|
|
41
|
+
tabsScreenContainerId,
|
|
42
|
+
node.parent?.id
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const isPartOfMenu = (focusableTree, id): boolean => {
|
|
47
|
+
const node = focusableTree.findInTree(id);
|
|
48
|
+
|
|
49
|
+
if (isNil(node)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (isRoot(node)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (isNavBar(node)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isContent(node)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return isPartOfMenu(focusableTree, node.parent?.id);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const isPartOfContent = (focusableTree, id) => {
|
|
69
|
+
const node = focusableTree.findInTree(id);
|
|
70
|
+
|
|
71
|
+
if (isNil(node)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (isRoot(node)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isNavBar(node)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (isContent(node)) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return isPartOfContent(focusableTree, node.parent?.id);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const isCurrentFocusOn = (id, node) => {
|
|
91
|
+
if (!node) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isRoot(node)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (node?.id === id) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return isCurrentFocusOn(id, node.parent);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const isChildOf = (focusableTree, childId, parentId) => {
|
|
107
|
+
if (isNil(childId) || isNil(parentId)) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const childNode = focusableTree.findInTree(childId);
|
|
112
|
+
|
|
113
|
+
if (isNil(childNode)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (childNode.parent?.id === parentId) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return isChildOf(focusableTree, childNode.parent?.id, parentId);
|
|
122
|
+
};
|
|
@@ -102,7 +102,7 @@ export const getNavbarNode = (focusableTree) => {
|
|
|
102
102
|
|
|
103
103
|
export const waitForContent = (focusableTree) => {
|
|
104
104
|
const contentHasAnyChildren = (): boolean => {
|
|
105
|
-
const countOfChildren = pathOr(
|
|
105
|
+
const countOfChildren = pathOr<number>(
|
|
106
106
|
0,
|
|
107
107
|
["children", "length"],
|
|
108
108
|
getContentNode(focusableTree)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { ReplaySubject } from "rxjs";
|
|
2
|
-
import { filter } from "rxjs/operators";
|
|
1
|
+
import { ReplaySubject, Subject } from "rxjs";
|
|
2
|
+
import { filter, switchMap, take, withLatestFrom, map } from "rxjs/operators";
|
|
3
|
+
|
|
3
4
|
import { BUTTON_PREFIX } from "@applicaster/zapp-react-native-ui-components/Components/MasterCell/DefaultComponents/tv/TvActionButtons/const";
|
|
4
5
|
import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
|
|
6
|
+
import { isPartOfMenu, isPartOfContent } from "./index.ios";
|
|
5
7
|
|
|
6
8
|
type FocusableID = string;
|
|
7
9
|
type RegistrationEvent = {
|
|
@@ -9,6 +11,25 @@ type RegistrationEvent = {
|
|
|
9
11
|
registered: boolean;
|
|
10
12
|
};
|
|
11
13
|
|
|
14
|
+
let focusableViewRegistrationSubject$ = new Subject<{
|
|
15
|
+
id: FocusableID;
|
|
16
|
+
groupId: FocusableID;
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
let focusableGroupRegistrationSubject$ = new ReplaySubject<{
|
|
20
|
+
id: FocusableID;
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
let focusableNativeViewRegistrationSubject$ = new Subject<{
|
|
24
|
+
id: FocusableID;
|
|
25
|
+
groupId: FocusableID;
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
let focusableNativeGroupRegistrationSubject$ = new ReplaySubject<{
|
|
29
|
+
id: FocusableID;
|
|
30
|
+
groupId: FocusableID;
|
|
31
|
+
}>();
|
|
32
|
+
|
|
12
33
|
const isFocusableButton = (id: Option<FocusableID>): boolean =>
|
|
13
34
|
id && id.includes?.(BUTTON_PREFIX);
|
|
14
35
|
|
|
@@ -22,14 +43,200 @@ export const focusableButtonsRegistration$ = (focusableGroupId: string) =>
|
|
|
22
43
|
)
|
|
23
44
|
);
|
|
24
45
|
|
|
25
|
-
export const
|
|
46
|
+
export const resetFocusableRegistration = () => {
|
|
47
|
+
// complete the old subject so subscribers are notified and resources are freed
|
|
48
|
+
if (!focusableViewRegistrationSubject$.closed) {
|
|
49
|
+
focusableViewRegistrationSubject$.complete();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!focusableGroupRegistrationSubject$.closed) {
|
|
53
|
+
focusableGroupRegistrationSubject$.complete();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!focusableNativeViewRegistrationSubject$.closed) {
|
|
57
|
+
focusableNativeViewRegistrationSubject$.complete();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!focusableNativeGroupRegistrationSubject$.closed) {
|
|
61
|
+
focusableNativeGroupRegistrationSubject$.complete();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
focusableViewRegistrationSubject$ = new Subject<{
|
|
65
|
+
id: FocusableID;
|
|
66
|
+
groupId: FocusableID;
|
|
67
|
+
}>();
|
|
68
|
+
|
|
69
|
+
focusableGroupRegistrationSubject$ = new ReplaySubject<{
|
|
70
|
+
id: FocusableID;
|
|
71
|
+
}>();
|
|
72
|
+
|
|
73
|
+
focusableNativeViewRegistrationSubject$ = new Subject<{
|
|
74
|
+
id: FocusableID;
|
|
75
|
+
groupId: FocusableID;
|
|
76
|
+
}>();
|
|
77
|
+
|
|
78
|
+
focusableNativeGroupRegistrationSubject$ = new ReplaySubject<{
|
|
79
|
+
id: FocusableID;
|
|
80
|
+
groupId: FocusableID;
|
|
81
|
+
}>();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const focusableNativeViewRegistration = ({ focusableView, focusableGroup }) => {
|
|
85
|
+
return focusableNativeViewRegistrationSubject$.pipe(
|
|
86
|
+
filter(
|
|
87
|
+
(focusableNativeView) => focusableNativeView.id === focusableView.id
|
|
88
|
+
),
|
|
89
|
+
take(1),
|
|
90
|
+
switchMap((focusableNativeView) =>
|
|
91
|
+
// start waiting registration of its parent FocusableNativeGroup
|
|
92
|
+
focusableNativeGroupRegistrationSubject$.pipe(
|
|
93
|
+
filter(
|
|
94
|
+
(focusableNativeGroup) =>
|
|
95
|
+
focusableNativeGroup.id === focusableNativeView.groupId
|
|
96
|
+
),
|
|
97
|
+
take(1),
|
|
98
|
+
map((focusableNativeGroup) => ({
|
|
99
|
+
focusableNativeGroup,
|
|
100
|
+
focusableNativeView,
|
|
101
|
+
focusableView,
|
|
102
|
+
focusableGroup,
|
|
103
|
+
}))
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const firstFocusableViewRegistrationFactory = () =>
|
|
110
|
+
focusableViewRegistrationSubject$.pipe(
|
|
111
|
+
take(1), // we care about only first FocusableView registration
|
|
112
|
+
switchMap((focusableView) =>
|
|
113
|
+
// start waiting registration of its parent FocusableGroup
|
|
114
|
+
focusableGroupRegistrationSubject$.pipe(
|
|
115
|
+
filter((focusableGroup) => focusableGroup.id === focusableView.groupId),
|
|
116
|
+
take(1),
|
|
117
|
+
map((focusableGroup) => ({
|
|
118
|
+
focusableView,
|
|
119
|
+
focusableGroup,
|
|
120
|
+
}))
|
|
121
|
+
)
|
|
122
|
+
),
|
|
123
|
+
// start waiting registration for FocusableNativeView and its parent FocusableNativeGroup
|
|
124
|
+
switchMap(({ focusableView, focusableGroup }) =>
|
|
125
|
+
focusableNativeViewRegistration({
|
|
126
|
+
focusableView,
|
|
127
|
+
focusableGroup,
|
|
128
|
+
})
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// registration on RN level(into RN focusManager)
|
|
133
|
+
export const emitRegistered = ({
|
|
134
|
+
id,
|
|
135
|
+
groupId,
|
|
136
|
+
isGroup,
|
|
137
|
+
}: {
|
|
138
|
+
id: Option<FocusableID>;
|
|
139
|
+
groupId: Option<FocusableID>;
|
|
140
|
+
isGroup: boolean;
|
|
141
|
+
}): void => {
|
|
26
142
|
if (isFocusableButton(id)) {
|
|
27
143
|
registeredSubject$.next({ id, registered: true });
|
|
28
144
|
}
|
|
145
|
+
|
|
146
|
+
if (isGroup && id) {
|
|
147
|
+
focusableGroupRegistrationSubject$.next({ id });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!isGroup && id && groupId) {
|
|
151
|
+
focusableViewRegistrationSubject$.next({ id, groupId });
|
|
152
|
+
}
|
|
29
153
|
};
|
|
30
154
|
|
|
155
|
+
// unregistration on RN level(into RN focusManager)
|
|
31
156
|
export const emitUnregistered = (id: Option<FocusableID>): void => {
|
|
32
157
|
if (isFocusableButton(id)) {
|
|
33
158
|
registeredSubject$.next({ id, registered: false });
|
|
34
159
|
}
|
|
35
160
|
};
|
|
161
|
+
|
|
162
|
+
// registration focusableNativeView and focusableNativeGroup
|
|
163
|
+
export const emitNativeRegistered = ({
|
|
164
|
+
id,
|
|
165
|
+
groupId,
|
|
166
|
+
isGroup,
|
|
167
|
+
}: {
|
|
168
|
+
id: Option<FocusableID>;
|
|
169
|
+
groupId: Option<FocusableID>;
|
|
170
|
+
isGroup: boolean;
|
|
171
|
+
}): void => {
|
|
172
|
+
if (!isGroup && id && groupId) {
|
|
173
|
+
focusableNativeViewRegistrationSubject$.next({ id, groupId });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (isGroup && id && groupId) {
|
|
177
|
+
focusableNativeGroupRegistrationSubject$.next({ id, groupId });
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Focusable has been focused
|
|
182
|
+
const willFocusedSubject$ = new Subject<FocusableID>();
|
|
183
|
+
const didFocusedSubject$ = new Subject<FocusableID>();
|
|
184
|
+
|
|
185
|
+
export const willFocused$ = willFocusedSubject$.asObservable();
|
|
186
|
+
|
|
187
|
+
export const didFocused$ = didFocusedSubject$.asObservable();
|
|
188
|
+
|
|
189
|
+
export const emitWillFocused = (id: FocusableID): void => {
|
|
190
|
+
willFocusedSubject$.next(id);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const emitDidFocused = (id: FocusableID): void => {
|
|
194
|
+
didFocusedSubject$.next(id);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const topMenuItemDidFocused$ = didFocused$.pipe(
|
|
198
|
+
filter((id) => id && isPartOfMenu(focusManager.focusableTree, id))
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
export const contentDidFocused$ = didFocused$.pipe(
|
|
202
|
+
filter((id) => {
|
|
203
|
+
const isContent = isPartOfContent(focusManager.focusableTree, id);
|
|
204
|
+
|
|
205
|
+
return id && isContent;
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
// Focusable has been focused
|
|
209
|
+
|
|
210
|
+
const createFocusableRegistry = () => {
|
|
211
|
+
const subject$ = new ReplaySubject<FocusableID | undefined>(1);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
observable$: subject$.asObservable(),
|
|
215
|
+
register: (id: FocusableID) => {
|
|
216
|
+
// save focusable_id on registration
|
|
217
|
+
subject$.next(id);
|
|
218
|
+
},
|
|
219
|
+
unregister: () => {
|
|
220
|
+
// reset focusable_id on unregistration
|
|
221
|
+
subject$.next(undefined);
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/// HOME_TOP_MENU_ITEM
|
|
227
|
+
export const HomeTopMenuItemRegistry = createFocusableRegistry();
|
|
228
|
+
|
|
229
|
+
export const homeTopMenuItemDidFocused$ = topMenuItemDidFocused$.pipe(
|
|
230
|
+
withLatestFrom(HomeTopMenuItemRegistry.observable$),
|
|
231
|
+
filter(([id, homeId]) => id === homeId)
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
/// TABS SCREEN
|
|
235
|
+
export const TabsScreenContentContainerRegistry = createFocusableRegistry();
|
|
236
|
+
|
|
237
|
+
export const TabsScreenScreenSelectorContainerRegistry =
|
|
238
|
+
createFocusableRegistry();
|
|
239
|
+
/// TABS SCREEN
|
|
240
|
+
|
|
241
|
+
/// SEARCH_INPUT
|
|
242
|
+
export const SearchInputRegistry = createFocusableRegistry();
|
|
@@ -10,10 +10,7 @@ import { Platform } from "react-native";
|
|
|
10
10
|
* platformKeys[Platform.OS] should only include keys
|
|
11
11
|
* that are unique to that platform, i.e. Exit: { keyCode: 10182 }
|
|
12
12
|
*/
|
|
13
|
-
export const KEYS = Object.assign(
|
|
14
|
-
platformKeys["web"],
|
|
15
|
-
platformKeys[Platform.OS]
|
|
16
|
-
);
|
|
13
|
+
export const KEYS = Object.assign(platformKeys.web, platformKeys[Platform.OS]);
|
|
17
14
|
|
|
18
15
|
export const ARROW_KEYS = [
|
|
19
16
|
KEYS.ArrowUp,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ReactNative from "react-native";
|
|
2
|
-
import {
|
|
2
|
+
import { useAppData } from "@applicaster/zapp-react-native-redux/hooks";
|
|
3
3
|
|
|
4
4
|
import { isTV, platformSelect } from "../reactUtils";
|
|
5
5
|
import { useIsTablet } from "../reactHooks";
|
|
@@ -184,9 +184,7 @@ export const getScreenOrientation = ({
|
|
|
184
184
|
|
|
185
185
|
export const useGetScreenOrientation = (screenData) => {
|
|
186
186
|
const isTablet = useIsTablet();
|
|
187
|
-
|
|
188
|
-
const { appData } = usePickFromState(["appData"]);
|
|
189
|
-
const isTabletPortrait = appData?.isTabletPortrait;
|
|
187
|
+
const { isTabletPortrait } = useAppData();
|
|
190
188
|
|
|
191
189
|
return getScreenOrientation({
|
|
192
190
|
screenData,
|