@accessibility-rn-js/react-native-accessibility-toolkit 1.0.0

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,169 @@
1
+ /**
2
+ * useThemeColors.js
3
+ * Hook to get dynamic colors based on accessibility settings
4
+ */
5
+
6
+ import { useMemo } from 'react';
7
+ import { useAccessibility } from '../context/AccessibilityContext';
8
+
9
+ // Helper to convert hex to RGB
10
+ const hexToRgb = (hex) => {
11
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
12
+ return result
13
+ ? {
14
+ r: parseInt(result[1], 16),
15
+ g: parseInt(result[2], 16),
16
+ b: parseInt(result[3], 16),
17
+ }
18
+ : null;
19
+ };
20
+
21
+ // Helper to convert RGB to hex
22
+ const rgbToHex = (r, g, b) => {
23
+ return '#' + [r, g, b].map((x) => {
24
+ const hex = Math.round(x).toString(16);
25
+ return hex.length === 1 ? '0' + hex : hex;
26
+ }).join('');
27
+ };
28
+
29
+ // Apply grayscale to a color
30
+ const applyGrayscale = (hex) => {
31
+ const rgb = hexToRgb(hex);
32
+ if (!rgb) return hex;
33
+ const gray = Math.round(0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b);
34
+ return rgbToHex(gray, gray, gray);
35
+ };
36
+
37
+ // Apply saturation adjustment
38
+ const applySaturation = (hex, factor) => {
39
+ const rgb = hexToRgb(hex);
40
+ if (!rgb) return hex;
41
+
42
+ const gray = 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b;
43
+ const r = Math.max(0, Math.min(255, gray + factor * (rgb.r - gray)));
44
+ const g = Math.max(0, Math.min(255, gray + factor * (rgb.g - gray)));
45
+ const b = Math.max(0, Math.min(255, gray + factor * (rgb.b - gray)));
46
+
47
+ return rgbToHex(r, g, b);
48
+ };
49
+
50
+ // Invert a color
51
+ const invertColor = (hex) => {
52
+ const rgb = hexToRgb(hex);
53
+ if (!rgb) return hex;
54
+ return rgbToHex(255 - rgb.r, 255 - rgb.g, 255 - rgb.b);
55
+ };
56
+
57
+ export const useThemeColors = () => {
58
+ const {
59
+ colorTheme,
60
+ highContrast,
61
+ colorInversion,
62
+ greyscale,
63
+ lowSaturation,
64
+ highSaturation,
65
+ } = useAccessibility();
66
+
67
+ const colors = useMemo(() => {
68
+ const isDark = colorTheme === 'dark';
69
+
70
+ // Base colors
71
+ let baseColors;
72
+
73
+ if (highContrast) {
74
+ // High contrast colors
75
+ if (isDark) {
76
+ baseColors = {
77
+ background: '#000000',
78
+ text: '#FFFFFF',
79
+ primary: '#FFFFFF',
80
+ secondary: '#FFFF00',
81
+ border: '#FFFFFF',
82
+ card: '#1A1A1A',
83
+ placeholder: '#CCCCCC',
84
+ };
85
+ } else {
86
+ baseColors = {
87
+ background: '#FFFFFF',
88
+ text: '#000000',
89
+ primary: '#0000FF',
90
+ secondary: '#000080',
91
+ border: '#000000',
92
+ card: '#F5F5F5',
93
+ placeholder: '#666666',
94
+ };
95
+ }
96
+ } else {
97
+ // Normal colors
98
+ if (isDark) {
99
+ baseColors = {
100
+ background: '#1E1E1E',
101
+ text: '#FFFFFF',
102
+ primary: '#007AFF',
103
+ secondary: '#5856D6',
104
+ border: '#3A3A3C',
105
+ card: '#2C2C2E',
106
+ placeholder: '#8E8E93',
107
+ };
108
+ } else {
109
+ baseColors = {
110
+ background: '#FFFFFF',
111
+ text: '#000000',
112
+ primary: '#007AFF',
113
+ secondary: '#5856D6',
114
+ border: '#C7C7CC',
115
+ card: '#F2F2F7',
116
+ placeholder: '#8E8E93',
117
+ };
118
+ }
119
+ }
120
+
121
+ // Apply color transformations
122
+ let processedColors = { ...baseColors };
123
+
124
+ // Apply grayscale
125
+ if (greyscale) {
126
+ processedColors = Object.keys(processedColors).reduce((acc, key) => {
127
+ acc[key] = applyGrayscale(processedColors[key]);
128
+ return acc;
129
+ }, {});
130
+ }
131
+
132
+ // Apply saturation adjustments
133
+ if (lowSaturation && !greyscale) {
134
+ processedColors = Object.keys(processedColors).reduce((acc, key) => {
135
+ if (key !== 'background' && key !== 'text') {
136
+ acc[key] = applySaturation(processedColors[key], 0.3);
137
+ } else {
138
+ acc[key] = processedColors[key];
139
+ }
140
+ return acc;
141
+ }, {});
142
+ }
143
+
144
+ if (highSaturation && !greyscale && !lowSaturation) {
145
+ processedColors = Object.keys(processedColors).reduce((acc, key) => {
146
+ if (key !== 'background' && key !== 'text') {
147
+ acc[key] = applySaturation(processedColors[key], 2.0);
148
+ } else {
149
+ acc[key] = processedColors[key];
150
+ }
151
+ return acc;
152
+ }, {});
153
+ }
154
+
155
+ // Apply color inversion
156
+ if (colorInversion) {
157
+ processedColors = Object.keys(processedColors).reduce((acc, key) => {
158
+ acc[key] = invertColor(processedColors[key]);
159
+ return acc;
160
+ }, {});
161
+ }
162
+
163
+ return processedColors;
164
+ }, [colorTheme, highContrast, colorInversion, greyscale, lowSaturation, highSaturation]);
165
+
166
+ return colors;
167
+ };
168
+
169
+ export default useThemeColors;
package/src/index.js ADDED
@@ -0,0 +1,49 @@
1
+ // Main exports for the accessibility toolkit
2
+ export { AccessibilityProvider, useAccessibility } from './context/AccessibilityContext';
3
+
4
+ // Components
5
+ export { default as AccessibilityButton } from './components/AccessibilityButton';
6
+ export { default as AccessibleText } from './components/AccessibleText';
7
+
8
+ // Hooks
9
+ export { default as useDynamicColors } from './hooks/useDynamicColors';
10
+ export { default as usePageRead } from './hooks/usePageRead';
11
+ export { default as useThemeColors } from './hooks/useThemeColors';
12
+
13
+ // Services
14
+ export { default as TTSService } from './services/TTSService';
15
+ export { default as NativeAccessibilityBridge } from './services/NativeAccessibilityBridge';
16
+ export {
17
+ loadAccessibilityPreferences,
18
+ saveAccessibilityPreferences,
19
+ clearAccessibilityPreferences
20
+ } from './services/AccessibilityStorage';
21
+
22
+ // Utils
23
+ export {
24
+ ACCESSIBILITY_PROFILES,
25
+ TEXT_ALIGNMENT,
26
+ COLOR_THEMES,
27
+ DEFAULT_ACCESSIBILITY_STATE,
28
+ PROFILE_CONFIGS,
29
+ applyProfile,
30
+ getAdjustedFontSize,
31
+ getAdjustedLineHeight,
32
+ getButtonPadding,
33
+ MIN_TOUCH_TARGET,
34
+ } from './utils/AccessibilityUtils';
35
+
36
+ export { getColors } from './utils/Colors';
37
+
38
+ // Default export for convenience
39
+ export default {
40
+ AccessibilityProvider,
41
+ useAccessibility,
42
+ AccessibilityButton,
43
+ AccessibleText,
44
+ useDynamicColors,
45
+ usePageRead,
46
+ useThemeColors,
47
+ TTSService,
48
+ NativeAccessibilityBridge,
49
+ };
@@ -0,0 +1,40 @@
1
+ import AsyncStorage from '@react-native-async-storage/async-storage';
2
+ import { DEFAULT_ACCESSIBILITY_STATE } from '../utils/AccessibilityUtils';
3
+
4
+ const STORAGE_KEY = '@accessibility_preferences';
5
+
6
+ // Save accessibility preferences
7
+ export const saveAccessibilityPreferences = async (preferences) => {
8
+ try {
9
+ await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
10
+ return true;
11
+ } catch (error) {
12
+ console.error('Error saving accessibility preferences:', error);
13
+ return false;
14
+ }
15
+ };
16
+
17
+ // Load accessibility preferences
18
+ export const loadAccessibilityPreferences = async () => {
19
+ try {
20
+ const stored = await AsyncStorage.getItem(STORAGE_KEY);
21
+ if (stored) {
22
+ return JSON.parse(stored);
23
+ }
24
+ return DEFAULT_ACCESSIBILITY_STATE;
25
+ } catch (error) {
26
+ console.error('Error loading accessibility preferences:', error);
27
+ return DEFAULT_ACCESSIBILITY_STATE;
28
+ }
29
+ };
30
+
31
+ // Clear accessibility preferences
32
+ export const clearAccessibilityPreferences = async () => {
33
+ try {
34
+ await AsyncStorage.removeItem(STORAGE_KEY);
35
+ return true;
36
+ } catch (error) {
37
+ console.error('Error clearing accessibility preferences:', error);
38
+ return false;
39
+ }
40
+ };
@@ -0,0 +1,171 @@
1
+ /**
2
+ * NativeAccessibilityBridge.js
3
+ * Bridge between React Native native accessibility features and custom implementation
4
+ */
5
+
6
+ import { AccessibilityInfo, Platform } from 'react-native';
7
+
8
+ class NativeAccessibilityBridge {
9
+ listeners = [];
10
+
11
+ /**
12
+ * Initialize native accessibility detection
13
+ */
14
+ async initialize(updateCallback) {
15
+ try {
16
+ // Detect Screen Reader
17
+ const screenReaderEnabled = await AccessibilityInfo.isScreenReaderEnabled();
18
+
19
+ // Detect Reduced Motion (iOS only)
20
+ let reducedMotionEnabled = false;
21
+ if (Platform.OS === 'ios') {
22
+ reducedMotionEnabled = await AccessibilityInfo.isReduceMotionEnabled();
23
+ }
24
+
25
+ // Detect Bold Text (iOS only)
26
+ let boldTextEnabled = false;
27
+ if (Platform.OS === 'ios') {
28
+ boldTextEnabled = await AccessibilityInfo.isBoldTextEnabled();
29
+ }
30
+
31
+ // Detect Grayscale (iOS only)
32
+ let grayscaleEnabled = false;
33
+ if (Platform.OS === 'ios') {
34
+ grayscaleEnabled = await AccessibilityInfo.isGrayscaleEnabled();
35
+ }
36
+
37
+ // Detect Invert Colors (iOS only)
38
+ let invertColorsEnabled = false;
39
+ if (Platform.OS === 'ios') {
40
+ invertColorsEnabled = await AccessibilityInfo.isInvertColorsEnabled();
41
+ }
42
+
43
+ // Detect Reduce Transparency (iOS only)
44
+ let reduceTransparencyEnabled = false;
45
+ if (Platform.OS === 'ios') {
46
+ reduceTransparencyEnabled = await AccessibilityInfo.isReduceTransparencyEnabled();
47
+ }
48
+
49
+ // Update with native settings
50
+ updateCallback({
51
+ nativeScreenReader: screenReaderEnabled,
52
+ nativeReducedMotion: reducedMotionEnabled,
53
+ nativeBoldText: boldTextEnabled,
54
+ nativeGrayscale: grayscaleEnabled,
55
+ nativeInvertColors: invertColorsEnabled,
56
+ nativeReduceTransparency: reduceTransparencyEnabled,
57
+ });
58
+
59
+ // Setup listeners
60
+ this.setupListeners(updateCallback);
61
+
62
+ return {
63
+ screenReaderEnabled,
64
+ reducedMotionEnabled,
65
+ boldTextEnabled,
66
+ grayscaleEnabled,
67
+ invertColorsEnabled,
68
+ reduceTransparencyEnabled,
69
+ };
70
+ } catch (error) {
71
+ console.error('Error initializing native accessibility:', error);
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Setup event listeners for native accessibility changes
78
+ */
79
+ setupListeners(updateCallback) {
80
+ // Screen Reader changed
81
+ const screenReaderListener = AccessibilityInfo.addEventListener(
82
+ 'screenReaderChanged',
83
+ (enabled) => {
84
+ updateCallback({ nativeScreenReader: enabled });
85
+ }
86
+ );
87
+ this.listeners.push(screenReaderListener);
88
+
89
+ // Reduced Motion changed (iOS)
90
+ if (Platform.OS === 'ios') {
91
+ const reducedMotionListener = AccessibilityInfo.addEventListener(
92
+ 'reduceMotionChanged',
93
+ (enabled) => {
94
+ updateCallback({ nativeReducedMotion: enabled });
95
+ }
96
+ );
97
+ this.listeners.push(reducedMotionListener);
98
+
99
+ // Bold Text changed
100
+ const boldTextListener = AccessibilityInfo.addEventListener(
101
+ 'boldTextChanged',
102
+ (enabled) => {
103
+ updateCallback({ nativeBoldText: enabled });
104
+ }
105
+ );
106
+ this.listeners.push(boldTextListener);
107
+
108
+ // Grayscale changed
109
+ const grayscaleListener = AccessibilityInfo.addEventListener(
110
+ 'grayscaleChanged',
111
+ (enabled) => {
112
+ updateCallback({ nativeGrayscale: enabled });
113
+ }
114
+ );
115
+ this.listeners.push(grayscaleListener);
116
+
117
+ // Invert Colors changed
118
+ const invertColorsListener = AccessibilityInfo.addEventListener(
119
+ 'invertColorsChanged',
120
+ (enabled) => {
121
+ updateCallback({ nativeInvertColors: enabled });
122
+ }
123
+ );
124
+ this.listeners.push(invertColorsListener);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Announce message for screen readers
130
+ */
131
+ announce(message, options = {}) {
132
+ AccessibilityInfo.announceForAccessibility(message);
133
+ }
134
+
135
+ /**
136
+ * Announce message for screen readers (with queue option for iOS)
137
+ */
138
+ announceForAccessibilityWithOptions(message, options = {}) {
139
+ if (Platform.OS === 'ios') {
140
+ AccessibilityInfo.announceForAccessibilityWithOptions(
141
+ message,
142
+ options // { queue: true/false }
143
+ );
144
+ } else {
145
+ AccessibilityInfo.announceForAccessibility(message);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Set accessibility focus to a specific element
151
+ */
152
+ setAccessibilityFocus(reactTag) {
153
+ if (Platform.OS === 'ios') {
154
+ AccessibilityInfo.setAccessibilityFocus(reactTag);
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Clean up listeners
160
+ */
161
+ cleanup() {
162
+ this.listeners.forEach(listener => {
163
+ if (listener && listener.remove) {
164
+ listener.remove();
165
+ }
166
+ });
167
+ this.listeners = [];
168
+ }
169
+ }
170
+
171
+ export default new NativeAccessibilityBridge();
@@ -0,0 +1,117 @@
1
+ /**
2
+ * TTSService.js
3
+ * Text-to-Speech service for reading page content
4
+ */
5
+
6
+ import { Platform } from 'react-native';
7
+ import Tts from 'react-native-tts';
8
+
9
+ class TTSService {
10
+ isSpeaking = false;
11
+ isPaused = false;
12
+
13
+ /**
14
+ * Initialize TTS
15
+ */
16
+ async init() {
17
+ try {
18
+ // Set up event listeners
19
+ Tts.addEventListener('tts-start', () => {
20
+ this.isSpeaking = true;
21
+ });
22
+
23
+ Tts.addEventListener('tts-finish', () => {
24
+ this.isSpeaking = false;
25
+ });
26
+
27
+ Tts.addEventListener('tts-cancel', () => {
28
+ this.isSpeaking = false;
29
+ });
30
+
31
+ // Set default settings
32
+ await Tts.setDefaultLanguage('en-US');
33
+ await Tts.setDefaultRate(0.5);
34
+ await Tts.setDefaultPitch(1.0);
35
+ } catch (error) {
36
+ console.error('TTS Init Error:', error);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Speak text aloud
42
+ */
43
+ async speak(text, options = {}) {
44
+ const {
45
+ language = 'en-US',
46
+ pitch = 1.0,
47
+ rate = 0.5,
48
+ } = options;
49
+
50
+ try {
51
+ this.isSpeaking = true;
52
+ this.isPaused = false;
53
+
54
+ await Tts.setDefaultLanguage(language);
55
+ await Tts.setDefaultRate(rate);
56
+ await Tts.setDefaultPitch(pitch);
57
+
58
+ Tts.speak(text);
59
+ } catch (error) {
60
+ this.isSpeaking = false;
61
+ console.error('TTS Error:', error);
62
+ throw error;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Stop speaking
68
+ */
69
+ stop() {
70
+ Tts.stop();
71
+ this.isSpeaking = false;
72
+ this.isPaused = false;
73
+ }
74
+
75
+ /**
76
+ * Check if TTS is available
77
+ */
78
+ async isAvailable() {
79
+ try {
80
+ const voices = await Tts.voices();
81
+ return voices && voices.length > 0;
82
+ } catch (error) {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Get available voices
89
+ */
90
+ async getVoices() {
91
+ try {
92
+ return await Tts.voices();
93
+ } catch (error) {
94
+ return [];
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Read entire page (utility function)
100
+ * Extracts text from components marked with accessibility labels
101
+ */
102
+ readPage(textContent) {
103
+ if (!textContent) return;
104
+
105
+ // Clean and prepare text for reading
106
+ const cleanText = textContent
107
+ .replace(/\s+/g, ' ')
108
+ .replace(/[^\w\s.,!?-]/g, '')
109
+ .trim();
110
+
111
+ this.speak(cleanText, {
112
+ rate: 0.9, // Slightly slower for better comprehension
113
+ });
114
+ }
115
+ }
116
+
117
+ export default new TTSService();