From c65ab55c0add6d20180c8204725b04f38f37c14c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 18 Dec 2023 23:08:20 +0100 Subject: [PATCH 01/13] bulk commit --- src/components/Hoverable/ActiveHoverable.tsx | 137 ++++++++++++ src/components/Hoverable/index.tsx | 215 ++----------------- src/components/Hoverable/types.ts | 12 +- src/components/Tooltip/BaseTooltip.js | 14 +- src/libs/assignRef.ts | 19 ++ src/pages/home/report/ReportActionItem.js | 2 +- 6 files changed, 188 insertions(+), 211 deletions(-) create mode 100644 src/components/Hoverable/ActiveHoverable.tsx create mode 100644 src/libs/assignRef.ts diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx new file mode 100644 index 000000000000..28ae36e8a556 --- /dev/null +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -0,0 +1,137 @@ +import {cloneElement, forwardRef, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import {DeviceEventEmitter} from 'react-native'; +import assignRef from '@libs/assignRef'; +import CONST from '@src/CONST'; +import HoverableProps from './types'; + +type ActiveHoverableProps = Omit; + +function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children}: ActiveHoverableProps, outerRef: Ref) { + const [isHovered, setIsHovered] = useState(false); + + const elementRef = useRef(null); + const isScrollingRef = useRef(false); + const isHoveredRef = useRef(false); + + const updateIsHovered = useCallback( + (hovered: boolean) => { + isHoveredRef.current = hovered; + if (shouldHandleScroll && isScrollingRef.current) { + return; + } + setIsHovered(hovered); + }, + [shouldHandleScroll], + ); + + // Expose inner ref to parent through outerRef. This enable us to use ref both in parent and child. + useImperativeHandle(outerRef, () => elementRef.current, []); + + useEffect(() => { + if (isHovered) { + onHoverIn?.(); + } else { + onHoverOut?.(); + } + }, [isHovered, onHoverIn, onHoverOut]); + + useEffect(() => { + if (!shouldHandleScroll) { + return; + } + + const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling) => { + isScrollingRef.current = scrolling; + if (!isScrollingRef.current) { + setIsHovered(isHoveredRef.current); + } + }); + + return () => scrollingListener.remove(); + }, [shouldHandleScroll]); + + useEffect(() => { + // Do not mount a listener if the component is not hovered + if (!isHovered) { + return; + } + + /** + * Checks the hover state of a component and updates it based on the event target. + * This is necessary to handle cases where the hover state might get stuck due to an unreliable mouseleave trigger, + * such as when an element is removed before the mouseleave event is triggered. + * @param event The hover event object. + */ + const unsetHoveredIfOutside = (event: MouseEvent) => { + if (!elementRef.current || elementRef.current.contains(event.target as Node)) { + return; + } + + setIsHovered(false); + }; + + document.addEventListener('mouseover', unsetHoveredIfOutside); + + return () => document.removeEventListener('mouseover', unsetHoveredIfOutside); + }, [isHovered, elementRef]); + + useEffect(() => { + const unsetHoveredWhenDocumentIsHidden = () => document.visibilityState === 'hidden' && setIsHovered(false); + + document.addEventListener('visibilitychange', unsetHoveredWhenDocumentIsHidden); + + return () => document.removeEventListener('visibilitychange', unsetHoveredWhenDocumentIsHidden); + }, []); + + const child = useMemo(() => (typeof children === 'function' ? children(!isScrollingRef.current && isHovered) : children), [children, isHovered]); + + const childOnMouseEnter = child.props.onMouseEnter; + const childOnMouseLeave = child.props.onMouseLeave; + + const hoverAndForwardOnMouseEnter = useCallback( + (e: MouseEvent) => { + updateIsHovered(true); + childOnMouseEnter?.(e); + }, + [updateIsHovered, childOnMouseEnter], + ); + + const unhoverAndForwardOnMouseLeave = useCallback( + (e: MouseEvent) => { + updateIsHovered(false); + childOnMouseLeave?.(e); + }, + [updateIsHovered, childOnMouseLeave], + ); + + const unhoverAndForwardOnBlur = useCallback( + (event: MouseEvent) => { + // Check if the blur event occurred due to clicking outside the element + // and the wrapperView contains the element that caused the blur and reset isHovered + if (!elementRef.current?.contains(event.target as Node) && !elementRef.current?.contains(event.relatedTarget as Node)) { + setIsHovered(false); + } + + child.props.onBlur?.(event); + }, + [child.props], + ); + + // We need to access the ref of a children from both parent and current component + // So we pass it to current ref and assign it once again to the child ref prop + const hijackRef = (el: HTMLElement) => { + elementRef.current = el; + if (child.ref) { + assignRef(child.ref, el); + } + }; + + return cloneElement(child, { + ref: hijackRef, + onMouseEnter: hoverAndForwardOnMouseEnter, + onMouseLeave: unhoverAndForwardOnMouseLeave, + onBlur: unhoverAndForwardOnBlur, + }); +} + +export default forwardRef(ActiveHoverable); diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx index 9c641cfc19be..1dee5a943e35 100644 --- a/src/components/Hoverable/index.tsx +++ b/src/components/Hoverable/index.tsx @@ -1,212 +1,27 @@ -import React, {ForwardedRef, forwardRef, MutableRefObject, ReactElement, RefAttributes, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {DeviceEventEmitter} from 'react-native'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import CONST from '@src/CONST'; +import React, {cloneElement, forwardRef, Ref} from 'react'; +import {hasHoverSupport} from '@libs/DeviceCapabilities'; +import ActiveHoverable from './ActiveHoverable'; import HoverableProps from './types'; -/** - * Maps the children of a Hoverable component to - * - a function that is called with the parameter - * - the child itself if it is the only child - * @param children The children to map. - * @param callbackParam The parameter to pass to the children function. - * @returns The mapped children. - */ -function mapChildren(children: ((isHovered: boolean) => ReactElement) | ReactElement | ReactElement[], callbackParam: boolean): ReactElement & RefAttributes { - if (Array.isArray(children)) { - return children[0]; - } - - if (typeof children === 'function') { - return children(callbackParam); - } - - return children; -} - -/** - * Assigns a ref to an element, either by setting the current property of the ref object or by calling the ref function - * @param ref The ref object or function. - * @param element The element to assign the ref to. - */ -function assignRef(ref: ((instance: HTMLElement | null) => void) | MutableRefObject, element: HTMLElement) { - if (!ref) { - return; - } - if (typeof ref === 'function') { - ref(element); - } else if ('current' in ref) { - // eslint-disable-next-line no-param-reassign - ref.current = element; - } -} - /** * It is necessary to create a Hoverable component instead of relying solely on Pressable support for hover state, * because nesting Pressables causes issues where the hovered state of the child cannot be easily propagated to the * parent. https://github.com/necolas/react-native-web/issues/1875 */ -function Hoverable( - {disabled = false, onHoverIn = () => {}, onHoverOut = () => {}, onMouseEnter = () => {}, onMouseLeave = () => {}, children, shouldHandleScroll = false}: HoverableProps, - outerRef: ForwardedRef, -) { - const [isHovered, setIsHovered] = useState(false); - - const isScrolling = useRef(false); - const isHoveredRef = useRef(false); - const ref = useRef(null); - - const updateIsHoveredOnScrolling = useCallback( - (hovered: boolean) => { - if (disabled) { - return; - } - - isHoveredRef.current = hovered; - - if (shouldHandleScroll && isScrolling.current) { - return; - } - setIsHovered(hovered); - }, - [disabled, shouldHandleScroll], - ); - - useEffect(() => { - const unsetHoveredWhenDocumentIsHidden = () => document.visibilityState === 'hidden' && setIsHovered(false); - - document.addEventListener('visibilitychange', unsetHoveredWhenDocumentIsHidden); - - return () => document.removeEventListener('visibilitychange', unsetHoveredWhenDocumentIsHidden); - }, []); - - useEffect(() => { - if (!shouldHandleScroll) { - return; - } - - const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling) => { - isScrolling.current = scrolling; - if (!scrolling) { - setIsHovered(isHoveredRef.current); - } - }); - - return () => scrollingListener.remove(); - }, [shouldHandleScroll]); - - useEffect(() => { - if (!DeviceCapabilities.hasHoverSupport()) { - return; - } - - /** - * Checks the hover state of a component and updates it based on the event target. - * This is necessary to handle cases where the hover state might get stuck due to an unreliable mouseleave trigger, - * such as when an element is removed before the mouseleave event is triggered. - * @param event The hover event object. - */ - const unsetHoveredIfOutside = (event: MouseEvent) => { - if (!ref.current || !isHovered) { - return; - } - - if (ref.current.contains(event.target as Node)) { - return; - } - - setIsHovered(false); - }; - - document.addEventListener('mouseover', unsetHoveredIfOutside); - - return () => document.removeEventListener('mouseover', unsetHoveredIfOutside); - }, [isHovered]); - - useEffect(() => { - if (!disabled || !isHovered) { - return; - } - setIsHovered(false); - }, [disabled, isHovered]); - - useEffect(() => { - if (disabled) { - return; - } - if (onHoverIn && isHovered) { - return onHoverIn(); - } - if (onHoverOut && !isHovered) { - return onHoverOut(); - } - }, [disabled, isHovered, onHoverIn, onHoverOut]); - - // Expose inner ref to parent through outerRef. This enable us to use ref both in parent and child. - useImperativeHandle(outerRef, () => ref.current, []); - - const child = useMemo(() => React.Children.only(mapChildren(children, isHovered)), [children, isHovered]); - - const enableHoveredOnMouseEnter = useCallback( - (event: MouseEvent) => { - updateIsHoveredOnScrolling(true); - onMouseEnter(event); - - if (typeof child.props.onMouseEnter === 'function') { - child.props.onMouseEnter(event); - } - }, - [child.props, onMouseEnter, updateIsHoveredOnScrolling], - ); - - const disableHoveredOnMouseLeave = useCallback( - (event: MouseEvent) => { - updateIsHoveredOnScrolling(false); - onMouseLeave(event); - - if (typeof child.props.onMouseLeave === 'function') { - child.props.onMouseLeave(event); - } - }, - [child.props, onMouseLeave, updateIsHoveredOnScrolling], - ); - - const disableHoveredOnBlur = useCallback( - (event: MouseEvent) => { - // Check if the blur event occurred due to clicking outside the element - // and the wrapperView contains the element that caused the blur and reset isHovered - if (!ref.current?.contains(event.target as Node) && !ref.current?.contains(event.relatedTarget as Node)) { - setIsHovered(false); - } - - if (typeof child.props.onBlur === 'function') { - child.props.onBlur(event); - } - }, - [child.props], - ); - - // We need to access the ref of a children from both parent and current component - // So we pass it to current ref and assign it once again to the child ref prop - const hijackRef = (el: HTMLElement) => { - ref.current = el; - if (child.ref) { - assignRef(child.ref, el); - } - }; - - if (!DeviceCapabilities.hasHoverSupport()) { - return React.cloneElement(child, { - ref: hijackRef, - }); +function Hoverable({isDisabled, ...props}: HoverableProps, ref: Ref) { + // If Hoverable is disabled, just render the child without additional logic or event listeners. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (isDisabled || !hasHoverSupport()) { + return cloneElement(typeof props.children === 'function' ? props.children(false) : props.children, {ref}); } - return React.cloneElement(child, { - ref: hijackRef, - onMouseEnter: enableHoveredOnMouseEnter, - onMouseLeave: disableHoveredOnMouseLeave, - onBlur: disableHoveredOnBlur, - }); + return ( + + ); } export default forwardRef(Hoverable); diff --git a/src/components/Hoverable/types.ts b/src/components/Hoverable/types.ts index 430b865f50c5..bea066bdb3bb 100644 --- a/src/components/Hoverable/types.ts +++ b/src/components/Hoverable/types.ts @@ -1,11 +1,11 @@ -import {ReactElement} from 'react'; +import {ReactElement, RefAttributes} from 'react'; type HoverableProps = { /** Children to wrap with Hoverable. */ - children: ((isHovered: boolean) => ReactElement) | ReactElement; + children: ((isHovered: boolean) => ReactElement & RefAttributes) | (ReactElement & RefAttributes); /** Whether to disable the hover action */ - disabled?: boolean; + isDisabled?: boolean; /** Function that executes when the mouse moves over the children. */ onHoverIn?: () => void; @@ -13,12 +13,6 @@ type HoverableProps = { /** Function that executes when the mouse leaves the children. */ onHoverOut?: () => void; - /** Direct pass-through of React's onMouseEnter event. */ - onMouseEnter?: (event: MouseEvent) => void; - - /** Direct pass-through of React's onMouseLeave event. */ - onMouseLeave?: (event: MouseEvent) => void; - /** Decides whether to handle the scroll behaviour to show hover once the scroll ends */ shouldHandleScroll?: boolean; }; diff --git a/src/components/Tooltip/BaseTooltip.js b/src/components/Tooltip/BaseTooltip.js index 3eb905e7a3e5..1aa5fa81e0a4 100644 --- a/src/components/Tooltip/BaseTooltip.js +++ b/src/components/Tooltip/BaseTooltip.js @@ -167,6 +167,16 @@ function Tooltip({children, numberOfLines, maxWidth, text, renderTooltipContent, setIsVisible(false); }, []); + const updateTargetPositionOnMouseEnter = useCallback( + (e) => { + updateTargetAndMousePosition(e); + if (children.props.onMouseEnter) { + children.props.onMouseEnter(e); + } + }, + [children.props, updateTargetAndMousePosition], + ); + // 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) { @@ -205,7 +215,9 @@ function Tooltip({children, numberOfLines, maxWidth, text, renderTooltipContent, onHoverOut={hideTooltip} shouldHandleScroll={shouldHandleScroll} > - {children} + {React.cloneElement(children, { + onMouseEnter: updateTargetPositionOnMouseEnter, + })} diff --git a/src/libs/assignRef.ts b/src/libs/assignRef.ts new file mode 100644 index 000000000000..f2c2b488519f --- /dev/null +++ b/src/libs/assignRef.ts @@ -0,0 +1,19 @@ +import {MutableRefObject, RefCallback} from 'react'; + +/** + * Assigns an element to ref, either by setting the `current` property of the ref object or by calling the ref function + * + * @param ref The ref object or function. + * @param element The element to assign the ref to. + */ +export default function assignRef(ref: RefCallback | MutableRefObject | undefined, element: E) { + if (!ref) { + return; + } + if (typeof ref === 'function') { + ref(element); + } else if ('current' in ref) { + // eslint-disable-next-line no-param-reassign + ref.current = element; + } +} diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 17d49bd0f486..88daec1ab6c7 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -671,7 +671,7 @@ function ReportActionItem(props) { > {(hovered) => ( From 989e0df4c90e2dbcd8e28bef4d563d88f910002b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 18 Dec 2023 23:15:24 +0100 Subject: [PATCH 02/13] hoverable v3 (3 commits ahead) --- src/components/Hoverable/ActiveHoverable.tsx | 3 ++- src/components/Hoverable/index.tsx | 3 ++- src/components/Hoverable/types.ts | 7 ++++++- src/components/Tooltip/BaseTooltip.js | 13 ++++++++----- src/libs/ValueUtils.ts | 4 ++++ 5 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 src/libs/ValueUtils.ts diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index 28ae36e8a556..7517f11e9a7b 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -1,6 +1,7 @@ import {cloneElement, forwardRef, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter} from 'react-native'; import assignRef from '@libs/assignRef'; +import {getFirstValue, getReturnValue} from '@libs/ValueUtils'; import CONST from '@src/CONST'; import HoverableProps from './types'; @@ -83,7 +84,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children}: return () => document.removeEventListener('visibilitychange', unsetHoveredWhenDocumentIsHidden); }, []); - const child = useMemo(() => (typeof children === 'function' ? children(!isScrollingRef.current && isHovered) : children), [children, isHovered]); + const child = useMemo(() => getFirstValue(getReturnValue(children, !isScrollingRef.current && isHovered)), [children, isHovered]); const childOnMouseEnter = child.props.onMouseEnter; const childOnMouseLeave = child.props.onMouseLeave; diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx index 1dee5a943e35..a81c3d6c5912 100644 --- a/src/components/Hoverable/index.tsx +++ b/src/components/Hoverable/index.tsx @@ -1,6 +1,7 @@ import React, {cloneElement, forwardRef, Ref} from 'react'; import {hasHoverSupport} from '@libs/DeviceCapabilities'; import ActiveHoverable from './ActiveHoverable'; +import {executeChildren, extractFirstChild} from './helpers'; import HoverableProps from './types'; /** @@ -12,7 +13,7 @@ function Hoverable({isDisabled, ...props}: HoverableProps, ref: Ref // If Hoverable is disabled, just render the child without additional logic or event listeners. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (isDisabled || !hasHoverSupport()) { - return cloneElement(typeof props.children === 'function' ? props.children(false) : props.children, {ref}); + return cloneElement(extractFirstChild(executeChildren(props.children, false)), {ref}); } return ( diff --git a/src/components/Hoverable/types.ts b/src/components/Hoverable/types.ts index bea066bdb3bb..6cbf257932f8 100644 --- a/src/components/Hoverable/types.ts +++ b/src/components/Hoverable/types.ts @@ -1,8 +1,11 @@ import {ReactElement, RefAttributes} from 'react'; +type HoverableChild = (ReactElement & RefAttributes) | [ReactElement & RefAttributes]; +type HoverableChildren = ((isHovered: boolean) => HoverableChild) | HoverableChild; + type HoverableProps = { /** Children to wrap with Hoverable. */ - children: ((isHovered: boolean) => ReactElement & RefAttributes) | (ReactElement & RefAttributes); + children: HoverableChildren; /** Whether to disable the hover action */ isDisabled?: boolean; @@ -18,3 +21,5 @@ type HoverableProps = { }; export default HoverableProps; + +export type {HoverableChild}; diff --git a/src/components/Tooltip/BaseTooltip.js b/src/components/Tooltip/BaseTooltip.js index 1aa5fa81e0a4..5a3d83648be3 100644 --- a/src/components/Tooltip/BaseTooltip.js +++ b/src/components/Tooltip/BaseTooltip.js @@ -1,6 +1,6 @@ import {BoundsObserver} from '@react-ng/bounds-observer'; import Str from 'expensify-common/lib/str'; -import React, {memo, useCallback, useEffect, useRef, useState} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {Animated} from 'react-native'; import _ from 'underscore'; import Hoverable from '@components/Hoverable'; @@ -8,6 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import {getFirstValue} from '@libs/ValueUtils'; import * as tooltipPropTypes from './tooltipPropTypes'; import TooltipRenderedOnPageBody from './TooltipRenderedOnPageBody'; import TooltipSense from './TooltipSense'; @@ -167,14 +168,16 @@ function Tooltip({children, numberOfLines, maxWidth, text, renderTooltipContent, setIsVisible(false); }, []); + const child = useMemo(() => getFirstValue(children), [children]); + const updateTargetPositionOnMouseEnter = useCallback( (e) => { updateTargetAndMousePosition(e); - if (children.props.onMouseEnter) { - children.props.onMouseEnter(e); + if (child.props.onMouseEnter) { + child.props.onMouseEnter(e); } }, - [children.props, updateTargetAndMousePosition], + [child, updateTargetAndMousePosition], ); // Skip the tooltip and return the children if the text is empty, @@ -215,7 +218,7 @@ function Tooltip({children, numberOfLines, maxWidth, text, renderTooltipContent, onHoverOut={hideTooltip} shouldHandleScroll={shouldHandleScroll} > - {React.cloneElement(children, { + {React.cloneElement(child, { onMouseEnter: updateTargetPositionOnMouseEnter, })} diff --git a/src/libs/ValueUtils.ts b/src/libs/ValueUtils.ts new file mode 100644 index 000000000000..002a8fb347d6 --- /dev/null +++ b/src/libs/ValueUtils.ts @@ -0,0 +1,4 @@ +const getReturnValue = (value: ((...p: P) => T) | T, ...p: P) => (typeof value === 'function' ? (value as (...p: P) => T)(...p) : value); +const getFirstValue = (value: T | [T]) => (Array.isArray(value) ? value[0] : value); + +export {getReturnValue, getFirstValue}; From 3ff7ed6610684a9de8512c10e7d25b50ccfacb54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 19 Dec 2023 12:43:06 +0100 Subject: [PATCH 03/13] remove nonexistent onMouseEnter usage --- src/components/Tooltip/BaseTooltip/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Tooltip/BaseTooltip/index.tsx b/src/components/Tooltip/BaseTooltip/index.tsx index aec46c504fa4..0e6ef6271231 100644 --- a/src/components/Tooltip/BaseTooltip/index.tsx +++ b/src/components/Tooltip/BaseTooltip/index.tsx @@ -230,7 +230,6 @@ function Tooltip( ref={ref} > Date: Tue, 19 Dec 2023 13:15:33 +0100 Subject: [PATCH 04/13] restrict Hoverable to receive single child only --- src/components/Hoverable/ActiveHoverable.tsx | 4 ++-- src/components/Hoverable/index.tsx | 4 ++-- src/components/Hoverable/types.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index 7517f11e9a7b..585818a0b796 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -1,7 +1,7 @@ import {cloneElement, forwardRef, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter} from 'react-native'; import assignRef from '@libs/assignRef'; -import {getFirstValue, getReturnValue} from '@libs/ValueUtils'; +import {getReturnValue} from '@libs/ValueUtils'; import CONST from '@src/CONST'; import HoverableProps from './types'; @@ -84,7 +84,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children}: return () => document.removeEventListener('visibilitychange', unsetHoveredWhenDocumentIsHidden); }, []); - const child = useMemo(() => getFirstValue(getReturnValue(children, !isScrollingRef.current && isHovered)), [children, isHovered]); + const child = useMemo(() => getReturnValue(children, !isScrollingRef.current && isHovered), [children, isHovered]); const childOnMouseEnter = child.props.onMouseEnter; const childOnMouseLeave = child.props.onMouseLeave; diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx index 8a1679b37423..0ce50d5bac9c 100644 --- a/src/components/Hoverable/index.tsx +++ b/src/components/Hoverable/index.tsx @@ -1,6 +1,6 @@ import React, {cloneElement, forwardRef, Ref} from 'react'; import {hasHoverSupport} from '@libs/DeviceCapabilities'; -import {getFirstValue, getReturnValue} from '@libs/ValueUtils'; +import {getReturnValue} from '@libs/ValueUtils'; import ActiveHoverable from './ActiveHoverable'; import HoverableProps from './types'; @@ -13,7 +13,7 @@ function Hoverable({isDisabled, ...props}: HoverableProps, ref: Ref // If Hoverable is disabled, just render the child without additional logic or event listeners. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (isDisabled || !hasHoverSupport()) { - return cloneElement(getFirstValue(getReturnValue(props.children, false)), {ref}); + return cloneElement(getReturnValue(props.children, false), {ref}); } return ( diff --git a/src/components/Hoverable/types.ts b/src/components/Hoverable/types.ts index 6cbf257932f8..35048a7ba445 100644 --- a/src/components/Hoverable/types.ts +++ b/src/components/Hoverable/types.ts @@ -1,6 +1,6 @@ import {ReactElement, RefAttributes} from 'react'; -type HoverableChild = (ReactElement & RefAttributes) | [ReactElement & RefAttributes]; +type HoverableChild = ReactElement & RefAttributes; type HoverableChildren = ((isHovered: boolean) => HoverableChild) | HoverableChild; type HoverableProps = { From 224e10769f3d7e72fcfef5d3d992bbf630821302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 19 Dec 2023 13:27:13 +0100 Subject: [PATCH 05/13] replace assignRef with mergeRefs --- src/components/Hoverable/ActiveHoverable.tsx | 18 +++--------------- src/libs/assignRef.ts | 19 ------------------- src/libs/mergeRefs.ts | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 34 deletions(-) delete mode 100644 src/libs/assignRef.ts create mode 100644 src/libs/mergeRefs.ts diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index 585818a0b796..8d9088b6f4f5 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -1,6 +1,6 @@ -import {cloneElement, forwardRef, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import {cloneElement, forwardRef, Ref, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter} from 'react-native'; -import assignRef from '@libs/assignRef'; +import mergeRefs from '@libs/mergeRefs'; import {getReturnValue} from '@libs/ValueUtils'; import CONST from '@src/CONST'; import HoverableProps from './types'; @@ -25,9 +25,6 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children}: [shouldHandleScroll], ); - // Expose inner ref to parent through outerRef. This enable us to use ref both in parent and child. - useImperativeHandle(outerRef, () => elementRef.current, []); - useEffect(() => { if (isHovered) { onHoverIn?.(); @@ -118,17 +115,8 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children}: [child.props], ); - // We need to access the ref of a children from both parent and current component - // So we pass it to current ref and assign it once again to the child ref prop - const hijackRef = (el: HTMLElement) => { - elementRef.current = el; - if (child.ref) { - assignRef(child.ref, el); - } - }; - return cloneElement(child, { - ref: hijackRef, + ref: mergeRefs(elementRef, outerRef, child.ref), onMouseEnter: hoverAndForwardOnMouseEnter, onMouseLeave: unhoverAndForwardOnMouseLeave, onBlur: unhoverAndForwardOnBlur, diff --git a/src/libs/assignRef.ts b/src/libs/assignRef.ts deleted file mode 100644 index f2c2b488519f..000000000000 --- a/src/libs/assignRef.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {MutableRefObject, RefCallback} from 'react'; - -/** - * Assigns an element to ref, either by setting the `current` property of the ref object or by calling the ref function - * - * @param ref The ref object or function. - * @param element The element to assign the ref to. - */ -export default function assignRef(ref: RefCallback | MutableRefObject | undefined, element: E) { - if (!ref) { - return; - } - if (typeof ref === 'function') { - ref(element); - } else if ('current' in ref) { - // eslint-disable-next-line no-param-reassign - ref.current = element; - } -} diff --git a/src/libs/mergeRefs.ts b/src/libs/mergeRefs.ts new file mode 100644 index 000000000000..d3b69ce1da52 --- /dev/null +++ b/src/libs/mergeRefs.ts @@ -0,0 +1,19 @@ +import {LegacyRef, MutableRefObject, RefCallback} from 'react'; + +/** + * Assigns element reference to multiple refs. + * Source: https://github.com/gregberge/react-merge-refs/blob/main/src/index.tsx + * @param refs The ref object or function arguments. + */ +export default function mergeRefs(...refs: Array | LegacyRef | undefined | null>): RefCallback { + return (value) => { + refs.forEach((ref) => { + if (typeof ref === 'function') { + ref(value); + } else if (ref != null) { + // eslint-disable-next-line no-param-reassign + (ref as MutableRefObject).current = value; + } + }); + }; +} From c0c8a7eba20421a7561325073ce53eb37b1328e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 27 Dec 2023 12:56:31 +0100 Subject: [PATCH 06/13] remove link --- src/libs/mergeRefs.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/mergeRefs.ts b/src/libs/mergeRefs.ts index d3b69ce1da52..b20f04f273e3 100644 --- a/src/libs/mergeRefs.ts +++ b/src/libs/mergeRefs.ts @@ -2,7 +2,6 @@ import {LegacyRef, MutableRefObject, RefCallback} from 'react'; /** * Assigns element reference to multiple refs. - * Source: https://github.com/gregberge/react-merge-refs/blob/main/src/index.tsx * @param refs The ref object or function arguments. */ export default function mergeRefs(...refs: Array | LegacyRef | undefined | null>): RefCallback { From 0af0d78214fa12e9c0bfdac97864da696895d039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 28 Dec 2023 13:04:37 +0100 Subject: [PATCH 07/13] fix OfflineWIthFeedback children & strikethrough --- src/components/OfflineWithFeedback.tsx | 14 +++++++++++--- src/components/Tooltip/BaseTooltip/index.tsx | 13 +++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 5fcf1fe7442b..9ddf90015108 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -96,8 +96,8 @@ function OfflineWithFeedback({ * This method applies the strikethrough to all the children passed recursively */ const applyStrikeThrough = useCallback( - (childrenProp: React.ReactNode): React.ReactNode => - React.Children.map(childrenProp, (child) => { + (childrenProp: React.ReactNode): React.ReactNode => { + const strikedThroughChildren = React.Children.map(childrenProp, (child) => { if (!React.isValidElement(child)) { return child; } @@ -111,7 +111,15 @@ function OfflineWithFeedback({ } return React.cloneElement(child, props); - }), + }); + + // If we only have one child, we want to return it as is, not as an array + if (Array.isArray(strikedThroughChildren) && strikedThroughChildren.length === 1) { + return strikedThroughChildren[0]; + } + + return strikedThroughChildren; + }, [StyleUtils, styles], ); diff --git a/src/components/Tooltip/BaseTooltip/index.tsx b/src/components/Tooltip/BaseTooltip/index.tsx index 0e6ef6271231..f085f39f636e 100644 --- a/src/components/Tooltip/BaseTooltip/index.tsx +++ b/src/components/Tooltip/BaseTooltip/index.tsx @@ -1,5 +1,5 @@ import {BoundsObserver} from '@react-ng/bounds-observer'; -import React, {ForwardedRef, forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {ForwardedRef, forwardRef, memo, useCallback, useEffect, useRef, useState} from 'react'; import {Animated} from 'react-native'; import Hoverable from '@components/Hoverable'; import TooltipRenderedOnPageBody from '@components/Tooltip/TooltipRenderedOnPageBody'; @@ -10,7 +10,6 @@ import usePrevious from '@hooks/usePrevious'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import StringUtils from '@libs/StringUtils'; -import {getFirstValue} from '@libs/ValueUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import callOrReturn from '@src/types/utils/callOrReturn'; @@ -185,16 +184,14 @@ function Tooltip( setIsVisible(false); }, []); - const child = useMemo(() => getFirstValue(children), [children]); - const updateTargetPositionOnMouseEnter = useCallback( (e: MouseEvent) => { updateTargetAndMousePosition(e); - if (child.props.onMouseEnter) { - child.props.onMouseEnter(e); + if (children.props.onMouseEnter) { + children.props.onMouseEnter(e); } }, - [child, updateTargetAndMousePosition], + [children, updateTargetAndMousePosition], ); // Skip the tooltip and return the children if the text is empty, @@ -234,7 +231,7 @@ function Tooltip( onHoverOut={hideTooltip} shouldHandleScroll={shouldHandleScroll} > - {React.cloneElement(child, { + {React.cloneElement(children, { onMouseEnter: updateTargetPositionOnMouseEnter, })} From 7b22bd90399c073987286e90d6a67d9bba51cf38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 3 Jan 2024 10:51:11 +0100 Subject: [PATCH 08/13] implement mapChildren helper --- src/components/OfflineWithFeedback.tsx | 8 ++----- src/libs/mapChildren.ts | 31 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 src/libs/mapChildren.ts diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 5d99b03a2231..5aea9bdd8467 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -3,6 +3,7 @@ import {ImageStyle, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import mapChildren from '@libs/mapChildren'; import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; import CONST from '@src/CONST'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; @@ -97,7 +98,7 @@ function OfflineWithFeedback({ */ const applyStrikeThrough = useCallback( (childrenProp: React.ReactNode): React.ReactNode => { - const strikedThroughChildren = React.Children.map(childrenProp, (child) => { + const strikedThroughChildren = mapChildren(childrenProp, (child) => { if (!React.isValidElement(child)) { return child; } @@ -113,11 +114,6 @@ function OfflineWithFeedback({ return React.cloneElement(child, props); }); - // If we only have one child, we want to return it as is, not as an array - if (Array.isArray(strikedThroughChildren) && strikedThroughChildren.length === 1) { - return strikedThroughChildren[0]; - } - return strikedThroughChildren; }, [StyleUtils, styles], diff --git a/src/libs/mapChildren.ts b/src/libs/mapChildren.ts new file mode 100644 index 000000000000..f94394df4e79 --- /dev/null +++ b/src/libs/mapChildren.ts @@ -0,0 +1,31 @@ +import {Children, ReactNode} from 'react'; + +/** + * A wrapper over `Children.map` which preserves the `ReactNode` structure. + * + * @param children - a `ReactNode` children structure (i.e. either a single React node or an iterable of React nodes) + * @param fn - a mapping function + * + * @returns - a single mapped React node if `children` was a single node, or an iterable of mapped React nodes if + * `children` was an iterable of React nodes + */ +function mapChildren(children: ReactNode, fn: (child: ReactNode) => ReactNode): ReactNode { + // Mirroring the `Children.map` handling of `null` and `undefined` + if (children === null || children === undefined) { + return children; + } + + if (typeof children === 'object' && typeof children !== 'string' && Symbol.iterator in children) { + // `Children.map` doesn't accept `Iterable` (at least on the type level), while `ReactNode` is defined as + // "single node OR `Iterable`". + return Children.map(Array.isArray(children) ? children : [...children], fn); + } + + const mappedChildren = Children.map(children, fn); + + // Per the `Children.map` contract, if it is provided with a single node `children` and a function mapping to a + // single node, it will return an array with the single mapped node. + return mappedChildren[0]; +} + +export default mapChildren; From 441ebb5d51a5a69cd878b481431c06c507550213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 4 Jan 2024 13:00:01 +0100 Subject: [PATCH 09/13] Revert "implement mapChildren helper" This reverts commit 7b22bd90399c073987286e90d6a67d9bba51cf38. --- src/components/OfflineWithFeedback.tsx | 8 +++++-- src/libs/mapChildren.ts | 31 -------------------------- 2 files changed, 6 insertions(+), 33 deletions(-) delete mode 100644 src/libs/mapChildren.ts diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 5aea9bdd8467..5d99b03a2231 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -3,7 +3,6 @@ import {ImageStyle, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import mapChildren from '@libs/mapChildren'; import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; import CONST from '@src/CONST'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; @@ -98,7 +97,7 @@ function OfflineWithFeedback({ */ const applyStrikeThrough = useCallback( (childrenProp: React.ReactNode): React.ReactNode => { - const strikedThroughChildren = mapChildren(childrenProp, (child) => { + const strikedThroughChildren = React.Children.map(childrenProp, (child) => { if (!React.isValidElement(child)) { return child; } @@ -114,6 +113,11 @@ function OfflineWithFeedback({ return React.cloneElement(child, props); }); + // If we only have one child, we want to return it as is, not as an array + if (Array.isArray(strikedThroughChildren) && strikedThroughChildren.length === 1) { + return strikedThroughChildren[0]; + } + return strikedThroughChildren; }, [StyleUtils, styles], diff --git a/src/libs/mapChildren.ts b/src/libs/mapChildren.ts deleted file mode 100644 index f94394df4e79..000000000000 --- a/src/libs/mapChildren.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {Children, ReactNode} from 'react'; - -/** - * A wrapper over `Children.map` which preserves the `ReactNode` structure. - * - * @param children - a `ReactNode` children structure (i.e. either a single React node or an iterable of React nodes) - * @param fn - a mapping function - * - * @returns - a single mapped React node if `children` was a single node, or an iterable of mapped React nodes if - * `children` was an iterable of React nodes - */ -function mapChildren(children: ReactNode, fn: (child: ReactNode) => ReactNode): ReactNode { - // Mirroring the `Children.map` handling of `null` and `undefined` - if (children === null || children === undefined) { - return children; - } - - if (typeof children === 'object' && typeof children !== 'string' && Symbol.iterator in children) { - // `Children.map` doesn't accept `Iterable` (at least on the type level), while `ReactNode` is defined as - // "single node OR `Iterable`". - return Children.map(Array.isArray(children) ? children : [...children], fn); - } - - const mappedChildren = Children.map(children, fn); - - // Per the `Children.map` contract, if it is provided with a single node `children` and a function mapping to a - // single node, it will return an array with the single mapped node. - return mappedChildren[0]; -} - -export default mapChildren; From ba7319c6ff1b8693197eae511c7054e596111e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 4 Jan 2024 13:37:05 +0100 Subject: [PATCH 10/13] add mapChildrenFlat helper --- src/components/OfflineWithFeedback.tsx | 8 ++------ src/libs/mapChildrenFlat.ts | 27 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 src/libs/mapChildrenFlat.ts diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 5d99b03a2231..435fab302b75 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -3,6 +3,7 @@ import {ImageStyle, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import mapChildrenFlat from '@libs/mapChildrenFlat'; import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; import CONST from '@src/CONST'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; @@ -97,7 +98,7 @@ function OfflineWithFeedback({ */ const applyStrikeThrough = useCallback( (childrenProp: React.ReactNode): React.ReactNode => { - const strikedThroughChildren = React.Children.map(childrenProp, (child) => { + const strikedThroughChildren = mapChildrenFlat(childrenProp, (child) => { if (!React.isValidElement(child)) { return child; } @@ -113,11 +114,6 @@ function OfflineWithFeedback({ return React.cloneElement(child, props); }); - // If we only have one child, we want to return it as is, not as an array - if (Array.isArray(strikedThroughChildren) && strikedThroughChildren.length === 1) { - return strikedThroughChildren[0]; - } - return strikedThroughChildren; }, [StyleUtils, styles], diff --git a/src/libs/mapChildrenFlat.ts b/src/libs/mapChildrenFlat.ts new file mode 100644 index 000000000000..238e57d47a83 --- /dev/null +++ b/src/libs/mapChildrenFlat.ts @@ -0,0 +1,27 @@ +import React from 'react'; + +/** + * Maps over the children of a React component and extracts the result of the mapping if there is only one child. + * + * @param children The children to map over. + * @param fn The callback to run for each child. + * @returns The flattened result of mapping over the children. + * + * @example + * // Usage example: + * const result = mapChildrenFlat(children, (child, index) => { + * // Your mapping logic here + * return modifiedChild; + * }); + */ +const mapChildrenFlat = (...args: Parameters>) => { + const mappedChildren = React.Children.map(...args); + + if (Array.isArray(mappedChildren) && mappedChildren.length === 1) { + return mappedChildren[0]; + } + + return mappedChildren; +}; + +export default mapChildrenFlat; From c0a3e84c3588734baf641cd0cfb1ba734123f860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 8 Jan 2024 12:53:54 +0100 Subject: [PATCH 11/13] fix typing and linting --- src/components/Hoverable/ActiveHoverable.tsx | 5 +++-- src/components/Tooltip/BaseTooltip/index.tsx | 2 +- src/libs/mergeRefs.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index 8d9088b6f4f5..028fdd30cf35 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -1,9 +1,10 @@ -import {cloneElement, forwardRef, Ref, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {Ref} from 'react'; +import {cloneElement, forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter} from 'react-native'; import mergeRefs from '@libs/mergeRefs'; import {getReturnValue} from '@libs/ValueUtils'; import CONST from '@src/CONST'; -import HoverableProps from './types'; +import type HoverableProps from './types'; type ActiveHoverableProps = Omit; diff --git a/src/components/Tooltip/BaseTooltip/index.tsx b/src/components/Tooltip/BaseTooltip/index.tsx index c5141283b2b2..48d57a4ccd4a 100644 --- a/src/components/Tooltip/BaseTooltip/index.tsx +++ b/src/components/Tooltip/BaseTooltip/index.tsx @@ -188,7 +188,7 @@ function Tooltip( const updateTargetPositionOnMouseEnter = useCallback( (e: MouseEvent) => { updateTargetAndMousePosition(e); - if (children.props.onMouseEnter) { + if (React.isValidElement(children)) { children.props.onMouseEnter(e); } }, diff --git a/src/libs/mergeRefs.ts b/src/libs/mergeRefs.ts index b20f04f273e3..d4f342d17ca4 100644 --- a/src/libs/mergeRefs.ts +++ b/src/libs/mergeRefs.ts @@ -1,4 +1,4 @@ -import {LegacyRef, MutableRefObject, RefCallback} from 'react'; +import type {LegacyRef, MutableRefObject, RefCallback} from 'react'; /** * Assigns element reference to multiple refs. From 116490eadff06f3f29bf02787dd8b020c0c8916b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 8 Jan 2024 13:45:59 +0100 Subject: [PATCH 12/13] fix Tooltip children typings --- src/components/Tooltip/BaseTooltip/index.tsx | 39 ++++++++++++-------- src/components/Tooltip/types.ts | 8 ++-- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/components/Tooltip/BaseTooltip/index.tsx b/src/components/Tooltip/BaseTooltip/index.tsx index 48d57a4ccd4a..2adde759b847 100644 --- a/src/components/Tooltip/BaseTooltip/index.tsx +++ b/src/components/Tooltip/BaseTooltip/index.tsx @@ -222,21 +222,30 @@ function Tooltip( key={[text, ...renderTooltipContentKey, preferredLocale].join('-')} /> )} - - - {React.cloneElement(children, { - onMouseEnter: updateTargetPositionOnMouseEnter, - })} - - + + { + // Checks if valid element so we can wrap the BoundsObserver around it + // If not, we just return the primitive children + React.isValidElement(children) ? ( + + + {React.cloneElement(children as React.ReactElement, { + onMouseEnter: updateTargetPositionOnMouseEnter, + })} + + + ) : ( + children + ) + } ); } diff --git a/src/components/Tooltip/types.ts b/src/components/Tooltip/types.ts index 253013f49761..739e9a4d0a22 100644 --- a/src/components/Tooltip/types.ts +++ b/src/components/Tooltip/types.ts @@ -1,7 +1,9 @@ -import type {ReactNode} from 'react'; -import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import type {ReactElement, ReactNode} from 'react'; + +type TooltipProps = { + /** The children to render the tooltip for. On web it has to be proper HTML element to attach event handlers to. */ + children: ReactElement | ReactNode; -type TooltipProps = ChildrenProps & { /** The text to display in the tooltip. If text is ommitted, only children will be rendered. */ text?: string; From b88474a1a85388ac24c6c923d64aee8e27d5887e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 8 Jan 2024 13:50:42 +0100 Subject: [PATCH 13/13] fix TooltipProps child --- src/components/Tooltip/types.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/Tooltip/types.ts b/src/components/Tooltip/types.ts index 739e9a4d0a22..253013f49761 100644 --- a/src/components/Tooltip/types.ts +++ b/src/components/Tooltip/types.ts @@ -1,9 +1,7 @@ -import type {ReactElement, ReactNode} from 'react'; - -type TooltipProps = { - /** The children to render the tooltip for. On web it has to be proper HTML element to attach event handlers to. */ - children: ReactElement | ReactNode; +import type {ReactNode} from 'react'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +type TooltipProps = ChildrenProps & { /** The text to display in the tooltip. If text is ommitted, only children will be rendered. */ text?: string;