Repository: callstack/react-native-paper
Pull request #1016 (2019-04-23 21:28:08)

add props `leftStyle` and `rightStyle` to `Card.Title` so devs can style the `left` and `right` independently
<!-- Please provide enough information so that others can review your pull request. -->
<!-- Keep pull requests small and focused on a single change. -->

### Motivation
This PR adds style APIs for the `left` and `right` of the `Card.Title` component. See also issue #1015. 

<!-- What existing problem does the pull request solve? Can you solve the issue with a different approach? -->

### Test plan
This can be tested by creating a card whose `Card.Title` has `left`, `right`, `leftStyle` and `rightStyle` props. The node passed to `left` should be seen do have the style passed to `leftStyle`, and the node passed to `right` should be seen to have the style passed to `rightStyle`.

<!-- List the steps with which we can test this change. Provide screenshots if this changes anything visual. -->


callstack-bot: Hey @chadlavi, thank you for your pull request 🤗. The documentation from this branch can be viewed [here](https://9856-71323749-gh.circle-artifacts.com/0/docs/index.html). Please remember to update Typescript types if you changed API.
jaulz: Looks fine to me. @Trancever what do you think?

Patch:
diff --git a/src/components/Appbar/AppbarHeader.js b/src/components/Appbar/AppbarHeader.js
index 2826e4069..367eb38d5 100644
--- a/src/components/Appbar/AppbarHeader.js
+++ b/src/components/Appbar/AppbarHeader.js
@@ -1,11 +1,12 @@
 /* @flow */
 
 import * as React from 'react';
-import { View, Platform, SafeAreaView, StyleSheet } from 'react-native';
+import { View, SafeAreaView, StyleSheet } from 'react-native';
 
 import Appbar, { DEFAULT_APPBAR_HEIGHT } from './Appbar';
 import shadow from '../../styles/shadow';
 import { withTheme } from '../../core/theming';
+import { APPROX_STATUSBAR_HEIGHT } from '../../constants';
 import type { Theme } from '../../types';
 
 type Props = React.ElementConfig<typeof Appbar> & {|
@@ -31,15 +32,6 @@ type Props = React.ElementConfig<typeof Appbar> & {|
   style?: any,
 |};
 
-const DEFAULT_STATUSBAR_HEIGHT_EXPO =
-  global.__expo && global.__expo.Constants
-    ? global.__expo.Constants.statusBarHeight
-    : 0;
-const DEFAULT_STATUSBAR_HEIGHT = Platform.select({
-  android: DEFAULT_STATUSBAR_HEIGHT_EXPO,
-  ios: Platform.Version < 11 ? DEFAULT_STATUSBAR_HEIGHT_EXPO : 0,
-});
-
 /**
  * A component to use as a header at the top of the screen.
  * It can contain the screen title, controls such as navigation buttons, menu button etc.
@@ -91,7 +83,7 @@ class AppbarHeader extends React.Component<Props> {
   render() {
     const {
       // Don't use default props since we check it to know whether we should use SafeAreaView
-      statusBarHeight = DEFAULT_STATUSBAR_HEIGHT,
+      statusBarHeight = APPROX_STATUSBAR_HEIGHT,
       style,
       ...rest
     } = this.props;
diff --git a/src/components/Button.js b/src/components/Button.js
index 6625fab5a..8b595c7e4 100644
--- a/src/components/Button.js
+++ b/src/components/Button.js
@@ -221,7 +221,8 @@ class Button extends React.Component<Props, State> {
     };
     const touchableStyle = { borderRadius: roundness };
     const textStyle = { color: textColor, fontFamily };
-    const elevation = disabled ? 0 : this.state.elevation;
+    const elevation =
+      disabled || mode !== 'contained' ? 0 : this.state.elevation;
 
     return (
       <Surface
diff --git a/src/components/Card/Card.js b/src/components/Card/Card.js
index 6789ad993..d4fc21134 100644
--- a/src/components/Card/Card.js
+++ b/src/components/Card/Card.js
@@ -19,7 +19,7 @@ type Props = React.ElementConfig<typeof Surface> & {|
   /**
    * Resting elevation of the card which controls the drop shadow.
    */
-  elevation?: number,
+  elevation: number,
   /**
    * Function to execute on long press.
    */
@@ -97,7 +97,6 @@ class Card extends React.Component<Props, State> {
   };
 
   state = {
-    /* $FlowFixMe: somehow default props are not respected */
     elevation: new Animated.Value(this.props.elevation),
   };
 
@@ -110,7 +109,6 @@ class Card extends React.Component<Props, State> {
 
   _handlePressOut = () => {
     Animated.timing(this.state.elevation, {
-      /* $FlowFixMe: somehow default props are not respected */
       toValue: this.props.elevation,
       duration: 150,
     }).start();
diff --git a/src/components/Card/CardTitle.js b/src/components/Card/CardTitle.js
index 24701ebf9..0604845b2 100644
--- a/src/components/Card/CardTitle.js
+++ b/src/components/Card/CardTitle.js
@@ -6,10 +6,11 @@ import { withTheme } from '../../core/theming';
 import Caption from './../Typography/Caption';
 import Title from './../Typography/Title';
 import type { Theme } from '../../types';
+import type { ViewStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet';
 
 type Props = React.ElementConfig<typeof View> & {|
   /**
-   * Text for the title.
+   * Text for the title. Note that this will only accept a string or `<Text>`-based node.
    */
   title: React.Node,
   /**
@@ -17,7 +18,7 @@ type Props = React.ElementConfig<typeof View> & {|
    */
   titleStyle?: any,
   /**
-   * Text for the subtitle.
+   * Text for the subtitle. Note that this will only accept a string or `<Text>`-based node.
    */
   subtitle?: React.Node,
   /**
@@ -28,10 +29,18 @@ type Props = React.ElementConfig<typeof View> & {|
    * Callback which returns a React element to display on the left side.
    */
   left?: (props: { size: number }) => React.Node,
+  /**
+   * Style for the left element wrapper.
+   */
+  leftStyle?: ViewStyleProp,
   /**
    * Callback which returns a React element to display on the right side.
    */
   right?: (props: { size: number }) => React.Node,
+  /**
+   * Style for the right element wrapper.
+   */
+  rightStyle?: ViewStyleProp,
   /**
    * @internal
    */
@@ -79,7 +88,9 @@ class CardTitle extends React.Component<Props> {
   render() {
     const {
       left,
+      leftStyle,
       right,
+      rightStyle,
       subtitle,
       subtitleStyle,
       style,
@@ -96,7 +107,7 @@ class CardTitle extends React.Component<Props> {
         ]}
       >
         {left ? (
-          <View style={[styles.left]}>
+          <View style={[styles.left, leftStyle]}>
             {left({
               size: LEFT_SIZE,
             })}
@@ -124,7 +135,7 @@ class CardTitle extends React.Component<Props> {
           ) : null}
         </View>
 
-        <View>{right ? right({ size: 24 }) : null}</View>
+        <View style={rightStyle}>{right ? right({ size: 24 }) : null}</View>
       </View>
     );
   }
diff --git a/src/components/List/ListAccordion.js b/src/components/List/ListAccordion.js
index 53dab52bc..4b3327c8b 100644
--- a/src/components/List/ListAccordion.js
+++ b/src/components/List/ListAccordion.js
@@ -8,6 +8,10 @@ import Icon from '../Icon';
 import Text from '../Typography/Text';
 import { withTheme } from '../../core/theming';
 import type { Theme } from '../../types';
+import type {
+  ViewStyleProp,
+  TextStyleProp,
+} from 'react-native/Libraries/StyleSheet/StyleSheet';
 
 type Props = {|
   /**
@@ -40,7 +44,18 @@ type Props = {|
    * @optional
    */
   theme: Theme,
-  style?: any,
+  /**
+   * Style that is passed to the wrapping TouchableRipple element.
+   */
+  style?: ViewStyleProp,
+  /**
+   * Style that is passed to Title element.
+   */
+  titleStyle?: TextStyleProp,
+  /**
+   * Style that is passed to Description element.
+   */
+  descriptionStyle?: TextStyleProp,
 |};
 
 type State = {
@@ -119,7 +134,16 @@ class ListAccordion extends React.Component<Props, State> {
   };
 
   render() {
-    const { left, title, description, children, theme, style } = this.props;
+    const {
+      left,
+      title,
+      description,
+      children,
+      theme,
+      titleStyle,
+      descriptionStyle,
+      style,
+    } = this.props;
     const titleColor = color(theme.colors.text)
       .alpha(0.87)
       .rgb()
@@ -157,6 +181,7 @@ class ListAccordion extends React.Component<Props, State> {
                   {
                     color: expanded ? theme.colors.primary : titleColor,
                   },
+                  titleStyle,
                 ]}
               >
                 {title}
@@ -169,6 +194,7 @@ class ListAccordion extends React.Component<Props, State> {
                     {
                       color: descriptionColor,
                     },
+                    descriptionStyle,
                   ]}
                 >
                   {description}
diff --git a/src/components/List/ListSection.js b/src/components/List/ListSection.js
index 5363a0d58..c0781f1ed 100644
--- a/src/components/List/ListSection.js
+++ b/src/components/List/ListSection.js
@@ -5,6 +5,7 @@ import { View, StyleSheet } from 'react-native';
 import ListSubheader from './ListSubheader';
 import { withTheme } from '../../core/theming';
 import type { Theme } from '../../types';
+import type { TextStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet';
 
 type Props = React.ElementConfig<typeof View> & {
   /**
@@ -19,6 +20,10 @@ type Props = React.ElementConfig<typeof View> & {
    * @optional
    */
   theme: Theme,
+  /**
+   * Style that is passed to Title element.
+   */
+  titleStyle?: TextStyleProp,
   style?: any,
 };
 
@@ -59,11 +64,11 @@ class ListSection extends React.Component<Props> {
   static displayName = 'List.Section';
 
   render() {
-    const { children, title, style, ...rest } = this.props;
+    const { children, title, titleStyle, style, ...rest } = this.props;
 
     return (
       <View {...rest} style={[styles.container, style]}>
-        {title && <ListSubheader>{title}</ListSubheader>}
+        {title && <ListSubheader style={titleStyle}>{title}</ListSubheader>}
         {children}
       </View>
     );
diff --git a/src/components/List/ListSubheader.js b/src/components/List/ListSubheader.js
index 2eb9b21c0..ec1d04137 100644
--- a/src/components/List/ListSubheader.js
+++ b/src/components/List/ListSubheader.js
@@ -12,6 +12,9 @@ type Props = React.ElementConfig<typeof Text> & {
    * @optional
    */
   theme: Theme,
+  /**
+   * Style that is passed to Text element.
+   */
   style?: any,
 };
 
diff --git a/src/components/Menu/Menu.js b/src/components/Menu/Menu.js
index 92a21bbb3..afc74ab12 100644
--- a/src/components/Menu/Menu.js
+++ b/src/components/Menu/Menu.js
@@ -11,12 +11,14 @@ import {
   I18nManager,
   BackHandler,
   Platform,
+  findNodeHandle,
 } from 'react-native';
 import { withTheme } from '../../core/theming';
 import type { Theme } from '../../types';
 import Portal from '../Portal/Portal';
 import Surface from '../Surface';
 import MenuItem from './MenuItem';
+import { APPROX_STATUSBAR_HEIGHT } from '../../constants';
 
 type Props = {
   /**
@@ -27,6 +29,13 @@ type Props = {
    * The anchor to open the menu from. In most cases, it will be a button that opens the manu.
    */
   anchor: React.Node,
+  /**
+   * Extra margin to add at the top of the menu to account for translucent status bar on Android.
+   * If you are using Expo, we assume translucent status bar and set a height for status bar automatically.
+   * Pass `0` or a custom value to and customize it.
+   * This is automatically handled on iOS.
+   */
+  statusBarHeight: number,
   /**
    * Callback called when Menu is dismissed. The `visible` prop needs to be updated when this is called.
    */
@@ -42,14 +51,16 @@ type Props = {
   theme: Theme,
 };
 
-type State = {
+type State = {|
+  rendered: boolean,
   top: number,
   left: number,
-  menuLayout: { height: number, width: number },
-  anchorLayout: { height: number, width: number },
+  windowLayout: {| height: number, width: number |},
+  menuLayout: {| height: number, width: number |},
+  anchorLayout: {| height: number, width: number |},
   opacityAnimation: Animated.Value,
   scaleAnimation: Animated.ValueXY,
-};
+|};
 
 // Minimum padding between the edge of the screen and the menu
 const SCREEN_INDENT = 8;
@@ -113,9 +124,23 @@ class Menu extends React.Component<Props, State> {
   // @component ./MenuItem.js
   static Item = MenuItem;
 
+  static defaultProps = {
+    statusBarHeight: APPROX_STATUSBAR_HEIGHT,
+  };
+
+  static getDerivedStateFromProps(nextProps, prevState) {
+    if (nextProps.visible && !prevState.rendered) {
+      return { rendered: true };
+    }
+
+    return null;
+  }
+
   state = {
+    rendered: this.props.visible,
     top: 0,
     left: 0,
+    windowLayout: { width: 0, height: 0 },
     menuLayout: { width: 0, height: 0 },
     anchorLayout: { width: 0, height: 0 },
     opacityAnimation: new Animated.Value(0),
@@ -129,11 +154,11 @@ class Menu extends React.Component<Props, State> {
   }
 
   componentWillUnmount() {
-    BackHandler.removeEventListener('hardwareBackPress', this.props.onDismiss);
+    this._removeListeners();
   }
 
-  _anchor: ?View;
-  _menu: ?View;
+  _anchor: View | null = null;
+  _menu: View | null = null;
 
   _measureMenuLayout = () =>
     new Promise(resolve => {
@@ -153,7 +178,11 @@ class Menu extends React.Component<Props, State> {
       }
     });
 
-  _updateVisibility = () => {
+  _updateVisibility = async () => {
+    // Menu is rendered in Portal, which updates items asynchronously
+    // We need to do the same here so that the ref is up-to-date
+    await Promise.resolve();
+
     if (this.props.visible) {
       this._show();
     } else {
@@ -161,9 +190,54 @@ class Menu extends React.Component<Props, State> {
     }
   };
 
-  _show = async () => {
-    BackHandler.addEventListener('hardwareBackPress', this.props.onDismiss);
+  _isBrowser = () => 'document' in global;
+
+  _focusFirstDOMNode = (el: View | null) => {
+    if (el && this._isBrowser()) {
+      // When in the browser, we want to focus the first focusable item on toggle
+      // For example, when menu is shown, focus the first item in the menu
+      // And when menu is dismissed, send focus back to the button to resume tabbing
+      const node: any = findNodeHandle(el);
+      const focusableNode = node.querySelector(
+        // This is a rough list of selectors that can be focused
+        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+      );
+
+      focusableNode && focusableNode.focus();
+    }
+  };
+
+  _handleDismiss = () => {
+    if (this.props.visible) {
+      this.props.onDismiss();
+    }
+    return true;
+  };
 
+  _handleKeypress = (e: KeyboardEvent) => {
+    if (e.key === 'Escape') {
+      this.props.onDismiss();
+    }
+  };
+
+  _attachListeners = () => {
+    BackHandler.addEventListener('hardwareBackPress', this._handleDismiss);
+    Dimensions.addEventListener('change', this._handleDismiss);
+
+    this._isBrowser() &&
+      document.addEventListener('keyup', this._handleKeypress);
+  };
+
+  _removeListeners = () => {
+    BackHandler.removeEventListener('hardwareBackPress', this._handleDismiss);
+    Dimensions.removeEventListener('change', this._handleDismiss);
+
+    this._isBrowser() &&
+      document.removeEventListener('keyup', this._handleKeypress);
+  };
+
+  _show = async () => {
+    const windowLayout = Dimensions.get('window');
     const [menuLayout, anchorLayout] = await Promise.all([
       this._measureMenuLayout(),
       this._measureAnchorLayout(),
@@ -176,18 +250,14 @@ class Menu extends React.Component<Props, State> {
     // so we have to wait until views are ready
     // and rerun this function to show menu
     if (
+      !windowLayout.width ||
+      !windowLayout.height ||
       !menuLayout.width ||
       !menuLayout.height ||
       !anchorLayout.width ||
       !anchorLayout.height
     ) {
-      BackHandler.removeEventListener(
-        'hardwareBackPress',
-        this.props.onDismiss
-      );
-      setTimeout(() => {
-        this._show();
-      }, ANIMATION_DURATION);
+      setTimeout(this._show, ANIMATION_DURATION);
       return;
     }
 
@@ -195,6 +265,10 @@ class Menu extends React.Component<Props, State> {
       {
         left: anchorLayout.x,
         top: anchorLayout.y,
+        windowLayout: {
+          height: windowLayout.height,
+          width: windowLayout.width,
+        },
         anchorLayout: {
           height: anchorLayout.height,
           width: anchorLayout.width,
@@ -205,6 +279,8 @@ class Menu extends React.Component<Props, State> {
         },
       },
       () => {
+        this._attachListeners();
+
         Animated.parallel([
           Animated.timing(this.state.scaleAnimation, {
             toValue: { x: menuLayout.width, y: menuLayout.height },
@@ -218,13 +294,17 @@ class Menu extends React.Component<Props, State> {
             easing: EASING,
             useNativeDriver: true,
           }),
-        ]).start();
+        ]).start(({ finished }) => {
+          if (finished) {
+            this._focusFirstDOMNode(this._menu);
+          }
+        });
       }
     );
   };
 
   _hide = () => {
-    BackHandler.removeEventListener('hardwareBackPress', this.props.onDismiss);
+    this._removeListeners();
 
     Animated.timing(this.state.opacityAnimation, {
       toValue: 0,
@@ -233,15 +313,28 @@ class Menu extends React.Component<Props, State> {
       useNativeDriver: true,
     }).start(finished => {
       if (finished) {
+        this._focusFirstDOMNode(this._anchor);
+
         this.state.scaleAnimation.setValue({ x: 0, y: 0 });
+        this.setState({ rendered: false });
       }
     });
   };
 
   render() {
-    const { visible, anchor, style, children, theme, onDismiss } = this.props;
+    const {
+      visible,
+      anchor,
+      style,
+      children,
+      theme,
+      statusBarHeight,
+      onDismiss,
+    } = this.props;
 
     const {
+      rendered,
+      windowLayout,
       menuLayout,
       anchorLayout,
       opacityAnimation,
@@ -250,7 +343,7 @@ class Menu extends React.Component<Props, State> {
 
     // I don't know why but on Android measure function is wrong by 24
     const additionalVerticalValue = Platform.select({
-      android: 24,
+      android: statusBarHeight,
       default: 0,
     });
 
@@ -274,12 +367,8 @@ class Menu extends React.Component<Props, State> {
     // We need to translate menu while animating scale to imitate transform origin for scale animation
     const positionTransforms = [];
 
-    const { width: screenWidth, height: screenHeight } = Dimensions.get(
-      'screen'
-    );
-
     // Check if menu fits horizontally and if not align it to right.
-    if (left <= screenWidth - menuLayout.width - SCREEN_INDENT) {
+    if (left <= windowLayout.width - menuLayout.width - SCREEN_INDENT) {
       positionTransforms.push({
         translateX: scaleAnimation.x.interpolate({
           inputRange: [0, menuLayout.width],
@@ -303,13 +392,16 @@ class Menu extends React.Component<Props, State> {
 
       const right = left + menuLayout.width;
       // Check if menu position has enough space from right side
-      if (right <= screenWidth && right > screenWidth - SCREEN_INDENT) {
-        left = screenWidth - SCREEN_INDENT - menuLayout.width;
+      if (
+        right <= windowLayout.width &&
+        right > windowLayout.width - SCREEN_INDENT
+      ) {
+        left = windowLayout.width - SCREEN_INDENT - menuLayout.width;
       }
     }
 
     // Check if menu fits vertically and if not align it to bottom.
-    if (top <= screenHeight - menuLayout.height - SCREEN_INDENT) {
+    if (top <= windowLayout.height - menuLayout.height - SCREEN_INDENT) {
       positionTransforms.push({
         translateY: scaleAnimation.y.interpolate({
           inputRange: [0, menuLayout.height],
@@ -333,9 +425,12 @@ class Menu extends React.Component<Props, State> {
 
       const bottom = top + menuLayout.height + additionalVerticalValue;
       // Check if menu position has enough space from bottom side
-      if (bottom <= screenHeight && bottom > screenHeight - SCREEN_INDENT) {
+      if (
+        bottom <= windowLayout.height &&
+        bottom > windowLayout.height - SCREEN_INDENT
+      ) {
         top =
-          screenHeight -
+          windowLayout.height -
           SCREEN_INDENT -
           menuLayout.height -
           additionalVerticalValue;
@@ -361,38 +456,29 @@ class Menu extends React.Component<Props, State> {
         collapsable={false}
       >
         {anchor}
-        <Portal>
-          {visible ? (
+        {rendered ? (
+          <Portal>
             <TouchableWithoutFeedback onPress={onDismiss}>
               <View style={StyleSheet.absoluteFill} />
             </TouchableWithoutFeedback>
-          ) : null}
-          <View
-            ref={ref => {
-              // This hack is needed to properly show menu
-              // when visible is `true` initially
-              // because in componentDidMount _menu ref is undefined
-              // because it's rendered in portal
-              if (!this._menu) {
+            <View
+              ref={ref => {
                 this._menu = ref;
-                if (visible) {
-                  this._show();
-                }
-              }
-            }}
-            collapsable={false}
-            pointerEvents={visible ? 'auto' : 'none'}
-            style={[styles.wrapper, positionStyle, style]}
-          >
-            <Animated.View style={{ transform: positionTransforms }}>
-              <Surface
-                style={[styles.shadowMenuContainer, shadowMenuContainerStyle]}
-              >
-                {children}
-              </Surface>
-            </Animated.View>
-          </View>
-        </Portal>
+              }}
+              collapsable={false}
+              accessibilityViewIsModal={visible}
+              style={[styles.wrapper, positionStyle, style]}
+            >
+              <Animated.View style={{ transform: positionTransforms }}>
+                <Surface
+                  style={[styles.shadowMenuContainer, shadowMenuContainerStyle]}
+                >
+                  {children}
+                </Surface>
+              </Animated.View>
+            </View>
+          </Portal>
+        ) : null}
       </View>
     );
   }
diff --git a/src/components/RadioButton.js b/src/components/RadioButton.js
index f293c46e6..39bb648c8 100644
--- a/src/components/RadioButton.js
+++ b/src/components/RadioButton.js
@@ -2,7 +2,7 @@
 
 import * as React from 'react';
 import { Platform } from 'react-native';
-import RadioButtonGroup from './RadioButtonGroup';
+import RadioButtonGroup, { RadioButtonContext } from './RadioButtonGroup';
 import RadioButtonAndroid from './RadioButtonAndroid';
 import RadioButtonIOS from './RadioButtonIOS';
 import { withTheme } from '../core/theming';
@@ -103,11 +103,32 @@ class RadioButton extends React.Component<Props> {
   // @component ./RadioButtonIOS.js
   static IOS = RadioButtonIOS;
 
+  handlePress = context => {
+    const { onPress } = this.props;
+    const { onValueChange } = context;
+
+    onPress || onValueChange(this.props.value);
+  };
+
+  isChecked = context =>
+    context.value === this.props.value ? 'checked' : 'unchecked';
+
   render() {
-    return Platform.OS === 'ios' ? (
-      <RadioButtonIOS {...this.props} />
-    ) : (
-      <RadioButtonAndroid {...this.props} />
+    const Button = Platform.select({
+      default: RadioButtonAndroid,
+      ios: RadioButtonIOS,
+    });
+
+    return (
+      <RadioButtonContext.Consumer>
+        {context => (
+          <Button
+            {...this.props}
+            status={this.props.status || (context && this.isChecked(context))}
+            onPress={() => context && this.handlePress(context)}
+          />
+        )}
+      </RadioButtonContext.Consumer>
     );
   }
 }
diff --git a/src/components/RadioButtonAndroid.js b/src/components/RadioButtonAndroid.js
index 1e7042d01..5c5c610ce 100644
--- a/src/components/RadioButtonAndroid.js
+++ b/src/components/RadioButtonAndroid.js
@@ -3,7 +3,6 @@
 import * as React from 'react';
 import { Animated, View, StyleSheet } from 'react-native';
 import color from 'color';
-import { RadioButtonContext } from './RadioButtonGroup';
 import TouchableRipple from './TouchableRipple';
 import { withTheme } from '../core/theming';
 import type { Theme, $RemoveChildren } from '../types';
@@ -92,89 +91,78 @@ class RadioButtonAndroid extends React.Component<Props, State> {
   }
 
   render() {
+    const { disabled, onPress, theme, ...rest } = this.props;
+    const checkedColor = this.props.color || theme.colors.accent;
+    const uncheckedColor =
+      this.props.uncheckedColor ||
+      color(theme.colors.text)
+        .alpha(theme.dark ? 0.7 : 0.54)
+        .rgb()
+        .string();
+
+    let rippleColor, radioColor;
+
+    const checked = this.props.status === 'checked';
+
+    if (disabled) {
+      rippleColor = color(theme.colors.text)
+        .alpha(0.16)
+        .rgb()
+        .string();
+      radioColor = theme.colors.disabled;
+    } else {
+      rippleColor = color(checkedColor)
+        .fade(0.32)
+        .rgb()
+        .string();
+      radioColor = checked ? checkedColor : uncheckedColor;
+    }
+
     return (
-      <RadioButtonContext.Consumer>
-        {context => {
-          const { disabled, onPress, theme, ...rest } = this.props;
-          const checkedColor = this.props.color || theme.colors.accent;
-          const uncheckedColor =
-            this.props.uncheckedColor ||
-            color(theme.colors.text)
-              .alpha(theme.dark ? 0.7 : 0.54)
-              .rgb()
-              .string();
-
-          let rippleColor, radioColor;
-
-          const checked = context
-            ? context.value === this.props.value
-            : this.props.status === 'checked';
-
-          if (disabled) {
-            rippleColor = color(theme.colors.text)
-              .alpha(0.16)
-              .rgb()
-              .string();
-            radioColor = theme.colors.disabled;
-          } else {
-            rippleColor = color(checkedColor)
-              .fade(0.32)
-              .rgb()
-              .string();
-            radioColor = checked ? checkedColor : uncheckedColor;
-          }
-
-          return (
-            <TouchableRipple
-              {...rest}
-              borderless
-              rippleColor={rippleColor}
-              onPress={
-                disabled
-                  ? undefined
-                  : () => {
-                      context && context.onValueChange(this.props.value);
-                      onPress && onPress();
-                    }
-              }
-              accessibilityTraits={disabled ? ['button', 'disabled'] : 'button'}
-              accessibilityComponentType={
-                checked ? 'radiobutton_checked' : 'radiobutton_unchecked'
+      <TouchableRipple
+        {...rest}
+        borderless
+        rippleColor={rippleColor}
+        onPress={
+          disabled
+            ? undefined
+            : () => {
+                onPress && onPress(this.props.value);
               }
-              accessibilityRole="button"
-              accessibilityStates={disabled ? ['disabled'] : undefined}
-              accessibilityLiveRegion="polite"
-              style={styles.container}
-            >
+        }
+        accessibilityTraits={disabled ? ['button', 'disabled'] : 'button'}
+        accessibilityComponentType={
+          checked ? 'radiobutton_checked' : 'radiobutton_unchecked'
+        }
+        accessibilityRole="button"
+        accessibilityStates={disabled ? ['disabled'] : undefined}
+        accessibilityLiveRegion="polite"
+        style={styles.container}
+      >
+        <Animated.View
+          style={[
+            styles.radio,
+            {
+              borderColor: radioColor,
+              borderWidth: this.state.borderAnim,
+            },
+          ]}
+        >
+          {checked ? (
+            <View style={[StyleSheet.absoluteFill, styles.radioContainer]}>
               <Animated.View
                 style={[
-                  styles.radio,
+                  styles.dot,
                   {
-                    borderColor: radioColor,
-                    borderWidth: this.state.borderAnim,
+                    backgroundColor: radioColor,
+                    transform: [{ scale: this.state.radioAnim }],
                   },
                 ]}
-              >
-                {checked ? (
-                  <View
-                    style={[StyleSheet.absoluteFill, styles.radioContainer]}
-                  >
-                    <Animated.View
-                      style={[
-                        styles.dot,
-                        {
-                          backgroundColor: radioColor,
-                          transform: [{ scale: this.state.radioAnim }],
-                        },
-                      ]}
-                    />
-                  </View>
-                ) : null}
-              </Animated.View>
-            </TouchableRipple>
-          );
-        }}
-      </RadioButtonContext.Consumer>
+              />
+            </View>
+          ) : null}
+        </Animated.View>
+      </TouchableRipple>
     );
   }
 }
diff --git a/src/components/RadioButtonIOS.js b/src/components/RadioButtonIOS.js
index 6417dd2d9..057795183 100644
--- a/src/components/RadioButtonIOS.js
+++ b/src/components/RadioButtonIOS.js
@@ -3,7 +3,6 @@
 import * as React from 'react';
 import { StyleSheet, View } from 'react-native';
 import color from 'color';
-import { RadioButtonContext } from './RadioButtonGroup';
 import Icon from './Icon';
 import TouchableRipple from './TouchableRipple';
 import { withTheme } from '../core/theming';
@@ -55,66 +54,57 @@ class RadioButtonIOS extends React.Component<Props> {
   static displayName = 'RadioButton.IOS';
 
   render() {
-    return (
-      <RadioButtonContext.Consumer>
-        {context => {
-          const { disabled, onPress, theme, ...rest } = this.props;
+    const { disabled, onPress, theme, ...rest } = this.props;
 
-          const checkedColor = disabled
-            ? theme.colors.disabled
-            : this.props.color || theme.colors.accent;
+    const checkedColor = disabled
+      ? theme.colors.disabled
+      : this.props.color || theme.colors.accent;
 
-          let rippleColor;
+    let rippleColor;
 
-          const checked = context
-            ? context.value === this.props.value
-            : this.props.status === 'checked';
+    const checked = this.props.status === 'checked';
 
-          if (disabled) {
-            rippleColor = color(theme.colors.text)
-              .alpha(0.16)
-              .rgb()
-              .string();
-          } else {
-            rippleColor = color(checkedColor)
-              .fade(0.32)
-              .rgb()
-              .string();
-          }
-          return (
-            <TouchableRipple
-              {...rest}
-              borderless
-              rippleColor={rippleColor}
-              onPress={
-                disabled
-                  ? undefined
-                  : () => {
-                      context && context.onValueChange(this.props.value);
-                      onPress && onPress();
-                    }
-              }
-              accessibilityTraits={disabled ? ['button', 'disabled'] : 'button'}
-              accessibilityComponentType={
-                checked ? 'radiobutton_checked' : 'radiobutton_unchecked'
+    if (disabled) {
+      rippleColor = color(theme.colors.text)
+        .alpha(0.16)
+        .rgb()
+        .string();
+    } else {
+      rippleColor = color(checkedColor)
+        .fade(0.32)
+        .rgb()
+        .string();
+    }
+    return (
+      <TouchableRipple
+        {...rest}
+        borderless
+        rippleColor={rippleColor}
+        onPress={
+          disabled
+            ? undefined
+            : () => {
+                onPress && onPress();
               }
-              accessibilityRole="button"
-              accessibilityStates={disabled ? ['disabled'] : undefined}
-              accessibilityLiveRegion="polite"
-              style={styles.container}
-            >
-              <View style={{ opacity: checked ? 1 : 0 }}>
-                <Icon
-                  allowFontScaling={false}
-                  source="done"
-                  size={24}
-                  color={checkedColor}
-                />
-              </View>
-            </TouchableRipple>
-          );
-        }}
-      </RadioButtonContext.Consumer>
+        }
+        accessibilityTraits={disabled ? ['button', 'disabled'] : 'button'}
+        accessibilityComponentType={
+          checked ? 'radiobutton_checked' : 'radiobutton_unchecked'
+        }
+        accessibilityRole="button"
+        accessibilityStates={disabled ? ['disabled'] : undefined}
+        accessibilityLiveRegion="polite"
+        style={styles.container}
+      >
+        <View style={{ opacity: checked ? 1 : 0 }}>
+          <Icon
+            allowFontScaling={false}
+            source="done"
+            size={24}
+            color={checkedColor}
+          />
+        </View>
+      </TouchableRipple>
     );
   }
 }
diff --git a/src/constants.js b/src/constants.js
new file mode 100644
index 000000000..7d46c74ad
--- /dev/null
+++ b/src/constants.js
@@ -0,0 +1,13 @@
+/* @flow */
+
+import { Platform } from 'react-native';
+
+const DEFAULT_STATUSBAR_HEIGHT_EXPO =
+  global.__expo && global.__expo.Constants
+    ? global.__expo.Constants.statusBarHeight
+    : 0;
+
+export const APPROX_STATUSBAR_HEIGHT = Platform.select({
+  android: DEFAULT_STATUSBAR_HEIGHT_EXPO,
+  ios: Platform.Version < 11 ? DEFAULT_STATUSBAR_HEIGHT_EXPO : 0,
+});
diff --git a/typings/components/Card.d.ts b/typings/components/Card.d.ts
index 6ec68ee27..83582b113 100644
--- a/typings/components/Card.d.ts
+++ b/typings/components/Card.d.ts
@@ -22,7 +22,10 @@ export interface CardTitleProps extends ViewProps {
   subtitleStyle?: StyleProp<TextStyle>;
   style?: StyleProp<ViewStyle>;
   left?: (props: { size:number }) => React.ReactNode;
+  leftStyle?: StyleProp<ViewStyle>;
   right?: (props: { size:number }) => React.ReactNode;
+  rightStyle?: StyleProp<ViewStyle>;
+
 }
 
 export interface CardProps {
diff --git a/typings/components/Chip.d.ts b/typings/components/Chip.d.ts
index d51c67022..00355163d 100644
--- a/typings/components/Chip.d.ts
+++ b/typings/components/Chip.d.ts
@@ -8,6 +8,7 @@ export interface ChipProps {
   icon?: IconSource;
   avatar?: React.ReactNode;
   selected?: boolean;
+  selectedColor?: string;
   disabled?: boolean;
   accessibilityLabel?: string;
   onPress?: () => any;
diff --git a/typings/components/List.d.ts b/typings/components/List.d.ts
index 6d2b0f4ba..3b9ca371c 100644
--- a/typings/components/List.d.ts
+++ b/typings/components/List.d.ts
@@ -17,6 +17,8 @@ export interface AccordionProps {
   onPress?: () => any;
   left?: (props: { color: string }) => React.ReactNode;
   style?: StyleProp<ViewStyle>;
+  titleStyle?: StyleProp<TextStyle>;
+  descriptionStyle?: StyleProp<TextStyle>;
   theme?: ThemeShape;
 }
 
@@ -47,6 +49,7 @@ export interface SectionProps extends ViewProps {
   children: React.ReactNode;
   title?: string;
   theme?: ThemeShape;
+  titleStyle?: StyleProp<TextStyle>,
 }
 
 export declare class Section extends React.Component<SectionProps> {}
diff --git a/typings/components/Menu.d.ts b/typings/components/Menu.d.ts
index bf5081d7b..53a1587e5 100644
--- a/typings/components/Menu.d.ts
+++ b/typings/components/Menu.d.ts
@@ -20,6 +20,6 @@ export interface ItemProps {
   style?: StyleProp<ViewStyle>;
 }
 
-export declare class Menu extends React.Component<MenuProps> {
-    static Item: React.ComponentType<ItemProps>;
+export class Menu extends React.Component<MenuProps> {
+  static Item: React.ComponentType<ItemProps>;
 }
diff --git a/typings/components/Modal.d.ts b/typings/components/Modal.d.ts
index b1f1564ff..d58048a20 100644
--- a/typings/components/Modal.d.ts
+++ b/typings/components/Modal.d.ts
@@ -1,4 +1,5 @@
 import * as React from 'react';
+import { StyleProp, ViewStyle } from 'react-native';
 import { ThemeShape } from '../types';
 
 export interface ModalProps {
@@ -6,6 +7,7 @@ export interface ModalProps {
   onDismiss?: () => any;
   visible: boolean;
   children: React.ReactNode;
+  contentContainerStyle?: StyleProp<ViewStyle>;
   theme?: ThemeShape;
 }
 
diff --git a/typings/components/Searchbar.d.ts b/typings/components/Searchbar.d.ts
index 5381d8977..451ab9dcd 100644
--- a/typings/components/Searchbar.d.ts
+++ b/typings/components/Searchbar.d.ts
@@ -1,8 +1,9 @@
 import * as React from 'react';
-import { TextInputProps } from 'react-native';
+import { StyleProp, TextInputProps, TextStyle } from 'react-native';
 import { ThemeShape, IconSource } from '../types';
 
 export interface SearchbarProps extends TextInputProps {
+  inputStyle?: StyleProp<TextStyle>;
   icon?: IconSource;
   theme?: ThemeShape;
   clearIcon?: IconSource;


Test patch:
diff --git a/__ts-tests__/Chip.test.tsx b/__ts-tests__/Chip.test.tsx
index ff3959429..9fc2fdd5b 100644
--- a/__ts-tests__/Chip.test.tsx
+++ b/__ts-tests__/Chip.test.tsx
@@ -3,7 +3,7 @@ import * as React from 'react';
 import { Chip } from '..';
 
 const MyComponent = () => (
-  <Chip icon="info" onPress={() => console.log('Pressed')}>Example Chip</Chip>
+  <Chip icon="info" selectedColor="#fff" onPress={() => console.log('Pressed')}>Example Chip</Chip>
 );
 
 export default MyComponent;
diff --git a/src/components/__tests__/BottomNavigation.test.js b/src/components/__tests__/BottomNavigation.test.js
index c46fe4549..1df339b7e 100644
--- a/src/components/__tests__/BottomNavigation.test.js
+++ b/src/components/__tests__/BottomNavigation.test.js
@@ -4,6 +4,8 @@ import * as React from 'react';
 import renderer from 'react-test-renderer';
 import BottomNavigation from '../BottomNavigation';
 
+jest.useFakeTimers();
+
 jest.mock('Animated', () => {
   const ActualAnimated = jest.requireActual('Animated');
 
diff --git a/src/components/__tests__/ListAccordion.test.js b/src/components/__tests__/ListAccordion.test.js
index 090681294..63be4be25 100644
--- a/src/components/__tests__/ListAccordion.test.js
+++ b/src/components/__tests__/ListAccordion.test.js
@@ -65,3 +65,20 @@ it('renders expanded accordion', () => {
 
   expect(tree).toMatchSnapshot();
 });
+
+it('renders list accordion with custom title and description styles', () => {
+  const tree = renderer
+    .create(
+      <ListAccordion
+        title="Accordion item 1"
+        description="Describes the expandable list item"
+        titleStyle={{ color: 'red' }}
+        descriptionStyle={{ color: 'red' }}
+      >
+        <ListItem title="List item 1" />
+      </ListAccordion>
+    )
+    .toJSON();
+
+  expect(tree).toMatchSnapshot();
+});
diff --git a/src/components/__tests__/ListSection.test.js b/src/components/__tests__/ListSection.test.js
index 190e48e90..db9e1ec9b 100644
--- a/src/components/__tests__/ListSection.test.js
+++ b/src/components/__tests__/ListSection.test.js
@@ -45,3 +45,22 @@ it('renders list section with subheader', () => {
 
   expect(tree).toMatchSnapshot();
 });
+
+it('renders list section with custom title style', () => {
+  const tree = renderer
+    .create(
+      <ListSection title="Some title" titleStyle={{ color: 'red' }}>
+        <ListItem
+          title="First Item"
+          left={props => <ListIcon {...props} icon="folder" />}
+        />
+        <ListItem
+          title="Second Item"
+          left={props => <ListIcon {...props} icon="folder" />}
+        />
+      </ListSection>
+    )
+    .toJSON();
+
+  expect(tree).toMatchSnapshot();
+});
diff --git a/src/components/__tests__/RadioButton.test.js b/src/components/__tests__/RadioButton.test.js
new file mode 100644
index 000000000..cbf678c68
--- /dev/null
+++ b/src/components/__tests__/RadioButton.test.js
@@ -0,0 +1,46 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { RadioButtonContext } from '../RadioButtonGroup';
+import RadioButton from '../RadioButton';
+
+describe('RadioButton', () => {
+  afterEach(() => jest.resetModules());
+
+  describe('on default platform', () => {
+    beforeAll(() => {
+      jest.doMock('Platform', () => ({ select: objs => objs.default }));
+    });
+
+    it('renders properly', () => {
+      const tree = renderer.create(<RadioButton value="first" />).toJSON();
+
+      expect(tree).toMatchSnapshot();
+    });
+  });
+
+  describe('on ios platform', () => {
+    beforeAll(() => {
+      jest.doMock('Platform', () => ({ select: objs => objs.ios }));
+    });
+
+    it('renders properly', () => {
+      const tree = renderer.create(<RadioButton value="first" />).toJSON();
+
+      expect(tree).toMatchSnapshot();
+    });
+  });
+
+  describe('when RadioButton is wrapped by RadioButtonContext.Provider', () => {
+    it('renders properly', () => {
+      const tree = renderer
+        .create(
+          <RadioButtonContext.Provider value="first" onValueChange={() => {}}>
+            <RadioButton value="first" />
+          </RadioButtonContext.Provider>
+        )
+        .toJSON();
+
+      expect(tree).toMatchSnapshot();
+    });
+  });
+});
diff --git a/src/components/__tests__/__snapshots__/Banner.test.js.snap b/src/components/__tests__/__snapshots__/Banner.test.js.snap
index f1255747d..50efb250a 100644
--- a/src/components/__tests__/__snapshots__/Banner.test.js.snap
+++ b/src/components/__tests__/__snapshots__/Banner.test.js.snap
@@ -195,13 +195,6 @@ exports[`renders visible banner, with action buttons and with image 1`] = `
               "elevation": 0,
               "margin": 4,
               "minWidth": "auto",
-              "shadowColor": "#000000",
-              "shadowOffset": Object {
-                "height": 0,
-                "width": 0,
-              },
-              "shadowOpacity": 0.24,
-              "shadowRadius": 0,
             }
           }
         >
@@ -364,13 +357,6 @@ exports[`renders visible banner, with action buttons and without image 1`] = `
               "elevation": 0,
               "margin": 4,
               "minWidth": "auto",
-              "shadowColor": "#000000",
-              "shadowOffset": Object {
-                "height": 0,
-                "width": 0,
-              },
-              "shadowOpacity": 0.24,
-              "shadowRadius": 0,
             }
           }
         >
@@ -454,13 +440,6 @@ exports[`renders visible banner, with action buttons and without image 1`] = `
               "elevation": 0,
               "margin": 4,
               "minWidth": "auto",
-              "shadowColor": "#000000",
-              "shadowOffset": Object {
-                "height": 0,
-                "width": 0,
-              },
-              "shadowOpacity": 0.24,
-              "shadowRadius": 0,
             }
           }
         >
diff --git a/src/components/__tests__/__snapshots__/Button.test.js.snap b/src/components/__tests__/__snapshots__/Button.test.js.snap
index 0f99c199f..7072a3874 100644
--- a/src/components/__tests__/__snapshots__/Button.test.js.snap
+++ b/src/components/__tests__/__snapshots__/Button.test.js.snap
@@ -11,13 +11,6 @@ exports[`renders button with color 1`] = `
       "borderWidth": 0,
       "elevation": 0,
       "minWidth": 64,
-      "shadowColor": "#000000",
-      "shadowOffset": Object {
-        "height": 0,
-        "width": 0,
-      },
-      "shadowOpacity": 0.24,
-      "shadowRadius": 0,
     }
   }
 >
@@ -101,13 +94,6 @@ exports[`renders button with icon 1`] = `
       "borderWidth": 0,
       "elevation": 0,
       "minWidth": 64,
-      "shadowColor": "#000000",
-      "shadowOffset": Object {
-        "height": 0,
-        "width": 0,
-      },
-      "shadowOpacity": 0.24,
-      "shadowRadius": 0,
     }
   }
 >
@@ -413,13 +399,6 @@ exports[`renders loading button 1`] = `
       "borderWidth": 0,
       "elevation": 0,
       "minWidth": 64,
-      "shadowColor": "#000000",
-      "shadowOffset": Object {
-        "height": 0,
-        "width": 0,
-      },
-      "shadowOpacity": 0.24,
-      "shadowRadius": 0,
     }
   }
 >
@@ -516,13 +495,6 @@ exports[`renders outlined button with mode 1`] = `
       "borderWidth": 0.5,
       "elevation": 0,
       "minWidth": 64,
-      "shadowColor": "#000000",
-      "shadowOffset": Object {
-        "height": 0,
-        "width": 0,
-      },
-      "shadowOpacity": 0.24,
-      "shadowRadius": 0,
     }
   }
 >
@@ -606,13 +578,6 @@ exports[`renders text button by default 1`] = `
       "borderWidth": 0,
       "elevation": 0,
       "minWidth": 64,
-      "shadowColor": "#000000",
-      "shadowOffset": Object {
-        "height": 0,
-        "width": 0,
-      },
-      "shadowOpacity": 0.24,
-      "shadowRadius": 0,
     }
   }
 >
@@ -696,13 +661,6 @@ exports[`renders text button with mode 1`] = `
       "borderWidth": 0,
       "elevation": 0,
       "minWidth": 64,
-      "shadowColor": "#000000",
-      "shadowOffset": Object {
-        "height": 0,
-        "width": 0,
-      },
-      "shadowOpacity": 0.24,
-      "shadowRadius": 0,
     }
   }
 >
diff --git a/src/components/__tests__/__snapshots__/ListAccordion.test.js.snap b/src/components/__tests__/__snapshots__/ListAccordion.test.js.snap
index 762c1b7bf..85f2a37d9 100644
--- a/src/components/__tests__/__snapshots__/ListAccordion.test.js.snap
+++ b/src/components/__tests__/__snapshots__/ListAccordion.test.js.snap
@@ -63,6 +63,7 @@ exports[`renders expanded accordion 1`] = `
                 Object {
                   "color": "#6200ee",
                 },
+                undefined,
               ],
             ]
           }
@@ -302,6 +303,7 @@ exports[`renders list accordion with children 1`] = `
                 Object {
                   "color": "rgba(0, 0, 0, 0.87)",
                 },
+                undefined,
               ],
             ]
           }
@@ -359,6 +361,159 @@ exports[`renders list accordion with children 1`] = `
 </View>
 `;
 
+exports[`renders list accordion with custom title and description styles 1`] = `
+<View>
+  <View
+    accessibilityRole="button"
+    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
+      pointerEvents="none"
+      style={
+        Object {
+          "alignItems": "center",
+          "flexDirection": "row",
+        }
+      }
+    >
+      <View
+        style={
+          Array [
+            Object {
+              "margin": 8,
+            },
+            Object {
+              "flex": 1,
+              "justifyContent": "center",
+            },
+          ]
+        }
+      >
+        <Text
+          numberOfLines={1}
+          style={
+            Array [
+              Object {
+                "color": "#000000",
+                "fontFamily": "Helvetica Neue",
+                "textAlign": "left",
+                "writingDirection": "ltr",
+              },
+              Array [
+                Object {
+                  "fontSize": 16,
+                },
+                Object {
+                  "color": "rgba(0, 0, 0, 0.87)",
+                },
+                Object {
+                  "color": "red",
+                },
+              ],
+            ]
+          }
+        >
+          Accordion item 1
+        </Text>
+        <Text
+          numberOfLines={2}
+          style={
+            Array [
+              Object {
+                "color": "#000000",
+                "fontFamily": "Helvetica Neue",
+                "textAlign": "left",
+                "writingDirection": "ltr",
+              },
+              Array [
+                Object {
+                  "fontSize": 14,
+                },
+                Object {
+                  "color": "rgba(0, 0, 0, 0.54)",
+                },
+                Object {
+                  "color": "red",
+                },
+              ],
+            ]
+          }
+        >
+          Describes the expandable list item
+        </Text>
+      </View>
+      <View
+        style={
+          Array [
+            Object {
+              "margin": 8,
+            },
+            Object {
+              "alignItems": "center",
+              "height": 40,
+              "justifyContent": "center",
+            },
+          ]
+        }
+      >
+        <Text
+          accessibilityElementsHidden={true}
+          allowFontScaling={false}
+          importantForAccessibility="no-hide-descendants"
+          pointerEvents="none"
+          style={
+            Array [
+              Object {
+                "color": "rgba(0, 0, 0, 0.87)",
+                "fontSize": 24,
+              },
+              Array [
+                Object {
+                  "transform": Array [
+                    Object {
+                      "scaleX": 1,
+                    },
+                  ],
+                },
+                Object {
+                  "backgroundColor": "transparent",
+                },
+              ],
+              Object {
+                "fontFamily": "Material Icons",
+                "fontStyle": "normal",
+                "fontWeight": "normal",
+              },
+              Object {},
+            ]
+          }
+        >
+          
+        </Text>
+      </View>
+    </View>
+  </View>
+</View>
+`;
+
 exports[`renders list accordion with left items 1`] = `
 <View>
   <View
@@ -472,6 +627,7 @@ exports[`renders list accordion with left items 1`] = `
                 Object {
                   "color": "rgba(0, 0, 0, 0.87)",
                 },
+                undefined,
               ],
             ]
           }
@@ -592,6 +748,7 @@ exports[`renders multiline list accordion 1`] = `
                 Object {
                   "color": "rgba(0, 0, 0, 0.87)",
                 },
+                undefined,
               ],
             ]
           }
@@ -615,6 +772,7 @@ exports[`renders multiline list accordion 1`] = `
                 Object {
                   "color": "rgba(0, 0, 0, 0.54)",
                 },
+                undefined,
               ],
             ]
           }
diff --git a/src/components/__tests__/__snapshots__/ListSection.test.js.snap b/src/components/__tests__/__snapshots__/ListSection.test.js.snap
index 829822663..870549ab4 100644
--- a/src/components/__tests__/__snapshots__/ListSection.test.js.snap
+++ b/src/components/__tests__/__snapshots__/ListSection.test.js.snap
@@ -1,5 +1,309 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`renders list section with custom title style 1`] = `
+<View
+  style={
+    Array [
+      Object {
+        "marginVertical": 8,
+      },
+      undefined,
+    ]
+  }
+  theme={
+    Object {
+      "colors": Object {
+        "accent": "#03dac4",
+        "backdrop": "rgba(0, 0, 0, 0.5)",
+        "background": "#f6f6f6",
+        "disabled": "rgba(0, 0, 0, 0.26)",
+        "error": "#B00020",
+        "notification": "#f50057",
+        "placeholder": "rgba(0, 0, 0, 0.54)",
+        "primary": "#6200ee",
+        "surface": "#ffffff",
+        "text": "#000000",
+      },
+      "dark": false,
+      "fonts": Object {
+        "light": "HelveticaNeue-Light",
+        "medium": "HelveticaNeue-Medium",
+        "regular": "Helvetica Neue",
+        "thin": "HelveticaNeue-Thin",
+      },
+      "roundness": 4,
+    }
+  }
+>
+  <Text
+    numberOfLines={1}
+    style={
+      Array [
+        Object {
+          "color": "#000000",
+          "fontFamily": "Helvetica Neue",
+          "textAlign": "left",
+          "writingDirection": "ltr",
+        },
+        Array [
+          Object {
+            "paddingHorizontal": 16,
+            "paddingVertical": 13,
+          },
+          Object {
+            "color": "rgba(0, 0, 0, 0.54)",
+            "fontFamily": "HelveticaNeue-Medium",
+          },
+          Object {
+            "color": "red",
+          },
+        ],
+      ]
+    }
+  >
+    Some title
+  </Text>
+  <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="box-none"
+        style={
+          Array [
+            Object {
+              "alignItems": "center",
+              "height": 40,
+              "justifyContent": "center",
+              "margin": 8,
+              "width": 40,
+            },
+            undefined,
+          ]
+        }
+      >
+        <Text
+          accessibilityElementsHidden={true}
+          allowFontScaling={false}
+          importantForAccessibility="no-hide-descendants"
+          pointerEvents="none"
+          style={
+            Array [
+              Object {
+                "color": "rgba(0, 0, 0, 0.54)",
+                "fontSize": 24,
+              },
+              Array [
+                Object {
+                  "transform": Array [
+                    Object {
+                      "scaleX": 1,
+                    },
+                  ],
+                },
+                Object {
+                  "backgroundColor": "transparent",
+                },
+              ],
+              Object {
+                "fontFamily": "Material Icons",
+                "fontStyle": "normal",
+                "fontWeight": "normal",
+              },
+              Object {},
+            ]
+          }
+        >
+          
+        </Text>
+      </View>
+      <View
+        pointerEvents="none"
+        style={
+          Array [
+            Object {
+              "margin": 8,
+            },
+            Object {
+              "flex": 1,
+              "justifyContent": "center",
+            },
+          ]
+        }
+      >
+        <Text
+          numberOfLines={1}
+          style={
+            Array [
+              Object {
+                "color": "#000000",
+                "fontFamily": "Helvetica Neue",
+                "textAlign": "left",
+                "writingDirection": "ltr",
+              },
+              Array [
+                Object {
+                  "fontSize": 16,
+                },
+                Object {
+                  "color": "rgba(0, 0, 0, 0.87)",
+                },
+                undefined,
+              ],
+            ]
+          }
+        >
+          First Item
+        </Text>
+      </View>
+    </View>
+  </View>
+  <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="box-none"
+        style={
+          Array [
+            Object {
+              "alignItems": "center",
+              "height": 40,
+              "justifyContent": "center",
+              "margin": 8,
+              "width": 40,
+            },
+            undefined,
+          ]
+        }
+      >
+        <Text
+          accessibilityElementsHidden={true}
+          allowFontScaling={false}
+          importantForAccessibility="no-hide-descendants"
+          pointerEvents="none"
+          style={
+            Array [
+              Object {
+                "color": "rgba(0, 0, 0, 0.54)",
+                "fontSize": 24,
+              },
+              Array [
+                Object {
+                  "transform": Array [
+                    Object {
+                      "scaleX": 1,
+                    },
+                  ],
+                },
+                Object {
+                  "backgroundColor": "transparent",
+                },
+              ],
+              Object {
+                "fontFamily": "Material Icons",
+                "fontStyle": "normal",
+                "fontWeight": "normal",
+              },
+              Object {},
+            ]
+          }
+        >
+          
+        </Text>
+      </View>
+      <View
+        pointerEvents="none"
+        style={
+          Array [
+            Object {
+              "margin": 8,
+            },
+            Object {
+              "flex": 1,
+              "justifyContent": "center",
+            },
+          ]
+        }
+      >
+        <Text
+          numberOfLines={1}
+          style={
+            Array [
+              Object {
+                "color": "#000000",
+                "fontFamily": "Helvetica Neue",
+                "textAlign": "left",
+                "writingDirection": "ltr",
+              },
+              Array [
+                Object {
+                  "fontSize": 16,
+                },
+                Object {
+                  "color": "rgba(0, 0, 0, 0.87)",
+                },
+                undefined,
+              ],
+            ]
+          }
+        >
+          Second Item
+        </Text>
+      </View>
+    </View>
+  </View>
+</View>
+`;
+
 exports[`renders list section with subheader 1`] = `
 <View
   style={
diff --git a/src/components/__tests__/__snapshots__/Menu.test.js.snap b/src/components/__tests__/__snapshots__/Menu.test.js.snap
index 57226cbcd..0e298c723 100644
--- a/src/components/__tests__/__snapshots__/Menu.test.js.snap
+++ b/src/components/__tests__/__snapshots__/Menu.test.js.snap
@@ -14,13 +14,6 @@ exports[`renders not visible menu 1`] = `
         "borderWidth": 0.5,
         "elevation": 0,
         "minWidth": 64,
-        "shadowColor": "#000000",
-        "shadowOffset": Object {
-          "height": 0,
-          "width": 0,
-        },
-        "shadowOpacity": 0.24,
-        "shadowRadius": 0,
       }
     }
   >
@@ -108,13 +101,6 @@ exports[`renders visible menu 1`] = `
         "borderWidth": 0.5,
         "elevation": 0,
         "minWidth": 64,
-        "shadowColor": "#000000",
-        "shadowOffset": Object {
-          "height": 0,
-          "width": 0,
-        },
-        "shadowOpacity": 0.24,
-        "shadowRadius": 0,
       }
     }
   >
diff --git a/src/components/__tests__/__snapshots__/RadioButton.test.js.snap b/src/components/__tests__/__snapshots__/RadioButton.test.js.snap
new file mode 100644
index 000000000..65babe805
--- /dev/null
+++ b/src/components/__tests__/__snapshots__/RadioButton.test.js.snap
@@ -0,0 +1,174 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RadioButton on default platform renders properly 1`] = `
+<View
+  accessibilityRole="button"
+  accessible={true}
+  isTVSelectable={true}
+  onResponderGrant={[Function]}
+  onResponderMove={[Function]}
+  onResponderRelease={[Function]}
+  onResponderTerminate={[Function]}
+  onResponderTerminationRequest={[Function]}
+  onStartShouldSetResponder={[Function]}
+  style={
+    Array [
+      Object {
+        "overflow": "hidden",
+      },
+      Object {
+        "borderRadius": 18,
+      },
+    ]
+  }
+>
+  <View
+    style={
+      Object {
+        "borderColor": "rgba(0, 0, 0, 0.54)",
+        "borderRadius": 10,
+        "borderWidth": 2,
+        "height": 20,
+        "margin": 8,
+        "width": 20,
+      }
+    }
+  />
+</View>
+`;
+
+exports[`RadioButton on ios platform renders properly 1`] = `
+<View
+  accessibilityRole="button"
+  accessible={true}
+  isTVSelectable={true}
+  onResponderGrant={[Function]}
+  onResponderMove={[Function]}
+  onResponderRelease={[Function]}
+  onResponderTerminate={[Function]}
+  onResponderTerminationRequest={[Function]}
+  onStartShouldSetResponder={[Function]}
+  style={
+    Array [
+      Object {
+        "overflow": "hidden",
+      },
+      Object {
+        "borderRadius": 18,
+        "padding": 6,
+      },
+    ]
+  }
+>
+  <View
+    style={
+      Object {
+        "opacity": 0,
+      }
+    }
+  >
+    <Text
+      accessibilityElementsHidden={true}
+      allowFontScaling={false}
+      importantForAccessibility="no-hide-descendants"
+      pointerEvents="none"
+      style={
+        Array [
+          Object {
+            "color": "#03dac4",
+            "fontSize": 24,
+          },
+          Array [
+            Object {
+              "transform": Array [
+                Object {
+                  "scaleX": 1,
+                },
+              ],
+            },
+            Object {
+              "backgroundColor": "transparent",
+            },
+          ],
+          Object {
+            "fontFamily": "Material Icons",
+            "fontStyle": "normal",
+            "fontWeight": "normal",
+          },
+          Object {},
+        ]
+      }
+    >
+      
+    </Text>
+  </View>
+</View>
+`;
+
+exports[`RadioButton when RadioButton is wrapped by RadioButtonContext.Provider renders properly 1`] = `
+<View
+  accessibilityRole="button"
+  accessible={true}
+  isTVSelectable={true}
+  onResponderGrant={[Function]}
+  onResponderMove={[Function]}
+  onResponderRelease={[Function]}
+  onResponderTerminate={[Function]}
+  onResponderTerminationRequest={[Function]}
+  onStartShouldSetResponder={[Function]}
+  style={
+    Array [
+      Object {
+        "overflow": "hidden",
+      },
+      Object {
+        "borderRadius": 18,
+        "padding": 6,
+      },
+    ]
+  }
+>
+  <View
+    style={
+      Object {
+        "opacity": 0,
+      }
+    }
+  >
+    <Text
+      accessibilityElementsHidden={true}
+      allowFontScaling={false}
+      importantForAccessibility="no-hide-descendants"
+      pointerEvents="none"
+      style={
+        Array [
+          Object {
+            "color": "#03dac4",
+            "fontSize": 24,
+          },
+          Array [
+            Object {
+              "transform": Array [
+                Object {
+                  "scaleX": 1,
+                },
+              ],
+            },
+            Object {
+              "backgroundColor": "transparent",
+            },
+          ],
+          Object {
+            "fontFamily": "Material Icons",
+            "fontStyle": "normal",
+            "fontWeight": "normal",
+          },
+          Object {},
+        ]
+      }
+    >
+      
+    </Text>
+  </View>
+</View>
+`;
diff --git a/src/components/__tests__/__snapshots__/Snackbar.test.js.snap b/src/components/__tests__/__snapshots__/Snackbar.test.js.snap
index 08ceece21..000431ed9 100644
--- a/src/components/__tests__/__snapshots__/Snackbar.test.js.snap
+++ b/src/components/__tests__/__snapshots__/Snackbar.test.js.snap
@@ -150,13 +150,6 @@ exports[`renders snackbar with action button 1`] = `
           "marginHorizontal": 8,
           "marginVertical": 6,
           "minWidth": "auto",
-          "shadowColor": "#000000",
-          "shadowOffset": Object {
-            "height": 0,
-            "width": 0,
-          },
-          "shadowOpacity": 0.24,
-          "shadowRadius": 0,
         }
       }
     >