Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { NavigationContainer, LinkingOptions } from '@react-navigation/native';
import { AuthProvider } from './src/contexts/AuthContext';
import { AccessibilityProvider } from './src/contexts/AccessibilityContext';
import { useAuth } from './src/hooks/useAuth';
import SplashScreen from './src/screens/SplashScreen';
import AuthNavigator from './src/navigation/AuthNavigator';
Expand Down Expand Up @@ -61,18 +62,20 @@ function RootNavigator() {

export default function App() {
return (
<AuthProvider>
<NavigationContainer<RootStackParamList>
linking={linkingConfig}
fallback={
<View style={styles.loading}>
<ActivityIndicator size="large" />
</View>
}
>
<RootNavigator />
</NavigationContainer>
</AuthProvider>
<AccessibilityProvider>
<AuthProvider>
<NavigationContainer<RootStackParamList>
linking={linkingConfig}
fallback={
<View style={styles.loading}>
<ActivityIndicator size="large" />
</View>
}
>
<RootNavigator />
</NavigationContainer>
</AuthProvider>
</AccessibilityProvider>
);
}

Expand Down
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ module.exports = {
'node_modules/(?!(react-native|@react-native|@react-navigation)/)'
],
testPathIgnorePatterns: ['/node_modules/', '/android/', '/ios/'],
setupFiles: ['<rootDir>/jest.setup.js'],
resetMocks: true,
};
3 changes: 3 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
jest.mock('@react-native-async-storage/async-storage', () =>

Check failure on line 1 in jest.setup.js

View workflow job for this annotation

GitHub Actions / Lint, Tipos e Testes

'jest' is not defined
require('@react-native-async-storage/async-storage/jest/async-storage-mock'),
);
75 changes: 46 additions & 29 deletions src/components/HorizontalMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import React, { useMemo } from 'react';
import { ScrollView, TouchableOpacity, Text, StyleSheet } from 'react-native';
import { Colors, Spacing, FontSize, BorderRadius } from '../theme';
import { Spacing, FontSize, BorderRadius } from '../theme';
import { ThemeColors } from '../theme/colors';
import { useThemeColors, useFontScale } from '../contexts/AccessibilityContext';

interface HorizontalMenuProps {
items: string[];
Expand All @@ -9,11 +11,16 @@ interface HorizontalMenuProps {
}

export default function HorizontalMenu({ items, selected, onSelect }: HorizontalMenuProps) {
const colors = useThemeColors();
const fontScale = useFontScale();
const styles = useStyles(colors, fontScale);

return (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.container}
accessibilityRole="tablist"
>
{items.map((item) => {
const isActive = item === selected;
Expand All @@ -22,6 +29,10 @@ export default function HorizontalMenu({ items, selected, onSelect }: Horizontal
key={item}
style={[styles.item, isActive && styles.itemActive]}
onPress={() => onSelect(item)}
accessibilityRole="tab"
accessibilityLabel={item}
accessibilityState={{ selected: isActive }}
accessibilityHint={`Filtrar por ${item}`}
>
<Text style={[styles.text, isActive && styles.textActive]}>
{item}
Expand All @@ -33,30 +44,36 @@ export default function HorizontalMenu({ items, selected, onSelect }: Horizontal
);
}

const styles = StyleSheet.create({
container: {
paddingHorizontal: Spacing.base,
paddingVertical: Spacing.sm,
gap: Spacing.sm,
},
item: {
paddingHorizontal: Spacing.base,
paddingVertical: Spacing.sm,
borderRadius: BorderRadius.xl,
backgroundColor: Colors.surface,
borderWidth: 1,
borderColor: Colors.border,
},
itemActive: {
backgroundColor: Colors.primary,
borderColor: Colors.primary,
},
text: {
fontSize: FontSize.md,
color: Colors.textSecondary,
},
textActive: {
color: Colors.textOnPrimary,
fontWeight: '600',
},
});
function useStyles(colors: ThemeColors, fontScale: number) {
return useMemo(
() =>
StyleSheet.create({
container: {
paddingHorizontal: Spacing.base,
paddingVertical: Spacing.sm,
gap: Spacing.sm,
},
item: {
paddingHorizontal: Spacing.base,
paddingVertical: Spacing.sm,
borderRadius: BorderRadius.xl,
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.border,
},
itemActive: {
backgroundColor: colors.primary,
borderColor: colors.primary,
},
text: {
fontSize: FontSize.md * fontScale,
color: colors.textSecondary,
},
textActive: {
color: colors.textOnPrimary,
fontWeight: '600',
},
}),
[colors, fontScale],
);
}
157 changes: 157 additions & 0 deletions src/contexts/AccessibilityContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React, {
createContext,
useContext,
useState,
useEffect,
useCallback,
useMemo,
ReactNode,
} from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Colors, HighContrastColors, ThemeColors } from '../theme/colors';

export type FontScale = 'normal' | 'grande' | 'maior';

// Multiplicador aplicado à tipografia conforme a preferência do usuário.
export const FONT_SCALE_FACTOR: Record<FontScale, number> = {
normal: 1,
grande: 1.2,
maior: 1.4,
};

const FONT_SCALE_LABEL: Record<FontScale, string> = {
normal: 'Normal',
grande: 'Grande',
maior: 'Maior',
};

const STORAGE_KEY_FONTE = '@App:acessibilidade:fontScale';
const STORAGE_KEY_CONTRASTE = '@App:acessibilidade:highContrast';

const ORDEM_FONTE: FontScale[] = ['normal', 'grande', 'maior'];

interface AccessibilityContextData {
fontScale: FontScale;
fontScaleFactor: number;
fontScaleLabel: string;
highContrast: boolean;
isLoading: boolean;
colors: ThemeColors;
setFontScale(escala: FontScale): Promise<void>;
cycleFontScale(): Promise<void>;
setHighContrast(ativo: boolean): Promise<void>;
toggleHighContrast(): Promise<void>;
}

const AccessibilityContext = createContext<AccessibilityContextData>(
{} as AccessibilityContextData,
);

interface AccessibilityProviderProps {
children: ReactNode;
}

export const AccessibilityProvider = ({ children }: AccessibilityProviderProps) => {
const [fontScale, setFontScaleState] = useState<FontScale>('normal');
const [highContrast, setHighContrastState] = useState(false);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
async function carregarPreferencias() {
try {
const [fonteSalva, contrasteSalvo] = await Promise.all([
AsyncStorage.getItem(STORAGE_KEY_FONTE),
AsyncStorage.getItem(STORAGE_KEY_CONTRASTE),
]);
if (fonteSalva && fonteSalva in FONT_SCALE_FACTOR) {
setFontScaleState(fonteSalva as FontScale);
}
if (contrasteSalvo === 'true') {
setHighContrastState(true);
}
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
}
carregarPreferencias();
}, []);

const setFontScale = useCallback(async (escala: FontScale) => {
setFontScaleState(escala);
try {
await AsyncStorage.setItem(STORAGE_KEY_FONTE, escala);
} catch (error) {
console.error(error);
}
}, []);

const cycleFontScale = useCallback(async () => {
const indiceAtual = ORDEM_FONTE.indexOf(fontScale);
const proxima = ORDEM_FONTE[(indiceAtual + 1) % ORDEM_FONTE.length];
await setFontScale(proxima);
}, [fontScale, setFontScale]);

const setHighContrast = useCallback(async (ativo: boolean) => {
setHighContrastState(ativo);
try {
await AsyncStorage.setItem(STORAGE_KEY_CONTRASTE, ativo ? 'true' : 'false');
} catch (error) {
console.error(error);
}
}, []);

const toggleHighContrast = useCallback(async () => {
await setHighContrast(!highContrast);
}, [highContrast, setHighContrast]);

const value = useMemo<AccessibilityContextData>(
() => ({
fontScale,
fontScaleFactor: FONT_SCALE_FACTOR[fontScale],
fontScaleLabel: FONT_SCALE_LABEL[fontScale],
highContrast,
isLoading,
colors: highContrast ? HighContrastColors : Colors,
setFontScale,
cycleFontScale,
setHighContrast,
toggleHighContrast,
}),
[
fontScale,
highContrast,
isLoading,
setFontScale,
cycleFontScale,
setHighContrast,
toggleHighContrast,
],
);

return (
<AccessibilityContext.Provider value={value}>
{children}
</AccessibilityContext.Provider>
);
};

export function useAccessibility(): AccessibilityContextData {
return useContext(AccessibilityContext);
}

// Retorna a paleta ativa conforme a preferência de contraste.
// Faz fallback para a paleta padrão fora do Provider.
export function useThemeColors(): ThemeColors {
const ctx = useContext(AccessibilityContext);
return ctx?.colors ?? Colors;
}

// Retorna o multiplicador de fonte ativo, com fallback seguro (1).
export function useFontScale(): number {
const ctx = useContext(AccessibilityContext);
return ctx?.fontScaleFactor ?? 1;
}

export default AccessibilityContext;
Loading
Loading