Skip to content

Commit

Permalink
feat: added dark theme to example (#2242)
Browse files Browse the repository at this point in the history
## Description

This PR adds dark theme to the example app. The theme changes
automatically based on the device's appearance setting and can be
changed by clicking a button.

## Changes

- added dark styles to screens and components
- added a state and listener for basic theme management
- added a dark mode button

## Screenshots / GIFs


https://github.com/software-mansion/react-native-screens/assets/91994767/c156eaa7-7603-4f92-b1a2-5599e33ecc3f


https://github.com/software-mansion/react-native-screens/assets/91994767/5e479905-b046-4ea4-bfc1-1667837eee60

## Test code and steps to reproduce

- Use the Example app
- Toggle the theme with a button or by changing device's setting
- Browse screens

## Checklist

- [x] Included code example that can be used to test this change
- [x] Ensured that CI passes
  • Loading branch information
alduzy authored Jul 11, 2024
1 parent 0c27805 commit c2ac40f
Show file tree
Hide file tree
Showing 14 changed files with 219 additions and 161 deletions.
161 changes: 95 additions & 66 deletions apps/Example.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import React from 'react';
import React, { createContext, useContext, useEffect, useState } from 'react';
import {
ScrollView,
StyleSheet,
Text,
I18nManager,
Platform,
StatusBar,
useColorScheme,
} from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import {
DarkTheme,
DefaultTheme,
NavigationContainer,
useTheme,
} from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import RNRestart from 'react-native-restart';

import { ListItem, SettingsSwitch } from './src/shared';
import { ListItem, SettingsSwitch, ThemedText } from './src/shared';

import SimpleNativeStack from './src/screens/SimpleNativeStack';
import SwipeBackAnimation from './src/screens/SwipeBackAnimation';
Expand Down Expand Up @@ -136,74 +141,98 @@ interface MainScreenProps {
navigation: StackNavigationProp<RootStackParamList, 'Main'>;
}

const MainScreen = ({ navigation }: MainScreenProps): React.JSX.Element => (
<ScrollView testID="root-screen-examples-scrollview">
<SettingsSwitch
style={styles.switch}
label="Right to left"
value={I18nManager.isRTL}
onValueChange={() => {
I18nManager.forceRTL(!I18nManager.isRTL);
RNRestart.Restart();
}}
/>
<Text style={styles.label} testID="root-screen-examples-header">
Examples
</Text>
{examples.map(name => (
<ListItem
key={name}
testID={`root-screen-example-${name}`}
title={SCREENS[name].title}
onPress={() => navigation.navigate(name)}
disabled={!isPlatformReady(name)}
const MainScreen = ({ navigation }: MainScreenProps): React.JSX.Element => {
const { toggleTheme } = useContext(ThemeToggle);
const isDark = useTheme().dark;

return (
<ScrollView testID="root-screen-examples-scrollview">
<SettingsSwitch
style={styles.switch}
label="Right to left"
value={I18nManager.isRTL}
onValueChange={() => {
I18nManager.forceRTL(!I18nManager.isRTL);
RNRestart.Restart();
}}
/>
))}
<Text style={styles.label}>Playgrounds</Text>
{playgrounds.map(name => (
<ListItem
key={name}
testID={`root-screen-playground-${name}`}
title={SCREENS[name].title}
onPress={() => navigation.navigate(name)}
disabled={!isPlatformReady(name)}
<SettingsSwitch
style={styles.switch}
label="Dark mode"
value={isDark}
onValueChange={toggleTheme}
/>
))}
</ScrollView>
);

const ExampleApp = (): React.JSX.Element => (
<GestureHandlerRootView style={{ flex: 1 }}>
<GestureDetectorProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Main"
options={{
title: `${
Platform.isTV ? '📺' : '📱'
} React Native Screens Examples`,
}}
component={MainScreen}
/>
{Object.keys(SCREENS).map(name => (
<Stack.Screen
key={name}
name={name}
getComponent={() => SCREENS[name].component}
options={{ headerShown: false }}
/>
))}
</Stack.Navigator>
</NavigationContainer>
</GestureDetectorProvider>
</GestureHandlerRootView>
);
<ThemedText style={styles.label} testID="root-screen-examples-header">
Examples
</ThemedText>
{examples.map(name => (
<ListItem
key={name}
testID={`root-screen-example-${name}`}
title={SCREENS[name].title}
onPress={() => navigation.navigate(name)}
disabled={!isPlatformReady(name)}
/>
))}
<ThemedText style={styles.label}>Playgrounds</ThemedText>
{playgrounds.map(name => (
<ListItem
key={name}
testID={`root-screen-playground-${name}`}
title={SCREENS[name].title}
onPress={() => navigation.navigate(name)}
disabled={!isPlatformReady(name)}
/>
))}
</ScrollView>
);
};

const ThemeToggle = createContext<{ toggleTheme: () => void }>(null!);

const ExampleApp = (): React.JSX.Element => {
const scheme = useColorScheme();
const [isDark, setIsDark] = useState(scheme === 'dark');

useEffect(() => setIsDark(scheme === 'dark'), [scheme]);

const toggleTheme = () => setIsDark(prev => !prev);

return (
<GestureHandlerRootView style={{ flex: 1 }}>
<GestureDetectorProvider>
<ThemeToggle.Provider value={{ toggleTheme }}>
<NavigationContainer theme={isDark ? DarkTheme : DefaultTheme}>
<Stack.Navigator
screenOptions={{ statusBarStyle: isDark ? 'light' : 'dark' }}>
<Stack.Screen
name="Main"
options={{
title: `${
Platform.isTV ? '📺' : '📱'
} React Native Screens Examples`,
}}
component={MainScreen}
/>
{Object.keys(SCREENS).map(name => (
<Stack.Screen
key={name}
name={name}
getComponent={() => SCREENS[name].component}
options={{ headerShown: false }}
/>
))}
</Stack.Navigator>
</NavigationContainer>
</ThemeToggle.Provider>
</GestureDetectorProvider>
</GestureHandlerRootView>
);
};

const styles = StyleSheet.create({
label: {
fontSize: 15,
color: 'black',
margin: 10,
marginTop: 15,
},
Expand Down
8 changes: 6 additions & 2 deletions apps/src/screens/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,9 @@ interface PrivacyScreenProps {
navigation: NativeStackNavigationProp<StackParamList, 'Main'>;
}

const PrivacyScreen = ({ navigation }: PrivacyScreenProps): React.JSX.Element => {
const PrivacyScreen = ({
navigation,
}: PrivacyScreenProps): React.JSX.Element => {
const toast = useToast();

useEffect(() => {
Expand Down Expand Up @@ -228,7 +230,9 @@ interface OptionsScreenProps {
navigation: NativeStackNavigationProp<StackParamList, 'Main'>;
}

const OptionsScreen = ({ navigation }: OptionsScreenProps): React.JSX.Element => {
const OptionsScreen = ({
navigation,
}: OptionsScreenProps): React.JSX.Element => {
const toast = useToast();

useEffect(() => {
Expand Down
1 change: 0 additions & 1 deletion apps/src/screens/HeaderOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,6 @@ const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 10,
backgroundColor: 'white',
},
heading: {
marginLeft: 10,
Expand Down
11 changes: 6 additions & 5 deletions apps/src/screens/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useLayoutEffect, useRef, useState } from 'react';
import { ScrollView, Text, StyleSheet } from 'react-native';
import { ScrollView, StyleSheet } from 'react-native';
import { SearchBarCommands, SearchBarProps } from 'react-native-screens';
import {
createNativeStackNavigator,
Expand All @@ -11,6 +11,7 @@ import {
SettingsInput,
SettingsPicker,
SettingsSwitch,
ThemedText,
ToastProvider,
useToast,
} from '../shared';
Expand Down Expand Up @@ -132,7 +133,7 @@ const MainScreen = ({ navigation }: MainScreenProps): React.JSX.Element => {
onValueChange={setAutoCapitalize}
items={['none', 'words', 'sentences', 'characters']}
/>
<Text style={styles.heading}>iOS only</Text>
<ThemedText style={styles.heading}>iOS only</ThemedText>
<SettingsSwitch
label="Hide navigation bar"
value={hideNavigationBar}
Expand All @@ -148,7 +149,7 @@ const MainScreen = ({ navigation }: MainScreenProps): React.JSX.Element => {
value={hideWhenScrolling}
onValueChange={setHideWhenScrolling}
/>
<Text style={styles.heading}>Android only</Text>
<ThemedText style={styles.heading}>Android only</ThemedText>
<SettingsPicker<InputType>
label="Input type"
value={inputType}
Expand All @@ -172,7 +173,7 @@ const MainScreen = ({ navigation }: MainScreenProps): React.JSX.Element => {
value={shouldShowHintSearchIcon}
onValueChange={setShouldShowHintSearchIcon}
/>
<Text style={styles.heading}>Imperative actions</Text>
<ThemedText style={styles.heading}>Imperative actions</ThemedText>
<Button onPress={() => searchBarRef.current?.blur()} title="Blur" />
<Button onPress={() => searchBarRef.current?.focus()} title="Focus" />
<Button
Expand All @@ -192,7 +193,7 @@ const MainScreen = ({ navigation }: MainScreenProps): React.JSX.Element => {
onPress={() => searchBarRef.current?.cancelSearch()}
title="Cancel search"
/>
<Text style={styles.heading}>Other</Text>
<ThemedText style={styles.heading}>Other</ThemedText>
<Button
onPress={() => navigation.navigate('Search')}
title="Other Searchbar example"
Expand Down
2 changes: 1 addition & 1 deletion apps/src/screens/StackPresentation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ interface FormScreenProps {
}

const FormScreen = ({ navigation }: FormScreenProps): React.JSX.Element => (
<View style={{ ...styles.container, backgroundColor: 'white' }}>
<View style={styles.container}>
<Form />
<Button
testID="stack-presentation-form-screen-go-back-button"
Expand Down
50 changes: 26 additions & 24 deletions apps/src/shared/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import React from 'react';
import { View, StyleSheet, Text, TextInput } from 'react-native';
import React, { Fragment } from 'react';
import { View, StyleSheet } from 'react-native';
import { ThemedText, ThemedTextInput } from '.';

export const Form = (): React.JSX.Element => (
<View testID="form" style={styles.wrapper}>
<Text testID="form-header" style={styles.heading}>
Example form
</Text>
<Text testID="form-first-name-label" style={styles.label}>
First Name *
</Text>
<TextInput testID="form-first-name-input" style={styles.input} />
<Text testID="form-last-name-label" style={styles.label}>
Last Name *
</Text>
<TextInput testID="form-last-name-input" style={styles.input} />
<Text testID="form-email-label" style={styles.label}>
Email *
</Text>
<TextInput testID="form-email-input" style={styles.input} />
</View>
);
const fields = [
{ name: 'form-first-name', placeholder: 'First Name *' },
{ name: 'form-last-name', placeholder: 'Last Name *' },
{ name: 'form-email', placeholder: 'Email *' },
];

export const Form = (): React.JSX.Element => {
return (
<View testID="form" style={styles.wrapper}>
<ThemedText testID="form-header" style={styles.heading}>
Example form
</ThemedText>
{fields.map(({ name, placeholder }) => (
<Fragment key={name}>
<ThemedText testID={`${name}-label`} style={styles.label}>
{placeholder}
</ThemedText>
<ThemedTextInput testID={`${name}-input`} style={styles.input} />
</Fragment>
))}
</View>
);
};

const styles = StyleSheet.create({
wrapper: {
Expand All @@ -28,19 +33,16 @@ const styles = StyleSheet.create({
heading: {
fontSize: 16,
fontWeight: 'bold',
color: 'black',
marginBottom: 16,
},
label: {
color: 'darkslategray',
textTransform: 'capitalize',
fontSize: 12,
marginBottom: 8,
},
input: {
borderWidth: 1,
borderRadius: 5,
borderColor: 'black',
marginBottom: 12,
height: 40,
},
Expand Down
23 changes: 10 additions & 13 deletions apps/src/shared/ListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useTheme } from '@react-navigation/native';
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { TouchableOpacity, StyleSheet } from 'react-native';
import { ThemedText, ThemedView } from '.';

interface Props {
title: string;
Expand All @@ -14,11 +16,15 @@ export const ListItem = ({
testID,
disabled,
}: Props): React.JSX.Element => {
const { colors } = useTheme();
return (
<TouchableOpacity onPress={onPress} testID={testID} disabled={disabled}>
<View style={styles.container}>
<Text style={[styles.title, disabled && styles.disabled]}>{disabled && '(N/A) '}{title}</Text>
</View>
<ThemedView style={[styles.container, { borderColor: colors.border }]}>
<ThemedText style={disabled ? styles.disabled : undefined}>
{disabled && '(N/A) '}
{title}
</ThemedText>
</ThemedView>
</TouchableOpacity>
);
};
Expand All @@ -29,18 +35,9 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
padding: 10,
backgroundColor: 'white',
borderColor: '#ccc',
borderWidth: 1,
},
disabled: {
color: 'gray',
},
title: {
color: 'black',
},
chevron: {
fontWeight: 'bold',
color: 'black',
},
});
Loading

0 comments on commit c2ac40f

Please sign in to comment.