@applicaster/zapp-react-native-utils
Version:
Applicaster Zapp React Native utilities package
197 lines (161 loc) • 5.65 kB
text/typescript
import { BehaviorSubject } from "rxjs";
import { accessibilityManagerLogger as logger } from "./logger";
import { TTSManager } from "../platform/platformUtils";
import { BUTTON_ACCESSIBILITY_KEYS } from "./const";
import { AccessibilityRole } from "react-native";
export class AccessibilityManager {
private static _instance: AccessibilityManager | null = null;
private headingTimeout: ReturnType<typeof setTimeout> | null = null;
private WORDS_PER_MINUTE = 160;
private MINIMUM_PAUSE = 500;
private state$ = new BehaviorSubject<AccessibilityState>({
screenReaderEnabled: false,
reduceMotionEnabled: false,
boldTextEnabled: false,
});
private announcements$ = new BehaviorSubject<{
message: string;
localizedMessage?: string | undefined;
timestamp: number;
} | null>(null);
private ttsManager = TTSManager.getInstance();
private localizations: { [key: string]: string } = {};
private headingQueue: string[] = [];
private constructor() {}
public static getInstance(): AccessibilityManager {
if (!AccessibilityManager._instance) {
AccessibilityManager._instance = new AccessibilityManager();
}
return AccessibilityManager._instance;
}
/**
* The method now accepts any object with localizations using a flattened structure
*
* i.e. { accessibility_close_label: "Close", accessibility_close_hint: "Press here to close" }
*
* No longer accepts:
*
* i.e. localizations: [{ en: { accessibility_close_label: "Close", accessibility_close_hint: "Press here to close" } }]
*/
public updateLocalizations(localizations: { [key: string]: string }) {
this.localizations = localizations;
}
public getState(): AccessibilityState {
return this.state$.getValue();
}
public getStateAsObservable() {
return this.state$.asObservable();
}
/** Calculates the reading time for a given text
* This method is a bit of a hack because we don't have a callback, or promise from VIZIO API
* @param text - The text to calculate the reading time for
* @returns The reading time in milliseconds
*/
private calculateReadingTime(text: string): number {
const words = text.trim().split(/\s+/).length;
return Math.max(
this.MINIMUM_PAUSE,
(words / this.WORDS_PER_MINUTE) * 60 * 1000
);
}
/**
* Adds a heading to the queue, headings will be read before the next text
* Each heading will be read once and removed from the queue
*/
public addHeading(heading: string) {
this.headingQueue.push(heading);
}
/**
* text you want to be read, if you want to use localized text pass keyOfLocalizedText instead
* keyOfLocalizedText is the key to the localized text
*/
public readText({
text,
keyOfLocalizedText,
}: {
text: string;
keyOfLocalizedText?: string;
}) {
let textToRead = text;
if (keyOfLocalizedText) {
if (!this.localizations) {
logger.error(
"Attempting to use localized key without initialized localizations"
);
return;
}
const localizedMessage = this.getLocalizedMessage(keyOfLocalizedText);
if (!localizedMessage) {
logger.warn(`No localization found for key: ${keyOfLocalizedText}`);
return;
}
textToRead = localizedMessage;
}
if (this.headingQueue.length > 0) {
const heading = this.headingQueue.shift()!;
this.ttsManager?.readText(heading);
if (this.headingTimeout) {
clearTimeout(this.headingTimeout);
}
const pauseTime = this.calculateReadingTime(heading);
this.headingTimeout = setTimeout(() => {
this.ttsManager?.readText(textToRead);
}, pauseTime);
} else {
this.ttsManager?.readText(textToRead);
}
this.announcements$.next({
message: textToRead,
localizedMessage: keyOfLocalizedText ? textToRead : undefined,
timestamp: Date.now(),
});
}
public getButtonAccessibilityProps(buttonName: string): AccessibilityProps {
const buttonConfig = BUTTON_ACCESSIBILITY_KEYS[buttonName];
if (!buttonConfig) {
return {
accessibilityLabel: buttonName,
accessibilityHint: `Press button to perform action on ${buttonName}`,
"aria-label": buttonName,
"aria-description": `Press button to perform action on ${buttonName}`,
accessibilityRole: "button" as AccessibilityRole,
"aria-role": "button",
};
}
const labelKey = buttonConfig.label;
const hintKey = buttonConfig.hint;
const label = this.getLocalizedMessage(labelKey) || buttonName;
const hint =
this.getLocalizedMessage(hintKey) ||
`Press button to perform action on ${buttonName}`;
return {
accessibilityLabel: label,
accessibilityHint: hint,
"aria-label": label,
"aria-description": hint,
accessibilityRole: "button" as AccessibilityRole,
"aria-role": "button",
};
}
public getInputAccessibilityProps(inputName: string): AccessibilityProps {
return {
accessibilityLabel: inputName,
accessibilityHint: `Enter text into ${inputName}`,
"aria-label": inputName,
"aria-description": `Enter text into ${inputName}`,
accessibilityRole: "textbox" as AccessibilityRole,
"aria-role": "textbox",
};
}
public getLocalizedMessage(key: string): string | void {
if (!key) return;
if (!this.localizations) {
logger.error(
"Attempting to use localized key without initialized localizations",
{ key }
);
return;
}
return this.localizations[key];
}
}