@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: "
|
|
181
|
-
"aria-role": "
|
|
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.
|
|
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.
|
|
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",
|