@applicaster/zapp-react-native-utils 13.0.5 → 13.0.6-alpha.1043196035

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.
@@ -0,0 +1,114 @@
1
+ import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager";
2
+
3
+ const mockReadText = jest.fn();
4
+
5
+ jest.mock(
6
+ "@applicaster/zapp-react-native-utils/appUtils/platform/platformUtils",
7
+ () => ({
8
+ TTSManager: {
9
+ getInstance: jest.fn(() => ({
10
+ readText: mockReadText,
11
+ })),
12
+ },
13
+ })
14
+ );
15
+
16
+ describe("AccessibilityManager", () => {
17
+ let accessibilityManager: AccessibilityManager;
18
+
19
+ beforeEach(() => {
20
+ jest.clearAllMocks();
21
+ jest.useFakeTimers();
22
+
23
+ (AccessibilityManager as any)._instance = null;
24
+
25
+ accessibilityManager = AccessibilityManager.getInstance();
26
+
27
+ accessibilityManager._resetTTSManager();
28
+ });
29
+
30
+ afterEach(() => {
31
+ jest.useRealTimers();
32
+ });
33
+
34
+ describe("announcement delay functionality", () => {
35
+ it("should delay announcements by the configured delay time", () => {
36
+ const testText = "Test announcement";
37
+
38
+ accessibilityManager.readText({ text: testText });
39
+
40
+ expect(mockReadText).not.toHaveBeenCalled();
41
+
42
+ jest.advanceTimersByTime(150);
43
+ expect(mockReadText).not.toHaveBeenCalled();
44
+
45
+ jest.advanceTimersByTime(50);
46
+ expect(mockReadText).toHaveBeenCalledWith(testText, undefined);
47
+ });
48
+
49
+ it("should cancel previous announcements when a new one is requested", () => {
50
+ const firstText = "First announcement";
51
+ const secondText = "Second announcement";
52
+
53
+ accessibilityManager.readText({ text: firstText });
54
+
55
+ jest.advanceTimersByTime(150);
56
+
57
+ accessibilityManager.readText({ text: secondText });
58
+
59
+ jest.advanceTimersByTime(200);
60
+
61
+ expect(mockReadText).toHaveBeenCalledTimes(1);
62
+ expect(mockReadText).toHaveBeenCalledWith(secondText, undefined);
63
+ });
64
+
65
+ it("should allow configuring the announcement delay", () => {
66
+ const customDelay = 300;
67
+ accessibilityManager.setAnnouncementDelay(customDelay);
68
+
69
+ expect(accessibilityManager.getAnnouncementDelay()).toBe(customDelay);
70
+
71
+ const testText = "Test announcement";
72
+ accessibilityManager.readText({ text: testText });
73
+
74
+ expect(mockReadText).not.toHaveBeenCalled();
75
+
76
+ jest.advanceTimersByTime(250);
77
+ expect(mockReadText).not.toHaveBeenCalled();
78
+
79
+ jest.advanceTimersByTime(50);
80
+ expect(mockReadText).toHaveBeenCalledWith(testText, undefined);
81
+ });
82
+
83
+ it("should cancel pending announcements when cancelPendingAnnouncement is called", () => {
84
+ const testText = "Test announcement";
85
+
86
+ accessibilityManager.readText({ text: testText });
87
+
88
+ accessibilityManager.cancelPendingAnnouncement();
89
+
90
+ jest.advanceTimersByTime(250);
91
+
92
+ expect(mockReadText).not.toHaveBeenCalled();
93
+ });
94
+
95
+ it("should handle localized text with delay", () => {
96
+ const localizations = {
97
+ test_key: "Localized text",
98
+ };
99
+
100
+ accessibilityManager.updateLocalizations(localizations);
101
+
102
+ accessibilityManager.readText({
103
+ text: "fallback",
104
+ keyOfLocalizedText: "test_key",
105
+ });
106
+
107
+ expect(mockReadText).not.toHaveBeenCalled();
108
+
109
+ jest.advanceTimersByTime(200);
110
+
111
+ expect(mockReadText).toHaveBeenCalledWith("Localized text", undefined);
112
+ });
113
+ });
114
+ });
@@ -0,0 +1,76 @@
1
+ # Accessibility Manager - Announcement Delay Feature
2
+
3
+ ## Overview
4
+
5
+ The Accessibility Manager now includes a delay mechanism to reduce noise during rapid navigation. This feature ensures that only elements the user actually lands on are announced, preventing excessive screen reader announcements during fast navigation.
6
+
7
+ ## Implementation Details
8
+
9
+ ### Key Features
10
+
11
+ 1. **Configurable Delay**: Default delay of 200ms (configurable via `setAnnouncementDelay()`)
12
+ 2. **Cancellation of Previous Announcements**: New announcements cancel pending ones
13
+ 3. **Manual Cancellation**: Ability to cancel pending announcements programmatically
14
+
15
+ ### New Methods
16
+
17
+ #### `setAnnouncementDelay(delay: number)`
18
+ Sets the announcement delay in milliseconds. The delay must be non-negative.
19
+
20
+ ```typescript
21
+ const accessibilityManager = AccessibilityManager.getInstance();
22
+ accessibilityManager.setAnnouncementDelay(200); // Set to 200ms
23
+ ```
24
+
25
+ #### `getAnnouncementDelay(): number`
26
+ Returns the current announcement delay setting.
27
+
28
+ ```typescript
29
+ const currentDelay = accessibilityManager.getAnnouncementDelay();
30
+ console.log(`Current delay: ${currentDelay}ms`);
31
+ ```
32
+
33
+ #### `cancelPendingAnnouncement()`
34
+ Cancels any pending announcement that hasn't been executed yet. Useful for clearing announcements when navigation is interrupted.
35
+
36
+ ```typescript
37
+ // Cancel any pending announcement
38
+ accessibilityManager.cancelPendingAnnouncement();
39
+ ```
40
+
41
+ ### How It Works
42
+
43
+ 1. When `readText()` is called, instead of immediately announcing the text, it schedules the announcement after the configured delay
44
+ 2. If `readText()` is called again before the delay expires, the previous announcement is cancelled and a new one is scheduled
45
+ 3. Only the most recent announcement will be executed after the delay period
46
+ 4. The delay applies to both regular text and localized text announcements
47
+
48
+ ### Example Usage
49
+
50
+ ```typescript
51
+ const accessibilityManager = AccessibilityManager.getInstance();
52
+
53
+ // Set a custom delay (optional)
54
+ accessibilityManager.setAnnouncementDelay(100);
55
+
56
+ // Rapid navigation scenario
57
+ accessibilityManager.readText({ text: "Home" }); // Cancelled by next call
58
+ accessibilityManager.readText({ text: "Settings" }); // Cancelled by next call
59
+ accessibilityManager.readText({ text: "Profile" }); // This will be announced after delay
60
+
61
+ // Only "Profile" will be announced, reducing noise
62
+ ```
63
+
64
+ ### Benefits
65
+
66
+ - **Reduced Noise**: Prevents excessive announcements during rapid navigation
67
+ - **Better UX**: Users only hear announcements for elements they actually focus on
68
+ - **Configurable**: Developers can adjust the delay based on their app's needs
69
+ - **Backward Compatible**: Existing code continues to work without changes
70
+
71
+ ### Default Behavior
72
+
73
+ - Default delay: 200ms
74
+ - Delay can be set to 0 to disable the feature (immediate announcements)
75
+ - Works with both regular text and localized text keys
76
+ - Maintains compatibility with heading queue functionality
@@ -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 = 200; // 200ms delay to reduce noise during rapid navigation
13
15
  private state$ = new BehaviorSubject<AccessibilityState>({
14
16
  screenReaderEnabled: false,
15
17
  reduceMotionEnabled: false,
@@ -82,13 +84,18 @@ export class AccessibilityManager {
82
84
  /**
83
85
  * text you want to be read, if you want to use localized text pass keyOfLocalizedText instead
84
86
  * keyOfLocalizedText is the key to the localized text
87
+ *
88
+ * Implements a delay mechanism to reduce noise during rapid navigation.
89
+ * Only the most recent announcement will be read after the delay period.
85
90
  */
86
91
  public readText({
87
92
  text,
88
93
  keyOfLocalizedText,
94
+ options,
89
95
  }: {
90
96
  text: string;
91
97
  keyOfLocalizedText?: string;
98
+ options?: any;
92
99
  }) {
93
100
  let textToRead = text;
94
101
 
@@ -112,9 +119,28 @@ export class AccessibilityManager {
112
119
  textToRead = localizedMessage;
113
120
  }
114
121
 
122
+ // Clear any existing announcement delay timeout to cancel previous announcements
123
+ if (this.announcementDelayTimeout) {
124
+ clearTimeout(this.announcementDelayTimeout);
125
+ }
126
+
127
+ // Set a new delay timeout for this announcement
128
+ this.announcementDelayTimeout = setTimeout(() => {
129
+ this.executeAnnouncement(textToRead, keyOfLocalizedText, options);
130
+ }, this.ANNOUNCEMENT_DELAY);
131
+ }
132
+
133
+ /**
134
+ * Internal method to execute the actual announcement after the delay
135
+ */
136
+ private executeAnnouncement(
137
+ textToRead: string,
138
+ keyOfLocalizedText?: string,
139
+ options?: any
140
+ ) {
115
141
  if (this.headingQueue.length > 0) {
116
142
  const heading = this.headingQueue.shift()!;
117
- this.ttsManager?.readText(heading);
143
+ this.ttsManager?.readText(heading, options);
118
144
 
119
145
  if (this.headingTimeout) {
120
146
  clearTimeout(this.headingTimeout);
@@ -123,10 +149,10 @@ export class AccessibilityManager {
123
149
  const pauseTime = this.calculateReadingTime(heading);
124
150
 
125
151
  this.headingTimeout = setTimeout(() => {
126
- this.ttsManager?.readText(textToRead);
152
+ this.ttsManager?.readText(textToRead, options);
127
153
  }, pauseTime);
128
154
  } else {
129
- this.ttsManager?.readText(textToRead);
155
+ this.ttsManager?.readText(textToRead, options);
130
156
  }
131
157
 
132
158
  this.announcements$.next({
@@ -143,12 +169,15 @@ export class AccessibilityManager {
143
169
 
144
170
  if (!buttonConfig) {
145
171
  return {
172
+ accessible: true,
146
173
  accessibilityLabel: buttonName,
147
174
  accessibilityHint: `Press button to perform action on ${buttonName}`,
148
175
  "aria-label": buttonName,
149
176
  "aria-description": `Press button to perform action on ${buttonName}`,
150
177
  accessibilityRole: "button" as AccessibilityRole,
151
178
  "aria-role": "button",
179
+ role: "button",
180
+ tabindex: 0,
152
181
  };
153
182
  }
154
183
 
@@ -162,23 +191,52 @@ export class AccessibilityManager {
162
191
  `Press button to perform action on ${buttonName}`;
163
192
 
164
193
  return {
194
+ accessible: true,
165
195
  accessibilityLabel: label,
166
196
  accessibilityHint: hint,
167
197
  "aria-label": label,
168
198
  "aria-description": hint,
169
199
  accessibilityRole: "button" as AccessibilityRole,
170
200
  "aria-role": "button",
201
+ role: "button",
202
+ tabindex: 0,
171
203
  };
172
204
  }
173
205
 
174
206
  public getInputAccessibilityProps(inputName: string): AccessibilityProps {
175
207
  return {
208
+ accessible: true,
176
209
  accessibilityLabel: inputName,
177
210
  accessibilityHint: `Enter text into ${inputName}`,
178
211
  "aria-label": inputName,
179
212
  "aria-description": `Enter text into ${inputName}`,
180
- accessibilityRole: "textbox" as AccessibilityRole,
181
- "aria-role": "textbox",
213
+ accessibilityRole: "text" as AccessibilityRole,
214
+ "aria-role": "text",
215
+ role: "text",
216
+ tabindex: 0,
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Extracts accessibility props from component props and returns them as HTML attributes
222
+ * @param props - Component props containing accessibility properties
223
+ * @returns Object with accessibility HTML attributes
224
+ */
225
+ public getWebAccessibilityProps(props: any): AccessibilityProps {
226
+ const {
227
+ "aria-label": ariaLabel,
228
+ "aria-description": ariaDescription,
229
+ "aria-role": ariaRole,
230
+ role,
231
+ tabindex,
232
+ } = props;
233
+
234
+ return {
235
+ "aria-label": ariaLabel,
236
+ "aria-description": ariaDescription,
237
+ "aria-role": ariaRole,
238
+ role: role || ariaRole,
239
+ tabindex,
182
240
  };
183
241
  }
184
242
 
@@ -196,4 +254,39 @@ export class AccessibilityManager {
196
254
 
197
255
  return this.localizations[key];
198
256
  }
257
+
258
+ /**
259
+ * Sets the announcement delay to reduce noise during rapid navigation
260
+ * @param delay - Delay in milliseconds (default: 150ms)
261
+ */
262
+ public setAnnouncementDelay(delay: number) {
263
+ this.ANNOUNCEMENT_DELAY = Math.max(0, delay);
264
+ }
265
+
266
+ /**
267
+ * Gets the current announcement delay setting
268
+ * @returns Current delay in milliseconds
269
+ */
270
+ public getAnnouncementDelay(): number {
271
+ return this.ANNOUNCEMENT_DELAY;
272
+ }
273
+
274
+ /**
275
+ * Cancels any pending announcement that hasn't been executed yet
276
+ * Useful for clearing announcements when navigation is interrupted
277
+ */
278
+ public cancelPendingAnnouncement() {
279
+ if (this.announcementDelayTimeout) {
280
+ clearTimeout(this.announcementDelayTimeout);
281
+ this.announcementDelayTimeout = null;
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Resets the TTSManager instance (mainly for testing purposes)
287
+ * @private
288
+ */
289
+ public _resetTTSManager() {
290
+ this.ttsManager = TTSManager.getInstance();
291
+ }
199
292
  }
@@ -211,9 +211,14 @@ export class TTSManager {
211
211
  return this.ttsState$.asObservable();
212
212
  }
213
213
 
214
- readText(text: string) {
214
+ readText(text: string, options: any = { skipAnnouncement: true }) {
215
215
  if (isSamsungPlatform() && window.speechSynthesis) {
216
216
  const utterance = new SpeechSynthesisUtterance(text);
217
+
218
+ if (options?.skipAnnouncement) {
219
+ window.speechSynthesis.cancel(); // Cancel previous speech before speaking new text
220
+ }
221
+
217
222
  window.speechSynthesis.speak(utterance);
218
223
  }
219
224
 
@@ -233,6 +238,7 @@ export class TTSManager {
233
238
  },
234
239
  parameters: {
235
240
  text,
241
+ clear: options?.skipAnnouncement, // Clear any pending speech before speaking new text
236
242
  },
237
243
  });
238
244
  } 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.5",
3
+ "version": "13.0.6-alpha.1043196035",
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.5",
30
+ "@applicaster/applicaster-types": "13.0.6-alpha.1043196035",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",