diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js index 0454327de284..398df07649cf 100644 --- a/src/components/Tooltip/index.js +++ b/src/components/Tooltip/index.js @@ -1,121 +1,114 @@ import _ from 'underscore'; -import React, {PureComponent} from 'react'; +import React, {memo, useCallback, useEffect, useRef, useState} from 'react'; import {Animated} from 'react-native'; import {BoundsObserver} from '@react-ng/bounds-observer'; import TooltipRenderedOnPageBody from './TooltipRenderedOnPageBody'; import Hoverable from '../Hoverable'; -import withWindowDimensions from '../withWindowDimensions'; import * as tooltipPropTypes from './tooltipPropTypes'; import TooltipSense from './TooltipSense'; import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; -import compose from '../../libs/compose'; -import withLocalize from '../withLocalize'; +import usePrevious from '../../hooks/usePrevious'; +import useLocalize from '../../hooks/useLocalize'; +import useWindowDimensions from '../../hooks/useWindowDimensions'; + +const hasHoverSupport = DeviceCapabilities.hasHoverSupport(); /** * A component used to wrap an element intended for displaying a tooltip. The term "tooltip's target" refers to the * wrapped element, which, upon hover, triggers the tooltip to be shown. + * @param {propTypes} props + * @returns {ReactNodeLike} */ -class Tooltip extends PureComponent { - constructor(props) { - super(props); - - this.state = { - // Is tooltip already rendered on the page's body? This happens once. - isRendered: false, - - // Is the tooltip currently visible? - isVisible: false, - - // The distance between the left side of the wrapper view and the left side of the window - xOffset: 0, - - // The distance between the top of the wrapper view and the top of the window - yOffset: 0, - - // The width and height of the wrapper view - wrapperWidth: 0, - wrapperHeight: 0, - }; - - // Whether the tooltip is first tooltip to activate the TooltipSense - this.isTooltipSenseInitiator = false; - this.animation = new Animated.Value(0); - this.hasHoverSupport = DeviceCapabilities.hasHoverSupport(); - - this.showTooltip = this.showTooltip.bind(this); - this.hideTooltip = this.hideTooltip.bind(this); - this.updateBounds = this.updateBounds.bind(this); - this.isAnimationCanceled = React.createRef(false); - } - - // eslint-disable-next-line rulesdir/prefer-early-return - componentDidUpdate(prevProps) { - // if the tooltip text changed before the initial animation was finished, then the tooltip won't be shown - // we need to show the tooltip again - if (this.state.isVisible && this.isAnimationCanceled.current && this.props.text && prevProps.text !== this.props.text) { - this.isAnimationCanceled.current = false; - this.showTooltip(); - } - } - - /** - * Update the tooltip bounding rectangle - * - * @param {Object} bounds - updated bounds - */ - updateBounds(bounds) { - if (bounds.width === 0) { - this.setState({isRendered: false}); - } - this.setState({ - wrapperWidth: bounds.width, - wrapperHeight: bounds.height, - xOffset: bounds.x, - yOffset: bounds.y, - }); - } +function Tooltip(props) { + const {children, numberOfLines, maxWidth, text, renderTooltipContent, renderTooltipContentKey} = props; + + const {preferredLocale} = useLocalize(); + const {windowWidth} = useWindowDimensions(); + + // Is tooltip already rendered on the page's body? happens once. + const [isRendered, setIsRendered] = useState(false); + // Is the tooltip currently visible? + const [isVisible, setIsVisible] = useState(false); + // The distance between the left side of the wrapper view and the left side of the window + const [xOffset, setXOffset] = useState(0); + // The distance between the top of the wrapper view and the top of the window + const [yOffset, setYOffset] = useState(0); + // The width and height of the wrapper view + const [wrapperWidth, setWrapperWidth] = useState(0); + const [wrapperHeight, setWrapperHeight] = useState(0); + + // Whether the tooltip is first tooltip to activate the TooltipSense + const isTooltipSenseInitiator = useRef(false); + const animation = useRef(new Animated.Value(0)); + const isAnimationCanceled = useRef(false); + const prevText = usePrevious(text); /** * Display the tooltip in an animation. */ - showTooltip() { - if (!this.state.isRendered) { - this.setState({isRendered: true}); + const showTooltip = useCallback(() => { + if (!isRendered) { + setIsRendered(true); } - this.setState({isVisible: true}); + setIsVisible(true); - this.animation.stopAnimation(); + animation.current.stopAnimation(); // When TooltipSense is active, immediately show the tooltip if (TooltipSense.isActive()) { - this.animation.setValue(1); + animation.current.setValue(1); } else { - this.isTooltipSenseInitiator = true; - Animated.timing(this.animation, { + isTooltipSenseInitiator.current = true; + Animated.timing(animation.current, { toValue: 1, duration: 140, delay: 500, useNativeDriver: false, }).start(({finished}) => { - this.isAnimationCanceled.current = !finished; + isAnimationCanceled.current = !finished; }); } TooltipSense.activate(); - } + }, [isRendered]); + + // eslint-disable-next-line rulesdir/prefer-early-return + useEffect(() => { + // if the tooltip text changed before the initial animation was finished, then the tooltip won't be shown + // we need to show the tooltip again + if (isVisible && isAnimationCanceled.current && text && prevText !== text) { + isAnimationCanceled.current = false; + showTooltip(); + } + }, [isVisible, text, prevText, showTooltip]); + + /** + * Update the tooltip bounding rectangle + * + * @param {Object} bounds - updated bounds + */ + const updateBounds = (bounds) => { + if (bounds.width === 0) { + setIsRendered(false); + } + setWrapperWidth(bounds.width); + setWrapperHeight(bounds.height); + setXOffset(bounds.x); + setYOffset(bounds.y); + }; /** * Hide the tooltip in an animation. */ - hideTooltip() { - this.animation.stopAnimation(); + const hideTooltip = () => { + animation.current.stopAnimation(); - if (TooltipSense.isActive() && !this.isTooltipSenseInitiator) { - this.animation.setValue(0); + if (TooltipSense.isActive() && !isTooltipSenseInitiator.current) { + animation.current.setValue(0); } else { // Hide the first tooltip which initiated the TooltipSense with animation - this.isTooltipSenseInitiator = false; - Animated.timing(this.animation, { + isTooltipSenseInitiator.current = false; + Animated.timing(animation.current, { toValue: 0, duration: 140, useNativeDriver: false, @@ -124,53 +117,51 @@ class Tooltip extends PureComponent { TooltipSense.deactivate(); - this.setState({isVisible: false}); - } + setIsVisible(false); + }; - render() { - // Skip the tooltip and return the children if the text is empty, - // we don't have a render function or the device does not support hovering - if ((_.isEmpty(this.props.text) && this.props.renderTooltipContent == null) || !this.hasHoverSupport) { - return this.props.children; - } + // Skip the tooltip and return the children if the text is empty, + // we don't have a render function or the device does not support hovering + if ((_.isEmpty(text) && renderTooltipContent == null) || !hasHoverSupport) { + return children; + } - return ( - <> - {this.state.isRendered && ( - - )} - + {isRendered && ( + + )} + + - - {this.props.children} - - - - ); - } + {children} + + + + ); } Tooltip.propTypes = tooltipPropTypes.propTypes; Tooltip.defaultProps = tooltipPropTypes.defaultProps; -export default compose(withWindowDimensions, withLocalize)(Tooltip); +export default memo(Tooltip); diff --git a/src/components/Tooltip/tooltipPropTypes.js b/src/components/Tooltip/tooltipPropTypes.js index f9a1847df242..af18c4cfa412 100644 --- a/src/components/Tooltip/tooltipPropTypes.js +++ b/src/components/Tooltip/tooltipPropTypes.js @@ -1,5 +1,4 @@ import PropTypes from 'prop-types'; -import {windowDimensionsPropTypes} from '../withWindowDimensions'; import variables from '../../styles/variables'; import CONST from '../../CONST'; @@ -13,9 +12,6 @@ const propTypes = { /** Children to wrap with Tooltip. */ children: PropTypes.node.isRequired, - /** Props inherited from withWindowDimensions */ - ...windowDimensionsPropTypes, - /** Any additional amount to manually adjust the horizontal position of the tooltip. A positive value shifts the tooltip to the right, and a negative value shifts it to the left. */ shiftHorizontal: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),