feat: add theme support

This commit is contained in:
Brent Vatne
2019-08-22 17:46:03 -07:00
parent 5b1a8fed60
commit 8d49ee2771
21 changed files with 1889 additions and 1986 deletions

View File

@@ -1,3 +1,4 @@
node_modules/
lib/
dist/
coverage/

View File

@@ -1,7 +1,10 @@
import React from 'react';
import { Asset, registerRootComponent } from 'expo';
import { FlatList, I18nManager } from 'react-native';
import { createAppContainer } from '@react-navigation/native';
import { registerRootComponent } from 'expo';
import { Asset } from 'expo-asset';
import { TouchableOpacity, View, FlatList, I18nManager } from 'react-native';
import { Themed, createAppContainer } from '@react-navigation/native';
import { ThemeContext, ThemeColors } from '@react-navigation/core';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import {
Assets as StackAssets,
@@ -42,6 +45,7 @@ const data = [
Asset.loadAsync(StackAssets);
class Home extends React.Component {
static contextType = ThemeContext;
static navigationOptions = {
title: 'Examples',
};
@@ -50,6 +54,8 @@ class Home extends React.Component {
<List.Item
title={item.title}
onPress={() => this.props.navigation.navigate(item.routeName)}
style={{ backgroundColor: ThemeColors[this.context].bodyContent }}
titleStyle={{ color: ThemeColors[this.context].label }}
/>
);
@@ -57,13 +63,22 @@ class Home extends React.Component {
render() {
return (
<FlatList
ItemSeparatorComponent={Divider}
renderItem={this._renderItem}
keyExtractor={this._keyExtractor}
data={data}
style={{ backgroundColor: '#fff' }}
/>
<>
<FlatList
ItemSeparatorComponent={() => (
<Divider
style={{
backgroundColor: ThemeColors[this.context].bodyBorder,
}}
/>
)}
renderItem={this._renderItem}
keyExtractor={this._keyExtractor}
data={data}
style={{ backgroundColor: ThemeColors[this.context].body }}
/>
<Themed.StatusBar />
</>
);
}
}
@@ -91,6 +106,51 @@ const Root = createStackNavigator(
}
);
const App = createAppContainer(Root);
export default App;
const Navigation = createAppContainer(Root);
const App = () => {
let [theme, setTheme] = React.useState('light');
return (
<View style={{ flex: 1 }}>
<Navigation theme={theme} />
<View style={{ position: 'absolute', bottom: 60, right: 20 }}>
<TouchableOpacity
onPress={() => {
setTheme(theme === 'light' ? 'dark' : 'light');
}}
>
<View
style={{
backgroundColor: ThemeColors[theme].bodyContent,
borderRadius: 25,
width: 50,
height: 50,
alignItems: 'center',
justifyContent: 'center',
borderColor: ThemeColors[theme].bodyBorder,
borderWidth: 1,
shadowColor: ThemeColors[theme].label,
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.4,
shadowRadius: 2,
elevation: 5,
}}
>
<MaterialCommunityIcons
name="theme-light-dark"
size={30}
color={ThemeColors[theme].label}
/>
</View>
</TouchableOpacity>
</View>
</View>
);
};
registerRootComponent(App);

View File

@@ -3,7 +3,7 @@
"name": "React Navigation Core Example",
"description": "Useful for experimenting with core and native together",
"slug": "react-navigation-core-demo",
"sdkVersion": "32.0.0",
"sdkVersion": "33.0.0",
"version": "1.0.0",
"primaryColor": "#2196f3",
"packagerOpts": {

View File

@@ -10,24 +10,28 @@
"postinstall": "rm -rf node_modules/@react-navigation/core/{.git,node_modules,example}"
},
"dependencies": {
"@react-navigation/native": "^3.4.1",
"expo": "~32.0.0",
"@expo/vector-icons": "10.0.5",
"@react-navigation/core": "3.5.0-alpha.7",
"@react-navigation/native": "^3.6.0-alpha.5",
"expo": "^33.0.7",
"expo-asset": "^5.0.1",
"expo-constants": "~5.0.1",
"hoist-non-react-statics": "^3.3.0",
"react": "16.8.3",
"react-native": "https://github.com/expo/react-native/archive/sdk-33.0.0.tar.gz",
"react-native-paper": "^2.15.2",
"path-to-regexp": "^3.0.0",
"prop-types": "^15.7.2",
"query-string": "^6.4.2",
"react": "16.5.0",
"react-is": "^16.8.6",
"react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz",
"react-native-paper": "^2.15.2",
"react-navigation-stack": "^1.3.0",
"react-navigation-tabs": "^1.0.2"
"react-navigation-stack": "1.4.0-alpha.0",
"react-navigation-tabs": "1.2.0-alpha.0"
},
"devDependencies": {
"babel-preset-expo": "^5.1.1"
},
"resolutions": {
"**/react": "16.5.0",
"**/react": "16.8.3",
"**/hoist-non-react-statics": "3.3.0"
}
}

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Button, ScrollView, View, Text } from 'react-native';
import { withNavigation } from '@react-navigation/core';
import { Themed } from '@react-navigation/native';
import { createStackNavigator } from 'react-navigation-stack';
const getColorOfEvent = evt => {
@@ -83,20 +84,17 @@ class SampleScreen extends React.Component {
ref={view => {
this.scrollView = view;
}}
style={{
flex: 1,
backgroundColor: '#fff',
}}
style={{ flex: 1 }}
>
<FocusTag />
<Text
<Themed.Text
onPress={() => {
this.props.navigation.push('PageTwo');
}}
>
Push
</Text>
<Text
</Themed.Text>
<Themed.Text
onPress={() => {
const { push, goBack } = this.props.navigation;
push('PageTwo');
@@ -106,14 +104,14 @@ class SampleScreen extends React.Component {
}}
>
Push and Pop Quickly
</Text>
<Text
</Themed.Text>
<Themed.Text
onPress={() => {
this.props.navigation.navigate('Home');
}}
>
Back to Examples
</Text>
</Themed.Text>
</ScrollView>
);
}

View File

@@ -1,7 +1,9 @@
import React from 'react';
import { Button, ScrollView, TouchableOpacity, View, Text } from 'react-native';
import { Button, ScrollView, TouchableOpacity, View } from 'react-native';
import { MaterialIcons } from 'react-native-vector-icons';
import { createStackNavigator } from 'react-navigation-stack';
import { Themed } from '@react-navigation/native';
import { ThemeContext } from '@react-navigation/core';
const LOREM_PAGE_ONE = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse in lacus malesuada tellus bibendum fringilla. Integer suscipit suscipit erat, sed molestie eros. Nullam fermentum odio vel mauris pulvinar accumsan. Duis blandit id nulla ac euismod. Nunc nec convallis mauris. Proin sit amet malesuada orci. Aliquam blandit mattis nisi ut eleifend. Morbi blandit ante neque, eu tincidunt est interdum in. Mauris pellentesque euismod nulla. Mauris posuere egestas nulla, sit amet eleifend quam egestas at. Maecenas odio erat, auctor eu consectetur eu, vulputate nec arcu. Praesent in felis massa. Nunc fermentum, massa vitae ultricies dictum, est mi posuere eros, sit amet posuere mi ante ac nulla. Etiam odio libero, tempor sit amet sagittis sed, fermentum ac lorem. Donec dignissim fermentum velit, ac ultrices nulla tristique vel.
Suspendisse auctor elit vitae elementum auctor. Vestibulum gravida auctor facilisis. Vivamus rhoncus ornare magna, non pharetra diam porta ac. Aliquam et justo vitae neque congue dignissim. Etiam et dui euismod, cursus mauris in, aliquam nunc. Mauris elit nulla, rutrum non aliquam a, imperdiet a erat. Nullam molestie elit risus, in posuere dui maximus ut. Integer ac sapien molestie, vestibulum ligula ultricies, pellentesque nisl. Duis elementum, ante ac tincidunt cursus, odio leo lacinia purus, at posuere mauris diam suscipit lorem. In hac habitasse platea dictumst. Pellentesque sagittis nunc non ipsum porttitor pellentesque. Phasellus dapibus accumsan aliquam. Etiam feugiat vitae magna condimentum tincidunt.
@@ -16,11 +18,12 @@ Nulla eu eros mi. Sed non lobortis risus. Donec fermentum augue ut scelerisque s
Donec eget mi a justo congue faucibus eu sed odio. Morbi condimentum, nulla non iaculis lobortis, mauris diam facilisis nisi, in tincidunt ex nulla bibendum ipsum. Nam interdum turpis eget leo convallis, lobortis sollicitudin elit posuere. Aliquam erat volutpat. Suspendisse in nibh interdum nibh porttitor accumsan. Nullam blandit, neque sed lacinia dapibus, nisl lacus egestas odio, sit amet molestie libero nibh ac massa. Quisque tempor placerat elit, non volutpat elit pellentesque quis. Etiam sit amet nisi at ex ornare commodo non vel tortor. Mauris ac dictum sem. Donec feugiat id augue at tempus. Nunc non aliquam odio, quis luctus augue. Maecenas vulputate urna aliquet ultricies tincidunt.`;
class LoremScreen extends React.Component {
static navigationOptions = ({ navigation }) => ({
static navigationOptions = ({ navigation, theme }) => ({
title: 'Lorem Ipsum',
headerRight: navigation.getParam('nextPage') ? (
<Button
title="Next"
color={theme === 'light' ? 'red' : 'white'}
onPress={() => navigation.navigate(navigation.getParam('nextPage'))}
/>
) : null,
@@ -40,21 +43,18 @@ class LoremScreen extends React.Component {
ref={view => {
this.scrollView = view;
}}
style={{
flex: 1,
backgroundColor: '#fff',
}}
style={{ flex: 1 }}
>
{this.props.navigation
.getParam('text')
.split('\n')
.map((p, i) => (
<Text
<Themed.Text
key={i}
style={{ marginBottom: 10, marginTop: 8, marginHorizontal: 10 }}
>
{p}
</Text>
</Themed.Text>
))}
</ScrollView>
);
@@ -85,6 +85,7 @@ const SimpleStack = createStackNavigator(
export default class StackWithRefocus extends React.Component {
static router = SimpleStack.router;
static context = ThemeContext;
_emitRefocus = () => {
this.props.navigation.emit('refocus', {});
@@ -96,7 +97,11 @@ export default class StackWithRefocus extends React.Component {
<SimpleStack navigation={this.props.navigation} />
<View style={{ position: 'absolute', bottom: 10, right: 10 }}>
<TouchableOpacity onPress={this._emitRefocus}>
<MaterialIcons name="center-focus-strong" size={30} />
<MaterialIcons
name="center-focus-strong"
size={30}
color={this.context === 'light' ? '#000' : '#fff'}
/>
</TouchableOpacity>
</View>
</View>

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { createBottomTabNavigator } from 'react-navigation-tabs';
import { Button, Text, View } from 'react-native';
import { Themed } from '@react-navigation/native';
import { Button, View } from 'react-native';
import { Feather } from '@expo/vector-icons';
class Screen extends React.Component {
@@ -14,11 +15,14 @@ class Screen extends React.Component {
render() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>{JSON.stringify(this.props.navigation.state.params)}</Text>
<Themed.Text>
{JSON.stringify(this.props.navigation.state.params)}
</Themed.Text>
<Button
title="Go back"
onPress={() => this.props.navigation.goBack(null)}
/>
<Themed.StatusBar />
</View>
);
}
@@ -48,8 +52,14 @@ export const createSimpleTabs = (options = {}) => {
backBehavior: 'history',
...options,
tabBarOptions: {
activeTintColor: '#000',
inactiveTintColor: '#eee',
activeTintColor: {
light: '#000',
dark: '#fff',
},
inactiveTintColor: {
light: 'rgba(0,0,0,0.2)',
dark: 'rgba(255,255,255,0.2)',
},
...options.tabBarOptions,
},
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@react-navigation/core",
"version": "3.4.2",
"version": "3.5.0",
"description": "Core utilities for the react-navigation framework",
"main": "lib/commonjs/index.js",
"react-native": "lib/module/index.js",
@@ -50,6 +50,7 @@
"@react-native-community/bob": "^0.3.4",
"@react-navigation/core": "^3.3.1",
"@react-navigation/native": "^3.4.1",
"@types/react": "~16.8.3",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.7.1",
@@ -64,7 +65,7 @@
"jest-expo": "^32.0.0",
"metro-react-native-babel-preset": "^0.53.1",
"prettier": "^1.17.0",
"react": "16.6.3",
"react": "16.8.3",
"react-dom": "16.6.3",
"react-native": "^0.58.6",
"react-native-testing-library": "^1.7.0",

View File

@@ -0,0 +1,18 @@
export default {
light: {
header: '#fff',
headerBorder: '#a7a7aa',
body: '#fff',
bodyBorder: '#a7a7aa',
bodyContent: '#fff',
label: 'rgba(0, 0, 0, 0.9)',
},
dark: {
header: '#2a2a2a',
headerBorder: '#3a3a3a',
body: '#000',
bodyBorder: '#343434',
bodyContent: '#2a2a2a',
label: '#fff',
},
};

View File

@@ -21,11 +21,27 @@ module.exports = {
get NavigationConsumer() {
return require('./views/NavigationContext').default.Consumer;
},
get createSwitchNavigator() {
return require('./navigators/createSwitchNavigator').default;
},
// Themes
get ThemeContext() {
return require('./views/ThemeContext').default;
},
get ThemeProvider() {
return require('./views/ThemeContext').default.Provider;
},
get ThemeConsumer() {
return require('./views/ThemeContext').default.Consumer;
},
get ThemeColors() {
return require('./ThemeColors').default;
},
get useTheme() {
return require('./useTheme').default;
},
// Actions
get NavigationActions() {
return require('./NavigationActions');

View File

@@ -1,18 +1,26 @@
import React from 'react';
import invariant from '../utils/invariant';
import ThemeContext from '../views/ThemeContext';
function createNavigator(NavigatorView, router, navigationConfig) {
class Navigator extends React.Component {
static contextType = ThemeContext;
static router = router;
static navigationOptions = navigationConfig.navigationOptions;
state = {
descriptors: {},
screenProps: this.props.screenProps,
};
constructor(props, context) {
super(props, context);
static getDerivedStateFromProps(nextProps, prevState) {
const prevDescriptors = prevState.descriptors;
this.state = {
descriptors: {},
screenProps: this.props.screenProps,
theme: context,
themeContext: context,
};
}
static getDerivedStateFromProps(nextProps, currentState) {
const prevDescriptors = currentState.descriptors;
const { navigation, screenProps } = nextProps;
invariant(
navigation != null,
@@ -33,7 +41,8 @@ function createNavigator(NavigatorView, router, navigationConfig) {
prevDescriptors &&
prevDescriptors[route.key] &&
route === prevDescriptors[route.key].state &&
screenProps === prevState.screenProps
screenProps === currentState.screenProps &&
currentState.themeContext === currentState.theme
) {
descriptors[route.key] = prevDescriptors[route.key];
return;
@@ -43,7 +52,11 @@ function createNavigator(NavigatorView, router, navigationConfig) {
route.routeName
);
const childNavigation = navigation.getChildNavigation(route.key);
const options = router.getScreenOptions(childNavigation, screenProps);
const options = router.getScreenOptions(
childNavigation,
screenProps,
currentState.themeContext
);
descriptors[route.key] = {
key: route.key,
getComponent,
@@ -53,7 +66,14 @@ function createNavigator(NavigatorView, router, navigationConfig) {
};
});
return { descriptors, screenProps };
return { descriptors, screenProps, theme: state.themeContext };
}
componentDidUpdate() {
if (this.context !== this.state.themeContext) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ themeContext: this.context });
}
}
render() {

View File

@@ -24,7 +24,8 @@ function applyConfig(configurer, navigationOptions, configProps) {
export default (routeConfigs, navigatorScreenConfig) => (
navigation,
screenProps
screenProps,
theme
) => {
const { state } = navigation;
const route = state;
@@ -42,7 +43,7 @@ export default (routeConfigs, navigatorScreenConfig) => (
routeConfig === Component ? null : routeConfig.navigationOptions;
const componentScreenConfig = Component.navigationOptions;
const configOptions = { navigation, screenProps: screenProps || {} };
const configOptions = { navigation, screenProps: screenProps || {}, theme };
let outputConfig = applyConfig(navigatorScreenConfig, {}, configOptions);
outputConfig = applyConfig(

View File

@@ -0,0 +1,4 @@
import { useContext } from 'react';
import ThemeContext from './views/ThemeContext';
export default () => useContext(ThemeContext);

View File

@@ -1,8 +1,12 @@
const getActiveChildNavigationOptions = (navigation, screenProps) => {
const getActiveChildNavigationOptions = (
navigation,
screenProps,
theme = 'light'
) => {
const { state, router, getChildNavigation } = navigation;
const activeRoute = state.routes[state.index];
const activeNavigation = getChildNavigation(activeRoute.key);
const options = router.getScreenOptions(activeNavigation, screenProps);
const options = router.getScreenOptions(activeNavigation, screenProps, theme);
return options;
};

View File

@@ -1,5 +0,0 @@
import NavigationContext from './NavigationContext';
const { Consumer } = NavigationContext;
export default Consumer;

View File

@@ -1,6 +0,0 @@
import React from 'react';
// TODO: change this to null on next major semver bump
const NavigationContext = React.createContext(undefined);
export default NavigationContext;

View File

@@ -0,0 +1,4 @@
import * as React from 'react';
// Change undefined to null in next major version bump
export default React.createContext(undefined);

View File

@@ -1,5 +0,0 @@
import NavigationContext from './NavigationContext';
const { Provider } = NavigationContext;
export default Provider;

View File

@@ -0,0 +1,5 @@
import * as React from 'react';
// Only light and dark are supported currently. Arbitrary theming not available.
export type ThemeContextType = 'light' | 'dark';
export default React.createContext<ThemeContextType>('light');

View File

@@ -1207,6 +1207,19 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.10.5.tgz#fbaca34086bdc118011e1f05c47688d432f2d571"
integrity sha512-DuIRlQbX4K+d5I+GMnv+UfnGh+ist0RdlvOp+JZ7ePJ6KQONCFQv/gKYSU1ZzbVdFSUCKZOltjmpFAGGv5MdYA==
"@types/prop-types@*":
version "15.7.1"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6"
integrity sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==
"@types/react@~16.8.3":
version "16.8.25"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.25.tgz#0247613ab58b1b11ba10fed662e1947c5f2bb89c"
integrity sha512-ydAAkLnNTC4oYSxJ3zwK/4QcVmEecACJ4ZdxXITbxz/dhahBSDKY6OQ1uawAW6rE/7kfHccxulYLSAIZVrSq0A==
dependencies:
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
@@ -2784,6 +2797,11 @@ cssstyle@^1.0.0:
dependencies:
cssom "0.3.x"
csstype@^2.2.0:
version "2.6.6"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.6.tgz#c34f8226a94bbb10c32cc0d714afdf942291fc41"
integrity sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==
currently-unhandled@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@@ -7945,15 +7963,15 @@ react-transform-hmr@^1.0.4:
global "^4.3.0"
react-proxy "^1.1.7"
react@16.6.3:
version "16.6.3"
resolved "https://registry.yarnpkg.com/react/-/react-16.6.3.tgz#25d77c91911d6bbdd23db41e70fb094cc1e0871c"
integrity sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==
react@16.8.3:
version "16.8.3"
resolved "https://registry.yarnpkg.com/react/-/react-16.8.3.tgz#c6f988a2ce895375de216edcfaedd6b9a76451d9"
integrity sha512-3UoSIsEq8yTJuSu0luO1QQWYbgGEILm+eJl2QN/VLDi7hL+EN18M3q3oVZwmVzzBJ3DkM7RMdRwBmZZ+b4IzSA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.11.2"
scheduler "^0.13.3"
read-pkg-up@^1.0.1:
version "1.0.1"
@@ -8525,18 +8543,18 @@ scheduler@^0.11.2:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.13.4:
version "0.13.4"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.4.tgz#8fef05e7a3580c76c0364d2df5e550e4c9140298"
integrity sha512-cvSOlRPxOHs5dAhP9yiS/6IDmVAVxmk33f0CtTJRkmUWcb1Us+t7b1wqdzoC0REw2muC9V5f1L/w5R5uKGaepA==
scheduler@^0.13.3, scheduler@^0.13.6:
version "0.13.6"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.13.6:
version "0.13.6"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==
scheduler@^0.13.4:
version "0.13.4"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.4.tgz#8fef05e7a3580c76c0364d2df5e550e4c9140298"
integrity sha512-cvSOlRPxOHs5dAhP9yiS/6IDmVAVxmk33f0CtTJRkmUWcb1Us+t7b1wqdzoC0REw2muC9V5f1L/w5R5uKGaepA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"