diff --git a/.eslintrc.js b/.eslintrc.js
index 630a8f04f884..a293c86a15f2 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -3,6 +3,7 @@ module.exports = {
parser: 'babel-eslint',
rules: {
'react/jsx-filename-extension': [1, {extensions: ['.js']}],
+ 'react/prop-types': [2, {ignore: ['children']}],
},
env: {
jest: true
diff --git a/package-lock.json b/package-lock.json
index 7217e032fe2e..1a6af9a821e7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5060,42 +5060,12 @@
}
},
"create-react-class": {
- "version": "15.6.3",
- "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz",
- "integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==",
+ "version": "15.7.0",
+ "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz",
+ "integrity": "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==",
"requires": {
- "fbjs": "^0.8.9",
"loose-envify": "^1.3.1",
"object-assign": "^4.1.1"
- },
- "dependencies": {
- "core-js": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
- "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
- },
- "fbjs": {
- "version": "0.8.17",
- "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
- "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
- "requires": {
- "core-js": "^1.0.0",
- "isomorphic-fetch": "^2.1.1",
- "loose-envify": "^1.0.0",
- "object-assign": "^4.1.0",
- "promise": "^7.1.1",
- "setimmediate": "^1.0.5",
- "ua-parser-js": "^0.7.18"
- }
- },
- "promise": {
- "version": "7.3.1",
- "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
- "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
- "requires": {
- "asap": "~2.0.3"
- }
- }
}
},
"crocket": {
@@ -5350,11 +5320,6 @@
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.9.1.tgz",
"integrity": "sha512-01NCTBg8cuMJG1OQc6PR7T66+AFYiPwgDvdJmvJBn29NGzIG+DIFxPLNjHzwz3cpFIvG+NcwIjP9hSaPVoOaDg=="
},
- "debounce": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
- "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
- },
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -14950,13 +14915,12 @@
}
},
"react-native-web": {
- "version": "0.13.5",
- "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.13.5.tgz",
- "integrity": "sha512-Z4hiqKZ0bFFVMlsc/6gQMkIiGYPrY8bH6SFpLrLljDGLousNZjD+H4hV7QQz72smo8786994UJEBC0bU+0kAMw==",
+ "version": "0.14.7",
+ "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.14.7.tgz",
+ "integrity": "sha512-C0RTq/FKKJJfXVBgz4q3cwtnQb/yCDAuTZXxm8gzXtFkDDgwfGQ8NSXfGFM54VysUeHMtrzhgaEQcsQzdr9kqA==",
"requires": {
"array-find-index": "^1.0.2",
"create-react-class": "^15.6.2",
- "debounce": "^1.2.0",
"deep-assign": "^3.0.0",
"fbjs": "^1.0.0",
"hyphenate-style-name": "^1.0.3",
diff --git a/package.json b/package.json
index a994c05ed942..591e7334b58a 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
"react-native-keyboard-spacer": "^0.4.1",
"react-native-render-html": "^4.2.3",
"react-native-safe-area-context": "^3.1.4",
- "react-native-web": "^0.13.5",
+ "react-native-web": "^0.14.7",
"react-native-webview": "^10.6.0",
"react-router-dom": "^5.2.0",
"react-router-native": "^5.2.0",
diff --git a/src/components/Tooltip/Triangle.js b/src/components/Tooltip/Triangle.js
new file mode 100644
index 000000000000..698161f3004c
--- /dev/null
+++ b/src/components/Tooltip/Triangle.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {View} from 'react-native';
+import styles from './styles';
+
+const propTypes = {
+ isPointingDown: PropTypes.bool
+};
+
+const defaultProps = {
+ isPointingDown: false
+};
+
+const Triangle = ({isPointingDown}) => (
+
+);
+
+Triangle.propTypes = propTypes;
+Triangle.defaultProps = defaultProps;
+
+export default Triangle;
diff --git a/src/components/Tooltip/getTooltipCoordinates.js b/src/components/Tooltip/getTooltipCoordinates.js
new file mode 100644
index 000000000000..b37a8f8f8255
--- /dev/null
+++ b/src/components/Tooltip/getTooltipCoordinates.js
@@ -0,0 +1,48 @@
+import {Dimensions} from 'react-native';
+
+/**
+ * ~Tooltip coordinate system:~
+ * The tooltip coordinates are based on the element which it is wrapping.
+ * If the element is too far to the left or right of the page, then we want the tooltip to
+ * be positioned on its right or left (respectively).
+ * If the element is on the top half of the page, we'll have the tooltip below it, and if
+ * the element is on the bottom half of the page, we'll have the tooltip above it.
+ *
+ * Currently we're using the value, 10, as a buffer distance from the tooltip to the element.
+ *
+ * @param {number} componentWidth
+ * @param {number} componentHeight
+ * @param {number} xOffset The distance between the left side of the screen and the left side of the component.
+ * @param {number} yOffset The distance between the top of the screen and the top of the component.
+ *
+ * @returns {object} Returns coordinates for the top left corner of the tooltip RELATIVE to the element being hovered.
+ */
+export default function (componentWidth, componentHeight, xOffset, yOffset) {
+ const windowWidth = Dimensions.get('screen').width;
+ const windowHeight = Dimensions.get('screen').height;
+
+
+ const centerX = (componentWidth / 2);
+ const centerY = (componentHeight / 2);
+
+ let gutter = '';
+ let inBottom = yOffset > (windowHeight / 2);
+ let tooltipX = centerX;
+ let tooltipY = inBottom ? componentHeight + 15 : 15;
+
+ // Determine if we're in a danger gutter
+ // Left Gutter
+ if(centerX < 75 ) {
+ // letting 10 be the buffer distance between the element and the tooltip
+ gutter = 'left';
+ tooltipX = componentWidth + 3;
+ }
+ // Right Gutter
+ if (windowWidth - centerX < 75) {
+ gutter = 'right';
+ tooltipX = - 3;
+ }
+
+ console.log(`tooltipX: ${tooltipX}, tooltipY: ${tooltipY}, gutter: ${gutter}, inBottom: ${inBottom}`);
+ return [tooltipX, tooltipY, gutter, inBottom];
+}
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
new file mode 100644
index 000000000000..d0710d617cfd
--- /dev/null
+++ b/src/components/Tooltip/index.js
@@ -0,0 +1,105 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ Animated, Pressable, Text, View
+} from 'react-native';
+
+import Triangle from './Triangle';
+import styles from './styles';
+
+const propTypes = {
+ textContent: PropTypes.string.isRequired,
+};
+
+class Tooltip extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ tooltipShown: false,
+ isAnimating: false,
+ };
+
+ this.animation = new Animated.Value(0);
+
+ // The outermost view rendered by this component
+ this.renderedContent = null;
+ this.wrappedElementWidth = 0;
+ this.wrappedElementHeight = 0;
+
+ // The distance between the left side of the rendered view and the left side of the window
+ this.xOffset = 0;
+
+ // The distance between the top of the rendered view and the top of the window
+ this.yOffset = 0;
+
+ this.getPosition = this.getPosition.bind(this);
+ this.toggleTooltip = this.toggleTooltip.bind(this);
+ }
+
+ getPosition() {
+ this.renderedContent.measureInWindow((x, y, width, height) => {
+ this.xOffset = x;
+ this.yOffset = y;
+ this.wrappedElementWidth = width;
+ this.wrappedElementHeight = height;
+ });
+ }
+
+ toggleTooltip() {
+ Animated.timing(this.animation, {
+ toValue: this.state.tooltipShown ? 0 : 1,
+ duration: 200,
+ }).start(() => {
+ this.setState(prevState => ({tooltipShown: !prevState.tooltipShown}));
+ });
+ }
+
+ render() {
+ const toolTipStyle = styles.getTooltipStyle(
+ this.wrappedElementWidth,
+ this.wrappedElementHeight,
+ this.xOffset,
+ this.yOffset,
+ );
+ const {pointerWrapperViewStyle, shouldPointDown} = styles.getPointerStyle(
+ this.wrappedElementWidth,
+ this.wrappedElementHeight,
+ this.xOffset,
+ this.yOffset,
+ toolTipStyle.top,
+ );
+ const interpolatedSize = this.animation.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, 1],
+ });
+
+ return (
+ (this.renderedContent = e)}
+ onLayout={e => (this.getPosition(e))}
+ collapsable={false}
+ >
+
+ {({hovered}) => {
+ if (!this.state.isAnimating && this.state.tooltipShown !== hovered) {
+ this.toggleTooltip();
+ }
+ return (this.props.children);
+ }}
+
+
+
+
+
+
+ {this.props.textContent}
+
+
+
+ );
+ }
+}
+
+Tooltip.propTypes = propTypes;
+export default Tooltip;
diff --git a/src/components/Tooltip/styles.js b/src/components/Tooltip/styles.js
new file mode 100644
index 000000000000..33e358eeaefc
--- /dev/null
+++ b/src/components/Tooltip/styles.js
@@ -0,0 +1,75 @@
+import gStyles, {colors} from '../../styles/StyleSheet';
+import getTooltipCoordinates from './getTooltipCoordinates';
+import {Dimensions} from 'react-native';
+
+const backgroundColor = `${colors.heading}cc`;
+
+function getTooltipStyle(elementWidth, elementHeight, xOffset, yOffset) {
+ const [x, y, gutter, inBottom] = getTooltipCoordinates(elementWidth, elementHeight, xOffset, yOffset);
+ const styles = {
+ position: 'absolute',
+ width: 150,
+ height: 60,
+ left: x,
+ color: colors.textReversed,
+ backgroundColor: colors.red,
+ display: 'flex',
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderRadius: 10,
+ padding: 10,
+ };
+ if (!gutter) {
+ // If we are NOT in a gutter, we will want to center the tooltip on the element
+ styles.transform = [{translateX: -75}];
+ }
+ if (inBottom) {
+ // If we're on the bottom of the page, we'll want to use the bottom attribute rather than the top
+ // this allows us to position the tooltip without actually knowing how large it is.
+
+ styles.bottom = y;
+ } else {
+ styles.top = y;
+ }
+ return styles;
+}
+
+function getPointerStyle(elementWidth, elementHeight, xOffset, yOffset, tooltipY) {
+ const shouldPointDown = yOffset > Dimensions.get('screen').height / 2;
+ console.log(`yOffset ${yOffset}`);
+ console.log(`shouldPointDown ${shouldPointDown}`);
+ console.log(`Dimensions.get('screen').height ${Dimensions.get('screen').height}`);
+ return {
+ pointerWrapperViewStyle: {
+ position: 'absolute',
+ top: shouldPointDown ? -(elementHeight + 15) : 0,
+ left: ((elementWidth) / 2) - 7.5,
+ },
+ shouldPointDown,
+ };
+}
+
+export default {
+ getTooltipStyle,
+ getPointerStyle,
+ tooltipText: {
+ color: colors.text,
+ ...gStyles.textMicro,
+ },
+ triangle: {
+ width: 0,
+ height: 0,
+ backgroundColor: 'transparent',
+ borderStyle: 'solid',
+ borderLeftWidth: 8,
+ borderRightWidth: 8,
+ borderBottomWidth: 15,
+ borderLeftColor: 'transparent',
+ borderRightColor: 'transparent',
+ borderBottomColor: colors.red,
+ },
+ down: {
+ transform: [{rotate: '180deg'}],
+ },
+};
diff --git a/src/pages/SignInPage.js b/src/pages/SignInPage.js
index a06d1ddcfa3e..8b3038068d38 100644
--- a/src/pages/SignInPage.js
+++ b/src/pages/SignInPage.js
@@ -5,6 +5,7 @@ import {
StatusBar,
TouchableOpacity,
TextInput,
+ Button,
Image,
View,
ActivityIndicator,
@@ -21,6 +22,8 @@ import withIon from '../components/withIon';
import styles, {colors} from '../styles/StyleSheet';
import logo from '../../assets/images/expensify-logo_reversed.png';
+import Tooltip from '../components/Tooltip';
+
const propTypes = {
// These are from withRouter
// eslint-disable-next-line react/forbid-prop-types
@@ -147,6 +150,38 @@ class App extends Component {
)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
>