Repository: callstack/react-native-paper
Pull request #1182 (2019-07-05 12:13:14)

fix: add animation scale to Menu
<!-- Please provide enough information so that others can review your pull request. -->
<!-- Keep pull requests small and focused on a single change. -->

### Motivation

Adds animation scale from theme to animations in Menu.

### Test plan

Change animation scale in theme temporary to 0 and see how the Menu will pop up immediately.


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

Patch:
diff --git a/docs/pages/4.fonts.md b/docs/pages/4.fonts.md
new file mode 100644
index 000000000..4c881f618
--- /dev/null
+++ b/docs/pages/4.fonts.md
@@ -0,0 +1,60 @@
+---
+title: Fonts
+---
+
+# Fonts
+
+## Installing custom fonts
+
+The easiest way to install custom fonts to your RN project is do as follow:
+
+  1. Define path to asssets directory with fonts in project '`package.json`'
+
+  Example:
+
+  ```js
+    ...
+      "rnpm": {
+        "assets": [
+          "fonts"
+        ]
+      },
+    ...
+  ```
+
+  Note: fonts is a folder with .ttf files
+
+  2. Place your fonts files in your assets folder
+  3. Link fonts files using '`react-native link`' command
+  4. Restart your project to refresh changes
+  5. You are able to use fontFamily based on fonts files
+
+## 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.js) to see what customization options are supported.
+
+Example:
+
+```js
+import * as React from 'react';
+import { DefaultTheme, Provider as PaperProvider } from 'react-native-paper';
+import App from './src/App';
+
+const theme = {
+  ...DefaultTheme,
+  fonts: {
+      regular: 'Roboto',
+      medium: 'Roboto',
+      light: 'Roboto Light',
+      thin: 'Roboto Thin',
+    },
+};
+
+export default function Main() {
+  return (
+    <PaperProvider theme={theme}>
+      <App />
+    </PaperProvider>
+  );
+}
+```
diff --git a/docs/pages/4.react-native-web.md b/docs/pages/5.react-native-web.md
similarity index 100%
rename from docs/pages/4.react-native-web.md
rename to docs/pages/5.react-native-web.md
diff --git a/docs/pages/5.showcase.js b/docs/pages/6.showcase.js
similarity index 100%
rename from docs/pages/5.showcase.js
rename to docs/pages/6.showcase.js
diff --git a/example/src/Examples/ToggleButtonExample.tsx b/example/src/Examples/ToggleButtonExample.tsx
index 3507f17f2..38f443600 100644
--- a/example/src/Examples/ToggleButtonExample.tsx
+++ b/example/src/Examples/ToggleButtonExample.tsx
@@ -32,7 +32,7 @@ class ToggleButtonExample extends React.Component<Props, State> {
     return (
       <View style={[styles.container, { backgroundColor: background }]}>
         <List.Section title="Single">
-          <View style={styles.row}>
+          <View style={styles.padding}>
             <ToggleButton
               icon="android"
               value="android"
@@ -46,24 +46,23 @@ class ToggleButtonExample extends React.Component<Props, State> {
           </View>
         </List.Section>
         <List.Section title="Group">
-          <View style={styles.toggleGroup}>
-            <ToggleButton.Group
-              value={this.state.first}
-              onValueChange={(value: string) =>
-                this.setState({
-                  first: value,
-                })
-              }
-            >
-              <ToggleButton disabled icon="format-italic" value="italic" />
-              <ToggleButton icon="format-bold" value="bold" />
-              <ToggleButton icon="format-underlined" value="underlined" />
-              <ToggleButton icon="format-color-text" value="format-color" />
-            </ToggleButton.Group>
-          </View>
+          <ToggleButton.Row
+            value={this.state.first}
+            onValueChange={(value: string) =>
+              this.setState({
+                first: value,
+              })
+            }
+            style={styles.padding}
+          >
+            <ToggleButton disabled icon="format-italic" value="italic" />
+            <ToggleButton icon="format-bold" value="bold" />
+            <ToggleButton icon="format-underlined" value="underlined" />
+            <ToggleButton icon="format-color-text" value="format-color" />
+          </ToggleButton.Row>
         </List.Section>
         <List.Section title="Custom">
-          <View style={styles.group}>
+          <View style={[styles.padding, styles.row]}>
             <ToggleButton.Group
               value={this.state.fruit}
               onValueChange={(value: string) =>
@@ -136,18 +135,11 @@ const styles = StyleSheet.create({
   container: {
     flex: 1,
   },
-  toggleGroup: {
-    flexDirection: 'row',
+  padding: {
     paddingHorizontal: 16,
   },
-  group: {
-    flexDirection: 'row',
-    marginLeft: 5,
-  },
   row: {
     flexDirection: 'row',
-    flexWrap: 'wrap',
-    paddingHorizontal: 16,
   },
 });
 
diff --git a/src/components/Appbar/AppbarBackAction.tsx b/src/components/Appbar/AppbarBackAction.tsx
index f985e0e1e..55b40e82a 100644
--- a/src/components/Appbar/AppbarBackAction.tsx
+++ b/src/components/Appbar/AppbarBackAction.tsx
@@ -8,10 +8,10 @@ import {
   StyleProp,
   ViewStyle,
 } from 'react-native';
-
+import { $Omit } from './../../types';
 import AppbarAction from './AppbarAction';
 
-type Props = Omit<
+type Props = $Omit<
   React.ComponentProps<typeof AppbarAction> & {
     /**
      *  Custom color for back icon.
diff --git a/src/components/Appbar/AppbarHeader.tsx b/src/components/Appbar/AppbarHeader.tsx
index 8b8a18982..53aa6e4a8 100644
--- a/src/components/Appbar/AppbarHeader.tsx
+++ b/src/components/Appbar/AppbarHeader.tsx
@@ -34,7 +34,6 @@ type Props = React.ComponentProps<typeof Appbar> & {
    */
   theme: Theme;
   style?: StyleProp<ViewStyle>;
-  __expo?: any;
 };
 
 /**
diff --git a/src/components/HelperText.tsx b/src/components/HelperText.tsx
index b588e6f55..2a092b342 100644
--- a/src/components/HelperText.tsx
+++ b/src/components/HelperText.tsx
@@ -9,10 +9,10 @@ import {
 } from 'react-native';
 import AnimatedText from './Typography/AnimatedText';
 import { withTheme } from '../core/theming';
-import { Theme } from '../types';
+import { Theme, $Omit } from '../types';
 
-type Props = Omit<
-  Omit<React.ComponentProps<typeof Animated.Text>, 'padding'>,
+type Props = $Omit<
+  $Omit<React.ComponentProps<typeof Animated.Text>, 'padding'>,
   'type'
 > & {
   /**
diff --git a/src/components/List/ListItem.tsx b/src/components/List/ListItem.tsx
index 146612354..e569ac108 100644
--- a/src/components/List/ListItem.tsx
+++ b/src/components/List/ListItem.tsx
@@ -116,7 +116,13 @@ class ListItem extends React.Component<Props> {
   ) {
     const { descriptionEllipsizeMode, descriptionStyle } = this.props;
 
-    return typeof description === 'string' ? (
+    return typeof description === 'function' ? (
+      description({
+        ellipsizeMode: descriptionEllipsizeMode,
+        color: descriptionColor,
+        fontSize: styles.description.fontSize,
+      })
+    ) : (
       <Text
         numberOfLines={2}
         ellipsizeMode={descriptionEllipsizeMode}
@@ -128,14 +134,6 @@ class ListItem extends React.Component<Props> {
       >
         {description}
       </Text>
-    ) : (
-      description &&
-        typeof description === 'function' &&
-        description({
-          ellipsizeMode: descriptionEllipsizeMode,
-          color: descriptionColor,
-          fontSize: styles.description.fontSize,
-        })
     );
   }
 
@@ -187,7 +185,9 @@ class ListItem extends React.Component<Props> {
             >
               {title}
             </Text>
-            {this.renderDescription(descriptionColor, description)}
+            {description
+              ? this.renderDescription(descriptionColor, description)
+              : null}
           </View>
           {right
             ? right({
diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx
index 75703749e..b9c3d2e11 100644
--- a/src/components/Menu/Menu.tsx
+++ b/src/components/Menu/Menu.tsx
@@ -16,7 +16,7 @@ import {
 } from 'react-native';
 
 import { withTheme } from '../../core/theming';
-import { Theme } from '../../types';
+import { Theme, $Omit } from '../../types';
 import Portal from '../Portal/Portal';
 import Surface from '../Surface';
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -54,7 +54,7 @@ type Props = {
   theme: Theme;
 };
 
-type Layout = Omit<Omit<LayoutRectangle, 'x'>, 'y'>;
+type Layout = $Omit<$Omit<LayoutRectangle, 'x'>, 'y'>;
 
 type State = {
   rendered: boolean;
@@ -262,7 +262,7 @@ class Menu extends React.Component<Props, State> {
       !anchorLayout.width ||
       !anchorLayout.height
     ) {
-      setTimeout(this._show, ANIMATION_DURATION);
+      requestAnimationFrame(this._show);
       return;
     }
 
@@ -286,16 +286,17 @@ class Menu extends React.Component<Props, State> {
       () => {
         this._attachListeners();
 
+        const { animation } = this.props.theme;
         Animated.parallel([
           Animated.timing(this.state.scaleAnimation, {
             toValue: { x: menuLayout.width, y: menuLayout.height },
-            duration: ANIMATION_DURATION,
+            duration: ANIMATION_DURATION * animation.scale,
             easing: EASING,
             useNativeDriver: true,
           }),
           Animated.timing(this.state.opacityAnimation, {
             toValue: 1,
-            duration: ANIMATION_DURATION,
+            duration: ANIMATION_DURATION * animation.scale,
             easing: EASING,
             useNativeDriver: true,
           }),
@@ -311,9 +312,10 @@ class Menu extends React.Component<Props, State> {
   _hide = () => {
     this._removeListeners();
 
+    const { animation } = this.props.theme;
     Animated.timing(this.state.opacityAnimation, {
       toValue: 0,
-      duration: ANIMATION_DURATION,
+      duration: ANIMATION_DURATION * animation.scale,
       easing: EASING,
       useNativeDriver: true,
     }).start(finished => {
diff --git a/src/components/Surface.tsx b/src/components/Surface.tsx
index 3e81c6a21..c530d1d7c 100644
--- a/src/components/Surface.tsx
+++ b/src/components/Surface.tsx
@@ -56,14 +56,14 @@ class Surface extends React.Component<Props> {
   render() {
     const { style, theme, ...rest } = this.props;
     const flattenedStyles = StyleSheet.flatten(style) || {};
-    const { elevation }: any = flattenedStyles;
+    const { elevation }: ViewStyle = flattenedStyles;
 
     return (
       <Animated.View
         {...rest}
         style={[
           { backgroundColor: theme.colors.surface },
-          elevation && shadow(elevation),
+          shadow(elevation),
           style,
         ]}
       />
diff --git a/src/components/TextInput/TextInputFlat.tsx b/src/components/TextInput/TextInputFlat.tsx
index 06fd2e4bc..72802fa9b 100644
--- a/src/components/TextInput/TextInputFlat.tsx
+++ b/src/components/TextInput/TextInputFlat.tsx
@@ -25,6 +25,7 @@ import {
   calculatePadding,
   adjustPaddingFlat,
   Padding,
+  interpolatePlaceholder,
 } from './helpers';
 
 const MINIMIZED_LABEL_Y_OFFSET = -18;
@@ -174,7 +175,7 @@ class TextInputFlat extends React.Component<ChildTextInputProps, {}> {
       -labelHalfHeight - (topPosition + MINIMIZED_LABEL_Y_OFFSET);
 
     const placeholderOpacity = hasActiveOutline
-      ? parentState.labeled
+      ? interpolatePlaceholder(parentState.labeled, hasActiveOutline)
       : parentState.labelLayout.measured
       ? 1
       : 0;
diff --git a/src/components/TextInput/TextInputOutlined.tsx b/src/components/TextInput/TextInputOutlined.tsx
index e2fb52b19..74478cfbf 100644
--- a/src/components/TextInput/TextInputOutlined.tsx
+++ b/src/components/TextInput/TextInputOutlined.tsx
@@ -27,6 +27,7 @@ import {
   calculatePadding,
   adjustPaddingOut,
   Padding,
+  interpolatePlaceholder,
 } from './helpers';
 
 const OUTLINE_MINIMIZED_LABEL_Y_OFFSET = -6;
@@ -153,7 +154,10 @@ class TextInputOutlined extends React.Component<ChildTextInputProps, {}> {
     const baseLabelTranslateY =
       -labelHalfHeight - (topPosition + OUTLINE_MINIMIZED_LABEL_Y_OFFSET);
 
-    const placeholderOpacity = hasActiveOutline ? parentState.labeled : 1;
+    const placeholderOpacity = interpolatePlaceholder(
+      parentState.labeled,
+      hasActiveOutline
+    );
 
     const labelProps = {
       label,
diff --git a/src/components/TextInput/helpers.tsx b/src/components/TextInput/helpers.tsx
index 2bb876663..66c8228df 100644
--- a/src/components/TextInput/helpers.tsx
+++ b/src/components/TextInput/helpers.tsx
@@ -1,3 +1,5 @@
+import { Animated } from 'react-native';
+
 type PaddingProps = {
   height: number | null;
   labelHalfHeight: number;
@@ -210,3 +212,12 @@ export const adjustPaddingFlat = ({
     paddingBottom: Math.max(0, bottomResult),
   };
 };
+
+export const interpolatePlaceholder = (
+  labeled: Animated.Value,
+  hasActiveOutline: boolean | undefined
+) =>
+  labeled.interpolate({
+    inputRange: [0, 1],
+    outputRange: [hasActiveOutline ? 0 : 1, 1],
+  });
diff --git a/src/components/TextInput/types.tsx b/src/components/TextInput/types.tsx
index be470f018..b0a2bbb61 100644
--- a/src/components/TextInput/types.tsx
+++ b/src/components/TextInput/types.tsx
@@ -1,5 +1,6 @@
 import { TextInput as NativeTextInput, Animated } from 'react-native';
 import { TextInputProps } from './TextInput';
+import { $Omit } from './../../types';
 
 export type RenderProps = {
   ref: (a: NativeTextInput | null | undefined) => void;
@@ -17,7 +18,7 @@ export type RenderProps = {
   value?: string;
   adjustsFontSizeToFit?: boolean;
 };
-type TextInputTypesWithoutMode = Omit<TextInputProps, 'mode'>;
+type TextInputTypesWithoutMode = $Omit<TextInputProps, 'mode'>;
 export type State = {
   labeled: Animated.Value;
   error: Animated.Value;
@@ -37,7 +38,7 @@ export type ChildTextInputProps = {
 export type LabelProps = {
   mode?: 'flat' | 'outlined';
   placeholderStyle: any;
-  placeholderOpacity: number | Animated.Value;
+  placeholderOpacity: number | Animated.Value | Animated.AnimatedInterpolation;
   baseLabelTranslateX: number;
   baseLabelTranslateY: number;
   wiggleOffsetX: number;
diff --git a/src/components/ToggleButton/ToggleButton.tsx b/src/components/ToggleButton/ToggleButton.tsx
index f658c072a..baa71f13c 100644
--- a/src/components/ToggleButton/ToggleButton.tsx
+++ b/src/components/ToggleButton/ToggleButton.tsx
@@ -11,6 +11,7 @@ import IconButton from '../IconButton';
 import ToggleButtonGroup, {
   ToggleButtonGroupContext,
 } from './ToggleButtonGroup';
+import ToggleButtonRow from './ToggleButtonRow';
 import { black, white } from '../../styles/colors';
 import { IconSource } from '../Icon';
 import { Theme } from '../../types';
@@ -94,6 +95,9 @@ class ToggleButton extends React.Component<Props> {
   // @component ./ToggleButtonGroup.tsx
   static Group = ToggleButtonGroup;
 
+  // @component ./ToggleButtonRow.tsx
+  static Row = ToggleButtonRow;
+
   render() {
     const {
       icon,
diff --git a/src/components/ToggleButton/ToggleButtonGroup.tsx b/src/components/ToggleButton/ToggleButtonGroup.tsx
index 983b3363e..1dfc89fc4 100644
--- a/src/components/ToggleButton/ToggleButtonGroup.tsx
+++ b/src/components/ToggleButton/ToggleButtonGroup.tsx
@@ -1,6 +1,4 @@
 import * as React from 'react';
-import { StyleSheet } from 'react-native';
-import ToggleButton from './ToggleButton';
 
 type Props = {
   /**
@@ -27,7 +25,8 @@ export const ToggleButtonGroupContext = React.createContext<
 >(null as any);
 
 /**
- * Toggle group allows to control a group of toggle buttons.
+ * Toggle group allows to control a group of toggle buttons.</br>
+ * It doesn't change the appearance of the toggle buttons. If you want to group them in a row, checkout <a href="/toggle-button-row.html">`ToggleButton.Row`</a>.
  *
  * ## Usage
  * ```js
@@ -59,7 +58,6 @@ class ToggleButtonGroup extends React.Component<Props> {
 
   render() {
     const { value, onValueChange, children } = this.props;
-    const count = React.Children.count(children);
 
     return (
       <ToggleButtonGroupContext.Provider
@@ -68,51 +66,10 @@ class ToggleButtonGroup extends React.Component<Props> {
           onValueChange,
         }}
       >
-        {React.Children.map(children, (child, i) => {
-          // @ts-ignore
-          if (child && child.type === ToggleButton) {
-            // @ts-ignore
-            return React.cloneElement(child, {
-              style: [
-                styles.button,
-                i === 0
-                  ? styles.first
-                  : i === count - 1
-                  ? styles.last
-                  : styles.middle,
-                // @ts-ignore
-                child.props.style,
-              ],
-            });
-          }
-
-          return child;
-        })}
+        {children}
       </ToggleButtonGroupContext.Provider>
     );
   }
 }
 
-const styles = StyleSheet.create({
-  button: {
-    borderWidth: StyleSheet.hairlineWidth,
-  },
-
-  first: {
-    borderTopRightRadius: 0,
-    borderBottomRightRadius: 0,
-  },
-
-  middle: {
-    borderRadius: 0,
-    borderLeftWidth: 0,
-  },
-
-  last: {
-    borderLeftWidth: 0,
-    borderTopLeftRadius: 0,
-    borderBottomLeftRadius: 0,
-  },
-});
-
 export default ToggleButtonGroup;
diff --git a/src/components/ToggleButton/ToggleButtonRow.tsx b/src/components/ToggleButton/ToggleButtonRow.tsx
new file mode 100644
index 000000000..158f3ee60
--- /dev/null
+++ b/src/components/ToggleButton/ToggleButtonRow.tsx
@@ -0,0 +1,110 @@
+import * as React from 'react';
+import { StyleSheet, View, StyleProp, ViewStyle } from 'react-native';
+import ToggleButton from './ToggleButton';
+
+type Props = {
+  /**
+   * Function to execute on selection change.
+   */
+  onValueChange: (value: string) => void;
+  /**
+   * Value of the currently selected toggle button.
+   */
+  value: string;
+  /**
+   * React elements containing toggle buttons.
+   */
+  children: React.ReactNode;
+  style?: StyleProp<ViewStyle>;
+};
+
+/**
+ * Toggle button row renders a group of toggle buttons in a row.
+ *
+ * ## Usage
+ * ```js
+ * import * as React from 'react';
+ * import { View } from 'react-native';
+ * import { ToggleButton } from 'react-native-paper';
+ *
+ * export default class MyComponent extends React.Component {
+ *   state = {
+ *     value: 'left',
+ *   };
+ *
+ *   render() {
+ *     return(
+ *       <ToggleButton.Row
+ *         onValueChange={value => this.setState({ value })}
+ *         value={this.state.value}
+ *       >
+ *           <ToggleButton icon="format-align-left" value="left" />
+ *           <ToggleButton icon="format-align-right" value="right" />
+ *       </ToggleButton.Row>
+ *     )
+ *   }
+ * }
+ *```
+ */
+class ToggleButtonRow extends React.Component<Props> {
+  static displayName = 'ToggleButton.Row';
+
+  render() {
+    const { value, onValueChange, children, style } = this.props;
+    const count = React.Children.count(children);
+
+    return (
+      <ToggleButton.Group value={value} onValueChange={onValueChange}>
+        <View style={[styles.row, style]}>
+          {React.Children.map(children, (child, i) => {
+            // @ts-ignore
+            if (child && child.type === ToggleButton) {
+              // @ts-ignore
+              return React.cloneElement(child, {
+                style: [
+                  styles.button,
+                  i === 0
+                    ? styles.first
+                    : i === count - 1
+                    ? styles.last
+                    : styles.middle,
+                  // @ts-ignore
+                  child.props.style,
+                ],
+              });
+            }
+
+            return child;
+          })}
+        </View>
+      </ToggleButton.Group>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  row: {
+    flexDirection: 'row',
+  },
+  button: {
+    borderWidth: StyleSheet.hairlineWidth,
+  },
+
+  first: {
+    borderTopRightRadius: 0,
+    borderBottomRightRadius: 0,
+  },
+
+  middle: {
+    borderRadius: 0,
+    borderLeftWidth: 0,
+  },
+
+  last: {
+    borderLeftWidth: 0,
+    borderTopLeftRadius: 0,
+    borderBottomLeftRadius: 0,
+  },
+});
+
+export default ToggleButtonRow;
diff --git a/src/components/Typography/Caption.tsx b/src/components/Typography/Caption.tsx
index 843b85605..e9ba68151 100644
--- a/src/components/Typography/Caption.tsx
+++ b/src/components/Typography/Caption.tsx
@@ -42,5 +42,6 @@ const styles = StyleSheet.create({
     fontSize: 12,
     lineHeight: 20,
     marginVertical: 2,
+    letterSpacing: 0.4,
   },
 });
diff --git a/src/components/Typography/Headline.tsx b/src/components/Typography/Headline.tsx
index 5cc51e710..c9baf189c 100644
--- a/src/components/Typography/Headline.tsx
+++ b/src/components/Typography/Headline.tsx
@@ -43,5 +43,6 @@ const styles = StyleSheet.create({
     fontSize: 24,
     lineHeight: 32,
     marginVertical: 2,
+    letterSpacing: 0,
   },
 });
diff --git a/src/components/Typography/Paragraph.tsx b/src/components/Typography/Paragraph.tsx
index ef27ae2ae..8b9fe7b1a 100644
--- a/src/components/Typography/Paragraph.tsx
+++ b/src/components/Typography/Paragraph.tsx
@@ -41,5 +41,6 @@ const styles = StyleSheet.create({
     fontSize: 14,
     lineHeight: 20,
     marginVertical: 2,
+    letterSpacing: 0.25,
   },
 });
diff --git a/src/components/Typography/Subheading.tsx b/src/components/Typography/Subheading.tsx
index fb04155e0..3b4b6780b 100644
--- a/src/components/Typography/Subheading.tsx
+++ b/src/components/Typography/Subheading.tsx
@@ -43,5 +43,6 @@ const styles = StyleSheet.create({
     fontSize: 16,
     lineHeight: 24,
     marginVertical: 2,
+    letterSpacing: 0.5,
   },
 });
diff --git a/src/components/Typography/Title.tsx b/src/components/Typography/Title.tsx
index a49a1727f..cd85cea96 100644
--- a/src/components/Typography/Title.tsx
+++ b/src/components/Typography/Title.tsx
@@ -41,5 +41,6 @@ const styles = StyleSheet.create({
     fontSize: 20,
     lineHeight: 30,
     marginVertical: 2,
+    letterSpacing: 0.15,
   },
 });
diff --git a/src/core/theming.tsx b/src/core/theming.tsx
index 4781cfebc..60d603e7b 100644
--- a/src/core/theming.tsx
+++ b/src/core/theming.tsx
@@ -2,4 +2,6 @@ import { createTheming } from '@callstack/react-theme-provider';
 import DefaultTheme from '../styles/DefaultTheme';
 import { Theme } from '../types';
 
-export const { ThemeProvider, withTheme } = createTheming<Theme>(DefaultTheme);
+export const { ThemeProvider, withTheme, useTheme } = createTheming<Theme>(
+  DefaultTheme
+);
diff --git a/src/index.tsx b/src/index.tsx
index 32c62781e..e54490408 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -5,11 +5,12 @@ export type Theme = _Theme;
 
 export { Colors };
 
-export { withTheme, ThemeProvider } from './core/theming';
+export { useTheme, withTheme, ThemeProvider } from './core/theming';
 
 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';
 
 import * as Avatar from './components/Avatar/Avatar';
 import * as List from './components/List/List';
diff --git a/src/styles/shadow.tsx b/src/styles/shadow.tsx
index f278a63d7..36a09d0f5 100644
--- a/src/styles/shadow.tsx
+++ b/src/styles/shadow.tsx
@@ -1,20 +1,34 @@
 import * as Colors from './colors';
 import { Animated } from 'react-native';
 
-export default function shadow(elevation: number | Animated.Value) {
-  let height, radius;
+const SHADOW_COLOR = Colors.black;
+const SHADOW_OPACITY = 0.24;
 
+export default function shadow(elevation: number | Animated.Value = 0) {
   if (elevation instanceof Animated.Value) {
-    height = elevation.interpolate({
-      inputRange: [0, 1, 2, 3, 8, 24],
-      outputRange: [0, 0.5, 0.75, 2, 7, 23],
-    });
+    const inputRange = [0, 1, 2, 3, 8, 24];
 
-    radius = elevation.interpolate({
-      inputRange: [0, 1, 2, 3, 8, 24],
-      outputRange: [0, 0.75, 1.5, 3, 8, 24],
-    });
+    return {
+      shadowColor: SHADOW_COLOR,
+      shadowOffset: {
+        width: new Animated.Value(0),
+        height: elevation.interpolate({
+          inputRange,
+          outputRange: [0, 0.5, 0.75, 2, 7, 23],
+        }),
+      },
+      shadowOpacity: new Animated.Value(SHADOW_OPACITY),
+      shadowRadius: elevation.interpolate({
+        inputRange,
+        outputRange: [0, 0.75, 1.5, 3, 8, 24],
+      }),
+    };
   } else {
+    if (elevation === 0) {
+      return {};
+    }
+
+    let height, radius;
     switch (elevation) {
       case 1:
         height = 0.5;
@@ -28,15 +42,15 @@ export default function shadow(elevation: number | Animated.Value) {
         height = elevation - 1;
         radius = elevation;
     }
-  }
 
-  return {
-    shadowColor: Colors.black,
-    shadowOffset: {
-      width: 0,
-      height,
-    },
-    shadowOpacity: 0.24,
-    shadowRadius: radius,
-  };
+    return {
+      shadowColor: SHADOW_COLOR,
+      shadowOffset: {
+        width: 0,
+        height,
+      },
+      shadowOpacity: SHADOW_OPACITY,
+      shadowRadius: radius,
+    };
+  }
 }
diff --git a/src/types.tsx b/src/types.tsx
index 9a1e638a4..9f2dc0901 100644
--- a/src/types.tsx
+++ b/src/types.tsx
@@ -44,7 +44,8 @@ export type Theme = {
   };
 };
 
-export type $RemoveChildren<T extends React.ComponentType<any>> = Omit<
+export type $Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
+export type $RemoveChildren<T extends React.ComponentType<any>> = $Omit<
   React.ComponentProps<T>,
   'children'
 >;


Test patch:
diff --git a/src/components/__tests__/ListItem.test.js b/src/components/__tests__/ListItem.test.js
index 425158e2f..64b60da61 100644
--- a/src/components/__tests__/ListItem.test.js
+++ b/src/components/__tests__/ListItem.test.js
@@ -1,8 +1,9 @@
 import * as React from 'react';
 import renderer from 'react-test-renderer';
-import { Text } from 'react-native';
+import { Text, View } from 'react-native';
 import ListItem from '../List/ListItem.tsx';
 import ListIcon from '../List/ListIcon.tsx';
+import Chip from '../Chip';
 
 it('renders list item with title and description', () => {
   const tree = renderer
@@ -64,3 +65,47 @@ it('renders list item with custom title and description styles', () => {
 
   expect(tree).toMatchSnapshot();
 });
+
+it('renders list item with custom description', () => {
+  const tree = renderer
+    .create(
+      <ListItem
+        title="List Item with custom description"
+        description={({ ellipsizeMode, color: descriptionColor, fontSize }) => (
+          <View>
+            <Text
+              numberOfLines={2}
+              ellipsizeMode={ellipsizeMode}
+              style={{ color: descriptionColor, fontSize }}
+            >
+              React Native Paper is a high-quality, standard-compliant Design
+              Design library that has you covered in all major use-cases.
+            </Text>
+            <View>
+              <Chip icon="picture-as-pdf" onPress={() => {}}>
+                DOCS.pdf
+              </Chip>
+            </View>
+          </View>
+        )}
+      />
+    )
+    .toJSON();
+
+  expect(tree).toMatchSnapshot();
+});
+
+it('renders with a description with typeof number', () => {
+  const tree = renderer
+    .create(
+      <ListItem
+        title="First Item"
+        description={123}
+        titleStyle={{ fontSize: 20 }}
+        descriptionStyle={{ color: 'red' }}
+      />
+    )
+    .toJSON();
+
+  expect(tree).toMatchSnapshot();
+});
diff --git a/src/components/__tests__/Typography/__snapshots__/Caption.test.js.snap b/src/components/__tests__/Typography/__snapshots__/Caption.test.js.snap
index 07ad367c1..643d79183 100644
--- a/src/components/__tests__/Typography/__snapshots__/Caption.test.js.snap
+++ b/src/components/__tests__/Typography/__snapshots__/Caption.test.js.snap
@@ -22,6 +22,7 @@ exports[`renders caption applying style 1`] = `
         Array [
           Object {
             "fontSize": 12,
+            "letterSpacing": 0.4,
             "lineHeight": 20,
             "marginVertical": 2,
           },
@@ -33,6 +34,7 @@ exports[`renders caption applying style 1`] = `
         Array [
           Object {
             "fontSize": 12,
+            "letterSpacing": 0.4,
             "lineHeight": 20,
             "marginVertical": 2,
           },
@@ -71,6 +73,7 @@ exports[`renders caption with children as content 1`] = `
         Array [
           Object {
             "fontSize": 12,
+            "letterSpacing": 0.4,
             "lineHeight": 20,
             "marginVertical": 2,
           },
@@ -79,6 +82,7 @@ exports[`renders caption with children as content 1`] = `
         Array [
           Object {
             "fontSize": 12,
+            "letterSpacing": 0.4,
             "lineHeight": 20,
             "marginVertical": 2,
           },
diff --git a/src/components/__tests__/__snapshots__/ListItem.test.js.snap b/src/components/__tests__/__snapshots__/ListItem.test.js.snap
index 296892037..802bad892 100644
--- a/src/components/__tests__/__snapshots__/ListItem.test.js.snap
+++ b/src/components/__tests__/__snapshots__/ListItem.test.js.snap
@@ -1,5 +1,215 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`renders list item with custom description 1`] = `
+<View
+  accessible={true}
+  isTVSelectable={true}
+  onResponderGrant={[Function]}
+  onResponderMove={[Function]}
+  onResponderRelease={[Function]}
+  onResponderTerminate={[Function]}
+  onResponderTerminationRequest={[Function]}
+  onStartShouldSetResponder={[Function]}
+  style={
+    Array [
+      false,
+      Array [
+        Object {
+          "padding": 8,
+        },
+        undefined,
+      ],
+    ]
+  }
+>
+  <View
+    style={
+      Object {
+        "flexDirection": "row",
+      }
+    }
+  >
+    <View
+      pointerEvents="none"
+      style={
+        Array [
+          Object {
+            "marginVertical": 6,
+            "paddingLeft": 8,
+          },
+          Object {
+            "flex": 1,
+            "justifyContent": "center",
+          },
+        ]
+      }
+    >
+      <Text
+        numberOfLines={1}
+        style={
+          Array [
+            Object {
+              "color": "#000000",
+              "fontFamily": "System",
+              "fontWeight": "400",
+              "textAlign": "left",
+              "writingDirection": "ltr",
+            },
+            Array [
+              Object {
+                "fontSize": 16,
+              },
+              Object {
+                "color": "rgba(0, 0, 0, 0.87)",
+              },
+              undefined,
+            ],
+          ]
+        }
+      >
+        List Item with custom description
+      </Text>
+      <View>
+        <Text
+          numberOfLines={2}
+          style={
+            Object {
+              "color": "rgba(0, 0, 0, 0.54)",
+              "fontSize": 14,
+            }
+          }
+        >
+          React Native Paper is a high-quality, standard-compliant Design Design library that has you covered in all major use-cases.
+        </Text>
+        <View>
+          <View
+            style={
+              Object {
+                "backgroundColor": "#ebebeb",
+                "borderColor": "#ebebeb",
+                "borderRadius": 16,
+                "borderStyle": "solid",
+                "borderWidth": 0.5,
+                "elevation": 0,
+              }
+            }
+          >
+            <View
+              accessibilityRole="button"
+              accessibilityStates={Array []}
+              accessible={true}
+              isTVSelectable={true}
+              onResponderGrant={[Function]}
+              onResponderMove={[Function]}
+              onResponderRelease={[Function]}
+              onResponderTerminate={[Function]}
+              onResponderTerminationRequest={[Function]}
+              onStartShouldSetResponder={[Function]}
+              style={
+                Array [
+                  Object {
+                    "overflow": "hidden",
+                  },
+                  Object {
+                    "borderRadius": 16,
+                  },
+                ]
+              }
+            >
+              <View
+                style={
+                  Object {
+                    "alignItems": "center",
+                    "flexDirection": "row",
+                    "paddingHorizontal": 4,
+                  }
+                }
+              >
+                <View
+                  style={
+                    Array [
+                      Object {
+                        "padding": 4,
+                      },
+                      null,
+                    ]
+                  }
+                >
+                  <Text
+                    accessibilityElementsHidden={true}
+                    allowFontScaling={false}
+                    importantForAccessibility="no-hide-descendants"
+                    pointerEvents="none"
+                    style={
+                      Array [
+                        Object {
+                          "color": "rgba(0, 0, 0, 0.54)",
+                          "fontSize": 18,
+                        },
+                        Array [
+                          Object {
+                            "transform": Array [
+                              Object {
+                                "scaleX": 1,
+                              },
+                            ],
+                          },
+                          Object {
+                            "backgroundColor": "transparent",
+                          },
+                        ],
+                        Object {
+                          "fontFamily": "Material Icons",
+                          "fontStyle": "normal",
+                          "fontWeight": "normal",
+                        },
+                        Object {},
+                      ]
+                    }
+                  >
+                    
+                  </Text>
+                </View>
+                <Text
+                  numberOfLines={1}
+                  style={
+                    Array [
+                      Object {
+                        "color": "#000000",
+                        "fontFamily": "System",
+                        "fontWeight": "400",
+                        "textAlign": "left",
+                        "writingDirection": "ltr",
+                      },
+                      Array [
+                        Object {
+                          "lineHeight": 24,
+                          "marginVertical": 4,
+                          "minHeight": 24,
+                          "textAlignVertical": "center",
+                        },
+                        Object {
+                          "color": "rgba(0, 0, 0, 0.87)",
+                          "marginLeft": 4,
+                          "marginRight": 8,
+                        },
+                        undefined,
+                      ],
+                    ]
+                  }
+                >
+                  DOCS.pdf
+                </Text>
+              </View>
+            </View>
+          </View>
+        </View>
+      </View>
+    </View>
+  </View>
+</View>
+`;
+
 exports[`renders list item with custom title and description styles 1`] = `
 <View
   accessible={true}
@@ -560,3 +770,106 @@ exports[`renders list item with title and description 1`] = `
   </View>
 </View>
 `;
+
+exports[`renders with a description with typeof number 1`] = `
+<View
+  accessible={true}
+  isTVSelectable={true}
+  onResponderGrant={[Function]}
+  onResponderMove={[Function]}
+  onResponderRelease={[Function]}
+  onResponderTerminate={[Function]}
+  onResponderTerminationRequest={[Function]}
+  onStartShouldSetResponder={[Function]}
+  style={
+    Array [
+      false,
+      Array [
+        Object {
+          "padding": 8,
+        },
+        undefined,
+      ],
+    ]
+  }
+>
+  <View
+    style={
+      Object {
+        "flexDirection": "row",
+      }
+    }
+  >
+    <View
+      pointerEvents="none"
+      style={
+        Array [
+          Object {
+            "marginVertical": 6,
+            "paddingLeft": 8,
+          },
+          Object {
+            "flex": 1,
+            "justifyContent": "center",
+          },
+        ]
+      }
+    >
+      <Text
+        numberOfLines={1}
+        style={
+          Array [
+            Object {
+              "color": "#000000",
+              "fontFamily": "System",
+              "fontWeight": "400",
+              "textAlign": "left",
+              "writingDirection": "ltr",
+            },
+            Array [
+              Object {
+                "fontSize": 16,
+              },
+              Object {
+                "color": "rgba(0, 0, 0, 0.87)",
+              },
+              Object {
+                "fontSize": 20,
+              },
+            ],
+          ]
+        }
+      >
+        First Item
+      </Text>
+      <Text
+        numberOfLines={2}
+        style={
+          Array [
+            Object {
+              "color": "#000000",
+              "fontFamily": "System",
+              "fontWeight": "400",
+              "textAlign": "left",
+              "writingDirection": "ltr",
+            },
+            Array [
+              Object {
+                "fontSize": 14,
+              },
+              Object {
+                "color": "rgba(0, 0, 0, 0.54)",
+              },
+              Object {
+                "color": "red",
+              },
+            ],
+          ]
+        }
+      >
+        123
+      </Text>
+    </View>
+  </View>
+</View>
+`;