From 8ca1a7e580ed05680ab7138f068c7a44c35c38bf Mon Sep 17 00:00:00 2001 From: jczekalski Date: Thu, 20 Jul 2023 15:38:12 +0200 Subject: [PATCH 1/6] WIP --- src/components/Tooltip/index.js | 224 +++++++++++++++----------------- 1 file changed, 105 insertions(+), 119 deletions(-) diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js index 0454327de284..db67335d719a 100644 --- a/src/components/Tooltip/index.js +++ b/src/components/Tooltip/index.js @@ -1,5 +1,5 @@ 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'; @@ -11,111 +11,99 @@ import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; import compose from '../../libs/compose'; import withLocalize from '../withLocalize'; +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) { + // 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); + + const isTooltipSenseInitiator = useRef(false); + const animation = useRef(new Animated.Value(0)); + const isAnimationCanceled = useRef(false); + const prevText = useRef(props.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.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]); + + 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 && props.text && prevText !== props.text) { + isAnimationCanceled.current = false; + showTooltip(); + } + + prevText.current = props.text; + }, [isVisible, props.text, 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 +112,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(props.text) && props.renderTooltipContent == null) || !hasHoverSupport) { + return props.children; + } - return ( - <> - {this.state.isRendered && ( - - )} - + {isRendered && ( + + )} + + - - {this.props.children} - - - - ); - } + {props.children} + + + + ); } Tooltip.propTypes = tooltipPropTypes.propTypes; Tooltip.defaultProps = tooltipPropTypes.defaultProps; -export default compose(withWindowDimensions, withLocalize)(Tooltip); +export default compose(withWindowDimensions, withLocalize)(memo(Tooltip)); From 3b8a08639c1539dcce61b2a330e7189d71773197 Mon Sep 17 00:00:00 2001 From: jczekalski Date: Thu, 20 Jul 2023 16:04:05 +0200 Subject: [PATCH 2/6] fix .current issues --- src/components/Tooltip/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js index db67335d719a..32eb54a9592b 100644 --- a/src/components/Tooltip/index.js +++ b/src/components/Tooltip/index.js @@ -51,7 +51,7 @@ function Tooltip(props) { // When TooltipSense is active, immediately show the tooltip if (TooltipSense.isActive()) { - animation.setValue(1); + animation.current.setValue(1); } else { isTooltipSenseInitiator.current = true; Animated.timing(animation.current, { @@ -125,7 +125,7 @@ function Tooltip(props) { <> {isRendered && ( Date: Thu, 20 Jul 2023 16:28:58 +0200 Subject: [PATCH 3/6] cleanup --- src/components/Tooltip/index.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js index 32eb54a9592b..ebb5afb420a8 100644 --- a/src/components/Tooltip/index.js +++ b/src/components/Tooltip/index.js @@ -10,13 +10,14 @@ import TooltipSense from './TooltipSense'; import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; import compose from '../../libs/compose'; import withLocalize from '../withLocalize'; +import usePrevious from '../../hooks/usePrevious'; 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 + * @param {propTypes} props * @returns {ReactNodeLike} */ function Tooltip(props) { @@ -35,7 +36,7 @@ function Tooltip(props) { const isTooltipSenseInitiator = useRef(false); const animation = useRef(new Animated.Value(0)); const isAnimationCanceled = useRef(false); - const prevText = useRef(props.text); + const prevText = usePrevious(props.text); /** * Display the tooltip in an animation. @@ -69,13 +70,14 @@ function Tooltip(props) { 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 && props.text && prevText !== props.text) { isAnimationCanceled.current = false; showTooltip(); } prevText.current = props.text; - }, [isVisible, props.text, showTooltip]); + }, [isVisible, props.text, prevText, showTooltip]); /** * Update the tooltip bounding rectangle @@ -137,8 +139,8 @@ function Tooltip(props) { maxWidth={props.maxWidth} numberOfLines={props.numberOfLines} renderTooltipContent={props.renderTooltipContent} - // We pass a key, so whenever the content changes component will completely remount with a fresh - // prevents flickering/moving while remaining performant. + // We pass a key, so whenever the content changes this component will completely remount with a fresh state. + // This prevents flickering/moving while remaining performant. key={[props.text, ...props.renderTooltipContentKey, props.preferredLocale]} /> )} From 237c036a24fc65b938e8ec3f43a874b229d47af7 Mon Sep 17 00:00:00 2001 From: jczekalski Date: Thu, 20 Jul 2023 16:39:49 +0200 Subject: [PATCH 4/6] clean up --- src/components/Tooltip/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js index ebb5afb420a8..d2af74cad2b5 100644 --- a/src/components/Tooltip/index.js +++ b/src/components/Tooltip/index.js @@ -67,16 +67,14 @@ function Tooltip(props) { 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 && props.text && prevText !== props.text) { isAnimationCanceled.current = false; showTooltip(); } - - prevText.current = props.text; }, [isVisible, props.text, prevText, showTooltip]); /** From efce4e4f39e5a1c75994b8ea6889cca39b71555a Mon Sep 17 00:00:00 2001 From: jczekalski Date: Thu, 20 Jul 2023 19:36:57 +0200 Subject: [PATCH 5/6] add missing comment --- src/components/Tooltip/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js index d2af74cad2b5..004d27ef969c 100644 --- a/src/components/Tooltip/index.js +++ b/src/components/Tooltip/index.js @@ -33,6 +33,7 @@ function Tooltip(props) { 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); From 58e6de5e9ffad782eb1464e82a237f7342338a02 Mon Sep 17 00:00:00 2001 From: jczekalski Date: Mon, 24 Jul 2023 10:42:15 +0200 Subject: [PATCH 6/6] replace HOCs with hooks, props destructuring --- src/components/Tooltip/index.js | 36 ++++++++++++---------- src/components/Tooltip/tooltipPropTypes.js | 4 --- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js index 004d27ef969c..398df07649cf 100644 --- a/src/components/Tooltip/index.js +++ b/src/components/Tooltip/index.js @@ -4,13 +4,12 @@ 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(); @@ -21,6 +20,11 @@ const hasHoverSupport = DeviceCapabilities.hasHoverSupport(); * @returns {ReactNodeLike} */ 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? @@ -37,7 +41,7 @@ function Tooltip(props) { const isTooltipSenseInitiator = useRef(false); const animation = useRef(new Animated.Value(0)); const isAnimationCanceled = useRef(false); - const prevText = usePrevious(props.text); + const prevText = usePrevious(text); /** * Display the tooltip in an animation. @@ -72,11 +76,11 @@ function Tooltip(props) { 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 && props.text && prevText !== props.text) { + if (isVisible && isAnimationCanceled.current && text && prevText !== text) { isAnimationCanceled.current = false; showTooltip(); } - }, [isVisible, props.text, prevText, showTooltip]); + }, [isVisible, text, prevText, showTooltip]); /** * Update the tooltip bounding rectangle @@ -118,8 +122,8 @@ function Tooltip(props) { // 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(props.text) && props.renderTooltipContent == null) || !hasHoverSupport) { - return props.children; + if ((_.isEmpty(text) && renderTooltipContent == null) || !hasHoverSupport) { + return children; } return ( @@ -127,20 +131,20 @@ function Tooltip(props) { {isRendered && ( )} - {props.children} + {children} @@ -160,4 +164,4 @@ function Tooltip(props) { Tooltip.propTypes = tooltipPropTypes.propTypes; Tooltip.defaultProps = tooltipPropTypes.defaultProps; -export default compose(withWindowDimensions, withLocalize)(memo(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]),