Skip to content

Commit

Permalink
Support light and dark themes in RNTester
Browse files Browse the repository at this point in the history
Summary:
Initial conversion of RNTester to support light and dark themes. Theming is implemented by providing the desired color theme via context. Example:

```
const ThemedContainer = props => (
  <RNTesterThemeContext.Consumer>
    {theme => {
      return (
        <View
          style={{
            paddingHorizontal: 8,
            paddingVertical: 16,
            backgroundColor: theme.SystemBackgroundColor,
          }}>
          {props.children}
        </View>
      );
    }}
  </RNTesterThemeContext.Consumer>
);
```

As RNTester's design follows the base iOS system appearance, I've chosen light and dark themes based on the actual iOS 13 semantic colors. The themes are RNTester-specific, however, and we'd expect individual apps to build their own color palettes.

## Examples

The new Appearance Examples screen demonstrates how context can be used to force a theme. It also displays the list of colors in each RNTester theme.

https://pxl.cl/HmzW (screenshot: Appearance Examples screen on RNTester with Dark Mode enabled. Displays useColorScheme hook, and context examples.)
https://pxl.cl/HmB3 (screenshot: Same screen, with light and dark RNTester themes visible)

Theming support in this diff mostly focused on the main screen and the Dark Mode examples screen. This required updating the components used by most of the examples, as you can see in this Image example:
https://pxl.cl/H0Hv (screenshot: Image Examples screen in Dark Mode theme)

Note that I have yet to go through every single example screen to update it. There's individual cases, such as the FlatList example screen, that are not fully converted to use a dark theme when appropriate. This can be taken care later as it's non-blocking.

Reviewed By: zackargyle

Differential Revision: D16681909

fbshipit-source-id: e47484d4b3f0963ef0cc3d8aff8ce3e9051ddbae
  • Loading branch information
hramos authored and facebook-github-bot committed Aug 31, 2019
1 parent ba56fa4 commit a397d33
Show file tree
Hide file tree
Showing 14 changed files with 663 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,12 @@ + (RCTManagedPointer *)JS_NativeAppState_SpecGetCurrentAppStateSuccessAppState:(

} // namespace react
} // namespace facebook
@implementation RCTCxxConvert (NativeAppearance_AppearancePreferences)
+ (RCTManagedPointer *)JS_NativeAppearance_AppearancePreferences:(id)json
{
return facebook::react::managedPointer<JS::NativeAppearance::AppearancePreferences>(json);
}
@end
folly::Optional<NativeAppearanceColorSchemeName> NSStringToNativeAppearanceColorSchemeName(NSString *value) {
static NSDictionary *dict = nil;
static dispatch_once_t onceToken;
Expand All @@ -510,12 +516,6 @@ + (RCTManagedPointer *)JS_NativeAppState_SpecGetCurrentAppStateSuccessAppState:(
});
return value.hasValue() ? dict[@(value.value())] : nil;
}
@implementation RCTCxxConvert (NativeAppearance_AppearancePreferences)
+ (RCTManagedPointer *)JS_NativeAppearance_AppearancePreferences:(id)json
{
return facebook::react::managedPointer<JS::NativeAppearance::AppearancePreferences>(json);
}
@end
@implementation RCTCxxConvert (NativeAsyncStorage_SpecMultiGetCallbackErrorsElement)
+ (RCTManagedPointer *)JS_NativeAsyncStorage_SpecMultiGetCallbackErrorsElement:(id)json
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,13 +452,6 @@ namespace facebook {
};
} // namespace react
} // namespace facebook
typedef NS_ENUM(NSInteger, NativeAppearanceColorSchemeName) {
NativeAppearanceColorSchemeNameLight = 0,
NativeAppearanceColorSchemeNameDark,
};

folly::Optional<NativeAppearanceColorSchemeName> NSStringToNativeAppearanceColorSchemeName(NSString *value);
NSString *NativeAppearanceColorSchemeNameToNSString(folly::Optional<NativeAppearanceColorSchemeName> value);

namespace JS {
namespace NativeAppearance {
Expand All @@ -475,6 +468,13 @@ namespace JS {
@interface RCTCxxConvert (NativeAppearance_AppearancePreferences)
+ (RCTManagedPointer *)JS_NativeAppearance_AppearancePreferences:(id)json;
@end
typedef NS_ENUM(NSInteger, NativeAppearanceColorSchemeName) {
NativeAppearanceColorSchemeNameLight = 0,
NativeAppearanceColorSchemeNameDark,
};

folly::Optional<NativeAppearanceColorSchemeName> NSStringToNativeAppearanceColorSchemeName(NSString *value);
NSString *NativeAppearanceColorSchemeNameToNSString(folly::Optional<NativeAppearanceColorSchemeName> value);

namespace JS {
namespace NativeAsyncStorage {
Expand Down
87 changes: 61 additions & 26 deletions RNTester/js/RNTesterApp.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ const SnapshotViewIOS = require('./examples/Snapshot/SnapshotViewIOS.ios');
const URIActionMap = require('./utils/URIActionMap');

const {
Appearance,
AppRegistry,
AsyncStorage,
BackHandler,
Button,
Linking,
Platform,
SafeAreaView,
StyleSheet,
Text,
Expand All @@ -35,6 +37,7 @@ const {
import type {RNTesterExample} from './types/RNTesterTypes';
import type {RNTesterAction} from './utils/RNTesterActions';
import type {RNTesterNavigationState} from './utils/RNTesterNavigationReducer';
import {RNTesterThemeContext, themes} from './components/RNTesterTheme';

type Props = {
exampleFromAppetizeParams?: ?string,
Expand All @@ -47,18 +50,40 @@ YellowBox.ignoreWarnings([
const APP_STATE_KEY = 'RNTesterAppState.v2';

const Header = ({onBack, title}: {onBack?: () => mixed, title: string}) => (
<SafeAreaView style={styles.headerContainer}>
<View style={styles.header}>
<View style={styles.headerCenter}>
<Text style={styles.title}>{title}</Text>
</View>
{onBack && (
<View style={styles.headerLeft}>
<Button title="Back" onPress={onBack} />
</View>
)}
</View>
</SafeAreaView>
<RNTesterThemeContext.Consumer>
{theme => {
return (
<SafeAreaView
style={[
styles.headerContainer,
{
borderBottomColor: theme.SeparatorColor,
backgroundColor: theme.TertiarySystemBackgroundColor,
},
]}>
<View style={styles.header}>
<View style={styles.headerCenter}>
<Text style={{...styles.title, ...{color: theme.LabelColor}}}>
{title}
</Text>
</View>
{onBack && (
<View>
<Button
title="Back"
onPress={onBack}
color={Platform.select({
ios: theme.LinkColor,
default: undefined,
})}
/>
</View>
)}
</View>
</SafeAreaView>
);
}}
</RNTesterThemeContext.Consumer>
);

class RNTesterApp extends React.Component<Props, RNTesterNavigationState> {
Expand Down Expand Up @@ -88,6 +113,14 @@ class RNTesterApp extends React.Component<Props, RNTesterNavigationState> {
Linking.addEventListener('url', url => {
this._handleAction(URIActionMap(url));
});

Appearance.addChangeListener(prefs => {
this._handleAction(
RNTesterActions.ThemeAction(
prefs.colorScheme === 'dark' ? themes.dark : themes.light,
),
);
});
}

componentWillUnmount() {
Expand All @@ -114,42 +147,44 @@ class RNTesterApp extends React.Component<Props, RNTesterNavigationState> {
if (!this.state) {
return null;
}
const theme = this.state.theme;
if (this.state.openExample) {
const Component = RNTesterList.Modules[this.state.openExample];
if (Component && Component.external) {
return <Component onExampleExit={this._handleBack} />;
} else {
return (
<View style={styles.exampleContainer}>
<Header onBack={this._handleBack} title={Component.title} />
<RNTesterExampleContainer module={Component} />
</View>
<RNTesterThemeContext.Provider value={theme}>
<View style={styles.exampleContainer}>
<Header onBack={this._handleBack} title={Component.title} />
<RNTesterExampleContainer module={Component} />
</View>
</RNTesterThemeContext.Provider>
);
}
}
return (
<View style={styles.exampleContainer}>
<Header title="RNTester" />
<RNTesterExampleList
onNavigate={this._handleAction}
list={RNTesterList}
/>
</View>
<RNTesterThemeContext.Provider value={theme}>
<View style={styles.exampleContainer}>
<Header title="RNTester" />
<RNTesterExampleList
onNavigate={this._handleAction}
list={RNTesterList}
/>
</View>
</RNTesterThemeContext.Provider>
);
}
}

const styles = StyleSheet.create({
headerContainer: {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#96969A',
backgroundColor: '#F5F5F6',
},
header: {
height: 40,
flexDirection: 'row',
},
headerLeft: {},
headerCenter: {
flex: 1,
position: 'absolute',
Expand Down
51 changes: 39 additions & 12 deletions RNTester/js/components/RNTesterBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
const React = require('react');

const {StyleSheet, Text, View} = require('react-native');
import {RNTesterThemeContext} from './RNTesterTheme';

type Props = $ReadOnly<{|
children?: React.Node,
Expand All @@ -29,17 +30,47 @@ class RNTesterBlock extends React.Component<Props, State> {

render(): React.Node {
const description = this.props.description ? (
<Text style={styles.descriptionText}>{this.props.description}</Text>
<RNTesterThemeContext.Consumer>
{theme => {
return (
<Text style={[styles.descriptionText, {color: theme.LabelColor}]}>
{this.props.description}
</Text>
);
}}
</RNTesterThemeContext.Consumer>
) : null;

return (
<View style={styles.container}>
<View style={styles.titleContainer}>
<Text style={styles.titleText}>{this.props.title}</Text>
{description}
</View>
<View style={styles.children}>{this.props.children}</View>
</View>
<RNTesterThemeContext.Consumer>
{theme => {
return (
<View
style={[
styles.container,
{
borderColor: theme.SeparatorColor,
backgroundColor: theme.SystemBackgroundColor,
},
]}>
<View
style={[
styles.titleContainer,
{
borderBottomColor: theme.SeparatorColor,
backgroundColor: theme.QuaternarySystemFillColor,
},
]}>
<Text style={[styles.titleText, {color: theme.LabelColor}]}>
{this.props.title}
</Text>
{description}
</View>
<View style={styles.children}>{this.props.children}</View>
</View>
);
}}
</RNTesterThemeContext.Consumer>
);
}
}
Expand All @@ -48,8 +79,6 @@ const styles = StyleSheet.create({
container: {
borderRadius: 3,
borderWidth: 0.5,
borderColor: '#d6d7da',
backgroundColor: '#ffffff',
margin: 10,
marginVertical: 5,
overflow: 'hidden',
Expand All @@ -58,8 +87,6 @@ const styles = StyleSheet.create({
borderBottomWidth: 0.5,
borderTopLeftRadius: 3,
borderTopRightRadius: 2.5,
borderBottomColor: '#d6d7da',
backgroundColor: '#f6f7f8',
paddingHorizontal: 10,
paddingVertical: 5,
},
Expand Down
52 changes: 34 additions & 18 deletions RNTester/js/components/RNTesterExampleFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
const React = require('react');

const {StyleSheet, TextInput, View} = require('react-native');
import {RNTesterThemeContext} from './RNTesterTheme';

type Props = {
filter: Function,
Expand Down Expand Up @@ -64,33 +65,48 @@ class RNTesterExampleFilter extends React.Component<Props, State> {
return null;
}
return (
<View style={styles.searchRow}>
<TextInput
autoCapitalize="none"
autoCorrect={false}
clearButtonMode="always"
onChangeText={text => {
this.setState(() => ({filter: text}));
}}
placeholder="Search..."
underlineColorAndroid="transparent"
style={styles.searchTextInput}
testID={this.props.testID}
value={this.state.filter}
/>
</View>
<RNTesterThemeContext.Consumer>
{theme => {
return (
<View
style={[
styles.searchRow,
{backgroundColor: theme.GroupedBackgroundColor},
]}>
<TextInput
autoCapitalize="none"
autoCorrect={false}
clearButtonMode="always"
onChangeText={text => {
this.setState(() => ({filter: text}));
}}
placeholder="Search..."
placeholderTextColor={theme.PlaceholderTextColor}
underlineColorAndroid="transparent"
style={[
styles.searchTextInput,
{
color: theme.LabelColor,
backgroundColor: theme.SecondaryGroupedBackgroundColor,
borderColor: theme.QuaternaryLabelColor,
},
]}
testID={this.props.testID}
value={this.state.filter}
/>
</View>
);
}}
</RNTesterThemeContext.Consumer>
);
}
}

const styles = StyleSheet.create({
searchRow: {
backgroundColor: '#eeeeee',
padding: 10,
},
searchTextInput: {
backgroundColor: 'white',
borderColor: '#cccccc',
borderRadius: 3,
borderWidth: 1,
paddingLeft: 8,
Expand Down
Loading

0 comments on commit a397d33

Please sign in to comment.