From 31358c914d42cdabdcecbaa745fba176818b7f78 Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Sat, 10 Feb 2024 13:59:12 -0300 Subject: [PATCH] fix: only update computed positions if it actually changed --- src/components/Tooltip/Tooltip.tsx | 45 +++++++++++++------------- src/utils/compute-positions-types.d.ts | 33 +++++++++++-------- src/utils/deep-equal.ts | 25 ++++++++++++++ 3 files changed, 66 insertions(+), 37 deletions(-) create mode 100644 src/utils/deep-equal.ts diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index f5b1862f..14005d90 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -6,7 +6,9 @@ import { useTooltip } from 'components/TooltipProvider' import useIsomorphicLayoutEffect from 'utils/use-isomorphic-layout-effect' import { getScrollParent } from 'utils/get-scroll-parent' import { computeTooltipPosition } from 'utils/compute-positions' +import type { IComputedPosition } from 'utils/compute-positions-types' import { cssTimeToMs } from 'utils/css-time-to-ms' +import { deepEqual } from 'utils/deep-equal' import coreStyles from './core-styles.module.css' import styles from './styles.module.css' import type { @@ -15,7 +17,6 @@ import type { GlobalCloseEvents, IPosition, ITooltip, - PlacesType, TooltipImperativeOpenOptions, } from './TooltipTypes' @@ -70,9 +71,11 @@ const Tooltip = ({ const tooltipShowDelayTimerRef = useRef(null) const tooltipHideDelayTimerRef = useRef(null) const missedTransitionTimerRef = useRef(null) - const [actualPlacement, setActualPlacement] = useState(place) - const [inlineStyles, setInlineStyles] = useState({}) - const [inlineArrowStyles, setInlineArrowStyles] = useState({}) + const [computedPosition, setComputedPosition] = useState({ + tooltipStyles: {}, + tooltipArrowStyles: {}, + place, + }) const [show, setShow] = useState(false) const [rendered, setRendered] = useState(false) const [imperativeOptions, setImperativeOptions] = useState( @@ -239,6 +242,14 @@ const Tooltip = ({ } }, [show]) + const handleComputedPosition = (newComputedPosition: IComputedPosition) => { + setComputedPosition((oldComputedPosition) => + deepEqual(oldComputedPosition, newComputedPosition) + ? oldComputedPosition + : newComputedPosition, + ) + } + const handleShowTooltipDelayed = (delay = delayShow) => { if (tooltipShowDelayTimerRef.current) { clearTimeout(tooltipShowDelayTimerRef.current) @@ -335,13 +346,7 @@ const Tooltip = ({ middlewares, border, }).then((computedStylesData) => { - if (Object.keys(computedStylesData.tooltipStyles).length) { - setInlineStyles(computedStylesData.tooltipStyles) - } - if (Object.keys(computedStylesData.tooltipArrowStyles).length) { - setInlineArrowStyles(computedStylesData.tooltipArrowStyles) - } - setActualPlacement(computedStylesData.place as PlacesType) + handleComputedPosition(computedStylesData) }) } @@ -439,13 +444,7 @@ const Tooltip = ({ // invalidate computed positions after remount return } - if (Object.keys(computedStylesData.tooltipStyles).length) { - setInlineStyles(computedStylesData.tooltipStyles) - } - if (Object.keys(computedStylesData.tooltipArrowStyles).length) { - setInlineArrowStyles(computedStylesData.tooltipArrowStyles) - } - setActualPlacement(computedStylesData.place as PlacesType) + handleComputedPosition(computedStylesData) }) }, [ show, @@ -819,7 +818,7 @@ const Tooltip = ({ }, [delayShow]) const actualContent = imperativeOptions?.content ?? content - const canShow = show && Object.keys(inlineStyles).length > 0 + const canShow = show && Object.keys(computedPosition.tooltipStyles).length > 0 useImperativeHandle(forwardRef, () => ({ open: (options) => { @@ -849,7 +848,7 @@ const Tooltip = ({ } }, activeAnchor, - place: actualPlacement, + place: computedPosition.place, isOpen: Boolean(rendered && !hidden && actualContent && canShow), })) @@ -863,7 +862,7 @@ const Tooltip = ({ styles['tooltip'], styles[variant], className, - `react-tooltip__place-${actualPlacement}`, + `react-tooltip__place-${computedPosition.place}`, coreStyles[canShow ? 'show' : 'closing'], canShow ? 'react-tooltip__show' : 'react-tooltip__closing', positionStrategy === 'fixed' && coreStyles['fixed'], @@ -882,7 +881,7 @@ const Tooltip = ({ }} style={{ ...externalStyles, - ...inlineStyles, + ...computedPosition.tooltipStyles, opacity: opacity !== undefined && canShow ? opacity : undefined, }} ref={tooltipRef} @@ -897,7 +896,7 @@ const Tooltip = ({ noArrow && coreStyles['noArrow'], )} style={{ - ...inlineArrowStyles, + ...computedPosition.tooltipArrowStyles, background: arrowColor ? `linear-gradient(to right bottom, transparent 50%, ${arrowColor} 50%)` : undefined, diff --git a/src/utils/compute-positions-types.d.ts b/src/utils/compute-positions-types.d.ts index 000f34c4..a8b83021 100644 --- a/src/utils/compute-positions-types.d.ts +++ b/src/utils/compute-positions-types.d.ts @@ -1,25 +1,30 @@ import { CSSProperties } from 'react' -import type { Middleware } from '../components/Tooltip/TooltipTypes' +import type { Middleware, PlacesType } from '../components/Tooltip/TooltipTypes' export interface IComputePositions { elementReference?: Element | HTMLElement | null tooltipReference?: Element | HTMLElement | null tooltipArrowReference?: Element | HTMLElement | null - place?: - | 'top' - | 'top-start' - | 'top-end' - | 'right' - | 'right-start' - | 'right-end' - | 'bottom' - | 'bottom-start' - | 'bottom-end' - | 'left' - | 'left-start' - | 'left-end' + place?: PlacesType offset?: number strategy?: 'absolute' | 'fixed' middlewares?: Middleware[] border?: CSSProperties['border'] } + +export interface IComputedPosition { + tooltipStyles: { + left?: string + top?: string + border?: CSSProperties['border'] + } + tooltipArrowStyles: { + left?: string + top?: string + right?: string + bottom?: string + borderRight?: CSSProperties['border'] + borderBottom?: CSSProperties['border'] + } + place: PlacesType +} diff --git a/src/utils/deep-equal.ts b/src/utils/deep-equal.ts new file mode 100644 index 00000000..57c989a4 --- /dev/null +++ b/src/utils/deep-equal.ts @@ -0,0 +1,25 @@ +const isObject = (object: unknown): object is Record => { + return object !== null && typeof object === 'object' +} + +export const deepEqual = (object1: unknown, object2: unknown): boolean => { + if (!isObject(object1) || !isObject(object2)) { + return object1 === object2 + } + + const keys1 = Object.keys(object1) + const keys2 = Object.keys(object2) + + if (keys1.length !== keys2.length) { + return false + } + + return keys1.every((key) => { + const val1 = object1[key] + const val2 = object2[key] + if (isObject(val1) && isObject(val2)) { + return deepEqual(val1, val2) + } + return val1 === val2 + }) +}