Repository: kiwicom/mobile
Pull request #1168 (2018-10-02 15:02:22)

Shared: Control status bar in layout through props

tbergq: @RobinCsl 🚀 


Patch:
diff --git a/app/hotels/src/allHotels/AllHotelsSearchRow.js b/app/hotels/src/allHotels/AllHotelsSearchRow.js
index 89fc7809..87034037 100644
--- a/app/hotels/src/allHotels/AllHotelsSearchRow.js
+++ b/app/hotels/src/allHotels/AllHotelsSearchRow.js
@@ -22,6 +22,8 @@ type PropsWithContext = {|
   +setHotelId: (hotelId: string) => void,
 |};
 
+const NetworkIndicator = () => <View style={[style.image, style.loading]} />;
+
 class AllHotelsSearchRow extends React.Component<PropsWithContext> {
   onGoToSingleHotel = () => {
     const hotelId = idx(this.props, _ => _.data.hotelId);
@@ -44,6 +46,7 @@ class AllHotelsSearchRow extends React.Component<PropsWithContext> {
             style={style.image}
             resizeMode="cover"
             source={{ uri: lowResUrl }}
+            indicator={NetworkIndicator}
           />
         </View>
         <View style={style.content}>
@@ -129,6 +132,9 @@ const style = StyleSheet.create({
       height: 70,
     },
   },
+  loading: {
+    backgroundColor: defaultTokens.paletteCloudLight,
+  },
   hotelTitle: {
     flex: 1,
   },
diff --git a/app/hotels/src/allHotels/RenderSearchResults.js b/app/hotels/src/allHotels/RenderSearchResults.js
index d54b5859..47a75a4c 100644
--- a/app/hotels/src/allHotels/RenderSearchResults.js
+++ b/app/hotels/src/allHotels/RenderSearchResults.js
@@ -37,6 +37,7 @@ type State = {|
 
 export const topValue = 1000;
 export const lowValue = -100;
+const transitionDuration = 250;
 
 export class RenderSearchResults extends React.Component<
   PropsWithContext,
@@ -90,12 +91,12 @@ export class RenderSearchResults extends React.Component<
     Animated.parallel([
       Animated.timing(this.mapAnimation, {
         toValue: 0,
-        duration: 500,
+        duration: transitionDuration,
         useNativeDriver: true,
       }),
       Animated.timing(this.listAnimation, {
         toValue: lowValue,
-        duration: 500,
+        duration: transitionDuration,
         useNativeDriver: true,
       }),
     ]).start();
@@ -105,12 +106,12 @@ export class RenderSearchResults extends React.Component<
     Animated.parallel([
       Animated.timing(this.mapAnimation, {
         toValue: topValue,
-        duration: 500,
+        duration: transitionDuration,
         useNativeDriver: true,
       }),
       Animated.timing(this.listAnimation, {
         toValue: 0,
-        duration: 500,
+        duration: transitionDuration,
         useNativeDriver: true,
       }),
     ]).start();
diff --git a/app/hotels/src/navigation/HotelsNavigationOptions.js b/app/hotels/src/navigation/HotelsNavigationOptions.js
index 76c00c19..0f20ede5 100644
--- a/app/hotels/src/navigation/HotelsNavigationOptions.js
+++ b/app/hotels/src/navigation/HotelsNavigationOptions.js
@@ -49,6 +49,7 @@ export default ({ cityName, checkin, checkout }: Props) => ({
       ios: 0,
       android: StatusBar.currentHeight,
     }),
+    borderBottomColor: defaultTokens.paletteInkLighter,
   },
   headerLeft: (
     <View style={styles.headerLeftcontainer}>
diff --git a/app/hotels/src/navigation/allHotels/SearchResultsScreen.js b/app/hotels/src/navigation/allHotels/SearchResultsScreen.js
index b3f86842..b2d7373c 100644
--- a/app/hotels/src/navigation/allHotels/SearchResultsScreen.js
+++ b/app/hotels/src/navigation/allHotels/SearchResultsScreen.js
@@ -12,6 +12,7 @@ import {
   CloseButton,
   Device,
 } from '@kiwicom/mobile-shared';
+import { defaultTokens } from '@kiwicom/mobile-orbit';
 
 import NewAllHotels from '../../allHotels/NewAllHotels';
 import MapHeaderButton from './MapHeaderButton';
@@ -53,9 +54,9 @@ class SearchResultsScreen extends React.Component<Props> {
     }
     const icon =
       show === 'list' ? (
-        <TextIcon code="&#xe001;" orbit={true} />
+        <TextIcon code="&#xe001;" orbit={true} style={styles.icon} />
       ) : (
-        <TextIcon code="&#xe115;" orbit={true} />
+        <TextIcon code="&#xe115;" orbit={true} style={styles.icon} />
       );
     return {
       ...HotelsNavigationOptions({ checkin, checkout, cityName }),
@@ -125,6 +126,7 @@ class SearchResultsScreen extends React.Component<Props> {
 const styles = StyleSheet.create({
   container: {
     flex: 1,
+    backgroundColor: defaultTokens.paletteWhite,
   },
   button: {
     position: 'absolute',
@@ -132,6 +134,10 @@ const styles = StyleSheet.create({
     start: 8,
     end: 8,
   },
+  icon: {
+    marginEnd: 2,
+    fontSize: 22,
+  },
 });
 
 export default withSearchResultsContext(state => ({
diff --git a/app/hotels/src/navigation/singleHotel/SingleHotelMapNavigationScreen.js b/app/hotels/src/navigation/singleHotel/SingleHotelMapNavigationScreen.js
index 288f49a4..d739295b 100644
--- a/app/hotels/src/navigation/singleHotel/SingleHotelMapNavigationScreen.js
+++ b/app/hotels/src/navigation/singleHotel/SingleHotelMapNavigationScreen.js
@@ -13,25 +13,20 @@ type Props = {
 };
 
 class SingleHotelMapNavigationScreen extends React.Component<Props> {
-  componentDidMount = () => {
-    StatusBar.setBarStyle('dark-content');
-  };
-
-  componentWillUnmount = () => {
-    StatusBar.setBarStyle('default');
-  };
-
   render() {
     return (
-      <SingleHotelMap
-        search={{
-          hotelId: this.props.hotelId,
-          checkin: this.props.checkin,
-          checkout: this.props.checkout,
-          roomsConfiguration: this.props.roomsConfiguration,
-        }}
-        currency={this.props.currency}
-      />
+      <React.Fragment>
+        <SingleHotelMap
+          search={{
+            hotelId: this.props.hotelId,
+            checkin: this.props.checkin,
+            checkout: this.props.checkout,
+            roomsConfiguration: this.props.roomsConfiguration,
+          }}
+          currency={this.props.currency}
+        />
+        <StatusBar barStyle="dark-content" />
+      </React.Fragment>
     );
   }
 }
diff --git a/app/hotels/src/singleHotel/HotelDetailScreen.js b/app/hotels/src/singleHotel/HotelDetailScreen.js
index 599b0764..4b17d552 100644
--- a/app/hotels/src/singleHotel/HotelDetailScreen.js
+++ b/app/hotels/src/singleHotel/HotelDetailScreen.js
@@ -1,7 +1,7 @@
 // @flow
 
 import * as React from 'react';
-import { ScrollView, View, StatusBar, Platform } from 'react-native';
+import { ScrollView, View, Platform } from 'react-native';
 import {
   GeneralError,
   LayoutSingleColumn,
@@ -11,6 +11,7 @@ import {
   AdaptableLayout,
   Dimensions,
   Device,
+  type BarStyle,
 } from '@kiwicom/mobile-shared';
 import { createFragmentContainer, graphql } from '@kiwicom/mobile-relay';
 import { Translation } from '@kiwicom/mobile-localization';
@@ -39,6 +40,7 @@ type State = {|
     [string]: number, // originalId: count
   },
   maxPersons: number,
+  barStyle: BarStyle,
 |};
 
 type NativeEvent = {|
@@ -53,16 +55,21 @@ export class HotelDetailScreen extends React.Component<
   PropsWithContext,
   State,
 > {
-  state = {
-    selected: {},
-    maxPersons: 0,
-  };
+  constructor(props: PropsWithContext) {
+    super(props);
+
+    this.state = {
+      selected: {},
+      maxPersons: 0,
+      barStyle: Platform.select({
+        android: 'default',
+        ios: 'light-content',
+      }),
+    };
+  }
 
   componentDidMount = () => {
     Logger.ancillaryDisplayed(Logger.Type.ANCILLARY_STEP_DETAILS);
-    if (Platform.OS === 'ios' && this.isNarrowLayout()) {
-      StatusBar.setBarStyle('light-content');
-    }
   };
 
   shouldComponentUpdate = (nextProps: PropsWithContext, nextState: State) => {
@@ -72,24 +79,29 @@ export class HotelDetailScreen extends React.Component<
     return !isPropsEqual || !isStateEqual;
   };
 
-  componentWillUnmount = () => {
-    if (Platform.OS === 'ios' && this.isNarrowLayout()) {
-      StatusBar.setBarStyle('dark-content');
-    }
-  };
-
   isNarrowLayout = () => {
     return this.props.width < Device.DEVICE_THRESHOLD;
   };
 
+  shouldSetStatusBarDark = (yOffset: number) =>
+    Platform.OS === 'ios' &&
+    this.isNarrowLayout() &&
+    yOffset > 180 &&
+    this.state.barStyle !== 'dark-content';
+
+  shouldSetStatusBarLight = (yOffset: number) =>
+    Platform.OS === 'ios' &&
+    this.isNarrowLayout() &&
+    yOffset <= 180 &&
+    this.state.barStyle !== 'light-content';
+
   onScroll = (e: NativeEvent) => {
-    const isIOS = Platform.OS === 'ios';
-    const isNarrow = this.isNarrowLayout();
-    const hasScrolledPastImage = e.nativeEvent.contentOffset.y > 180;
-    if (isIOS && isNarrow && hasScrolledPastImage) {
-      StatusBar.setBarStyle('dark-content');
-    } else if (isIOS && isNarrow && !hasScrolledPastImage) {
-      StatusBar.setBarStyle('light-content');
+    const yOffset = e.nativeEvent.contentOffset.y;
+
+    if (this.shouldSetStatusBarDark(yOffset)) {
+      this.setState({ barStyle: 'dark-content' });
+    } else if (this.shouldSetStatusBarLight(yOffset)) {
+      this.setState({ barStyle: 'light-content' });
     }
   };
 
@@ -163,7 +175,7 @@ export class HotelDetailScreen extends React.Component<
             onScroll={this.onScroll}
             scrollEventThrottle={500}
           >
-            <LayoutSingleColumn>
+            <LayoutSingleColumn barStyle={this.state.barStyle}>
               <Header hotel={availableHotel.hotel} />
               <HotelInformation hotel={availableHotel.hotel} />
               <RoomList
diff --git a/packages/shared/index.js b/packages/shared/index.js
index fc4a5e6f..03f8763c 100644
--- a/packages/shared/index.js
+++ b/packages/shared/index.js
@@ -140,3 +140,5 @@ export type {
 export type { CancellablePromiseType } from './src/CancellablePromise';
 
 export type { DimensionType } from './types/Objects';
+
+export type { BarStyle } from './src/view/Layout';
diff --git a/packages/shared/src/errors/GeneralError.js b/packages/shared/src/errors/GeneralError.js
index fe5c4a75..d675cc5e 100644
--- a/packages/shared/src/errors/GeneralError.js
+++ b/packages/shared/src/errors/GeneralError.js
@@ -3,6 +3,7 @@
 import * as React from 'react';
 import { View } from 'react-native';
 import type { TranslationType } from '@kiwicom/mobile-localization';
+import { defaultTokens } from '@kiwicom/mobile-orbit';
 
 import Text from '../Text';
 import StyleSheet from '../PlatformStyleSheet';
@@ -26,6 +27,7 @@ const styleSheet = StyleSheet.create({
     justifyContent: 'center',
     alignItems: 'center',
     padding: 20,
+    backgroundColor: defaultTokens.paletteWhite,
   },
   text: {
     textAlign: 'center',
diff --git a/packages/shared/src/image/NetworkImage.js b/packages/shared/src/image/NetworkImage.js
index 8ace34a6..9a134c98 100644
--- a/packages/shared/src/image/NetworkImage.js
+++ b/packages/shared/src/image/NetworkImage.js
@@ -17,6 +17,7 @@ type Props = {|
   +resizeMode?:
     | 'cover' // scales the image uniformly - image will be equal or larger
     | 'contain', // scales the image uniformly - image will be equal or smaller
+  +indicator?: React.ElementType,
 |};
 
 /**
@@ -33,7 +34,6 @@ export const NetworkImage = function NetworkImage(imageProps: Props) {
 
   const newProps = {
     ...imageProps,
-    indicator: IconLoading,
     style: [
       imageProps.style,
       {
@@ -44,6 +44,10 @@ export const NetworkImage = function NetworkImage(imageProps: Props) {
   return <Image {...newProps} />;
 };
 
+NetworkImage.defaultProps = {
+  indicator: IconLoading,
+};
+
 export default function NetworkImageWithContext(props: Props) {
   return (
     <ConfigContext.Consumer>
diff --git a/packages/shared/src/popup/Popup.js b/packages/shared/src/popup/Popup.js
index 1b185679..53ece06b 100644
--- a/packages/shared/src/popup/Popup.js
+++ b/packages/shared/src/popup/Popup.js
@@ -16,32 +16,13 @@ type Props = {|
 |};
 
 export default class Popup extends React.Component<Props> {
-  scrollView: ?React$ElementRef<ScrollView>;
-
-  constructor() {
-    super();
-
-    this.scrollView = null;
-  }
-
   onClose = () => this.props.onClose();
 
-  scrollToEnd = () => {
-    if (this.scrollView) {
-      this.scrollView.scrollToEnd({ animated: true });
-    }
-  };
-
-  saveRef = (ref: React$ElementRef<ScrollView>): void =>
-    (this.scrollView = ref);
-
   render = () => {
     const modalChild = (
       <ScrollView
         contentContainerStyle={styles.content}
         alwaysBounceVertical={false}
-        ref={this.saveRef}
-        onContentSizeChange={this.scrollToEnd}
       >
         <SafeAreaView style={styles.safeArea}>
           {this.props.children}
diff --git a/packages/shared/src/view/Layout.js b/packages/shared/src/view/Layout.js
index 4ee3e587..9831addc 100644
--- a/packages/shared/src/view/Layout.js
+++ b/packages/shared/src/view/Layout.js
@@ -5,8 +5,11 @@ import { StatusBar } from 'react-native';
 
 import Color from '../Color';
 
+export type BarStyle = 'default' | 'dark-content' | 'light-content';
+
 type Props = {|
   +children: React.Node,
+  +barStyle: BarStyle,
 |};
 
 export default function Layout(props: Props) {
@@ -14,10 +17,14 @@ export default function Layout(props: Props) {
     <React.Fragment>
       <StatusBar
         animated={true}
-        barStyle="default"
+        barStyle={props.barStyle}
         backgroundColor={Color.transparent.black.$30} // Android only
       />
       {props.children}
     </React.Fragment>
   );
 }
+
+Layout.defaultProps = {
+  barStyle: 'default',
+};
diff --git a/packages/shared/src/view/LayoutSingleColumn.js b/packages/shared/src/view/LayoutSingleColumn.js
index 204d4e48..946c9648 100644
--- a/packages/shared/src/view/LayoutSingleColumn.js
+++ b/packages/shared/src/view/LayoutSingleColumn.js
@@ -5,11 +5,12 @@ import { View } from 'react-native';
 
 import StyleSheet from '../PlatformStyleSheet';
 import Device from '../Device';
-import Layout from './Layout';
+import Layout, { type BarStyle } from './Layout';
 
 type Props = {|
   +children: React.Node,
   +testID?: string,
+  +barStyle?: BarStyle,
 |};
 
 /**
@@ -24,7 +25,7 @@ export default function LayoutSingleColumn(props: Props) {
   };
 
   return (
-    <Layout>
+    <Layout barStyle={props.barStyle}>
       <View style={styleSheet.wrapper}>
         <View style={innerStyle} testID={props.testID}>
           {props.children}
@@ -34,6 +35,10 @@ export default function LayoutSingleColumn(props: Props) {
   );
 }
 
+LayoutSingleColumn.defaultProps = {
+  barStyle: 'default',
+};
+
 const styleSheet = StyleSheet.create({
   wrapper: {
     flex: 1,


Test patch:
diff --git a/packages/shared/src/image/__tests__/__snapshots__/NetworkImage.test.js.snap b/packages/shared/src/image/__tests__/__snapshots__/NetworkImage.test.js.snap
index 99206c41..1bc96502 100644
--- a/packages/shared/src/image/__tests__/__snapshots__/NetworkImage.test.js.snap
+++ b/packages/shared/src/image/__tests__/__snapshots__/NetworkImage.test.js.snap
@@ -49,6 +49,7 @@ exports[`passes props to the original Image component 1`] = `
 
 exports[`returns MissingImage component for invalid URI 1`] = `
 <MissingImage
+  indicator={[Function]}
   source={
     Object {
       "uri": null,
@@ -59,6 +60,7 @@ exports[`returns MissingImage component for invalid URI 1`] = `
 
 exports[`returns MissingImage component for invalid URI 2`] = `
 <MissingImage
+  indicator={[Function]}
   source={
     Object {
       "uri": undefined,
@@ -70,6 +72,7 @@ exports[`returns MissingImage component for invalid URI 2`] = `
 exports[`returns MissingImage component when data saver enabled 1`] = `
 <MissingImage
   dataSaverEnabled={true}
+  indicator={[Function]}
   source={
     Object {
       "uri": "http://image.png",