Repository: callstack/react-native-paper
Pull request #1367 (2019-10-07 10:16:12)

Feat/adaptive and exact dark theme

callstack-bot: Hey @pan-pawel, thank you for your pull request 🤗. The documentation from this branch can be viewed [here](https://12882-71323749-gh.circle-artifacts.com/0/docs/index.html).

Patch:
diff --git a/docs/pages/2.theming.md b/docs/pages/2.theming.md
index 41b27a38f..8589b7588 100644
--- a/docs/pages/2.theming.md
+++ b/docs/pages/2.theming.md
@@ -36,7 +36,7 @@ const theme = {
     ...DefaultTheme.colors,
     primary: '#3498db',
     accent: '#f1c40f',
-  }
+  },
 };
 
 export default function Main() {
@@ -53,6 +53,7 @@ You can change the theme prop dynamically and all the components will automatica
 A theme usually contains the following properties:
 
 - `dark` (`boolean`): whether this is a dark theme or light theme.
+- `mode` (`'adaptive' | 'exact'`): color mode for dark theme (See [Dark Theme](#dark-theme)).
 - `roundness` (`number`): roundness of common elements, such as buttons.
 - `colors` (`object`): various colors used throughout different elements.
   - `primary` - primary color for your app, usually your brand color.
@@ -123,6 +124,18 @@ export default function FancyButton(props) {
 
 Now you can use your `FancyButton` component everywhere instead of using `Button` from Paper.
 
+## Dark Theme
+
+Since 3.0 We adapt Dark theme to follow [Material design guidelines](https://material.io/design/color/dark-theme.html). </br>
+In opposition to light theme, dark theme by default uses `surface` colour instead of `primary` on large components like `AppBar` or `BottomNavigation`.</br>
+The Dark theme adds a white overlay with opacity depending on elevation of surfaces. It uses it for the better accentuation of surface elevation. Using only shadow is highly imperceptible on dark surfaces.
+
+We are aware that users often use Dark theme in their own ways and they maybe don't want to use default dark theme features from guidelines</br>
+That's why if you are using the dark mode you can switch between two types of dark theme `mode`:
+
+`exact` where everything is like it was before. Appbar and BottomNavigation will still use primary colour by default</br>
+`adaptive` where we follow [Material design guidelines](https://material.io/design/color/dark-theme.html), the surface will use white overlay with opacity to show elevation, `Appbar` and `BottomNavigation` will use surface colour as a background.
+
 ## Gotchas
 
 The `Provider` exposes the theme to the components via [React's context API](https://reactjs.org/docs/context.html), which means that the component must be in the same tree as the `Provider`. Some React Native components will render a different tree such as a `Modal`, in which case the components inside the `Modal` won't be able to access the theme. The work around is to get the theme using the `withTheme` HOC and pass it down to the components as props, or expose it again with the exported `ThemeProvider` component.
diff --git a/docs/pages/4.fonts.md b/docs/pages/4.fonts.md
index b4850ee34..b5001b897 100644
--- a/docs/pages/4.fonts.md
+++ b/docs/pages/4.fonts.md
@@ -40,23 +40,42 @@ The easiest way to install custom fonts to your RN project is do as follows:
 
 ## Configuring fonts in ThemeProvider
 
-After you configure the fonts you need to pass them as font object in a custom theme with the `Provider` component. Check the [default theme](https://github.com/callstack/react-native-paper/blob/master/src/styles/DefaultTheme.tsx) to see what customization options are supported.
+To create a custom font you need to prepare `fontConfig` where fonts are divided by platforms. 
+After you have to pass the `fontConfig` into `configureFonts` method in a custom theme. 
 
-Example:
+Note: To override font on all platforms use `deafult` key.
+
+Check the [default theme](https://github.com/callstack/react-native-paper/blob/master/src/styles/DefaultTheme.tsx) to see what customization options are supported.
 
 ```js
 import * as React from 'react';
-import { DefaultTheme, Provider as PaperProvider } from 'react-native-paper';
+import { configureFonts, DefaultTheme, Provider as PaperProvider } from 'react-native-paper';
 import App from './src/App';
 
+const fontConfig = {
+  default: {
+    regular: {
+      fontFamily: 'sans-serif',
+      fontWeight: 'normal',
+    },
+    medium: {
+      fontFamily: 'sans-serif-medium',
+      fontWeight: 'normal',
+    },
+    light: {
+      fontFamily: 'sans-serif-light',
+      fontWeight: 'normal',
+    },
+    thin: {
+      fontFamily: 'sans-serif-thin',
+      fontWeight: 'normal',
+    },
+  },
+};
+
 const theme = {
   ...DefaultTheme,
-  fonts: {
-      regular: 'Roboto',
-      medium: 'Roboto',
-      light: 'Roboto Light',
-      thin: 'Roboto Thin',
-    },
+  fonts: configureFonts(fontConfig),
 };
 
 export default function Main() {
diff --git a/example/src/Examples/AppbarExample.tsx b/example/src/Examples/AppbarExample.tsx
index 2813665ff..e4fdbfa29 100644
--- a/example/src/Examples/AppbarExample.tsx
+++ b/example/src/Examples/AppbarExample.tsx
@@ -20,8 +20,7 @@ const initialParams = {
   showSubtitle: true,
   showSearchIcon: true,
   showMoreIcon: true,
-  showBottomPrimary: false,
-  showTopPrimary: false,
+  showExactTheme: false,
 };
 
 const MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical';
@@ -34,8 +33,10 @@ class AppbarExample extends React.Component<Props> {
     return {
       header: (
         <Appbar.Header
-          primary={params.showTopPrimary}
           style={params.showCustomColor ? { backgroundColor: '#ffff00' } : null}
+          theme={{
+            mode: params.showExactTheme ? 'exact' : 'adaptive',
+          }}
         >
           {params.showLeftIcon && (
             <Appbar.BackAction onPress={() => navigation.goBack()} />
@@ -129,28 +130,33 @@ class AppbarExample extends React.Component<Props> {
           />
         </View>
         <View style={styles.row}>
-          <Paragraph>Bottom bar primary (dark theme)</Paragraph>
+          <Paragraph>Adaptive Dark Theme</Paragraph>
           <Switch
-            value={params.showBottomPrimary}
+            value={!params.showExactTheme}
             onValueChange={value =>
               navigation.setParams({
-                showBottomPrimary: value,
+                showExactTheme: !value,
               })
             }
           />
         </View>
         <View style={styles.row}>
-          <Paragraph>Header bar primary (dark theme)</Paragraph>
+          <Paragraph>Exact Dark Theme</Paragraph>
           <Switch
-            value={params.showTopPrimary}
+            value={params.showExactTheme}
             onValueChange={value =>
               navigation.setParams({
-                showTopPrimary: value,
+                showExactTheme: value,
               })
             }
           />
         </View>
-        <Appbar style={styles.bottom} primary={params.showBottomPrimary}>
+        <Appbar
+          style={[styles.bottom]}
+          theme={{
+            mode: params.showExactTheme ? 'exact' : 'adaptive',
+          }}
+        >
           <Appbar.Action icon="archive" onPress={() => {}} />
           <Appbar.Action icon="email" onPress={() => {}} />
           <Appbar.Action icon="label" onPress={() => {}} />
diff --git a/example/src/Examples/BottomNavigationExample.tsx b/example/src/Examples/BottomNavigationExample.tsx
index 48df82a58..57345e936 100644
--- a/example/src/Examples/BottomNavigationExample.tsx
+++ b/example/src/Examples/BottomNavigationExample.tsx
@@ -47,7 +47,7 @@ export default class BottomNavigationExample extends React.Component<
   state = {
     index: 0,
     routes: [
-      { key: 'album', title: 'Album', icon: 'image-album' },
+      { key: 'album', title: 'Album', icon: 'image-album', color: '#272727' },
       {
         key: 'library',
         title: 'Library',
diff --git a/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx
index a3bb7f569..cfeae69ea 100644
--- a/src/components/Appbar/Appbar.tsx
+++ b/src/components/Appbar/Appbar.tsx
@@ -21,11 +21,6 @@ type Props = Partial<React.ComponentProps<typeof View>> & {
    * Whether the background color is a dark color. A dark appbar will render light text and vice-versa.
    */
   dark?: boolean;
-  /**
-   * Pass `true` if you want Appbar to use theme primary color even in dark mode.
-   * By default in dark mode Appbar use surface color.
-   */
-  primary?: boolean;
   /**
    * Content of the `Appbar`.
    */
@@ -44,6 +39,9 @@ export const DEFAULT_APPBAR_HEIGHT = 56;
  * The top bar usually contains the screen title, controls such as navigation buttons, menu button etc.
  * The bottom bar usually provides access to a drawer and up to four actions.
  *
+ * By default Appbar uses primary color as a background, in dark theme with `adaptive` mode it will use surface colour instead.
+ * See [Dark Theme](https://callstack.github.io/react-native-paper/theming.html#dark-theme) for more informations
+ *
  * <div class="screenshots">
  *   <img class="medium" src="screenshots/appbar.png" />
  * </div>
@@ -88,18 +86,22 @@ class Appbar extends React.Component<Props> {
   static Header = AppbarHeader;
 
   render() {
-    const { children, dark, style, theme, primary, ...rest } = this.props;
+    const { children, dark, style, theme, ...rest } = this.props;
 
-    const { colors, dark: isDarkTheme } = theme;
+    const { colors, dark: isDarkTheme, mode } = theme;
     const {
-      backgroundColor = isDarkTheme && !primary
-        ? overlay(4, colors.surface)
-        : colors.primary,
+      backgroundColor: customBackground,
+      elevation = 4,
       ...restStyle
-    } = StyleSheet.flatten(style) || {};
+    }: ViewStyle = StyleSheet.flatten(style) || {};
 
     let isDark: boolean;
 
+    const backgroundColor = customBackground
+      ? customBackground
+      : isDarkTheme && mode === 'adaptive'
+      ? overlay(elevation, colors.surface)
+      : colors.primary;
     if (typeof dark === 'boolean') {
       isDark = dark;
     } else {
@@ -136,7 +138,8 @@ class Appbar extends React.Component<Props> {
     }
     return (
       <Surface
-        style={[{ backgroundColor }, styles.appbar, restStyle]}
+        //@ts-ignore
+        style={[{ backgroundColor }, styles.appbar, { elevation }, restStyle]}
         {...rest}
       >
         {shouldAddLeftSpacing ? <View style={styles.spacing} /> : null}
diff --git a/src/components/Appbar/AppbarHeader.tsx b/src/components/Appbar/AppbarHeader.tsx
index 545c93e9e..2260d6dce 100644
--- a/src/components/Appbar/AppbarHeader.tsx
+++ b/src/components/Appbar/AppbarHeader.tsx
@@ -27,11 +27,6 @@ type Props = React.ComponentProps<typeof Appbar> & {
    * Pass `0` or a custom value to disable the default behaviour, and customize the height.
    */
   statusBarHeight?: number;
-  /**
-   * Pass `true` if you want Appbar to use theme primary color even in dark mode.
-   * By default in dark mode Appbar use surface color.
-   */
-  primary?: boolean;
   /**
    * Content of the header.
    */
@@ -96,21 +91,22 @@ class AppbarHeader extends React.Component<Props> {
       // Don't use default props since we check it to know whether we should use SafeAreaView
       statusBarHeight = APPROX_STATUSBAR_HEIGHT,
       style,
-      primary,
       dark,
       ...rest
     } = this.props;
-    const { dark: isDarkTheme, colors } = rest.theme;
+    const { dark: isDarkTheme, colors, mode } = rest.theme;
     const {
       height = DEFAULT_APPBAR_HEIGHT,
       elevation = 4,
-      backgroundColor = isDarkTheme && !primary
-        ? overlay(4, colors.surface)
-        : colors.primary,
+      backgroundColor: customBackground,
       zIndex = 0,
       ...restStyle
-    } = StyleSheet.flatten(style) || {};
-
+    }: ViewStyle = StyleSheet.flatten(style) || {};
+    const backgroundColor = customBackground
+      ? customBackground
+      : isDarkTheme && mode === 'adaptive'
+      ? overlay(elevation, colors.surface)
+      : colors.primary;
     // Let the user override the behaviour
     const Wrapper =
       typeof this.props.statusBarHeight === 'number' ? View : SafeAreaView;
@@ -136,13 +132,13 @@ class AppbarHeader extends React.Component<Props> {
       >
         {/* $FlowFixMe: There seems to be conflict between Appbar's props and Header's props */}
         <Appbar
+          //@ts-ignore
           style={[
             { height, backgroundColor, marginTop: statusBarHeight },
             styles.appbar,
             restStyle,
           ]}
           dark={dark}
-          primary={primary}
           {...rest}
         />
       </Wrapper>
diff --git a/src/components/BottomNavigation.tsx b/src/components/BottomNavigation.tsx
index b34a06ad2..cc2f8aea7 100644
--- a/src/components/BottomNavigation.tsx
+++ b/src/components/BottomNavigation.tsx
@@ -275,6 +275,9 @@ class SceneComponent extends React.PureComponent<any> {
  *
  * For integration with React Navigation, you can use [react-navigation-material-bottom-tab-navigator](https://github.com/react-navigation/react-navigation-material-bottom-tab-navigator).
  *
+ * By default Bottom navigation uses primary color as a background, in dark theme with `adaptive` mode it will use surface colour instead.
+ * See [Dark Theme](https://callstack.github.io/react-native-paper/theming.html#dark-theme) for more informations
+ *
  * <div class="screenshots">
  *   <img class="medium" src="screenshots/bottom-navigation.gif" />
  * </div>
@@ -579,19 +582,22 @@ class BottomNavigation extends React.Component<Props, State> {
 
     const { layout, loaded } = this.state;
     const { routes } = navigationState;
-    const { colors, dark: isDarkTheme } = theme;
+    const { colors, dark: isDarkTheme, mode } = theme;
 
     const shifting = this._isShifting();
 
-    const {
-      backgroundColor: approxBackgroundColor = isDarkTheme
-        ? overlay(styles.bar.elevation)
-        : colors.primary,
-    } = StyleSheet.flatten(barStyle) || {};
+    const { backgroundColor: customBackground, elevation = 4 }: ViewStyle =
+      StyleSheet.flatten(barStyle) || {};
+
+    const approxBackgroundColor =
+      customBackground || (isDarkTheme && mode === 'adaptive')
+        ? overlay(elevation, colors.surface)
+        : colors.primary;
 
     const backgroundColor = shifting
       ? this.state.index.interpolate({
           inputRange: routes.map((_, i) => i),
+          //@ts-ignore
           outputRange: routes.map(
             route => getColor({ route }) || approxBackgroundColor
           ),
@@ -611,7 +617,7 @@ class BottomNavigation extends React.Component<Props, State> {
             .rgb()
             .string();
 
-    const touchColor = color(activeColor)
+    const touchColor = color(activeColor || activeTintColor)
       .alpha(0.12)
       .rgb()
       .string();
diff --git a/src/components/Surface.tsx b/src/components/Surface.tsx
index 1a482e784..9ca61410e 100644
--- a/src/components/Surface.tsx
+++ b/src/components/Surface.tsx
@@ -19,8 +19,9 @@ type Props = React.ComponentProps<typeof View> & {
 
 /**
  * Surface is a basic container that can give depth to an element with elevation shadow.
- * On dark theme surface is constructed by also placing a semi-transparent white overlay over a component surface.
- * A overlay and/or shadow can be applied by specifying the `elevation` property both on Android and iOS.
+ * On dark theme with `adaptive` mode, surface is constructed by also placing a semi-transparent white overlay over a component surface.
+ * See [Dark Theme](https://callstack.github.io/react-native-paper/theming.html#dark-theme) for more informations.
+ * Overlay and/or shadow can be applied by specifying the `elevation` property both on Android and iOS.
  *
  * <div class="screenshots">
  *   <img src="screenshots/surface-1.png" />
@@ -64,13 +65,16 @@ class Surface extends React.Component<Props> {
     const { style, theme, ...rest } = this.props;
     const flattenedStyles = StyleSheet.flatten(style) || {};
     const { elevation = 4 }: ViewStyle = flattenedStyles;
-    const { dark: isDarkTheme, colors } = theme;
+    const { dark: isDarkTheme, mode, colors } = theme;
     return (
       <Animated.View
         {...rest}
         style={[
           {
-            backgroundColor: isDarkTheme ? overlay(elevation) : colors.surface,
+            backgroundColor:
+              isDarkTheme && mode === 'adaptive'
+                ? overlay(elevation, colors.surface)
+                : colors.surface,
           },
           elevation && shadow(elevation),
           style,
diff --git a/src/index.tsx b/src/index.tsx
index e54490408..486d406bb 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -11,6 +11,7 @@ export { default as Provider } from './core/Provider';
 export { default as DefaultTheme } from './styles/DefaultTheme';
 export { default as DarkTheme } from './styles/DarkTheme';
 export { default as shadow } from './styles/shadow';
+export { default as configureFonts } from './styles/fonts';
 
 import * as Avatar from './components/Avatar/Avatar';
 import * as List from './components/List/List';
diff --git a/src/styles/DarkTheme.tsx b/src/styles/DarkTheme.tsx
index 82b318482..aaa3f2023 100644
--- a/src/styles/DarkTheme.tsx
+++ b/src/styles/DarkTheme.tsx
@@ -6,6 +6,7 @@ import { Theme } from '../types';
 const DarkTheme: Theme = {
   ...DefaultTheme,
   dark: true,
+  mode: 'adaptive',
   colors: {
     ...DefaultTheme.colors,
     primary: '#BB86FC',
diff --git a/src/styles/DefaultTheme.tsx b/src/styles/DefaultTheme.tsx
index b8ff6f2c4..5f3d5aa8c 100644
--- a/src/styles/DefaultTheme.tsx
+++ b/src/styles/DefaultTheme.tsx
@@ -1,8 +1,8 @@
 import color from 'color';
 import { black, white, pinkA400 } from './colors';
-import fonts from './fonts';
+import configureFonts from './fonts';
 
-import { Theme, Fonts } from '../types';
+import { Theme } from '../types';
 
 const DefaultTheme: Theme = {
   dark: false,
@@ -30,7 +30,7 @@ const DefaultTheme: Theme = {
       .string(),
     notification: pinkA400,
   },
-  fonts: fonts as Fonts,
+  fonts: configureFonts(),
   animation: {
     scale: 1.0,
   },
diff --git a/src/styles/fonts.tsx b/src/styles/fonts.tsx
index a0ace2d81..dc696faa0 100644
--- a/src/styles/fonts.tsx
+++ b/src/styles/fonts.tsx
@@ -1,60 +1,66 @@
-import { Platform } from 'react-native';
+import { Platform, PlatformOSType } from 'react-native';
+import { Fonts } from '../types';
 
-const fonts = Platform.select({
+const fontConfig = {
   web: {
     regular: {
       fontFamily: 'Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif',
-      fontWeight: '400',
+      fontWeight: '400' as '400',
     },
     medium: {
       fontFamily: 'Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif',
-      fontWeight: '500',
+      fontWeight: '500' as '500',
     },
     light: {
       fontFamily: 'Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif',
-      fontWeight: '300',
+      fontWeight: '300' as '300',
     },
     thin: {
       fontFamily: 'Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif',
-      fontWeight: '100',
+      fontWeight: '100' as '100',
     },
   },
   ios: {
     regular: {
       fontFamily: 'System',
-      fontWeight: '400',
+      fontWeight: '400' as '400',
     },
     medium: {
       fontFamily: 'System',
-      fontWeight: '500',
+      fontWeight: '500' as '500',
     },
     light: {
       fontFamily: 'System',
-      fontWeight: '300',
+      fontWeight: '300' as '300',
     },
     thin: {
       fontFamily: 'System',
-      fontWeight: '100',
+      fontWeight: '100' as '100',
     },
   },
   default: {
     regular: {
       fontFamily: 'sans-serif',
-      fontWeight: 'normal',
+      fontWeight: 'normal' as 'normal',
     },
     medium: {
       fontFamily: 'sans-serif-medium',
-      fontWeight: 'normal',
+      fontWeight: 'normal' as 'normal',
     },
     light: {
       fontFamily: 'sans-serif-light',
-      fontWeight: 'normal',
+      fontWeight: 'normal' as 'normal',
     },
     thin: {
       fontFamily: 'sans-serif-thin',
-      fontWeight: 'normal',
+      fontWeight: 'normal' as 'normal',
     },
   },
-});
+};
 
-export default fonts;
+export default function configureFonts(
+  config?: { [platform in PlatformOSType | 'default']?: Fonts }
+): Fonts {
+  const fonts = Platform.select({ ...fontConfig, ...config }) as Fonts;
+  return fonts;
+}
diff --git a/src/styles/overlay.tsx b/src/styles/overlay.tsx
index d6272c778..2e06af173 100644
--- a/src/styles/overlay.tsx
+++ b/src/styles/overlay.tsx
@@ -1,10 +1,23 @@
 import color from 'color';
+import { Animated } from 'react-native';
 import DarkTheme from './DarkTheme';
 
 export default function overlay(
-  elevation: number = 1,
+  elevation: number | Animated.Value = 1,
   surfaceColor: string = DarkTheme.colors.surface
 ) {
+  if (elevation instanceof Animated.Value) {
+    const inputRange = [0, 1, 2, 3, 8, 24];
+    return elevation.interpolate({
+      inputRange,
+      outputRange: inputRange.map(elevation => {
+        return calculateColor(surfaceColor, elevation);
+      }),
+    });
+  }
+  return calculateColor(surfaceColor, elevation);
+}
+function calculateColor(surfaceColor: string, elevation: number) {
   let overlayTransparency: number;
   if (elevation >= 1 && elevation <= 24) {
     overlayTransparency = elevationOverlayTransparency[elevation];
@@ -13,12 +26,10 @@ export default function overlay(
   } else {
     overlayTransparency = elevationOverlayTransparency[1];
   }
-  const backgroundColor = color(surfaceColor)
+  return color(surfaceColor)
     .mix(color('white'), overlayTransparency * 0.01)
     .hex();
-  return backgroundColor;
 }
-
 const elevationOverlayTransparency: { [id: number]: number } = {
   1: 5,
   2: 7,
diff --git a/src/types.tsx b/src/types.tsx
index b1a49264e..d6dd31c11 100644
--- a/src/types.tsx
+++ b/src/types.tsx
@@ -25,6 +25,7 @@ export type Fonts = {
 
 export type Theme = {
   dark: boolean;
+  mode?: 'adaptive' | 'exact';
   roundness: number;
   colors: {
     primary: string;


Test patch:
diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.js.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.js.snap
index 54911d011..0b944eb61 100644
--- a/src/components/__tests__/__snapshots__/BottomNavigation.test.js.snap
+++ b/src/components/__tests__/__snapshots__/BottomNavigation.test.js.snap
@@ -2948,7 +2948,7 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom
       style={
         Object {
           "alignItems": "center",
-          "backgroundColor": "#E96A82",
+          "backgroundColor": "#FFFFFF",
           "overflow": "hidden",
         }
       }
@@ -3861,7 +3861,7 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav
       style={
         Object {
           "alignItems": "center",
-          "backgroundColor": "rgba(233, 106, 130, 1)",
+          "backgroundColor": "rgba(255, 255, 255, 1)",
           "overflow": "hidden",
         }
       }