Repository: callstack/react-native-paper
Pull request #982 (2019-04-10 17:12:36)

fix: component RadioGroup animation
**What existing problem does the pull request solve?**
`RadioButtonAndroid` wasn't triggering the animation on `componentDidUpdate` when it was wrapped by a `RadioButtonGroup` because it was getting the props in the context.  

Before:
![ezgif com-video-to-gif](https://user-images.githubusercontent.com/6487206/55899085-56270600-5b9a-11e9-971a-05621b1ea288.gif)

Fixed
![ezgif com-video-to-gif (1)](https://user-images.githubusercontent.com/6487206/55899178-8ff80c80-5b9a-11e9-8b3b-d361cb64d964.gif)


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

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/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/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..3b961f47e 100644
--- a/src/components/Menu/Menu.js
+++ b/src/components/Menu/Menu.js
@@ -17,6 +17,7 @@ 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 +28,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 +50,15 @@ type Props = {
   theme: Theme,
 };
 
-type State = {
+type State = {|
   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 +122,14 @@ class Menu extends React.Component<Props, State> {
   // @component ./MenuItem.js
   static Item = MenuItem;
 
+  static defaultProps = {
+    statusBarHeight: APPROX_STATUSBAR_HEIGHT,
+  };
+
   state = {
     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,7 +143,8 @@ class Menu extends React.Component<Props, State> {
   }
 
   componentWillUnmount() {
-    BackHandler.removeEventListener('hardwareBackPress', this.props.onDismiss);
+    BackHandler.removeEventListener('hardwareBackPress', this._handleDismiss);
+    Dimensions.removeEventListener('change', this._handleDismiss);
   }
 
   _anchor: ?View;
@@ -161,9 +176,18 @@ class Menu extends React.Component<Props, State> {
     }
   };
 
+  _handleDismiss = () => {
+    if (this.props.visible) {
+      this.props.onDismiss();
+    }
+    return true;
+  };
+
   _show = async () => {
-    BackHandler.addEventListener('hardwareBackPress', this.props.onDismiss);
+    BackHandler.addEventListener('hardwareBackPress', this._handleDismiss);
+    Dimensions.addEventListener('change', this._handleDismiss);
 
+    const windowLayout = Dimensions.get('window');
     const [menuLayout, anchorLayout] = await Promise.all([
       this._measureMenuLayout(),
       this._measureAnchorLayout(),
@@ -176,18 +200,15 @@ 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);
+      BackHandler.removeEventListener('hardwareBackPress', this._handleDismiss);
+      setTimeout(this._show, ANIMATION_DURATION);
       return;
     }
 
@@ -195,6 +216,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,
@@ -224,7 +249,8 @@ class Menu extends React.Component<Props, State> {
   };
 
   _hide = () => {
-    BackHandler.removeEventListener('hardwareBackPress', this.props.onDismiss);
+    BackHandler.removeEventListener('hardwareBackPress', this._handleDismiss);
+    Dimensions.removeEventListener('change', this._handleDismiss);
 
     Animated.timing(this.state.opacityAnimation, {
       toValue: 0,
@@ -239,9 +265,18 @@ class Menu extends React.Component<Props, State> {
   };
 
   render() {
-    const { visible, anchor, style, children, theme, onDismiss } = this.props;
+    const {
+      visible,
+      anchor,
+      style,
+      children,
+      theme,
+      statusBarHeight,
+      onDismiss,
+    } = this.props;
 
     const {
+      windowLayout,
       menuLayout,
       anchorLayout,
       opacityAnimation,
@@ -250,7 +285,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 +309,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 +334,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 +367,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;
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/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..730ea72cf 100644
--- a/typings/components/Menu.d.ts
+++ b/typings/components/Menu.d.ts
@@ -7,6 +7,7 @@ export interface MenuProps {
   anchor: React.ReactNode;
   onDismiss: () => void;
   children: React.ReactNode;
+  statusBarHeight?: number,
   style?: StyleProp<ViewStyle>;
   theme?: ThemeShape;
 }


Test patch:
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__/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__/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>
+`;