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 { )} + +