mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Support light and dark themes in RNTester
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
This commit is contained in:
committed by
Facebook Github Bot
parent
ba56fa43f0
commit
a397d330a4
@@ -487,6 +487,12 @@ namespace facebook {
|
||||
|
||||
} // 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;
|
||||
@@ -510,12 +516,6 @@ NSString *NativeAppearanceColorSchemeNameToNSString(folly::Optional<NativeAppear
|
||||
});
|
||||
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
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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> {
|
||||
@@ -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() {
|
||||
@@ -114,27 +147,32 @@ 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -142,14 +180,11 @@ class RNTesterApp extends React.Component<Props, RNTesterNavigationState> {
|
||||
const styles = StyleSheet.create({
|
||||
headerContainer: {
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: '#96969A',
|
||||
backgroundColor: '#F5F5F6',
|
||||
},
|
||||
header: {
|
||||
height: 40,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
headerLeft: {},
|
||||
headerCenter: {
|
||||
flex: 1,
|
||||
position: 'absolute',
|
||||
|
||||
@@ -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,
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -48,8 +79,6 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 3,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#d6d7da',
|
||||
backgroundColor: '#ffffff',
|
||||
margin: 10,
|
||||
marginVertical: 5,
|
||||
overflow: 'hidden',
|
||||
@@ -58,8 +87,6 @@ const styles = StyleSheet.create({
|
||||
borderBottomWidth: 0.5,
|
||||
borderTopLeftRadius: 3,
|
||||
borderTopRightRadius: 2.5,
|
||||
borderBottomColor: '#d6d7da',
|
||||
backgroundColor: '#f6f7f8',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
const React = require('react');
|
||||
|
||||
const {StyleSheet, TextInput, View} = require('react-native');
|
||||
import {RNTesterThemeContext} from './RNTesterTheme';
|
||||
|
||||
type Props = {
|
||||
filter: Function,
|
||||
@@ -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,
|
||||
|
||||
@@ -26,6 +26,8 @@ const {
|
||||
import type {ViewStyleProp} from '../../../Libraries/StyleSheet/StyleSheet';
|
||||
import type {RNTesterExample} from '../types/RNTesterTypes';
|
||||
|
||||
import {RNTesterThemeContext} from './RNTesterTheme';
|
||||
|
||||
type Props = {
|
||||
onNavigate: Function,
|
||||
list: {
|
||||
@@ -52,21 +54,54 @@ class RowComponent extends React.PureComponent<{
|
||||
render() {
|
||||
const {item} = this.props;
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onShowUnderlay={this.props.onShowUnderlay}
|
||||
onHideUnderlay={this.props.onHideUnderlay}
|
||||
onPress={this._onPress}>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.rowTitleText}>{item.module.title}</Text>
|
||||
<Text style={styles.rowDetailText}>{item.module.description}</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
<RNTesterThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onShowUnderlay={this.props.onShowUnderlay}
|
||||
onHideUnderlay={this.props.onHideUnderlay}
|
||||
onPress={this._onPress}>
|
||||
<View
|
||||
style={[
|
||||
styles.row,
|
||||
{backgroundColor: theme.SystemBackgroundColor},
|
||||
]}>
|
||||
<Text style={[styles.rowTitleText, {color: theme.LabelColor}]}>
|
||||
{item.module.title}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.rowDetailText,
|
||||
{color: theme.SecondaryLabelColor},
|
||||
]}>
|
||||
{item.module.description}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}}
|
||||
</RNTesterThemeContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const renderSectionHeader = ({section}) => (
|
||||
<Text style={styles.sectionHeader}>{section.title}</Text>
|
||||
<RNTesterThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
styles.sectionHeader,
|
||||
{
|
||||
color: theme.SecondaryLabelColor,
|
||||
backgroundColor: theme.GroupedBackgroundColor,
|
||||
},
|
||||
]}>
|
||||
{section.title}
|
||||
</Text>
|
||||
);
|
||||
}}
|
||||
</RNTesterThemeContext.Consumer>
|
||||
);
|
||||
|
||||
class RNTesterExampleList extends React.Component<Props, $FlowFixMeState> {
|
||||
@@ -89,29 +124,46 @@ class RNTesterExampleList extends React.Component<Props, $FlowFixMeState> {
|
||||
];
|
||||
|
||||
return (
|
||||
<View style={[styles.listContainer, this.props.style]}>
|
||||
{this._renderTitleRow()}
|
||||
<RNTesterExampleFilter
|
||||
testID="explorer_search"
|
||||
sections={sections}
|
||||
filter={filter}
|
||||
render={({filteredSections}) => (
|
||||
<SectionList
|
||||
ItemSeparatorComponent={ItemSeparator}
|
||||
contentContainerStyle={styles.sectionListContentContainer}
|
||||
style={styles.list}
|
||||
sections={filteredSections}
|
||||
renderItem={this._renderItem}
|
||||
enableEmptySections={true}
|
||||
itemShouldUpdate={this._itemShouldUpdate}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
automaticallyAdjustContentInsets={false}
|
||||
keyboardDismissMode="on-drag"
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
<RNTesterThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.listContainer,
|
||||
this.props.style,
|
||||
{backgroundColor: theme.SecondaryGroupedBackgroundColor},
|
||||
]}>
|
||||
{this._renderTitleRow()}
|
||||
<RNTesterExampleFilter
|
||||
testID="explorer_search"
|
||||
sections={sections}
|
||||
filter={filter}
|
||||
render={({filteredSections}) => (
|
||||
<SectionList
|
||||
ItemSeparatorComponent={ItemSeparator}
|
||||
contentContainerStyle={{
|
||||
backgroundColor: theme.SeparatorColor,
|
||||
}}
|
||||
style={{backgroundColor: theme.SystemBackgroundColor}}
|
||||
sections={filteredSections}
|
||||
renderItem={this._renderItem}
|
||||
enableEmptySections={true}
|
||||
itemShouldUpdate={this._itemShouldUpdate}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
automaticallyAdjustContentInsets={false}
|
||||
keyboardDismissMode="on-drag"
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
backgroundColor={Platform.select({
|
||||
ios: 'transparent',
|
||||
default: undefined,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
</RNTesterThemeContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -157,39 +209,44 @@ class RNTesterExampleList extends React.Component<Props, $FlowFixMeState> {
|
||||
}
|
||||
|
||||
const ItemSeparator = ({highlighted}) => (
|
||||
<View style={highlighted ? styles.separatorHighlighted : styles.separator} />
|
||||
<RNTesterThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<View
|
||||
style={
|
||||
highlighted
|
||||
? [
|
||||
styles.separatorHighlighted,
|
||||
{backgroundColor: theme.OpaqueSeparatorColor},
|
||||
]
|
||||
: [styles.separator, {backgroundColor: theme.SeparatorColor}]
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</RNTesterThemeContext.Consumer>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
list: {
|
||||
backgroundColor: '#eeeeee',
|
||||
},
|
||||
sectionHeader: {
|
||||
backgroundColor: '#eeeeee',
|
||||
padding: 5,
|
||||
fontWeight: '500',
|
||||
fontSize: 11,
|
||||
},
|
||||
row: {
|
||||
backgroundColor: 'white',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 15,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
separator: {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
backgroundColor: '#bbbbbb',
|
||||
marginLeft: 15,
|
||||
},
|
||||
separatorHighlighted: {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
backgroundColor: 'rgb(217, 217, 217)',
|
||||
},
|
||||
sectionListContentContainer: {
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
rowTitleText: {
|
||||
fontSize: 17,
|
||||
@@ -197,7 +254,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
rowDetailText: {
|
||||
fontSize: 15,
|
||||
color: '#888888',
|
||||
lineHeight: 20,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
const RNTesterTitle = require('./RNTesterTitle');
|
||||
const React = require('react');
|
||||
|
||||
const {ScrollView, StyleSheet, View} = require('react-native');
|
||||
import {RNTesterThemeContext} from './RNTesterTheme';
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
children?: React.Node,
|
||||
@@ -39,20 +39,29 @@ class RNTesterPage extends React.Component<Props> {
|
||||
) : null;
|
||||
const spacer = this.props.noSpacer ? null : <View style={styles.spacer} />;
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{title}
|
||||
<ContentWrapper style={styles.wrapper} {...wrapperProps}>
|
||||
{this.props.children}
|
||||
{spacer}
|
||||
</ContentWrapper>
|
||||
</View>
|
||||
<RNTesterThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{backgroundColor: theme.SecondarySystemBackgroundColor},
|
||||
]}>
|
||||
{title}
|
||||
<ContentWrapper style={styles.wrapper} {...wrapperProps}>
|
||||
{this.props.children}
|
||||
{spacer}
|
||||
</ContentWrapper>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
</RNTesterThemeContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: '#e9eaed',
|
||||
flex: 1,
|
||||
},
|
||||
spacer: {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as React from 'react';
|
||||
import {Appearance} from 'react-native';
|
||||
|
||||
export type RNTesterTheme = {
|
||||
LabelColor: string,
|
||||
SecondaryLabelColor: string,
|
||||
TertiaryLabelColor: string,
|
||||
QuaternaryLabelColor: string,
|
||||
PlaceholderTextColor: string,
|
||||
SystemBackgroundColor: string,
|
||||
SecondarySystemBackgroundColor: string,
|
||||
TertiarySystemBackgroundColor: string,
|
||||
GroupedBackgroundColor: string,
|
||||
SecondaryGroupedBackgroundColor: string,
|
||||
TertiaryGroupedBackgroundColor: string,
|
||||
SystemFillColor: string,
|
||||
SecondarySystemFillColor: string,
|
||||
TertiarySystemFillColor: string,
|
||||
QuaternarySystemFillColor: string,
|
||||
SeparatorColor: string,
|
||||
OpaqueSeparatorColor: string,
|
||||
LinkColor: string,
|
||||
SystemPurpleColor: string,
|
||||
ToolbarColor: string,
|
||||
};
|
||||
|
||||
export const RNTesterLightTheme = {
|
||||
LabelColor: '#000000ff',
|
||||
SecondaryLabelColor: '#3c3c4399',
|
||||
TertiaryLabelColor: '#3c3c434c',
|
||||
QuaternaryLabelColor: '#3c3c432d',
|
||||
PlaceholderTextColor: '#3c3c434c',
|
||||
SystemBackgroundColor: '#ffffffff',
|
||||
SecondarySystemBackgroundColor: '#f2f2f7ff',
|
||||
TertiarySystemBackgroundColor: '#ffffffff',
|
||||
GroupedBackgroundColor: '#f2f2f7ff',
|
||||
SecondaryGroupedBackgroundColor: '#ffffffff',
|
||||
TertiaryGroupedBackgroundColor: '#f2f2f7ff',
|
||||
SystemFillColor: '#78788033',
|
||||
SecondarySystemFillColor: '#78788028',
|
||||
TertiarySystemFillColor: '#7676801e',
|
||||
QuaternarySystemFillColor: '#74748014',
|
||||
SeparatorColor: '#3c3c4349',
|
||||
OpaqueSeparatorColor: '#c6c6c8ff',
|
||||
LinkColor: '#007affff',
|
||||
SystemPurpleColor: '#af52deff',
|
||||
ToolbarColor: '#e9eaedff',
|
||||
};
|
||||
|
||||
export const RNTesterDarkTheme = {
|
||||
LabelColor: '#ffffffff',
|
||||
SecondaryLabelColor: '#ebebf599',
|
||||
TertiaryLabelColor: '#ebebf54c',
|
||||
QuaternaryLabelColor: '#ebebf528',
|
||||
PlaceholderTextColor: '#ebebf54c',
|
||||
SystemBackgroundColor: '#000000ff',
|
||||
SecondarySystemBackgroundColor: '#1c1c1eff',
|
||||
TertiarySystemBackgroundColor: '#2c2c2eff',
|
||||
GroupedBackgroundColor: '#000000ff',
|
||||
SecondaryGroupedBackgroundColor: '#1c1c1eff',
|
||||
TertiaryGroupedBackgroundColor: '#2c2c2eff',
|
||||
SystemFillColor: '#7878805b',
|
||||
SecondarySystemFillColor: '#78788051',
|
||||
TertiarySystemFillColor: '#7676803d',
|
||||
QuaternarySystemFillColor: '#7676802d',
|
||||
SeparatorColor: '#54545899',
|
||||
OpaqueSeparatorColor: '#38383aff',
|
||||
LinkColor: '#0984ffff',
|
||||
SystemPurpleColor: '#bf5af2ff',
|
||||
ToolbarColor: '#3c3c43ff',
|
||||
};
|
||||
|
||||
export const themes = {light: RNTesterLightTheme, dark: RNTesterDarkTheme};
|
||||
export const RNTesterThemeContext: React.Context<RNTesterTheme> = React.createContext(
|
||||
Appearance.getColorScheme() === 'dark' ? themes.dark : themes.light,
|
||||
);
|
||||
@@ -13,13 +13,29 @@
|
||||
const React = require('react');
|
||||
|
||||
const {StyleSheet, Text, View} = require('react-native');
|
||||
import {RNTesterThemeContext} from './RNTesterTheme';
|
||||
|
||||
class RNTesterTitle extends React.Component<$FlowFixMeProps> {
|
||||
render(): React.Node {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.text}>{this.props.title}</Text>
|
||||
</View>
|
||||
<RNTesterThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
borderColor: theme.SeparatorColor,
|
||||
backgroundColor: theme.SystemBackgroundColor,
|
||||
},
|
||||
]}>
|
||||
<Text style={[styles.text, {color: theme.LabelColor}]}>
|
||||
{this.props.title}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
</RNTesterThemeContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -28,12 +44,10 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 4,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#d6d7da',
|
||||
margin: 10,
|
||||
marginBottom: 0,
|
||||
height: 45,
|
||||
padding: 10,
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
text: {
|
||||
fontSize: 19,
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as React from 'react';
|
||||
import {Appearance, Text, View} from 'react-native';
|
||||
import type {AppearancePreferences} from '../../../../Libraries/Utilities/NativeAppearance';
|
||||
|
||||
import {RNTesterThemeContext, themes} from '../../components/RNTesterTheme';
|
||||
|
||||
class ColorSchemeSubscription extends React.Component<
|
||||
{},
|
||||
{colorScheme: ?string},
|
||||
> {
|
||||
state = {
|
||||
colorScheme: Appearance.getColorScheme(),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
Appearance.addChangeListener(this._handleAppearanceChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Appearance.removeChangeListener(this._handleAppearanceChange);
|
||||
}
|
||||
|
||||
_handleAppearanceChange = (preferences: AppearancePreferences) => {
|
||||
const {colorScheme} = preferences;
|
||||
this.setState({colorScheme});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<RNTesterThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<ThemedContainer>
|
||||
<ThemedText>{this.state.colorScheme}</ThemedText>
|
||||
</ThemedContainer>
|
||||
);
|
||||
}}
|
||||
</RNTesterThemeContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ThemedContainer = props => (
|
||||
<RNTesterThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 16,
|
||||
backgroundColor: theme.SystemBackgroundColor,
|
||||
}}>
|
||||
{props.children}
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
</RNTesterThemeContext.Consumer>
|
||||
);
|
||||
|
||||
const ThemedText = props => (
|
||||
<RNTesterThemeContext.Consumer>
|
||||
{theme => {
|
||||
return <Text style={{color: theme.LabelColor}}>{props.children}</Text>;
|
||||
}}
|
||||
</RNTesterThemeContext.Consumer>
|
||||
);
|
||||
|
||||
exports.title = 'Appearance';
|
||||
exports.description = 'Light and dark user interface examples.';
|
||||
exports.examples = [
|
||||
{
|
||||
title: 'Non-component `getColorScheme` API',
|
||||
render(): React.Element<any> {
|
||||
return <ColorSchemeSubscription />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Consuming Context',
|
||||
render(): React.Element<any> {
|
||||
return (
|
||||
<RNTesterThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<ThemedContainer>
|
||||
<ThemedText>
|
||||
This block of text inherits its theme via Context.
|
||||
</ThemedText>
|
||||
</ThemedContainer>
|
||||
);
|
||||
}}
|
||||
</RNTesterThemeContext.Consumer>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Context forced to light theme',
|
||||
render(): React.Element<any> {
|
||||
return (
|
||||
<RNTesterThemeContext.Provider value={themes.light}>
|
||||
<ThemedContainer>
|
||||
<ThemedText>
|
||||
This block of text will always render with a light theme.
|
||||
</ThemedText>
|
||||
</ThemedContainer>
|
||||
</RNTesterThemeContext.Provider>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Context forced to dark theme',
|
||||
render(): React.Element<any> {
|
||||
return (
|
||||
<RNTesterThemeContext.Provider value={themes.dark}>
|
||||
<ThemedContainer>
|
||||
<ThemedText>
|
||||
This block of text will always render with a dark theme.
|
||||
</ThemedText>
|
||||
</ThemedContainer>
|
||||
</RNTesterThemeContext.Provider>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'RNTester App Colors',
|
||||
description: 'A light and a dark theme based on standard iOS 13 colors.',
|
||||
render(): React.Element<any> {
|
||||
const ColorShowcase = props => (
|
||||
<RNTesterThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
marginVertical: 20,
|
||||
backgroundColor: theme.SystemBackgroundColor,
|
||||
}}>
|
||||
<Text style={{fontWeight: '700', color: theme.LabelColor}}>
|
||||
{props.themeName}
|
||||
</Text>
|
||||
{Object.keys(theme).map(key => (
|
||||
<View style={{flexDirection: 'row'}} key={key}>
|
||||
<View
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
backgroundColor: theme[key],
|
||||
}}
|
||||
/>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 2,
|
||||
color: theme.LabelColor,
|
||||
fontWeight: '600',
|
||||
}}>
|
||||
{key}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 2,
|
||||
color: theme.LabelColor,
|
||||
}}>
|
||||
{theme[key]}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
</RNTesterThemeContext.Consumer>
|
||||
);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<RNTesterThemeContext.Provider value={themes.light}>
|
||||
<ColorShowcase themeName="Light Mode" />
|
||||
</RNTesterThemeContext.Provider>
|
||||
<RNTesterThemeContext.Provider value={themes.dark}>
|
||||
<ColorShowcase themeName="Dark Mode" />
|
||||
</RNTesterThemeContext.Provider>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -13,6 +13,7 @@
|
||||
const React = require('react');
|
||||
|
||||
const {Alert, Button, View, StyleSheet} = require('react-native');
|
||||
import {RNTesterThemeContext} from '../../components/RNTesterTheme';
|
||||
|
||||
function onButtonPress(buttonName) {
|
||||
Alert.alert(`${buttonName} has been pressed!`);
|
||||
@@ -31,12 +32,19 @@ exports.examples = [
|
||||
'everyone.': string),
|
||||
render: function(): React.Node {
|
||||
return (
|
||||
<Button
|
||||
onPress={() => onButtonPress('Simple')}
|
||||
testID="simple_button"
|
||||
title="Press Me"
|
||||
accessibilityLabel="See an informative alert"
|
||||
/>
|
||||
<RNTesterThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<Button
|
||||
onPress={() => onButtonPress('Simple')}
|
||||
testID="simple_button"
|
||||
color={theme.LinkColor}
|
||||
title="Press Me"
|
||||
accessibilityLabel="See an informative alert"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</RNTesterThemeContext.Consumer>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -47,13 +55,19 @@ exports.examples = [
|
||||
'Android, the color adjusts the background color of the button.': string),
|
||||
render: function(): React.Node {
|
||||
return (
|
||||
<Button
|
||||
onPress={() => onButtonPress('Purple')}
|
||||
testID="purple_button"
|
||||
title="Press Purple"
|
||||
color="#841584"
|
||||
accessibilityLabel="Learn more about purple"
|
||||
/>
|
||||
<RNTesterThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<Button
|
||||
onPress={() => onButtonPress('Purple')}
|
||||
testID="purple_button"
|
||||
color={theme.SystemPurpleColor}
|
||||
title="Press Purple"
|
||||
accessibilityLabel="Learn more about purple"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</RNTesterThemeContext.Consumer>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -63,21 +77,28 @@ exports.examples = [
|
||||
'the button': string),
|
||||
render: function(): React.Node {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button
|
||||
onPress={() => onButtonPress('Left')}
|
||||
testID="left_button"
|
||||
title="This looks great!"
|
||||
accessibilityLabel="This sounds great!"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => onButtonPress('Right')}
|
||||
testID="right_button"
|
||||
title="Ok!"
|
||||
color="#841584"
|
||||
accessibilityLabel="Ok, Great!"
|
||||
/>
|
||||
</View>
|
||||
<RNTesterThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button
|
||||
onPress={() => onButtonPress('Left')}
|
||||
testID="left_button"
|
||||
color={theme.LinkColor}
|
||||
title="This looks great!"
|
||||
accessibilityLabel="This sounds great!"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => onButtonPress('Right')}
|
||||
testID="right_button"
|
||||
color={theme.SystemPurpleColor}
|
||||
title="Ok!"
|
||||
accessibilityLabel="Ok, Great!"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
</RNTesterThemeContext.Consumer>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
* @flow strict
|
||||
* @flow strict-local
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import type {RNTesterTheme} from '../components/RNTesterTheme';
|
||||
|
||||
export type RNTesterBackAction = {
|
||||
type: 'RNTesterBackAction',
|
||||
};
|
||||
@@ -23,10 +25,16 @@ export type RNTesterExampleAction = {
|
||||
openExample: string,
|
||||
};
|
||||
|
||||
export type RNTesterThemeAction = {
|
||||
type: 'RNTesterThemeAction',
|
||||
theme: RNTesterTheme,
|
||||
};
|
||||
|
||||
export type RNTesterAction =
|
||||
| RNTesterBackAction
|
||||
| RNTesterListAction
|
||||
| RNTesterExampleAction;
|
||||
| RNTesterExampleAction
|
||||
| RNTesterThemeAction;
|
||||
|
||||
function Back(): RNTesterBackAction {
|
||||
return {
|
||||
@@ -47,10 +55,18 @@ function ExampleAction(openExample: string): RNTesterExampleAction {
|
||||
};
|
||||
}
|
||||
|
||||
function ThemeAction(theme: RNTesterTheme): RNTesterThemeAction {
|
||||
return {
|
||||
type: 'RNTesterThemeAction',
|
||||
theme,
|
||||
};
|
||||
}
|
||||
|
||||
const RNTesterActions = {
|
||||
Back,
|
||||
ExampleList,
|
||||
ExampleAction,
|
||||
ThemeAction,
|
||||
};
|
||||
|
||||
module.exports = RNTesterActions;
|
||||
|
||||
@@ -205,6 +205,11 @@ const APIExamples: Array<RNTesterExample> = [
|
||||
module: require('../examples/Animated/AnimatedGratuitousApp/AnExApp'),
|
||||
supportsTVOS: true,
|
||||
},
|
||||
{
|
||||
key: 'AppearanceExample',
|
||||
module: require('../examples/Appearance/AppearanceExample'),
|
||||
supportsTVOS: false,
|
||||
},
|
||||
{
|
||||
key: 'AppStateExample',
|
||||
module: require('../examples/AppState/AppStateExample'),
|
||||
|
||||
@@ -10,10 +10,15 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import {themes} from '../components/RNTesterTheme';
|
||||
import type {RNTesterTheme} from '../components/RNTesterTheme';
|
||||
|
||||
const RNTesterList = require('./RNTesterList');
|
||||
import {Appearance} from 'react-native';
|
||||
|
||||
export type RNTesterNavigationState = {
|
||||
openExample: ?string,
|
||||
theme: RNTesterTheme,
|
||||
};
|
||||
|
||||
function RNTesterNavigationReducer(
|
||||
@@ -31,6 +36,8 @@ function RNTesterNavigationReducer(
|
||||
return {
|
||||
// A null openExample will cause the views to display the RNTester example list
|
||||
openExample: null,
|
||||
theme:
|
||||
Appearance.getColorScheme() === 'dark' ? themes.dark : themes.light,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,6 +48,16 @@ function RNTesterNavigationReducer(
|
||||
if (ExampleModule) {
|
||||
return {
|
||||
openExample: action.openExample,
|
||||
theme: state.theme,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (action.type === 'RNTesterThemeAction') {
|
||||
if (action.colorScheme) {
|
||||
return {
|
||||
openExample: state.openExample,
|
||||
theme: action.colorScheme === 'dark' ? themes.dark : themes.light,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user