@applicaster/zapp-react-native-utils 14.0.0-alpha.2482261241 → 14.0.0-alpha.2802628909
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/AnalyticPlayerListener.ts +5 -2
- package/analyticsUtils/playerAnalyticsTracker.ts +2 -1
- package/appUtils/accessibilityManager/const.ts +13 -0
- package/appUtils/accessibilityManager/hooks.ts +35 -1
- package/appUtils/accessibilityManager/index.ts +151 -30
- package/appUtils/accessibilityManager/utils.ts +24 -0
- package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +4 -1
- package/appUtils/focusManager/events.ts +2 -0
- package/appUtils/focusManager/index.ios.ts +18 -1
- package/appUtils/focusManager/index.ts +31 -27
- package/appUtils/focusManagerAux/utils/index.ts +20 -14
- package/appUtils/focusManagerAux/utils/utils.ios.ts +35 -0
- package/appUtils/platform/platformUtils.ts +33 -3
- package/appUtils/playerManager/conts.ts +21 -0
- package/arrayUtils/__tests__/allTruthy.test.ts +24 -0
- package/arrayUtils/__tests__/anyThruthy.test.ts +24 -0
- package/arrayUtils/index.ts +5 -0
- package/configurationUtils/index.ts +1 -1
- package/focusManager/FocusManager.ts +78 -4
- package/focusManager/aux/index.ts +98 -0
- package/focusManager/utils.ts +12 -6
- package/index.d.ts +1 -1
- package/manifestUtils/defaultManifestConfigurations/player.js +188 -2
- package/manifestUtils/index.js +4 -0
- package/manifestUtils/keys.js +12 -0
- package/manifestUtils/sharedConfiguration/screenPicker/stylesFields.js +6 -0
- package/navigationUtils/index.ts +20 -17
- package/package.json +2 -2
- package/playerUtils/PlayerTTS/PlayerTTS.ts +359 -0
- package/playerUtils/PlayerTTS/index.ts +1 -0
- package/playerUtils/getPlayerActionButtons.ts +1 -1
- package/playerUtils/usePlayerTTS.ts +21 -0
- package/reactHooks/feed/useFeedLoader.tsx +0 -9
- package/reactHooks/feed/useInflatedUrl.ts +23 -29
- package/reactHooks/feed/useLoadPipesDataDispatch.ts +7 -1
- package/reactHooks/layout/index.ts +1 -1
- package/screenPickerUtils/index.ts +6 -0
- package/utils/__tests__/endsWith.test.ts +30 -0
- package/utils/__tests__/omit.test.ts +19 -0
- package/utils/__tests__/path.test.ts +33 -0
- package/utils/__tests__/take.test.ts +40 -0
- package/utils/endsWith.ts +9 -0
- package/utils/index.ts +12 -1
- package/utils/omit.ts +5 -0
- package/utils/path.ts +5 -0
- package/utils/take.ts +5 -0
|
@@ -35,8 +35,11 @@ export class AnalyticPlayerListener
|
|
|
35
35
|
this.handleAnalyticEvent(PLAYBACK_EVENT.complete);
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
onError = (err:
|
|
39
|
-
this.handleAnalyticEvent(
|
|
38
|
+
onError = (err: QuickBrickPlayer.PlayerErrorI) => {
|
|
39
|
+
this.handleAnalyticEvent(
|
|
40
|
+
PLAYBACK_EVENT.error,
|
|
41
|
+
err.toObject?.() || { message: err.message }
|
|
42
|
+
);
|
|
40
43
|
};
|
|
41
44
|
|
|
42
45
|
onPlayerPause = (event) => {
|
|
@@ -105,7 +105,8 @@ export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
|
|
|
105
105
|
return this.getDateTimestamp();
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
this.mediaTime =
|
|
108
|
+
this.mediaTime =
|
|
109
|
+
this.playerState?.contentPosition || eventData?.currentTime;
|
|
109
110
|
|
|
110
111
|
return this.mediaTime;
|
|
111
112
|
}
|
|
@@ -65,4 +65,17 @@ export const BUTTON_ACCESSIBILITY_KEYS = {
|
|
|
65
65
|
label: "accessibility_menu_item_label",
|
|
66
66
|
hint: "accessibility_menu_item_hint",
|
|
67
67
|
},
|
|
68
|
+
skip_intro: {
|
|
69
|
+
label: "accessibility_skip_intro_label",
|
|
70
|
+
hint: "accessibility_skip_intro_hint",
|
|
71
|
+
},
|
|
72
|
+
// Top Menu Bar-specific buttons
|
|
73
|
+
top_menu_bar_item_selected: {
|
|
74
|
+
label: "accessibility_top_menu_bar_item_selected_label",
|
|
75
|
+
hint: "accessibility_top_menu_bar_item_selected_hint",
|
|
76
|
+
},
|
|
77
|
+
top_menu_title: {
|
|
78
|
+
label: "accessibility_top_menu_title_label",
|
|
79
|
+
hint: "accessibility_top_menu_hint",
|
|
80
|
+
},
|
|
68
81
|
} as const;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo } from "react";
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { AccessibilityManager } from "./index";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -38,3 +38,37 @@ export const useAccessibilityManager = (
|
|
|
38
38
|
|
|
39
39
|
return accessibilityManager;
|
|
40
40
|
};
|
|
41
|
+
|
|
42
|
+
export const useInitialAnnouncementReady = (
|
|
43
|
+
accessibilityManager: AccessibilityManager
|
|
44
|
+
) => {
|
|
45
|
+
const [isReady, setIsReady] = useState(
|
|
46
|
+
accessibilityManager.isInitialPlayerAnnouncementReady
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const subscription = accessibilityManager
|
|
51
|
+
.getInitialAnnouncementReadyObservable()
|
|
52
|
+
.subscribe(setIsReady);
|
|
53
|
+
|
|
54
|
+
return () => subscription.unsubscribe();
|
|
55
|
+
}, [accessibilityManager]);
|
|
56
|
+
|
|
57
|
+
return isReady;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const useAnnouncementActive = (
|
|
61
|
+
accessibilityManager: AccessibilityManager
|
|
62
|
+
) => {
|
|
63
|
+
const [isActive, setIsActive] = useState(false);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const subscription = accessibilityManager
|
|
67
|
+
.getTTSStateObservable()
|
|
68
|
+
.subscribe(setIsActive);
|
|
69
|
+
|
|
70
|
+
return () => subscription.unsubscribe();
|
|
71
|
+
}, [accessibilityManager]);
|
|
72
|
+
|
|
73
|
+
return isActive;
|
|
74
|
+
};
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
+
import * as R from "ramda";
|
|
1
2
|
import { BehaviorSubject } from "rxjs";
|
|
2
3
|
import { accessibilityManagerLogger as logger } from "./logger";
|
|
3
4
|
import { TTSManager } from "../platform";
|
|
4
5
|
import { BUTTON_ACCESSIBILITY_KEYS } from "./const";
|
|
5
6
|
import { toString } from "../../utils";
|
|
7
|
+
import { calculateReadingTime } from "./utils";
|
|
6
8
|
import { AccessibilityRole } from "react-native";
|
|
7
9
|
|
|
8
10
|
export class AccessibilityManager {
|
|
9
11
|
private static _instance: AccessibilityManager | null = null;
|
|
10
12
|
private headingTimeout: NodeJS.Timeout | null = null;
|
|
11
|
-
private
|
|
13
|
+
private announcementDelayTimeout: NodeJS.Timeout | null = null;
|
|
14
|
+
private WORDS_PER_MINUTE = 140;
|
|
12
15
|
private MINIMUM_PAUSE = 500;
|
|
16
|
+
private ANNOUNCEMENT_DELAY = 700;
|
|
13
17
|
private state$ = new BehaviorSubject<AccessibilityState>({
|
|
14
18
|
screenReaderEnabled: false,
|
|
15
19
|
reduceMotionEnabled: false,
|
|
@@ -25,6 +29,12 @@ export class AccessibilityManager {
|
|
|
25
29
|
private ttsManager = TTSManager.getInstance();
|
|
26
30
|
private localizations: { [key: string]: string } = {};
|
|
27
31
|
private headingQueue: string[] = [];
|
|
32
|
+
private currentFocusId: string | null = null;
|
|
33
|
+
private headingFocusMap: Map<string, string> = new Map();
|
|
34
|
+
private pendingFocusId: string | null = null;
|
|
35
|
+
private isInitialPlayerAnnouncementReady$ = new BehaviorSubject<boolean>(
|
|
36
|
+
false
|
|
37
|
+
);
|
|
28
38
|
|
|
29
39
|
private constructor() {}
|
|
30
40
|
|
|
@@ -36,6 +46,26 @@ export class AccessibilityManager {
|
|
|
36
46
|
return AccessibilityManager._instance;
|
|
37
47
|
}
|
|
38
48
|
|
|
49
|
+
public get isInitialPlayerAnnouncementReady(): boolean {
|
|
50
|
+
return this.isInitialPlayerAnnouncementReady$.getValue();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public setInitialPlayerAnnouncementReady(): void {
|
|
54
|
+
this.isInitialPlayerAnnouncementReady$.next(true);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public resetInitialPlayerAnnouncementReady(): void {
|
|
58
|
+
this.isInitialPlayerAnnouncementReady$.next(false);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public getInitialAnnouncementReadyObservable() {
|
|
62
|
+
return this.isInitialPlayerAnnouncementReady$.asObservable();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public getTTSStateObservable() {
|
|
66
|
+
return this.ttsManager.getStateAsObservable();
|
|
67
|
+
}
|
|
68
|
+
|
|
39
69
|
/**
|
|
40
70
|
* The method now accepts any object with localizations using a flattened structure
|
|
41
71
|
*
|
|
@@ -46,7 +76,9 @@ export class AccessibilityManager {
|
|
|
46
76
|
* i.e. localizations: [{ en: { accessibility_close_label: "Close", accessibility_close_hint: "Press here to close" } }]
|
|
47
77
|
*/
|
|
48
78
|
public updateLocalizations(localizations: { [key: string]: string }) {
|
|
49
|
-
|
|
79
|
+
if (!R.isEmpty(localizations)) {
|
|
80
|
+
this.localizations = localizations;
|
|
81
|
+
}
|
|
50
82
|
}
|
|
51
83
|
|
|
52
84
|
public getState(): AccessibilityState {
|
|
@@ -57,31 +89,25 @@ export class AccessibilityManager {
|
|
|
57
89
|
return this.state$.asObservable();
|
|
58
90
|
}
|
|
59
91
|
|
|
60
|
-
/** Calculates the reading time for a given text
|
|
61
|
-
* This method is a bit of a hack because we don't have a callback, or promise from VIZIO API
|
|
62
|
-
* @param text - The text to calculate the reading time for
|
|
63
|
-
* @returns The reading time in milliseconds
|
|
64
|
-
*/
|
|
65
|
-
private calculateReadingTime(text: string): number {
|
|
66
|
-
const words = text.trim().split(/\s+/).length;
|
|
67
|
-
|
|
68
|
-
return Math.max(
|
|
69
|
-
this.MINIMUM_PAUSE,
|
|
70
|
-
(words / this.WORDS_PER_MINUTE) * 60 * 1000
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
92
|
/**
|
|
75
93
|
* Adds a heading to the queue, headings will be read before the next text
|
|
76
94
|
* Each heading will be read once and removed from the queue
|
|
77
95
|
*/
|
|
78
96
|
public addHeading(heading: string) {
|
|
97
|
+
if (!this.pendingFocusId) {
|
|
98
|
+
this.pendingFocusId = Date.now().toString();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.headingFocusMap.set(heading, this.pendingFocusId);
|
|
79
102
|
this.headingQueue.push(heading);
|
|
80
103
|
}
|
|
81
104
|
|
|
82
105
|
/**
|
|
83
106
|
* text you want to be read, if you want to use localized text pass keyOfLocalizedText instead
|
|
84
107
|
* keyOfLocalizedText is the key to the localized text
|
|
108
|
+
*
|
|
109
|
+
* Implements a delay mechanism to reduce noise during rapid navigation.
|
|
110
|
+
* Only the most recent announcement will be read after the delay period.
|
|
85
111
|
*/
|
|
86
112
|
public readText({
|
|
87
113
|
text,
|
|
@@ -112,19 +138,27 @@ export class AccessibilityManager {
|
|
|
112
138
|
textToRead = localizedMessage;
|
|
113
139
|
}
|
|
114
140
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
141
|
+
const focusId = this.pendingFocusId || Date.now().toString();
|
|
142
|
+
this.currentFocusId = focusId;
|
|
143
|
+
this.pendingFocusId = null;
|
|
118
144
|
|
|
119
|
-
|
|
120
|
-
clearTimeout(this.headingTimeout);
|
|
121
|
-
}
|
|
145
|
+
this.clearAnnouncement();
|
|
122
146
|
|
|
123
|
-
|
|
147
|
+
this.announcementDelayTimeout = setTimeout(() => {
|
|
148
|
+
this.executeAnnouncement(textToRead, keyOfLocalizedText, focusId);
|
|
149
|
+
}, this.ANNOUNCEMENT_DELAY);
|
|
150
|
+
}
|
|
124
151
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Internal method to execute the actual announcement after the delay
|
|
154
|
+
*/
|
|
155
|
+
private executeAnnouncement(
|
|
156
|
+
textToRead: string,
|
|
157
|
+
keyOfLocalizedText?: string,
|
|
158
|
+
focusId?: string
|
|
159
|
+
) {
|
|
160
|
+
if (this.headingQueue.length > 0) {
|
|
161
|
+
this.processHeadingQueue(textToRead, focusId);
|
|
128
162
|
} else {
|
|
129
163
|
this.ttsManager?.readText(textToRead);
|
|
130
164
|
}
|
|
@@ -136,6 +170,54 @@ export class AccessibilityManager {
|
|
|
136
170
|
});
|
|
137
171
|
}
|
|
138
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Recursively processes all headings in the queue, reading them one by one
|
|
175
|
+
*/
|
|
176
|
+
private processHeadingQueue(textToRead: string, focusId?: string) {
|
|
177
|
+
// If focus has changed, abort this announcement
|
|
178
|
+
if (focusId && this.currentFocusId !== focusId) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (this.headingQueue.length === 0) {
|
|
183
|
+
if (focusId && this.currentFocusId === focusId) {
|
|
184
|
+
this.ttsManager?.readText(textToRead);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const heading = this.headingQueue.shift()!;
|
|
191
|
+
|
|
192
|
+
const headingFocusId = this.headingFocusMap.get(heading);
|
|
193
|
+
|
|
194
|
+
if (headingFocusId && headingFocusId !== focusId) {
|
|
195
|
+
// This heading belongs to a previous focus, skip it
|
|
196
|
+
this.headingFocusMap.delete(heading);
|
|
197
|
+
this.processHeadingQueue(textToRead, focusId);
|
|
198
|
+
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.ttsManager?.readText(heading);
|
|
203
|
+
this.headingFocusMap.delete(heading); // Clean up after reading
|
|
204
|
+
|
|
205
|
+
if (this.headingTimeout) {
|
|
206
|
+
clearTimeout(this.headingTimeout);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const pauseTime = calculateReadingTime(
|
|
210
|
+
heading,
|
|
211
|
+
this.WORDS_PER_MINUTE,
|
|
212
|
+
this.MINIMUM_PAUSE,
|
|
213
|
+
this.ANNOUNCEMENT_DELAY
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
this.headingTimeout = setTimeout(() => {
|
|
217
|
+
this.processHeadingQueue(textToRead, focusId);
|
|
218
|
+
}, pauseTime);
|
|
219
|
+
}
|
|
220
|
+
|
|
139
221
|
public getButtonAccessibilityProps(name: string): AccessibilityProps {
|
|
140
222
|
const buttonName = toString(name);
|
|
141
223
|
|
|
@@ -143,12 +225,15 @@ export class AccessibilityManager {
|
|
|
143
225
|
|
|
144
226
|
if (!buttonConfig) {
|
|
145
227
|
return {
|
|
228
|
+
accessible: true,
|
|
146
229
|
accessibilityLabel: buttonName,
|
|
147
230
|
accessibilityHint: `Press button to perform action on ${buttonName}`,
|
|
148
231
|
"aria-label": buttonName,
|
|
149
232
|
"aria-description": `Press button to perform action on ${buttonName}`,
|
|
150
|
-
accessibilityRole: "button",
|
|
233
|
+
accessibilityRole: "button" as AccessibilityRole,
|
|
151
234
|
"aria-role": "button",
|
|
235
|
+
role: "button",
|
|
236
|
+
tabindex: 0,
|
|
152
237
|
};
|
|
153
238
|
}
|
|
154
239
|
|
|
@@ -162,23 +247,52 @@ export class AccessibilityManager {
|
|
|
162
247
|
`Press button to perform action on ${buttonName}`;
|
|
163
248
|
|
|
164
249
|
return {
|
|
250
|
+
accessible: true,
|
|
165
251
|
accessibilityLabel: label,
|
|
166
252
|
accessibilityHint: hint,
|
|
167
253
|
"aria-label": label,
|
|
168
254
|
"aria-description": hint,
|
|
169
|
-
accessibilityRole: "button",
|
|
255
|
+
accessibilityRole: "button" as AccessibilityRole,
|
|
170
256
|
"aria-role": "button",
|
|
257
|
+
role: "button",
|
|
258
|
+
tabindex: 0,
|
|
171
259
|
};
|
|
172
260
|
}
|
|
173
261
|
|
|
174
262
|
public getInputAccessibilityProps(inputName: string): AccessibilityProps {
|
|
175
263
|
return {
|
|
264
|
+
accessible: true,
|
|
176
265
|
accessibilityLabel: inputName,
|
|
177
266
|
accessibilityHint: `Enter text into ${inputName}`,
|
|
178
267
|
"aria-label": inputName,
|
|
179
268
|
"aria-description": `Enter text into ${inputName}`,
|
|
180
|
-
accessibilityRole: "
|
|
181
|
-
"aria-role": "
|
|
269
|
+
accessibilityRole: "searchbox" as AccessibilityRole,
|
|
270
|
+
"aria-role": "searchbox",
|
|
271
|
+
role: "searchbox",
|
|
272
|
+
tabindex: 0,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Extracts accessibility props from component props and returns them as HTML attributes
|
|
278
|
+
* @param props - Component props containing accessibility properties
|
|
279
|
+
* @returns Object with accessibility HTML attributes
|
|
280
|
+
*/
|
|
281
|
+
public getWebAccessibilityProps(props: any): AccessibilityProps {
|
|
282
|
+
const {
|
|
283
|
+
"aria-label": ariaLabel,
|
|
284
|
+
"aria-description": ariaDescription,
|
|
285
|
+
"aria-role": ariaRole,
|
|
286
|
+
role,
|
|
287
|
+
tabindex,
|
|
288
|
+
} = props;
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
"aria-label": ariaLabel,
|
|
292
|
+
"aria-description": ariaDescription,
|
|
293
|
+
"aria-role": ariaRole,
|
|
294
|
+
role: role || ariaRole,
|
|
295
|
+
tabindex,
|
|
182
296
|
};
|
|
183
297
|
}
|
|
184
298
|
|
|
@@ -196,4 +310,11 @@ export class AccessibilityManager {
|
|
|
196
310
|
|
|
197
311
|
return this.localizations[key];
|
|
198
312
|
}
|
|
313
|
+
|
|
314
|
+
private clearAnnouncement() {
|
|
315
|
+
if (this.announcementDelayTimeout) {
|
|
316
|
+
clearTimeout(this.announcementDelayTimeout);
|
|
317
|
+
this.announcementDelayTimeout = null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
199
320
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 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: 160)
|
|
5
|
+
* @param minimumPause - Minimum pause time in milliseconds (default: 500)
|
|
6
|
+
* @param announcementDelay - Additional delay for announcement in milliseconds (default: 700)
|
|
7
|
+
* @returns The reading time in milliseconds
|
|
8
|
+
*/
|
|
9
|
+
export function calculateReadingTime(
|
|
10
|
+
text: string,
|
|
11
|
+
wordsPerMinute: number = 140,
|
|
12
|
+
minimumPause: number = 500,
|
|
13
|
+
announcementDelay: number = 700
|
|
14
|
+
): number {
|
|
15
|
+
const words = text
|
|
16
|
+
.trim()
|
|
17
|
+
.split(/(?<=\d)(?=[a-zA-Z])|(?<=[a-zA-Z])(?=\d)|[^\w\s]+|\s+/)
|
|
18
|
+
.filter(Boolean).length;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
Math.max(minimumPause, (words / wordsPerMinute) * 60 * 1000) +
|
|
22
|
+
announcementDelay
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -6,7 +6,8 @@ exports[`focusManager should be defined 1`] = `
|
|
|
6
6
|
"disableFocus": [Function],
|
|
7
7
|
"enableFocus": [Function],
|
|
8
8
|
"findPreferredFocusChild": [Function],
|
|
9
|
-
"
|
|
9
|
+
"focusOnSelectedTab": [Function],
|
|
10
|
+
"focusOnSelectedTopMenuItem": [Function],
|
|
10
11
|
"focusableTree": Tree {
|
|
11
12
|
"loadingCounter": 0,
|
|
12
13
|
"root": {
|
|
@@ -29,6 +30,7 @@ exports[`focusManager should be defined 1`] = `
|
|
|
29
30
|
"isFocusOnContent": [Function],
|
|
30
31
|
"isFocusOnMenu": [Function],
|
|
31
32
|
"isGroupItemFocused": [Function],
|
|
33
|
+
"isTabsScreenContentFocused": [Function],
|
|
32
34
|
"longPress": [Function],
|
|
33
35
|
"moveFocus": [Function],
|
|
34
36
|
"on": [Function],
|
|
@@ -67,6 +69,7 @@ exports[`focusManagerIOS should be defined 1`] = `
|
|
|
67
69
|
"getGroupRootById": [Function],
|
|
68
70
|
"getPreferredFocusChild": [Function],
|
|
69
71
|
"invokeHandler": [Function],
|
|
72
|
+
"isChildOf": [Function],
|
|
70
73
|
"isFocusOn": [Function],
|
|
71
74
|
"isGroupItemFocused": [Function],
|
|
72
75
|
"moveFocus": [Function],
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { NativeModules } from "react-native";
|
|
2
2
|
import * as R from "ramda";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
isCurrentFocusOn,
|
|
6
|
+
isChildOf as isChildOfUtils,
|
|
7
|
+
} from "../focusManagerAux/utils";
|
|
5
8
|
import { Tree } from "./treeDataStructure/Tree";
|
|
6
9
|
import { findFocusableNode } from "./treeDataStructure/Utils";
|
|
7
10
|
import { subscriber } from "../../functionUtils";
|
|
8
11
|
import { findChild } from "./utils";
|
|
9
12
|
|
|
13
|
+
import {
|
|
14
|
+
emitRegistered,
|
|
15
|
+
emitUnregistered,
|
|
16
|
+
} from "../focusManagerAux/utils/utils.ios";
|
|
17
|
+
|
|
10
18
|
const { FocusableManagerModule } = NativeModules;
|
|
11
19
|
|
|
12
20
|
/**
|
|
@@ -180,10 +188,14 @@ export const focusManager = (function () {
|
|
|
180
188
|
function register({ id, component }) {
|
|
181
189
|
const { isGroup = false } = component;
|
|
182
190
|
|
|
191
|
+
emitRegistered(id);
|
|
192
|
+
|
|
183
193
|
return isGroup ? registerGroup(id, component) : registerItem(id, component);
|
|
184
194
|
}
|
|
185
195
|
|
|
186
196
|
function unregister(id, { group = false } = {}) {
|
|
197
|
+
emitUnregistered(id);
|
|
198
|
+
|
|
187
199
|
group ? unregisterGroup(id) : unregisterItem(id);
|
|
188
200
|
}
|
|
189
201
|
|
|
@@ -400,6 +412,10 @@ export const focusManager = (function () {
|
|
|
400
412
|
return id && isCurrentFocusOn(id, currentFocusNode);
|
|
401
413
|
}
|
|
402
414
|
|
|
415
|
+
function isChildOf(childId, parentId): boolean {
|
|
416
|
+
return isChildOfUtils(focusableTree, childId, parentId);
|
|
417
|
+
}
|
|
418
|
+
|
|
403
419
|
return {
|
|
404
420
|
on,
|
|
405
421
|
invokeHandler,
|
|
@@ -422,5 +438,6 @@ export const focusManager = (function () {
|
|
|
422
438
|
isGroupItemFocused,
|
|
423
439
|
getPreferredFocusChild,
|
|
424
440
|
isFocusOn,
|
|
441
|
+
isChildOf,
|
|
425
442
|
};
|
|
426
443
|
})();
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
isNilOrEmpty,
|
|
4
4
|
isNotNil,
|
|
5
5
|
} from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
|
|
6
|
+
import { getFocusableId } from "@applicaster/zapp-react-native-utils/screenPickerUtils";
|
|
6
7
|
|
|
7
8
|
import { Tree } from "./treeDataStructure/Tree";
|
|
8
9
|
import {
|
|
@@ -15,9 +16,7 @@ import { coreLogger } from "../../logger";
|
|
|
15
16
|
import { ACTION } from "./utils/enums";
|
|
16
17
|
|
|
17
18
|
import {
|
|
18
|
-
|
|
19
|
-
findSelectedMenuId,
|
|
20
|
-
isTabsScreenContentFocused,
|
|
19
|
+
isTabsScreenOnContentFocused,
|
|
21
20
|
isCurrentFocusOnContent,
|
|
22
21
|
isCurrentFocusOnMenu,
|
|
23
22
|
isCurrentFocusOn,
|
|
@@ -300,37 +299,40 @@ export const focusManager = (function () {
|
|
|
300
299
|
return isCurrentFocusOnMenu(currentFocusNode);
|
|
301
300
|
}
|
|
302
301
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// set focus on selected menu item
|
|
308
|
-
const direction = undefined;
|
|
302
|
+
function landFocusToWithoutScrolling(id) {
|
|
303
|
+
if (id) {
|
|
304
|
+
// set focus on selected menu item
|
|
305
|
+
const direction = undefined;
|
|
309
306
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
307
|
+
const context: FocusManager.FocusContext = {
|
|
308
|
+
source: "back",
|
|
309
|
+
preserveScroll: true,
|
|
310
|
+
};
|
|
314
311
|
|
|
315
|
-
|
|
312
|
+
logger.log({ message: "landFocusTo", data: { id } });
|
|
316
313
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
314
|
+
blur(direction);
|
|
315
|
+
setFocus(id, direction, context);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
321
318
|
|
|
322
|
-
|
|
323
|
-
|
|
319
|
+
function isTabsScreenContentFocused() {
|
|
320
|
+
return isTabsScreenOnContentFocused(currentFocusNode);
|
|
321
|
+
}
|
|
324
322
|
|
|
325
|
-
|
|
326
|
-
|
|
323
|
+
function focusOnSelectedTab(item: ZappEntry): void {
|
|
324
|
+
// Move focus to appropriate top navigation tab with context
|
|
325
|
+
const selectedTabId = getFocusableId(item.id);
|
|
327
326
|
|
|
328
|
-
|
|
329
|
-
|
|
327
|
+
// Set focus with back button context to tabs-menu
|
|
328
|
+
landFocusToWithoutScrolling(selectedTabId);
|
|
329
|
+
}
|
|
330
330
|
|
|
331
|
-
|
|
331
|
+
function focusOnSelectedTopMenuItem(
|
|
332
|
+
selectedMenuItemId: Option<string>
|
|
333
|
+
): void {
|
|
332
334
|
// Set focus with back button context to top-menu
|
|
333
|
-
|
|
335
|
+
landFocusToWithoutScrolling(selectedMenuItemId);
|
|
334
336
|
}
|
|
335
337
|
|
|
336
338
|
/**
|
|
@@ -643,9 +645,11 @@ export const focusManager = (function () {
|
|
|
643
645
|
recoverFocus,
|
|
644
646
|
isCurrentFocusOnTheTopScreen,
|
|
645
647
|
findPreferredFocusChild,
|
|
646
|
-
focusTopNavigation,
|
|
647
648
|
isFocusOnContent,
|
|
648
649
|
isFocusOnMenu,
|
|
649
650
|
isFocusOn,
|
|
651
|
+
focusOnSelectedTopMenuItem,
|
|
652
|
+
focusOnSelectedTab,
|
|
653
|
+
isTabsScreenContentFocused,
|
|
650
654
|
};
|
|
651
655
|
})();
|
|
@@ -123,19 +123,7 @@ export const findSelectedTabId = (item: ZappEntry): string => {
|
|
|
123
123
|
return selectedTabId;
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
-
export const
|
|
127
|
-
// Set focus with back button context
|
|
128
|
-
const navbar = getNavbarNode(focusableTree);
|
|
129
|
-
|
|
130
|
-
const selectedMenuItemId = find(
|
|
131
|
-
(child) => child.component.props.selected,
|
|
132
|
-
navbar.children
|
|
133
|
-
)?.id;
|
|
134
|
-
|
|
135
|
-
return selectedMenuItemId;
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
export const isTabsScreenContentFocused = (node) => {
|
|
126
|
+
export const isTabsScreenOnContentFocused = (node) => {
|
|
139
127
|
if (isRoot(node)) {
|
|
140
128
|
return false;
|
|
141
129
|
}
|
|
@@ -152,7 +140,7 @@ export const isTabsScreenContentFocused = (node) => {
|
|
|
152
140
|
return true;
|
|
153
141
|
}
|
|
154
142
|
|
|
155
|
-
return
|
|
143
|
+
return isTabsScreenOnContentFocused(node.parent);
|
|
156
144
|
};
|
|
157
145
|
|
|
158
146
|
export const isCurrentFocusOnMenu = (node) => {
|
|
@@ -202,3 +190,21 @@ export const isCurrentFocusOn = (id, node) => {
|
|
|
202
190
|
|
|
203
191
|
return isCurrentFocusOn(id, node.parent);
|
|
204
192
|
};
|
|
193
|
+
|
|
194
|
+
export const isChildOf = (focusableTree, childId, parentId) => {
|
|
195
|
+
if (isNil(childId) || isNil(parentId)) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const childNode = focusableTree.findInTree(childId);
|
|
200
|
+
|
|
201
|
+
if (isNil(childNode)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (childNode.parent?.id === parentId) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return isChildOf(focusableTree, childNode.parent?.id, parentId);
|
|
210
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ReplaySubject } from "rxjs";
|
|
2
|
+
import { filter } from "rxjs/operators";
|
|
3
|
+
import { BUTTON_PREFIX } from "@applicaster/zapp-react-native-ui-components/Components/MasterCell/DefaultComponents/tv/TvActionButtons/const";
|
|
4
|
+
import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
|
|
5
|
+
|
|
6
|
+
type FocusableID = string;
|
|
7
|
+
type RegistrationEvent = {
|
|
8
|
+
id: FocusableID;
|
|
9
|
+
registered: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const isFocusableButton = (id: Option<FocusableID>): boolean =>
|
|
13
|
+
id && id.includes?.(BUTTON_PREFIX);
|
|
14
|
+
|
|
15
|
+
const registeredSubject$ = new ReplaySubject<RegistrationEvent>(1);
|
|
16
|
+
|
|
17
|
+
export const focusableButtonsRegistration$ = (focusableGroupId: string) =>
|
|
18
|
+
registeredSubject$.pipe(
|
|
19
|
+
filter(
|
|
20
|
+
(value) =>
|
|
21
|
+
value.registered && focusManager.isChildOf(value.id, focusableGroupId)
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const emitRegistered = (id: Option<FocusableID>): void => {
|
|
26
|
+
if (isFocusableButton(id)) {
|
|
27
|
+
registeredSubject$.next({ id, registered: true });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const emitUnregistered = (id: Option<FocusableID>): void => {
|
|
32
|
+
if (isFocusableButton(id)) {
|
|
33
|
+
registeredSubject$.next({ id, registered: false });
|
|
34
|
+
}
|
|
35
|
+
};
|