Repository: kiwicom/mobile (js)

Issue:
Modal close button should be an "X" on Android
Check Location modal and Guests modal.

Pull request:
Refactor Header buttons to make them consistent on Android and iOS
Fixes #913.

- [x] Fix Location and Guests modals
- [x] Remove `HeaderButton` duplication from `shared` and `navigation`

Before:

![from](https://user-images.githubusercontent.com/774577/43212586-a6b43a3e-9034-11e8-9901-295490725e0c.png)

After:

![to](https://user-images.githubusercontent.com/774577/43212595-aaa0cf0e-9034-11e8-87f4-3506d38e4ba7.png)


Patch:
diff --git a/app/MMB/src/components/ShareButton.js b/app/MMB/src/components/ShareButton.js
index 41075cb2..55443999 100644
--- a/app/MMB/src/components/ShareButton.js
+++ b/app/MMB/src/components/ShareButton.js
@@ -2,12 +2,8 @@
 
 import * as React from 'react';
 import { Platform } from 'react-native';
-import {
-  TextIcon,
-  HeaderButton,
-  Color,
-  StyleSheet,
-} from '@kiwicom/mobile-shared';
+import { TextIcon, Color, StyleSheet } from '@kiwicom/mobile-shared';
+import { HeaderButton } from '@kiwicom/mobile-navigation';
 
 export default function ShareButton({ onPress }: {| +onPress: () => void |}) {
   return (
diff --git a/app/MMB/src/navigation/TravelDocumentModalScreen.js b/app/MMB/src/navigation/TravelDocumentModalScreen.js
index 20fc8e95..49d89ba7 100644
--- a/app/MMB/src/navigation/TravelDocumentModalScreen.js
+++ b/app/MMB/src/navigation/TravelDocumentModalScreen.js
@@ -4,8 +4,8 @@ import * as React from 'react';
 import { View } from 'react-native';
 import {
   type NavigationType,
-  HeaderTitle,
   HeaderButton,
+  HeaderTitle,
 } from '@kiwicom/mobile-navigation';
 import { Translation, DateFormatter } from '@kiwicom/mobile-localization';
 import { StyleSheet, Color, IconLoading } from '@kiwicom/mobile-shared';
@@ -48,23 +48,21 @@ export class TravelDocumentModalScreen extends React.Component<Props, State> {
     }
     const onSave = navigation.state.params.onSave || noop;
     return {
-      headerLeft: (
-        <HeaderButton onPress={goBack}>
-          <Translation id="mmb.missing_informaiton.travel_document_modal_screen.close" />
-        </HeaderButton>
-      ),
+      headerLeft: <HeaderButton.CloseModal onPress={goBack} />,
       headerTitle: (
         <HeaderTitle>
           <Translation id="mmb.fill_travel_document_screen_modal.title" />
         </HeaderTitle>
       ),
       headerRight: (
-        <HeaderButton
-          onPress={onSave}
+        <HeaderButton.Right
           disabled={navigation.state.params.disabled}
+          onPress={onSave}
         >
-          <Translation id="mmb.missing_informaiton.travel_document_modal_screen.save" />
-        </HeaderButton>
+          <HeaderButton.Text>
+            <Translation id="mmb.missing_informaiton.travel_document_modal_screen.save" />
+          </HeaderButton.Text>
+        </HeaderButton.Right>
       ),
       gesturesEnabled: false,
     };
diff --git a/app/hotels/src/navigation/allHotels/GuestsModalScreen.js b/app/hotels/src/navigation/allHotels/GuestsModalScreen.js
index cb31aa8b..0c5685e5 100644
--- a/app/hotels/src/navigation/allHotels/GuestsModalScreen.js
+++ b/app/hotels/src/navigation/allHotels/GuestsModalScreen.js
@@ -3,10 +3,9 @@
 import * as React from 'react';
 import {
   type NavigationType,
-  HeaderTitle,
   HeaderButton,
+  HeaderTitle,
 } from '@kiwicom/mobile-navigation';
-import { Text, StyleSheet } from '@kiwicom/mobile-shared';
 import { Translation } from '@kiwicom/mobile-localization';
 
 import HotelsSearchContext from '../../HotelsSearchContext';
@@ -20,12 +19,6 @@ import type {
 } from '../../allHotels/searchForm/guests/GuestsTypes';
 import GuestsModal from '../../allHotels/searchForm/guests/GuestsModal';
 
-const styles = StyleSheet.create({
-  saveButtonText: {
-    fontWeight: '600',
-  },
-});
-
 type PropsWithContext = {
   ...Props,
   guests: Guests,
@@ -137,22 +130,18 @@ export default class GuestsModalScreenWithContext extends React.Component<
     }
 
     return {
-      headerLeft: (
-        <HeaderButton onPress={goBack}>
-          <Translation id="hotels_search.guests_modal.close" />
-        </HeaderButton>
-      ),
+      headerLeft: <HeaderButton.CloseModal onPress={goBack} />,
       headerTitle: (
         <HeaderTitle>
           <Translation id="hotels_search.guests_modal.header" />
         </HeaderTitle>
       ),
       headerRight: (
-        <HeaderButton onPress={onSave}>
-          <Text style={styles.saveButtonText}>
+        <HeaderButton.Right onPress={onSave}>
+          <HeaderButton.Text>
             <Translation id="hotels_search.guests_modal.save" />
-          </Text>
-        </HeaderButton>
+          </HeaderButton.Text>
+        </HeaderButton.Right>
       ),
       gesturesEnabled: false,
     };
diff --git a/app/hotels/src/navigation/allHotels/LocationPickerScreen.js b/app/hotels/src/navigation/allHotels/LocationPickerScreen.js
index a8935237..1bf917ff 100644
--- a/app/hotels/src/navigation/allHotels/LocationPickerScreen.js
+++ b/app/hotels/src/navigation/allHotels/LocationPickerScreen.js
@@ -1,14 +1,13 @@
 // @flow
 
 import * as React from 'react';
-import { Text, StyleSheet } from '@kiwicom/mobile-shared';
+import idx from 'idx';
 import {
   type NavigationType,
-  HeaderTitle,
   HeaderButton,
+  HeaderTitle,
 } from '@kiwicom/mobile-navigation';
 import { Translation } from '@kiwicom/mobile-localization';
-import idx from 'idx';
 
 import LocationPicker from '../../allHotels/searchForm/locationPicker/LocationPicker';
 
@@ -27,14 +26,6 @@ type NavigationProps = {|
   navigation: NavigationType,
 |};
 
-const styles = StyleSheet.create({
-  confirmButtonText: {
-    android: {
-      fontWeight: '600',
-    },
-  },
-});
-
 export default class LocationPickerScreen extends React.Component<Props> {
   static navigationOptions = ({ navigation }: NavigationProps) => {
     function goBack() {
@@ -43,11 +34,10 @@ export default class LocationPickerScreen extends React.Component<Props> {
 
     return {
       headerLeft: (
-        <HeaderButton onPress={goBack}>
-          <Text style={styles.confirmButtonText}>
-            <Translation id="hotels_search.location_picker.cancel" />
-          </Text>
-        </HeaderButton>
+        <HeaderButton.CloseModal
+          onPress={goBack}
+          text={<Translation id="hotels_search.location_picker.cancel" />}
+        />
       ),
       headerTitle: (
         <HeaderTitle>
diff --git a/app/hotels/src/navigation/allHotels/MapHeaderButton.android.js b/app/hotels/src/navigation/allHotels/MapHeaderButton.android.js
index 0b02915d..5286df84 100644
--- a/app/hotels/src/navigation/allHotels/MapHeaderButton.android.js
+++ b/app/hotels/src/navigation/allHotels/MapHeaderButton.android.js
@@ -1,7 +1,8 @@
 // @flow
 
 import * as React from 'react';
-import { HeaderButton, Color, Icon } from '@kiwicom/mobile-shared';
+import { HeaderButton } from '@kiwicom/mobile-navigation';
+import { Color, Icon } from '@kiwicom/mobile-shared';
 import RNTooltips from 'react-native-tooltips';
 
 type Props = {|
diff --git a/app/hotels/src/navigation/allHotels/MapHeaderButton.ios.js b/app/hotels/src/navigation/allHotels/MapHeaderButton.ios.js
index f93df20e..5a9eb27f 100644
--- a/app/hotels/src/navigation/allHotels/MapHeaderButton.ios.js
+++ b/app/hotels/src/navigation/allHotels/MapHeaderButton.ios.js
@@ -1,7 +1,8 @@
 // @flow
 
 import * as React from 'react';
-import { HeaderButton, Icon, Color } from '@kiwicom/mobile-shared';
+import { HeaderButton } from '@kiwicom/mobile-navigation';
+import { Icon, Color } from '@kiwicom/mobile-shared';
 
 type Props = {|
   +onPress: () => void,
diff --git a/packages/localization/index.js b/packages/localization/index.js
index a77f6f19..dde2a78b 100644
--- a/packages/localization/index.js
+++ b/packages/localization/index.js
@@ -7,6 +7,7 @@ import TranslationFragment from './src/TranslationFragment';
 import DateFormatter from './src/DateFormatter';
 import DateUtils from './src/DateUtils';
 import DeviceInfo from './src/GetDeviceLocale';
+import type { TranslationKeys } from './src/DefaultVocabulary';
 
 export {
   TranslationFragment,
@@ -16,6 +17,8 @@ export {
   Translation,
 };
 
+export type TranslationKeysType = TranslationKeys;
+
 export type DateFormatterFunctions =
   | 'formatToDate'
   | 'formatToTime'
diff --git a/packages/navigation/index.js b/packages/navigation/index.js
index 75063d56..62f16988 100644
--- a/packages/navigation/index.js
+++ b/packages/navigation/index.js
@@ -6,14 +6,13 @@ import ReactNavigation from 'react-navigation';
 import {
   Color,
   Device,
-  Text,
   StyleSheet,
-  Touchable,
   Dimensions,
   type StylePropType,
   type DimensionType,
 } from '@kiwicom/mobile-shared';
-import type { TranslationType } from '@kiwicom/mobile-localization';
+
+import HeaderTitle from './src/HeaderTitle';
 
 export { default as MenuGroup } from './src/MenuGroup';
 export { default as MenuGroupTitle } from './src/MenuGroupTitle';
@@ -21,6 +20,8 @@ export { default as MenuItem } from './src/MenuItem';
 export { default as TodoMenuItem } from './src/TodoMenuItem';
 export { SeparatorTrimmed, SeparatorFullWidth } from './src/Separators';
 export { default as TitledMenuGroup } from './src/TitledMenuGroup';
+export { default as HeaderTitle } from './src/HeaderTitle';
+export { default as HeaderButton } from './src/HeaderButton';
 export { withNavigation } from 'react-navigation';
 
 const createNavigationOptions = () => {
@@ -50,65 +51,6 @@ const createNavigationOptions = () => {
   return navigationOptions;
 };
 
-export const HeaderTitle = ({ children }: {| children: TranslationType |}) => {
-  return <Text style={styleSheet.title}>{children}</Text>;
-};
-
-export const HeaderButton = ({
-  children,
-  onPress,
-  disabled,
-}: {|
-  +children: React.Element<any>,
-  +onPress: () => void,
-  +disabled: boolean,
-|}) => {
-  return (
-    <Touchable
-      borderlessRipple={true}
-      onPress={onPress}
-      style={styleSheet.headerButton}
-      disabled={disabled}
-    >
-      <Text
-        style={[
-          styleSheet.headerButtonText,
-          disabled ? styleSheet.disabledButton : null,
-        ]}
-      >
-        {children}
-      </Text>
-    </Touchable>
-  );
-};
-
-HeaderButton.defaultProps = {
-  disabled: false,
-};
-
-const styleSheet = StyleSheet.create({
-  title: {
-    color: Color.textDark,
-    fontWeight: '600',
-    android: {
-      fontSize: 18,
-    },
-    ios: {
-      fontSize: 17,
-    },
-  },
-  headerButton: {
-    padding: 8,
-  },
-  headerButtonText: {
-    color: Color.brand,
-    fontSize: 17,
-  },
-  disabledButton: {
-    color: Color.inputBackground,
-  },
-});
-
 export const StackNavigator = (
   RouteConfigs: {
     [string]: {|
diff --git a/packages/navigation/src/HeaderButton.js b/packages/navigation/src/HeaderButton.js
new file mode 100644
index 00000000..4cf41946
--- /dev/null
+++ b/packages/navigation/src/HeaderButton.js
@@ -0,0 +1,165 @@
+// @flow
+
+import * as React from 'react';
+import { Platform, View } from 'react-native';
+import {
+  Touchable,
+  Color,
+  StyleSheet,
+  Icon,
+  Text,
+  type StylePropType,
+} from '@kiwicom/mobile-shared';
+import {
+  type TranslationType,
+  Translation,
+} from '@kiwicom/mobile-localization';
+
+type Props = {|
+  +onPress: () => void,
+  +onLongPress?: (React.ElementRef<typeof Touchable>) => void,
+  +children:
+    | React.Element<typeof HeaderButton.Text>
+    | React.Element<typeof Icon>,
+  +left?: boolean,
+  +style?: StylePropType,
+  +disabled?: boolean,
+|};
+
+type State = {|
+  reference: React.ElementRef<typeof Touchable> | null,
+|};
+
+type CloseModalProps = {|
+  +onPress: () => void,
+  +text?: TranslationType,
+|};
+
+export default class HeaderButton extends React.PureComponent<Props, State> {
+  static Right = (props: Props) => (
+    <HeaderButton {...props} style={[props.style, styles.right]} />
+  );
+
+  static Left = (props: Props) => (
+    <HeaderButton {...props} style={[props.style, styles.left]} />
+  );
+
+  static Text = ({
+    children,
+    disabled,
+  }: {|
+    +children: TranslationType,
+    +disabled?: boolean,
+  |}) => (
+    <Text
+      style={[styles.headerButtonText, disabled ? styles.disabledButton : null]}
+    >
+      {Platform.OS === 'android'
+        ? React.cloneElement(children, {
+            textTransform: 'uppercase',
+          })
+        : children}
+    </Text>
+  );
+
+  static CloseModal = ({ onPress, text }: CloseModalProps) => (
+    <HeaderButton.Left onPress={onPress}>
+      {Platform.OS === 'android' ? (
+        <Icon name="close" size={24} color={Color.brand} />
+      ) : (
+        <HeaderButton.Text>
+          {text || <Translation id="shared.button.close" />}
+        </HeaderButton.Text>
+      )}
+    </HeaderButton.Left>
+  );
+
+  static defaultProps = {
+    disabled: false,
+  };
+
+  state = {
+    reference: null,
+  };
+
+  storeReference = (reference: React.ElementRef<typeof Touchable> | null) => {
+    this.setState({ reference });
+  };
+
+  onLongPress = () => {
+    if (this.state.reference !== null && this.props.onLongPress !== undefined) {
+      this.props.onLongPress(this.state.reference);
+    }
+  };
+
+  render = () => {
+    const { disabled, onPress, style } = this.props;
+
+    return (
+      <Touchable
+        accessibilityComponentType="button"
+        accessibilityTraits="button"
+        delayPressIn={0}
+        onPress={onPress}
+        style={styles.container}
+        borderlessRipple={true}
+        onLongPress={this.onLongPress}
+        ref={this.storeReference}
+        disabled={disabled}
+      >
+        <View style={styles.container}>
+          <View style={[styles.innerContainer, style]}>
+            {this.props.children.type === HeaderButton.Text
+              ? // $FlowExpectedError: flow is having a hard time here
+                React.cloneElement(this.props.children, { disabled })
+              : this.props.children}
+          </View>
+        </View>
+      </Touchable>
+    );
+  };
+}
+
+const styles = StyleSheet.create({
+  container: {
+    alignItems: 'center',
+    flexDirection: 'row',
+    backgroundColor: 'transparent',
+  },
+  innerContainer: {
+    ios: {
+      paddingVertical: 8,
+    },
+    android: {
+      margin: 16,
+    },
+  },
+  right: {
+    ios: {
+      marginStart: 22,
+      marginEnd: 10,
+    },
+  },
+  left: {
+    ios: {
+      marginStart: 10,
+      marginEnd: 22,
+    },
+  },
+  headerButtonText: {
+    color: Color.brand,
+    ios: {
+      fontSize: 17,
+    },
+    android: {
+      fontWeight: '600',
+    },
+  },
+  disabledButton: {
+    color: Color.inputBackground,
+  },
+});
+
+HeaderButton.Left.displayName = 'HeaderButton.Left';
+HeaderButton.Right.displayName = 'HeaderButton.Right';
+HeaderButton.Text.displayName = 'HeaderButton.Text';
diff --git a/packages/navigation/src/HeaderTitle.js b/packages/navigation/src/HeaderTitle.js
new file mode 100644
index 00000000..be7e16f4
--- /dev/null
+++ b/packages/navigation/src/HeaderTitle.js
@@ -0,0 +1,25 @@
+// @flow
+
+import * as React from 'react';
+import { Color, StyleSheet, Text } from '@kiwicom/mobile-shared';
+
+import type { TranslationType } from '../../localization';
+
+const HeaderTitle = ({ children }: {| children: TranslationType |}) => (
+  <Text style={styles.title}>{children}</Text>
+);
+
+const styles = StyleSheet.create({
+  title: {
+    color: Color.textDark,
+    fontWeight: '600',
+    android: {
+      fontSize: 18,
+    },
+    ios: {
+      fontSize: 17,
+    },
+  },
+});
+
+export default HeaderTitle;
diff --git a/packages/shared/index.js b/packages/shared/index.js
index 23d9b48a..79ccf2dd 100644
--- a/packages/shared/index.js
+++ b/packages/shared/index.js
@@ -38,7 +38,6 @@ export { default as Modal } from './src/Modal';
 
 export { default as AdaptableBadge } from './src/AdaptableBadge';
 export { default as DropMarker } from './src/DropMarker';
-export { default as HeaderButton } from './src/HeaderButton';
 export { default as Price } from './src/Price';
 export { default as Stars } from './src/Stars';
 export { default as Text } from './src/Text';
diff --git a/packages/shared/src/HeaderButton.js b/packages/shared/src/HeaderButton.js
deleted file mode 100644
index 2f086c24..00000000
--- a/packages/shared/src/HeaderButton.js
+++ /dev/null
@@ -1,95 +0,0 @@
-// @flow
-
-import * as React from 'react';
-import { View } from 'react-native';
-import { type StylePropType } from '@kiwicom/mobile-shared';
-
-import StyleSheet from './PlatformStyleSheet';
-import Touchable from './Touchable';
-
-type Props = {|
-  +onPress: () => void,
-  +onLongPress?: (React.ElementRef<typeof Touchable>) => void,
-  +children?: React.Node,
-  +left?: boolean,
-  +style?: StylePropType,
-|};
-
-type State = {|
-  reference: React.ElementRef<typeof Touchable> | null,
-|};
-
-export default class HeaderButton extends React.PureComponent<Props, State> {
-  static Right = (props: Props) => (
-    <HeaderButton {...props} style={[props.style, styles.right]} />
-  );
-
-  static Left = (props: Props) => (
-    <HeaderButton {...props} style={[props.style, styles.left]} />
-  );
-
-  state = {
-    reference: null,
-  };
-
-  storeReference = (reference: React.ElementRef<typeof Touchable> | null) => {
-    this.setState({ reference });
-  };
-
-  onLongPress = () => {
-    if (this.state.reference !== null && this.props.onLongPress !== undefined) {
-      this.props.onLongPress(this.state.reference);
-    }
-  };
-
-  render = () => {
-    const { onPress, style } = this.props;
-
-    return (
-      <Touchable
-        accessibilityComponentType="button"
-        accessibilityTraits="button"
-        delayPressIn={0}
-        onPress={onPress}
-        style={styles.container}
-        borderlessRipple={true}
-        onLongPress={this.onLongPress}
-        ref={this.storeReference}
-      >
-        <View style={styles.container}>
-          <View style={[styles.innerContainer, style]}>
-            {this.props.children}
-          </View>
-        </View>
-      </Touchable>
-    );
-  };
-}
-
-const styles = StyleSheet.create({
-  container: {
-    alignItems: 'center',
-    flexDirection: 'row',
-    backgroundColor: 'transparent',
-  },
-  innerContainer: {
-    ios: {
-      paddingVertical: 12,
-    },
-    android: {
-      margin: 16,
-    },
-  },
-  right: {
-    ios: {
-      marginStart: 22,
-      marginEnd: 10,
-    },
-  },
-  left: {
-    ios: {
-      marginStart: 10,
-      marginEnd: 22,
-    },
-  },
-});
diff --git a/packages/shared/src/WithStandaloneScreen.js b/packages/shared/src/WithStandaloneScreen.js
index 0f467a38..13462942 100644
--- a/packages/shared/src/WithStandaloneScreen.js
+++ b/packages/shared/src/WithStandaloneScreen.js
@@ -3,14 +3,8 @@
 import * as React from 'react';
 import { HeaderBackButton } from 'react-navigation';
 import { Translation } from '@kiwicom/mobile-localization';
-import {
-  GestureController,
-  Color,
-  HeaderButton,
-  StyleSheet,
-  Text,
-} from '@kiwicom/mobile-shared';
-import { type NavigationType } from '@kiwicom/mobile-navigation';
+import { GestureController, Color } from '@kiwicom/mobile-shared';
+import { HeaderButton, type NavigationType } from '@kiwicom/mobile-navigation';
 
 type NavProps = {
   ...NavigationType,
@@ -32,9 +26,9 @@ function withStandaloneScreen<Props>(
         headerLeft:
           props.lastNavigationMode && props.lastNavigationMode === 'present' ? (
             <HeaderButton.Left onPress={closeNativeModal}>
-              <Text style={styles.closeButton}>
+              <HeaderButton.Text>
                 <Translation id="shared.button.close" />
-              </Text>
+              </HeaderButton.Text>
             </HeaderButton.Left>
           ) : (
             <HeaderBackButton
@@ -65,17 +59,4 @@ function withStandaloneScreen<Props>(
   };
 }
 
-const styles = StyleSheet.create({
-  closeButton: {
-    color: Color.brand,
-    fontWeight: '600',
-    android: {
-      fontSize: 18,
-    },
-    ios: {
-      fontSize: 17,
-    },
-  },
-});
-
 export default withStandaloneScreen;


Test patch:
diff --git a/app/hotels/src/navigation/allHotels/__tests__/__snapshots__/AllHotelsNavigationScreen.test.js.snap b/app/hotels/src/navigation/allHotels/__tests__/__snapshots__/AllHotelsNavigationScreen.test.js.snap
index 4527f632..e206957e 100644
--- a/app/hotels/src/navigation/allHotels/__tests__/__snapshots__/AllHotelsNavigationScreen.test.js.snap
+++ b/app/hotels/src/navigation/allHotels/__tests__/__snapshots__/AllHotelsNavigationScreen.test.js.snap
@@ -118,6 +118,7 @@ exports[`renders arrow icon button with \`goBack\` onPress function when lastNav
 
 exports[`renders close button with \`closeNativeModal\` onPress function when lastNavigationMode is \`present\` 1`] = `
 <HeaderButton
+  disabled={false}
   onPress={[Function]}
   style={
     Array [
@@ -129,18 +130,10 @@ exports[`renders close button with \`closeNativeModal\` onPress function when la
     ]
   }
 >
-  <Text
-    style={
-      Object {
-        "color": "#00a991",
-        "fontSize": 17,
-        "fontWeight": "600",
-      }
-    }
-  >
+  <HeaderButton.Text>
     <Translation
       id="shared.button.close"
     />
-  </Text>
+  </HeaderButton.Text>
 </HeaderButton>
 `;