Repository: callstack/react-native-paper
Pull request #720 (2019-01-04 10:17:59)

feat: animated visibility for FAB
### Motivation

Adds expanding animation for visibility to FAB as described here:
https://material.io/design/components/buttons-floating-action-button.html#behavior

### Test plan

Open examples on device and click first FAB to toggle visibility.



Patch:
diff --git a/example/src/FABExample.js b/example/src/FABExample.js
index eccb19540..efeea873f 100644
--- a/example/src/FABExample.js
+++ b/example/src/FABExample.js
@@ -9,6 +9,7 @@ type Props = {
 };
 
 type State = {
+  visible: boolean,
   open: boolean,
 };
 
@@ -16,6 +17,7 @@ class ButtonExample extends React.Component<Props, State> {
   static title = 'Floating Action Button';
 
   state = {
+    visible: true,
     open: false,
   };
 
@@ -29,19 +31,38 @@ class ButtonExample extends React.Component<Props, State> {
     return (
       <View style={[styles.container, { backgroundColor: background }]}>
         <View style={styles.row}>
-          <FAB small icon="add" style={styles.fab} onPress={() => {}} />
-          <FAB icon="favorite" style={styles.fab} onPress={() => {}} />
+          <FAB
+            small
+            icon={this.state.visible ? 'visibility-off' : 'visibility'}
+            style={styles.fab}
+            onPress={() => {
+              this.setState({
+                visible: !this.state.visible,
+              });
+            }}
+          />
+        </View>
+
+        <View style={styles.row}>
+          <FAB
+            icon="favorite"
+            style={styles.fab}
+            onPress={() => {}}
+            visible={this.state.visible}
+          />
           <FAB
             icon="done"
             label="Extended FAB"
             style={styles.fab}
             onPress={() => {}}
+            visible={this.state.visible}
           />
           <FAB
             icon="cancel"
             label="Disabled FAB"
             style={styles.fab}
             onPress={() => {}}
+            visible={this.state.visible}
             disabled
           />
           <Portal>
@@ -60,6 +81,7 @@ class ButtonExample extends React.Component<Props, State> {
                   // do something if the speed dial is open
                 }
               }}
+              visible={this.state.visible}
             />
           </Portal>
         </View>
@@ -76,7 +98,6 @@ const styles = StyleSheet.create({
   },
 
   row: {
-    flex: 1,
     justifyContent: 'center',
     alignItems: 'center',
   },
diff --git a/src/components/FAB/FAB.js b/src/components/FAB/FAB.js
index 57a91a5f2..f1c4d3978 100644
--- a/src/components/FAB/FAB.js
+++ b/src/components/FAB/FAB.js
@@ -2,7 +2,7 @@
 
 import color from 'color';
 import * as React from 'react';
-import { View, StyleSheet } from 'react-native';
+import { Animated, View, StyleSheet } from 'react-native';
 import FABGroup from './FABGroup';
 import Surface from '../Surface';
 import CrossFadeIcon from '../CrossFadeIcon';
@@ -39,6 +39,10 @@ type Props = $RemoveChildren<typeof Surface> & {|
    * Whether `FAB` is disabled. A disabled button is greyed out and `onPress` is not called on touch.
    */
   disabled?: boolean,
+  /**
+   * Whether `FAB` is currently visible.
+   */
+  visible: boolean,
   /**
    * Function to execute on press.
    */
@@ -50,6 +54,10 @@ type Props = $RemoveChildren<typeof Surface> & {|
   theme: Theme,
 |};
 
+type State = {
+  visibility: Animated.Value,
+};
+
 /**
  * A floating action button represents the primary action in an application.
  *
@@ -85,10 +93,38 @@ type Props = $RemoveChildren<typeof Surface> & {|
  * export default MyComponent;
  * ```
  */
-class FAB extends React.Component<Props> {
+class FAB extends React.Component<Props, State> {
   // @component ./FABGroup.js
   static Group = FABGroup;
 
+  static defaultProps = {
+    visible: true,
+  };
+
+  state = {
+    visibility: new Animated.Value(this.props.visible ? 1 : 0),
+  };
+
+  componentDidUpdate(prevProps) {
+    if (this.props.visible === prevProps.visible) {
+      return;
+    }
+
+    if (this.props.visible) {
+      Animated.timing(this.state.visibility, {
+        toValue: 1,
+        duration: 200,
+        useNativeDriver: true,
+      }).start();
+    } else {
+      Animated.timing(this.state.visibility, {
+        toValue: 0,
+        duration: 150,
+        useNativeDriver: true,
+      }).start();
+    }
+  }
+
   render() {
     const {
       small,
@@ -102,6 +138,7 @@ class FAB extends React.Component<Props> {
       style,
       ...rest
     } = this.props;
+    const { visibility } = this.state;
 
     const disabledColor = color(theme.dark ? white : black)
       .alpha(0.12)
@@ -135,7 +172,15 @@ class FAB extends React.Component<Props> {
       <Surface
         {...rest}
         style={[
-          { backgroundColor },
+          {
+            backgroundColor,
+            opacity: visibility,
+            transform: [
+              {
+                scale: visibility,
+              },
+            ],
+          },
           styles.container,
           disabled && styles.disabled,
           style,
diff --git a/src/components/FAB/FABGroup.js b/src/components/FAB/FABGroup.js
index 3c7320626..b35526836 100644
--- a/src/components/FAB/FABGroup.js
+++ b/src/components/FAB/FABGroup.js
@@ -62,6 +62,10 @@ type Props = {|
    * The open state needs to be updated when it's called, otherwise the change is dropped.
    */
   onStateChange: (state: { open: boolean }) => mixed,
+  /**
+   * Whether `FAB` is currently visible.
+   */
+  visible: boolean,
   /**
    * Style for the group. You can use it to pass additional styles if you need.
    * For example, you can set an additional margin if you have a tab bar at the bottom.
@@ -194,6 +198,7 @@ class FABGroup extends React.Component<Props, State> {
       accessibilityLabel,
       theme,
       style,
+      visible,
     } = this.props;
     const { colors } = theme;
 
@@ -310,6 +315,7 @@ class FABGroup extends React.Component<Props, State> {
           accessibilityComponentType="button"
           accessibilityRole="button"
           style={styles.fab}
+          visible={visible}
         />
       </View>
     );
diff --git a/src/components/TextInput.js b/src/components/TextInput.js
index 035da912f..8ad1d437f 100644
--- a/src/components/TextInput.js
+++ b/src/components/TextInput.js
@@ -472,7 +472,8 @@ class TextInput extends React.Component<Props, State> {
           translateX: this.state.labeled.interpolate({
             inputRange: [0, 1],
             outputRange: [
-              -(1 - MINIMIZED_LABEL_FONT_SIZE / MAXIMIZED_LABEL_FONT_SIZE) *
+              (I18nManager.isRTL ? 1 : -1) *
+                (1 - MINIMIZED_LABEL_FONT_SIZE / MAXIMIZED_LABEL_FONT_SIZE) *
                 (this.state.labelLayout.width / 2),
               0,
             ],
diff --git a/typings/components/FAB.d.ts b/typings/components/FAB.d.ts
index 56d6f7c45..bf75b0473 100644
--- a/typings/components/FAB.d.ts
+++ b/typings/components/FAB.d.ts
@@ -19,6 +19,7 @@ export interface FABGroupProps {
   onPress?: () => any;
   open: boolean;
   onStateChange: (state: { open: boolean }) => any;
+  visible?: boolean;
   style?: any;
   theme?: ThemeShape;
 }
@@ -30,6 +31,7 @@ export interface FABProps extends ViewProps {
   small?: boolean;
   color?: string;
   disabled?: boolean;
+  visible?: boolean;
   onPress?: () => any;
   theme?: ThemeShape;
 }


Test patch:
diff --git a/src/components/__tests__/__snapshots__/FAB.test.js.snap b/src/components/__tests__/__snapshots__/FAB.test.js.snap
index 6b9ab3bb4..ca6462df4 100644
--- a/src/components/__tests__/__snapshots__/FAB.test.js.snap
+++ b/src/components/__tests__/__snapshots__/FAB.test.js.snap
@@ -7,6 +7,7 @@ exports[`renders extended FAB 1`] = `
       "backgroundColor": "#03dac4",
       "borderRadius": 28,
       "elevation": 6,
+      "opacity": 1,
       "shadowColor": "#000000",
       "shadowOffset": Object {
         "height": 5,
@@ -14,8 +15,14 @@ exports[`renders extended FAB 1`] = `
       },
       "shadowOpacity": 0.24,
       "shadowRadius": 6,
+      "transform": Array [
+        Object {
+          "scale": 1,
+        },
+      ],
     }
   }
+  visible={true}
 >
   <View
     accessibilityLabel="Add items"
@@ -152,6 +159,7 @@ exports[`renders normal FAB 1`] = `
       "backgroundColor": "#03dac4",
       "borderRadius": 28,
       "elevation": 6,
+      "opacity": 1,
       "shadowColor": "#000000",
       "shadowOffset": Object {
         "height": 5,
@@ -159,8 +167,14 @@ exports[`renders normal FAB 1`] = `
       },
       "shadowOpacity": 0.24,
       "shadowRadius": 6,
+      "transform": Array [
+        Object {
+          "scale": 1,
+        },
+      ],
     }
   }
+  visible={true}
 >
   <View
     accessibilityRole="button"
@@ -273,6 +287,7 @@ exports[`renders small FAB 1`] = `
       "backgroundColor": "#03dac4",
       "borderRadius": 28,
       "elevation": 6,
+      "opacity": 1,
       "shadowColor": "#000000",
       "shadowOffset": Object {
         "height": 5,
@@ -280,8 +295,14 @@ exports[`renders small FAB 1`] = `
       },
       "shadowOpacity": 0.24,
       "shadowRadius": 6,
+      "transform": Array [
+        Object {
+          "scale": 1,
+        },
+      ],
     }
   }
+  visible={true}
 >
   <View
     accessibilityRole="button"