@applicaster/zapp-react-native-utils 13.0.6 → 13.0.7-alpha.2991546311

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.
@@ -65,4 +65,8 @@ 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
+ },
68
72
  } as const;
@@ -8,8 +8,10 @@ import _ from "lodash";
8
8
  export class AccessibilityManager {
9
9
  private static _instance: AccessibilityManager | null = null;
10
10
  private headingTimeout: NodeJS.Timeout | null = null;
11
+ private announcementDelayTimeout: NodeJS.Timeout | null = null;
11
12
  private WORDS_PER_MINUTE = 160;
12
13
  private MINIMUM_PAUSE = 500;
14
+ private ANNOUNCEMENT_DELAY = 500;
13
15
  private state$ = new BehaviorSubject<AccessibilityState>({
14
16
  screenReaderEnabled: false,
15
17
  reduceMotionEnabled: false,
@@ -25,6 +27,9 @@ export class AccessibilityManager {
25
27
  private ttsManager = TTSManager.getInstance();
26
28
  private localizations: { [key: string]: string } = {};
27
29
  private headingQueue: string[] = [];
30
+ private currentFocusId: string | null = null;
31
+ private headingFocusMap: Map<string, string> = new Map();
32
+ private pendingFocusId: string | null = null;
28
33
 
29
34
  private constructor() {}
30
35
 
@@ -65,9 +70,11 @@ export class AccessibilityManager {
65
70
  private calculateReadingTime(text: string): number {
66
71
  const words = text.trim().split(/\s+/).length;
67
72
 
68
- return Math.max(
69
- this.MINIMUM_PAUSE,
70
- (words / this.WORDS_PER_MINUTE) * 60 * 1000
73
+ return (
74
+ Math.max(
75
+ this.MINIMUM_PAUSE,
76
+ (words / this.WORDS_PER_MINUTE) * 60 * 1000
77
+ ) + 300
71
78
  );
72
79
  }
73
80
 
@@ -76,12 +83,20 @@ export class AccessibilityManager {
76
83
  * Each heading will be read once and removed from the queue
77
84
  */
78
85
  public addHeading(heading: string) {
86
+ if (!this.pendingFocusId) {
87
+ this.pendingFocusId = Date.now().toString();
88
+ }
89
+
90
+ this.headingFocusMap.set(heading, this.pendingFocusId);
79
91
  this.headingQueue.push(heading);
80
92
  }
81
93
 
82
94
  /**
83
95
  * text you want to be read, if you want to use localized text pass keyOfLocalizedText instead
84
96
  * keyOfLocalizedText is the key to the localized text
97
+ *
98
+ * Implements a delay mechanism to reduce noise during rapid navigation.
99
+ * Only the most recent announcement will be read after the delay period.
85
100
  */
86
101
  public readText({
87
102
  text,
@@ -112,19 +127,27 @@ export class AccessibilityManager {
112
127
  textToRead = localizedMessage;
113
128
  }
114
129
 
115
- if (this.headingQueue.length > 0) {
116
- const heading = this.headingQueue.shift()!;
117
- this.ttsManager?.readText(heading);
130
+ const focusId = this.pendingFocusId || Date.now().toString();
131
+ this.currentFocusId = focusId;
132
+ this.pendingFocusId = null;
118
133
 
119
- if (this.headingTimeout) {
120
- clearTimeout(this.headingTimeout);
121
- }
134
+ this.clearAnnouncement();
122
135
 
123
- const pauseTime = this.calculateReadingTime(heading);
136
+ this.announcementDelayTimeout = setTimeout(() => {
137
+ this.executeAnnouncement(textToRead, keyOfLocalizedText, focusId);
138
+ }, this.ANNOUNCEMENT_DELAY);
139
+ }
124
140
 
125
- this.headingTimeout = setTimeout(() => {
126
- this.ttsManager?.readText(textToRead);
127
- }, pauseTime);
141
+ /**
142
+ * Internal method to execute the actual announcement after the delay
143
+ */
144
+ private executeAnnouncement(
145
+ textToRead: string,
146
+ keyOfLocalizedText?: string,
147
+ focusId?: string
148
+ ) {
149
+ if (this.headingQueue.length > 0) {
150
+ this.processHeadingQueue(textToRead, focusId);
128
151
  } else {
129
152
  this.ttsManager?.readText(textToRead);
130
153
  }
@@ -136,6 +159,49 @@ export class AccessibilityManager {
136
159
  });
137
160
  }
138
161
 
162
+ /**
163
+ * Recursively processes all headings in the queue, reading them one by one
164
+ */
165
+ private processHeadingQueue(textToRead: string, focusId?: string) {
166
+ // If focus has changed, abort this announcement
167
+ if (focusId && this.currentFocusId !== focusId) {
168
+ return;
169
+ }
170
+
171
+ if (this.headingQueue.length === 0) {
172
+ if (focusId && this.currentFocusId === focusId) {
173
+ this.ttsManager?.readText(textToRead);
174
+ }
175
+
176
+ return;
177
+ }
178
+
179
+ const heading = this.headingQueue.shift()!;
180
+
181
+ const headingFocusId = this.headingFocusMap.get(heading);
182
+
183
+ if (headingFocusId && headingFocusId !== focusId) {
184
+ // This heading belongs to a previous focus, skip it
185
+ this.headingFocusMap.delete(heading);
186
+ this.processHeadingQueue(textToRead, focusId);
187
+
188
+ return;
189
+ }
190
+
191
+ this.ttsManager?.readText(heading);
192
+ this.headingFocusMap.delete(heading); // Clean up after reading
193
+
194
+ if (this.headingTimeout) {
195
+ clearTimeout(this.headingTimeout);
196
+ }
197
+
198
+ const pauseTime = this.calculateReadingTime(heading);
199
+
200
+ this.headingTimeout = setTimeout(() => {
201
+ this.processHeadingQueue(textToRead, focusId);
202
+ }, pauseTime);
203
+ }
204
+
139
205
  public getButtonAccessibilityProps(name: string): AccessibilityProps {
140
206
  const buttonName = _.toString(name);
141
207
 
@@ -143,12 +209,15 @@ export class AccessibilityManager {
143
209
 
144
210
  if (!buttonConfig) {
145
211
  return {
212
+ accessible: true,
146
213
  accessibilityLabel: buttonName,
147
214
  accessibilityHint: `Press button to perform action on ${buttonName}`,
148
215
  "aria-label": buttonName,
149
216
  "aria-description": `Press button to perform action on ${buttonName}`,
150
217
  accessibilityRole: "button" as AccessibilityRole,
151
218
  "aria-role": "button",
219
+ role: "button",
220
+ tabindex: 0,
152
221
  };
153
222
  }
154
223
 
@@ -162,23 +231,52 @@ export class AccessibilityManager {
162
231
  `Press button to perform action on ${buttonName}`;
163
232
 
164
233
  return {
234
+ accessible: true,
165
235
  accessibilityLabel: label,
166
236
  accessibilityHint: hint,
167
237
  "aria-label": label,
168
238
  "aria-description": hint,
169
239
  accessibilityRole: "button" as AccessibilityRole,
170
240
  "aria-role": "button",
241
+ role: "button",
242
+ tabindex: 0,
171
243
  };
172
244
  }
173
245
 
174
246
  public getInputAccessibilityProps(inputName: string): AccessibilityProps {
175
247
  return {
248
+ accessible: true,
176
249
  accessibilityLabel: inputName,
177
250
  accessibilityHint: `Enter text into ${inputName}`,
178
251
  "aria-label": inputName,
179
252
  "aria-description": `Enter text into ${inputName}`,
180
- accessibilityRole: "textbox" as AccessibilityRole,
181
- "aria-role": "textbox",
253
+ accessibilityRole: "text" as AccessibilityRole,
254
+ "aria-role": "text",
255
+ role: "text",
256
+ tabindex: 0,
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Extracts accessibility props from component props and returns them as HTML attributes
262
+ * @param props - Component props containing accessibility properties
263
+ * @returns Object with accessibility HTML attributes
264
+ */
265
+ public getWebAccessibilityProps(props: any): AccessibilityProps {
266
+ const {
267
+ "aria-label": ariaLabel,
268
+ "aria-description": ariaDescription,
269
+ "aria-role": ariaRole,
270
+ role,
271
+ tabindex,
272
+ } = props;
273
+
274
+ return {
275
+ "aria-label": ariaLabel,
276
+ "aria-description": ariaDescription,
277
+ "aria-role": ariaRole,
278
+ role: role || ariaRole,
279
+ tabindex,
182
280
  };
183
281
  }
184
282
 
@@ -196,4 +294,11 @@ export class AccessibilityManager {
196
294
 
197
295
  return this.localizations[key];
198
296
  }
297
+
298
+ private clearAnnouncement() {
299
+ if (this.announcementDelayTimeout) {
300
+ clearTimeout(this.announcementDelayTimeout);
301
+ this.announcementDelayTimeout = null;
302
+ }
303
+ }
199
304
  }
@@ -214,6 +214,8 @@ export class TTSManager {
214
214
  readText(text: string) {
215
215
  if (isSamsungPlatform() && window.speechSynthesis) {
216
216
  const utterance = new SpeechSynthesisUtterance(text);
217
+
218
+ window.speechSynthesis.cancel(); // Cancel previous speech before speaking new text
217
219
  window.speechSynthesis.speak(utterance);
218
220
  }
219
221
 
@@ -233,6 +235,7 @@ export class TTSManager {
233
235
  },
234
236
  parameters: {
235
237
  text,
238
+ clear: true, // Clear any pending speech before speaking new text
236
239
  },
237
240
  });
238
241
  } catch (error) {
package/index.d.ts CHANGED
@@ -139,10 +139,13 @@ declare type AccessibilityState = {
139
139
  };
140
140
 
141
141
  declare type AccessibilityProps = {
142
+ accessible?: boolean;
142
143
  accessibilityLabel?: string;
143
144
  accessibilityHint?: string;
144
145
  "aria-label"?: string;
145
146
  "aria-description"?: string;
146
147
  accessibilityRole?: string;
147
148
  "aria-role"?: string;
149
+ role?: string;
150
+ tabindex?: number;
148
151
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "13.0.6",
3
+ "version": "13.0.7-alpha.2991546311",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "homepage": "https://github.com/applicaster/quickbrick#readme",
29
29
  "dependencies": {
30
- "@applicaster/applicaster-types": "13.0.6",
30
+ "@applicaster/applicaster-types": "13.0.7-alpha.2991546311",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -0,0 +1,213 @@
1
+ import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager";
2
+ import uuidv4 from "uuid/v4";
3
+ import { Player } from "../../appUtils/playerManager/player";
4
+
5
+ enum SEEK_DIRECTION {
6
+ FORWARD = "forward",
7
+ REWIND = "back",
8
+ }
9
+
10
+ export class PlayerTTS {
11
+ private player: Player;
12
+ private accessibilityManager: AccessibilityManager;
13
+ private seekStartPosition: number | null = null;
14
+ private isSeeking: boolean = false;
15
+ private listenerId: string;
16
+ private isInitialPlayerOpen: boolean = true;
17
+
18
+ constructor(player: Player, accessibilityManager: AccessibilityManager) {
19
+ this.player = player;
20
+ this.accessibilityManager = accessibilityManager;
21
+ this.listenerId = `player-tts-${uuidv4()}`;
22
+ }
23
+
24
+ private numberToWords(num: number): string {
25
+ const ones = [
26
+ "",
27
+ "one",
28
+ "two",
29
+ "three",
30
+ "four",
31
+ "five",
32
+ "six",
33
+ "seven",
34
+ "eight",
35
+ "nine",
36
+ "ten",
37
+ "eleven",
38
+ "twelve",
39
+ "thirteen",
40
+ "fourteen",
41
+ "fifteen",
42
+ "sixteen",
43
+ "seventeen",
44
+ "eighteen",
45
+ "nineteen",
46
+ ];
47
+
48
+ const tens = [
49
+ "",
50
+ "",
51
+ "twenty",
52
+ "thirty",
53
+ "forty",
54
+ "fifty",
55
+ "sixty",
56
+ "seventy",
57
+ "eighty",
58
+ "ninety",
59
+ ];
60
+
61
+ if (num === 0) return "zero";
62
+ if (num < 20) return ones[num];
63
+
64
+ const ten = Math.floor(num / 10);
65
+ const one = num % 10;
66
+
67
+ if (one === 0) return tens[ten];
68
+
69
+ return `${tens[ten]} ${ones[one]}`;
70
+ }
71
+
72
+ private secondsToTime(
73
+ seconds: number,
74
+ format: "natural" | "standard" = "natural"
75
+ ): string {
76
+ if (seconds < 0) return format === "natural" ? "zero" : "0";
77
+
78
+ const minutes = Math.floor(seconds / 60);
79
+ const remainingSeconds = Math.floor(seconds % 60);
80
+
81
+ if (format === "standard") {
82
+ if (minutes === 0) {
83
+ if (remainingSeconds === 0) return "0";
84
+
85
+ return `${remainingSeconds} second${remainingSeconds !== 1 ? "s" : ""}`;
86
+ }
87
+
88
+ if (remainingSeconds === 0) {
89
+ return `${minutes} minute${minutes !== 1 ? "s" : ""}`;
90
+ }
91
+
92
+ return `${minutes} minute${minutes !== 1 ? "s" : ""} ${remainingSeconds} second${remainingSeconds !== 1 ? "s" : ""}`;
93
+ } else {
94
+ if (minutes === 0) {
95
+ if (remainingSeconds === 0) return "zero";
96
+
97
+ if (remainingSeconds < 10) {
98
+ return `zero o ${this.numberToWords(remainingSeconds)}`;
99
+ }
100
+
101
+ return `zero ${this.numberToWords(remainingSeconds)}`;
102
+ }
103
+
104
+ if (remainingSeconds === 0) {
105
+ return `${this.numberToWords(minutes)}`;
106
+ }
107
+
108
+ if (remainingSeconds < 10) {
109
+ return `${this.numberToWords(minutes)} o ${this.numberToWords(remainingSeconds)}`;
110
+ }
111
+
112
+ return `${this.numberToWords(minutes)} ${this.numberToWords(remainingSeconds)}`;
113
+ }
114
+ }
115
+
116
+ private announceLoading = () => {
117
+ this.accessibilityManager.addHeading("Loading");
118
+ };
119
+
120
+ private announcePause = () => {
121
+ if (!this.isSeeking) {
122
+ this.accessibilityManager.addHeading(
123
+ `Paused - ${this.secondsToTime(this.player.playerState.contentPosition, "standard")}`
124
+ );
125
+ }
126
+ };
127
+
128
+ private announceBufferComplete = (event: any) => {
129
+ if (this.isInitialPlayerOpen) {
130
+ const timeRemaining = event.duration - event.currentTime;
131
+ const title = (this.player.entry.title as string) || "";
132
+ const summary = (this.player.entry.summary as string) || "";
133
+ this.accessibilityManager.addHeading(`Playing - ${title}`);
134
+ this.accessibilityManager.addHeading(summary);
135
+
136
+ this.accessibilityManager.addHeading(
137
+ `Playing from ${this.secondsToTime(event.currentTime, "standard")}`
138
+ );
139
+
140
+ this.accessibilityManager.addHeading(
141
+ `${this.secondsToTime(timeRemaining, "standard")} remaining.`
142
+ );
143
+
144
+ this.isInitialPlayerOpen = false;
145
+ }
146
+ };
147
+
148
+ private announceResume = () => {
149
+ if (!this.isSeeking && !this.isInitialPlayerOpen) {
150
+ this.accessibilityManager.addHeading(
151
+ `Playing - ${this.secondsToTime(this.player.playerState.contentPosition, "standard")}`
152
+ );
153
+ }
154
+ };
155
+
156
+ private handleVideoProgress = (event: any) => {
157
+ if (event.currentTime > 0) {
158
+ this.seekStartPosition = event.currentTime;
159
+ }
160
+ };
161
+
162
+ private handleSeekComplete = (event: any) => {
163
+ if (this.seekStartPosition !== null) {
164
+ const seekDirection =
165
+ event.currentTime > this.seekStartPosition
166
+ ? SEEK_DIRECTION.FORWARD
167
+ : SEEK_DIRECTION.REWIND;
168
+
169
+ const seekAmount = Math.round(
170
+ Math.abs(event.currentTime - this.seekStartPosition)
171
+ );
172
+
173
+ this.accessibilityManager.readText({
174
+ text: `Skipped ${seekDirection} ${this.secondsToTime(seekAmount, "standard")}`,
175
+ });
176
+
177
+ this.seekStartPosition = event.currentTime;
178
+ }
179
+
180
+ this.isSeeking = false;
181
+ };
182
+
183
+ private handleSeekStart = () => {
184
+ this.isSeeking = true;
185
+ };
186
+
187
+ public init(): () => void {
188
+ if (!this.player) {
189
+ return () => {};
190
+ }
191
+
192
+ return this.player.addListener({
193
+ id: this.listenerId,
194
+ listener: {
195
+ onBufferStart: this.announceLoading,
196
+ onBufferComplete: this.announceBufferComplete,
197
+ onPlayerResume: this.announceResume,
198
+ onPlayerPause: this.announcePause,
199
+ onVideoProgress: this.handleVideoProgress,
200
+ onPlayerSeekStart: this.handleSeekStart,
201
+ onPlayerSeekComplete: this.handleSeekComplete,
202
+ },
203
+ });
204
+ }
205
+
206
+ public destroy(): void {
207
+ if (this.player) {
208
+ this.player.removeListener(this.listenerId);
209
+ }
210
+
211
+ this.seekStartPosition = null;
212
+ }
213
+ }
@@ -0,0 +1 @@
1
+ export { PlayerTTS } from "./PlayerTTS";
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
3
+ import { useAccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager/hooks";
4
+ import { PlayerTTS } from "@applicaster/zapp-react-native-utils/playerUtils/PlayerTTS";
5
+
6
+ export const usePlayerTTS = () => {
7
+ const player = usePlayer();
8
+ const accessibilityManager = useAccessibilityManager({});
9
+
10
+ React.useEffect(() => {
11
+ if (player && accessibilityManager) {
12
+ const playerTTS = new PlayerTTS(player, accessibilityManager);
13
+ const unsubscribe = playerTTS.init();
14
+
15
+ return () => {
16
+ unsubscribe();
17
+ playerTTS.destroy();
18
+ };
19
+ }
20
+ }, [player, accessibilityManager]);
21
+ };