UNPKG

@applicaster/zapp-react-native-utils

Version:

Applicaster Zapp React Native utilities package

197 lines (161 loc) • 5.65 kB
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]; } }