From 67b7f741bc54da4004f7141da8ae120394ed9838 Mon Sep 17 00:00:00 2001 From: Rory Abraham Date: Thu, 5 Nov 2020 17:50:18 -0800 Subject: [PATCH 1/4] draft PR --- package-lock.json | 48 ++--------- package.json | 2 +- src/components/Tooltip/Triangle.js | 21 +++++ .../Tooltip/getTooltipCoordinates.js | 46 ++++++++++ src/components/Tooltip/index.js | 85 +++++++++++++++++++ src/components/Tooltip/styles.js | 60 +++++++++++++ src/pages/SignInPage.js | 10 +++ 7 files changed, 229 insertions(+), 43 deletions(-) create mode 100644 src/components/Tooltip/Triangle.js create mode 100644 src/components/Tooltip/getTooltipCoordinates.js create mode 100644 src/components/Tooltip/index.js create mode 100644 src/components/Tooltip/styles.js 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..62ac25ee6b53 --- /dev/null +++ b/src/components/Tooltip/getTooltipCoordinates.js @@ -0,0 +1,46 @@ +import {Dimensions} from 'react-native'; + +/** + * ~Tooltip coordinate system:~ + * The tooltip coordinates are based on the element which it is wrapping. + * We take the x and y coordinates of the element and divide the surroundings + * in four quadrants, and check for the one with biggest area. + * Once we know the quadrant with the biggest area, we can place the tooltip in that + * direction. + * + * To find the areas we first get 5 coordinate points: the center and the 4 extreme points. + * Once we know the coordinates, we can get the length of the sides which form each quadrant. + * + * @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} + */ +export default function (componentWidth, componentHeight, xOffset, yOffset) { + const windowWidth = Dimensions.get('window').width; + const windowHeight = Dimensions.get('window').height; + + const centerX = xOffset + (componentWidth / 2); + const centerY = yOffset + (componentHeight / 2); + + // Determine which cartesian quadrant our component's center is in + if (centerX < (windowWidth / 2)) { + if (centerY < (windowHeight / 2)) { + // Q2 + return [centerX - (componentWidth / 2), centerY + (componentHeight / 2)]; + } + + // Q3 + return [centerX - (componentWidth / 2), centerY - (componentHeight / 2)]; + } + + if (centerY < (windowHeight / 2)) { + // Q1 + return [centerX + (componentWidth / 2), centerY + (componentHeight / 2)]; + } + + // Q4 + return [centerX + (componentWidth / 2), centerY - (componentHeight / 2)]; +} diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js new file mode 100644 index 000000000000..96d0e06d187d --- /dev/null +++ b/src/components/Tooltip/index.js @@ -0,0 +1,85 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Modal, 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); + + // 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; + } + + getPosition() { + this.renderedContent.measureInWindow((x, y, width, height) => { + this.xOffset = x; + this.yOffset = y; + this.wrappedElementWidth = width; + this.wrappedElementHeight = height; + }); + } + + 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, + ); + + return ( + (this.renderedContent = e)} + onLayout={e => (this.getPosition(e))} + collapsable={false} + > + + {({hovered}) => ( + + {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..1a89a9cb5045 --- /dev/null +++ b/src/components/Tooltip/styles.js @@ -0,0 +1,60 @@ +import gStyles, {colors} from '../../styles/StyleSheet'; +import getTooltipCoordinates from './getTooltipCoordinates'; + +const backgroundColor = `${colors.heading}cc`; + +function getTooltipStyle(elementWidth, elementHeight, xOffset, yOffset) { + const [x, y] = getTooltipCoordinates(elementWidth, elementHeight, xOffset, yOffset); + return { + position: 'absolute', + left: x, + top: y, + width: 40, + height: 150, + color: colors.textReversed, + backgroundColor, + display: 'flex', + flex: 1, + justifyContent: 'center', + alignItems: 'center', + borderRadius: 10, + padding: 10, + }; +} + +function getPointerStyle(elementWidth, elementHeight, xOffset, yOffset, tooltipY) { + const shouldPointDown = yOffset > tooltipY; + return { + pointerWrapperViewStyle: { + position: 'absolute', + top: shouldPointDown ? yOffset - 13 : (yOffset + elementHeight) - 2, + left: ((xOffset + 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: backgroundColor, + }, + down: { + transform: [{rotate: '180deg'}], + }, +}; diff --git a/src/pages/SignInPage.js b/src/pages/SignInPage.js index a06d1ddcfa3e..66083c6dad29 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,13 @@ class App extends Component { )} + +