From f6c3290613c3c6cf321b648ac9c0e8bdedacce47 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 24 Jun 2024 23:42:35 +0700 Subject: [PATCH] make anchorAlignment work on native --- src/components/MenuItem.tsx | 12 +- src/components/PopoverMenu.tsx | 3 + .../BaseGenericTooltip/index.native.tsx | 7 + .../FloatingActionButtonAndPopover.tsx | 4 +- .../computeHorizontalShift/index.native.ts | 5 + .../computeHorizontalShift/index.ts | 42 ++++ .../computeHorizontalShift/types.ts | 3 + .../TooltipStyleUtils/index.native.ts | 179 ------------------ .../generators/TooltipStyleUtils/index.ts | 122 ++++-------- .../isOverlappingAtTop/index.native.ts | 5 + .../isOverlappingAtTop/index.ts | 47 +++++ .../isOverlappingAtTop/types.ts | 5 + .../tooltipPlatformStyles/index.native.ts | 7 + .../tooltipPlatformStyles/index.ts | 7 + .../generators/TooltipStyleUtils/types.ts | 33 ---- 15 files changed, 182 insertions(+), 299 deletions(-) create mode 100644 src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.native.ts create mode 100644 src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts create mode 100644 src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts delete mode 100644 src/styles/utils/generators/TooltipStyleUtils/index.native.ts create mode 100644 src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.native.ts create mode 100644 src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.ts create mode 100644 src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/types.ts create mode 100644 src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.native.ts create mode 100644 src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.ts delete mode 100644 src/styles/utils/generators/TooltipStyleUtils/types.ts diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 2667eca0833c..b2834bac5deb 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -297,6 +297,12 @@ type MenuItemBaseProps = { /** Additional styles for tooltip wrapper */ tooltipWrapperStyle?: StyleProp; + /** Any additional amount to manually adjust the horizontal position of the tooltip */ + tooltipShiftHorizontal?: number; + + /** Any additional amount to manually adjust the vertical position of the tooltip */ + tooltipShiftVertical?: number; + /** Render custom content inside the tooltip. */ renderTooltipContent?: () => ReactNode; }; @@ -386,6 +392,8 @@ function MenuItem( shouldRenderTooltip = false, tooltipAnchorAlignment, tooltipWrapperStyle = {}, + tooltipShiftHorizontal = 0, + tooltipShiftVertical = 0, renderTooltipContent, }: MenuItemProps, ref: PressableRef, @@ -494,8 +502,8 @@ function MenuItem( anchorAlignment={tooltipAnchorAlignment} renderTooltipContent={renderTooltipContent} wrapperStyle={tooltipWrapperStyle} - shiftHorizontal={styles.popoverMenuItem.paddingHorizontal} - shiftVertical={styles.popoverMenuItem.paddingVertical / 2} + shiftHorizontal={tooltipShiftHorizontal} + shiftVertical={tooltipShiftVertical} > diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index b9cff08d020a..cb280d5b3aa5 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -244,6 +244,9 @@ function PopoverMenu({ success={item.success} containerStyle={item.containerStyle} shouldRenderTooltip={item.shouldRenderTooltip} + tooltipAnchorAlignment={item.tooltipAnchorAlignment} + tooltipShiftHorizontal={item.tooltipShiftHorizontal} + tooltipShiftVertical={item.tooltipShiftVertical} tooltipWrapperStyle={item.tooltipWrapperStyle} renderTooltipContent={item.renderTooltipContent} /> diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index c4ce198cf5ae..2dafbecf84d0 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -4,6 +4,7 @@ import {Animated, View} from 'react-native'; import type {Text as RNText, View as RNView} from 'react-native'; import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; +import CONST from '@src/CONST'; import type {BaseGenericTooltipProps} from './types'; // Props will change frequently. @@ -25,6 +26,10 @@ function BaseGenericTooltip({ maxWidth = 0, renderTooltipContent, shouldForceRenderingBelow = false, + anchorAlignment = { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + }, wrapperStyle = {}, }: BaseGenericTooltipProps) { // The width of tooltip's inner content. Has to be undefined in the beginning @@ -65,6 +70,7 @@ function BaseGenericTooltip({ manualShiftHorizontal: shiftHorizontal, manualShiftVertical: shiftVertical, shouldForceRenderingBelow, + anchorAlignment, wrapperStyle, }), [ @@ -81,6 +87,7 @@ function BaseGenericTooltip({ shiftHorizontal, shiftVertical, shouldForceRenderingBelow, + anchorAlignment, wrapperStyle, ], ); diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 3fe4fa6c2aa6..3d5f712623af 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -471,11 +471,13 @@ function FloatingActionButtonAndPopover( numberOfLinesDescription: 1, onSelected: () => interceptAnonymousUser(() => navigateToQuickAction()), shouldShowSubscriptRightAvatar: ReportUtils.isPolicyExpenseChat(quickActionReport), - shouldRenderTooltip: true, + shouldRenderTooltip: quickAction.isFirstQuickAction, tooltipAnchorAlignment: { vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, }, + tooltipShiftHorizontal: styles.popoverMenuItem.paddingHorizontal, + tooltipShiftVertical: styles.popoverMenuItem.paddingVertical / 2, renderTooltipContent: renderQuickActionTooltip, tooltipWrapperStyle: styles.quickActionTooltipWrapper, }, diff --git a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.native.ts b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.native.ts new file mode 100644 index 000000000000..61c10170a9b7 --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.native.ts @@ -0,0 +1,5 @@ +import type ComputeHorizontalShift from './types'; + +const computeHorizontalShift: ComputeHorizontalShift = () => 0; + +export default computeHorizontalShift; diff --git a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts new file mode 100644 index 000000000000..339ddf306197 --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts @@ -0,0 +1,42 @@ +import roundToNearestMultipleOfFour from '@libs/roundToNearestMultipleOfFour'; +import variables from '@styles/variables'; +import type ComputeHorizontalShift from './types'; + +/** This defines the proximity with the edge of the window in which tooltips should not be displayed. + * If a tooltip is too close to the edge of the screen, we'll shift it towards the center. */ +const GUTTER_WIDTH = variables.gutterWidth; + +/** + * Compute the amount the tooltip needs to be horizontally shifted in order to keep it from displaying in the gutters. + * + * @param windowWidth - The width of the window. + * @param xOffset - The distance between the left edge of the window + * and the left edge of the wrapped component. + * @param componentWidth - The width of the wrapped component. + * @param tooltipWidth - The width of the tooltip itself. + * @param [manualShiftHorizontal] - Any additional amount to manually shift the tooltip to the left or right. + * A positive value shifts it to the right, + * and a negative value shifts it to the left. + */ +const computeHorizontalShift: ComputeHorizontalShift = (windowWidth, xOffset, componentWidth, tooltipWidth, manualShiftHorizontal) => { + // First find the left and right edges of the tooltip (by default, it is centered on the component). + const componentCenter = xOffset + componentWidth / 2 + manualShiftHorizontal; + const tooltipLeftEdge = componentCenter - tooltipWidth / 2; + const tooltipRightEdge = componentCenter + tooltipWidth / 2; + + if (tooltipLeftEdge < GUTTER_WIDTH) { + // Tooltip is in left gutter, shift right by a multiple of four. + return roundToNearestMultipleOfFour(GUTTER_WIDTH - tooltipLeftEdge); + } + + if (tooltipRightEdge > windowWidth - GUTTER_WIDTH) { + // Tooltip is in right gutter, shift left by a multiple of four. + return roundToNearestMultipleOfFour(windowWidth - GUTTER_WIDTH - tooltipRightEdge); + } + + // Tooltip is not in the gutter, so no need to shift it horizontally + return 0; +}; + +export {GUTTER_WIDTH}; +export default computeHorizontalShift; diff --git a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts new file mode 100644 index 000000000000..983155e811aa --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts @@ -0,0 +1,3 @@ +type ComputeHorizontalShift = (windowWidth: number, xOffset: number, componentWidth: number, tooltipWidth: number, manualShiftHorizontal: number) => number; + +export default ComputeHorizontalShift; diff --git a/src/styles/utils/generators/TooltipStyleUtils/index.native.ts b/src/styles/utils/generators/TooltipStyleUtils/index.native.ts deleted file mode 100644 index fa4264f45b1c..000000000000 --- a/src/styles/utils/generators/TooltipStyleUtils/index.native.ts +++ /dev/null @@ -1,179 +0,0 @@ -import {Animated, StyleSheet} from 'react-native'; -import FontUtils from '@styles/utils/FontUtils'; -// eslint-disable-next-line no-restricted-imports -import type StyleUtilGenerator from '@styles/utils/generators/types'; -// eslint-disable-next-line no-restricted-imports -import positioning from '@styles/utils/positioning'; -// eslint-disable-next-line no-restricted-imports -import spacing from '@styles/utils/spacing'; -import variables from '@styles/variables'; -import type {GetTooltipStylesStyleUtil} from './types'; - -/** The height of a tooltip pointer */ -const POINTER_HEIGHT = 4; - -/** The width of a tooltip pointer */ -const POINTER_WIDTH = 12; - -/** - * Generate styles for the tooltip component. - * - * @param tooltip - The reference to the tooltip's root element - * @param currentSize - The current size of the tooltip used in the scaling animation. - * @param windowWidth - The width of the window. - * @param xOffset - The distance between the left edge of the wrapped component - * and the left edge of the parent component. - * @param yOffset - The distance between the top edge of the wrapped component - * and the top edge of the parent component. - * @param tooltipTargetWidth - The width of the tooltip's target - * @param tooltipTargetHeight - The height of the tooltip's target - * @param maxWidth - The tooltip's max width. - * @param tooltipContentWidth - The tooltip's inner content measured width. - * @param tooltipWrapperHeight - The tooltip's wrapper measured height. - * @param [manualShiftHorizontal] - Any additional amount to manually shift the tooltip to the left or right. - * A positive value shifts it to the right, - * and a negative value shifts it to the left. - * @param [manualShiftVertical] - Any additional amount to manually shift the tooltip up or down. - * A positive value shifts it down, and a negative value shifts it up. - * @param [shouldForceRenderingBelow] - Should display tooltip below the wrapped component. - * @param [shouldForceRenderingLeft] - Align the tooltip left relative to the wrapped component instead of horizontally align center. - * @param [wrapperStyle] - Any additional styles for the root wrapper. - */ -const createTooltipStyleUtils: StyleUtilGenerator = ({theme, styles}) => ({ - getTooltipStyles: ({ - currentSize, - xOffset, - yOffset, - tooltipTargetWidth, - maxWidth, - tooltipContentWidth, - tooltipWrapperHeight, - manualShiftHorizontal = 0, - manualShiftVertical = 0, - shouldForceRenderingLeft = false, - wrapperStyle = {}, - }) => { - const customWrapperStyle = StyleSheet.flatten(wrapperStyle); - const tooltipVerticalPadding = spacing.pv1; - - // We calculate tooltip width based on the tooltip's content width - // so the tooltip wrapper is just big enough to fit content and prevent white space. - // NOTE: Add 1 to the tooltipWidth to prevent truncated text in Safari - const tooltipWidth = tooltipContentWidth && tooltipContentWidth + spacing.ph2.paddingHorizontal * 2 + 1; - const tooltipHeight = tooltipWrapperHeight; - - const isTooltipSizeReady = tooltipWidth !== undefined && tooltipHeight !== undefined; - - // Set the scale to 1 to be able to measure the tooltip size correctly when it's not ready yet. - let scale = new Animated.Value(1); - let rootWrapperTop = 0; - let rootWrapperLeft = 0; - let pointerWrapperTop = 0; - let pointerWrapperLeft = 0; - let opacity = 0; - - if (isTooltipSizeReady) { - // When the tooltip size is ready, we can start animating the scale. - scale = currentSize; - - // Because it uses absolute positioning, the top-left corner of the tooltip is aligned - // with the top-left corner of the wrapped component by default. - // we will use yOffset to position the tooltip relative to the Wrapped Component - // So we need to shift the tooltip vertically and horizontally to position it correctly. - // - // First, we'll position it vertically. - // To shift the tooltip down, we'll give `top` a positive value. - // To shift the tooltip up, we'll give `top` a negative value. - rootWrapperTop = yOffset - (tooltipHeight + POINTER_HEIGHT) + manualShiftVertical; - - // Next, we'll position it horizontally. - // we will use xOffset to position the tooltip relative to the Wrapped Component - // To shift the tooltip right, we'll give `left` a positive value. - // To shift the tooltip left, we'll give `left` a negative value. - // - // So we'll: - // 1a) Horizontally align left: No need for shifting. - // 1b) Horizontally align center: - // - Shift the tooltip right (+) to the center of the component, - // so the left edge lines up with the component center. - // - Shift it left (-) to by half the tooltip's width, - // so the tooltip's center lines up with the center of the wrapped component. - // 2) Add the manual horizontal shift passed in as a parameter. - rootWrapperLeft = xOffset + (shouldForceRenderingLeft ? 0 : tooltipTargetWidth / 2 - tooltipWidth / 2) + manualShiftHorizontal; - - // By default, the pointer's top-left will align with the top-left of the tooltip wrapper. - // - // To align it vertically, the pointer up (-) by the pointer's height - // so that the bottom of the pointer lines up with the top of the tooltip - pointerWrapperTop = tooltipHeight; - - // To align it horizontally, we'll: - // 1) Left align: Shift the pointer to the right (+) by half the pointer's width, - // so the left edge of the pointer does not overlap with the wrapper's border radius. - // 2) Center align: - // - Shift the pointer to the right (+) by the half the tooltipWidth's width, - // so the left edge of the pointer lines up with the tooltipWidth's center. - // - To the left (-) by half the pointer's width, - // so the pointer's center lines up with the tooltipWidth's center. - pointerWrapperLeft = shouldForceRenderingLeft ? POINTER_WIDTH / 2 : tooltipWidth / 2 - POINTER_WIDTH / 2; - - // React Native's measure() is asynchronous, we temporarily hide the tooltip until its bound is calculated - opacity = 100; - } - - return { - animationStyle: { - // remember Transform causes a new Local cordinate system - // https://drafts.csswg.org/css-transforms-1/#transform-rendering - // so Position fixed children will be relative to this new Local cordinate system - transform: [{scale}], - }, - rootWrapperStyle: { - ...positioning.pAbsolute, - backgroundColor: theme.heading, - borderRadius: variables.componentBorderRadiusSmall, - ...tooltipVerticalPadding, - ...spacing.ph2, - zIndex: variables.tooltipzIndex, - width: tooltipWidth, - maxWidth, - top: rootWrapperTop, - left: rootWrapperLeft, - opacity, - ...customWrapperStyle, - - // We are adding this to prevent the tooltip text from being selected and copied on CTRL + A. - ...styles.userSelectNone, - ...styles.pointerEventsNone, - }, - textStyle: { - color: theme.textReversed, - fontFamily: FontUtils.fontFamily.platform.EXP_NEUE, - fontSize: variables.fontSizeSmall, - overflow: 'hidden', - lineHeight: variables.lineHeightSmall, - textAlign: 'center', - }, - pointerWrapperStyle: { - ...positioning.pAbsolute, - top: pointerWrapperTop, - left: pointerWrapperLeft, - opacity, - }, - pointerStyle: { - width: 0, - height: 0, - backgroundColor: theme.transparent, - borderStyle: 'solid', - borderLeftWidth: POINTER_WIDTH / 2, - borderRightWidth: POINTER_WIDTH / 2, - borderTopWidth: POINTER_HEIGHT, - borderLeftColor: theme.transparent, - borderRightColor: theme.transparent, - borderTopColor: customWrapperStyle.backgroundColor ?? theme.heading, - }, - }; - }, -}); - -export default createTooltipStyleUtils; diff --git a/src/styles/utils/generators/TooltipStyleUtils/index.ts b/src/styles/utils/generators/TooltipStyleUtils/index.ts index 8e98c5b23218..588b054b7157 100644 --- a/src/styles/utils/generators/TooltipStyleUtils/index.ts +++ b/src/styles/utils/generators/TooltipStyleUtils/index.ts @@ -1,22 +1,18 @@ -import type {View} from 'react-native'; +import type {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import {Animated, StyleSheet} from 'react-native'; -import roundToNearestMultipleOfFour from '@libs/roundToNearestMultipleOfFour'; import FontUtils from '@styles/utils/FontUtils'; // eslint-disable-next-line no-restricted-imports import type StyleUtilGenerator from '@styles/utils/generators/types'; // eslint-disable-next-line no-restricted-imports -import positioning from '@styles/utils/positioning'; -// eslint-disable-next-line no-restricted-imports import spacing from '@styles/utils/spacing'; // eslint-disable-next-line no-restricted-imports import titleBarHeight from '@styles/utils/titleBarHeight'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type {GetTooltipStylesStyleUtil} from './types'; - -/** This defines the proximity with the edge of the window in which tooltips should not be displayed. - * If a tooltip is too close to the edge of the screen, we'll shift it towards the center. */ -const GUTTER_WIDTH = variables.gutterWidth; +import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment'; +import computeHorizontalShift, {GUTTER_WIDTH} from './computeHorizontalShift'; +import isOverlappingAtTop from './isOverlappingAtTop'; +import tooltipPlatformStyle from './tooltipPlatformStyles'; /** The height of a tooltip pointer */ const POINTER_HEIGHT = 4; @@ -24,81 +20,33 @@ const POINTER_HEIGHT = 4; /** The width of a tooltip pointer */ const POINTER_WIDTH = 12; -/** - * Compute the amount the tooltip needs to be horizontally shifted in order to keep it from displaying in the gutters. - * - * @param windowWidth - The width of the window. - * @param xOffset - The distance between the left edge of the window - * and the left edge of the wrapped component. - * @param componentWidth - The width of the wrapped component. - * @param tooltipWidth - The width of the tooltip itself. - * @param [manualShiftHorizontal] - Any additional amount to manually shift the tooltip to the left or right. - * A positive value shifts it to the right, - * and a negative value shifts it to the left. - */ -function computeHorizontalShift(windowWidth: number, xOffset: number, componentWidth: number, tooltipWidth: number, manualShiftHorizontal: number): number { - // First find the left and right edges of the tooltip (by default, it is centered on the component). - const componentCenter = xOffset + componentWidth / 2 + manualShiftHorizontal; - const tooltipLeftEdge = componentCenter - tooltipWidth / 2; - const tooltipRightEdge = componentCenter + tooltipWidth / 2; - - if (tooltipLeftEdge < GUTTER_WIDTH) { - // Tooltip is in left gutter, shift right by a multiple of four. - return roundToNearestMultipleOfFour(GUTTER_WIDTH - tooltipLeftEdge); - } +type TooltipStyles = { + animationStyle: ViewStyle; + rootWrapperStyle: ViewStyle; + textStyle: TextStyle; + pointerWrapperStyle: ViewStyle; + pointerStyle: ViewStyle; +}; - if (tooltipRightEdge > windowWidth - GUTTER_WIDTH) { - // Tooltip is in right gutter, shift left by a multiple of four. - return roundToNearestMultipleOfFour(windowWidth - GUTTER_WIDTH - tooltipRightEdge); - } +type TooltipParams = { + tooltip: View | HTMLDivElement | null; + currentSize: Animated.Value; + windowWidth: number; + xOffset: number; + yOffset: number; + tooltipTargetWidth: number; + tooltipTargetHeight: number; + maxWidth: number; + tooltipContentWidth?: number; + tooltipWrapperHeight?: number; + manualShiftHorizontal?: number; + manualShiftVertical?: number; + shouldForceRenderingBelow?: boolean; + wrapperStyle: StyleProp; + anchorAlignment?: TooltipAnchorAlignment; +}; - // Tooltip is not in the gutter, so no need to shift it horizontally - return 0; -} - -/** - * Determines if there is an overlapping element at the top of a given coordinate. - * (targetCenterX, y) - * | - * v - * _ _ _ _ _ - * | | - * | | - * | | - * | | - * |_ _ _ _ _| - * - * @param tooltip - The reference to the tooltip's root element - * @param xOffset - The distance between the left edge of the window - * and the left edge of the wrapped component. - * @param yOffset - The distance between the top edge of the window - * and the top edge of the wrapped component. - * @param tooltipTargetWidth - The width of the tooltip's target - * @param tooltipTargetHeight - The height of the tooltip's target - */ -function isOverlappingAtTop(tooltip: View | HTMLDivElement, xOffset: number, yOffset: number, tooltipTargetWidth: number, tooltipTargetHeight: number) { - if (typeof document.elementFromPoint !== 'function') { - return false; - } - - // Use the x center position of the target to prevent wrong element returned by elementFromPoint - // in case the target has a border radius or is a multiline text. - const targetCenterX = xOffset + tooltipTargetWidth / 2; - const elementAtTargetCenterX = document.elementFromPoint(targetCenterX, yOffset); - - // Ensure it's not the already rendered element of this very tooltip, so the tooltip doesn't try to "avoid" itself - if (!elementAtTargetCenterX || ('contains' in tooltip && tooltip.contains(elementAtTargetCenterX))) { - return false; - } - - const rectAtTargetCenterX = elementAtTargetCenterX.getBoundingClientRect(); - - // Ensure it's not overlapping with another element by checking if the yOffset is greater than the top of the element - // and less than the bottom of the element. Also ensure the tooltip target is not completely inside the elementAtTargetCenterX by vertical direction - const isOverlappingAtTargetCenterX = yOffset > rectAtTargetCenterX.top && yOffset < rectAtTargetCenterX.bottom && yOffset + tooltipTargetHeight > rectAtTargetCenterX.bottom; - - return isOverlappingAtTargetCenterX; -} +type GetTooltipStylesStyleUtil = {getTooltipStyles: (props: TooltipParams) => TooltipStyles}; /** * Generate styles for the tooltip component. @@ -166,6 +114,7 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( let pointerWrapperTop = 0; let pointerWrapperLeft = 0; let pointerAdditionalStyle = {}; + let opacity = 0; if (isTooltipSizeReady) { // Determine if the tooltip should display below the wrapped component. @@ -258,6 +207,9 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( } pointerAdditionalStyle = shouldShowBelow ? styles.flipUpsideDown : {}; + + // React Native's measure() is asynchronous, we temporarily hide the tooltip until its bound is calculated + opacity = 100; } return { @@ -268,7 +220,7 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( transform: [{scale}], }, rootWrapperStyle: { - ...positioning.pFixed, + ...tooltipPlatformStyle, backgroundColor: theme.heading, borderRadius: variables.componentBorderRadiusSmall, ...tooltipVerticalPadding, @@ -278,6 +230,7 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( maxWidth, top: rootWrapperTop, left: rootWrapperLeft, + opacity, ...customWrapperStyle, // We are adding this to prevent the tooltip text from being selected and copied on CTRL + A. @@ -293,9 +246,10 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( textAlign: 'center', }, pointerWrapperStyle: { - ...positioning.pFixed, + ...tooltipPlatformStyle, top: pointerWrapperTop, left: pointerWrapperLeft, + opacity, }, pointerStyle: { width: 0, diff --git a/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.native.ts b/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.native.ts new file mode 100644 index 000000000000..fa80f4471870 --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.native.ts @@ -0,0 +1,5 @@ +import type IsOverlappingAtTop from './types'; + +const isOverlappingAtTop: IsOverlappingAtTop = () => false; + +export default isOverlappingAtTop; diff --git a/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.ts b/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.ts new file mode 100644 index 000000000000..081d1a0a693e --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.ts @@ -0,0 +1,47 @@ +import type IsOverlappingAtTop from './types'; + +/** + * Determines if there is an overlapping element at the top of a given coordinate. + * (targetCenterX, y) + * | + * v + * _ _ _ _ _ + * | | + * | | + * | | + * | | + * |_ _ _ _ _| + * + * @param tooltip - The reference to the tooltip's root element + * @param xOffset - The distance between the left edge of the window + * and the left edge of the wrapped component. + * @param yOffset - The distance between the top edge of the window + * and the top edge of the wrapped component. + * @param tooltipTargetWidth - The width of the tooltip's target + * @param tooltipTargetHeight - The height of the tooltip's target + */ +const isOverlappingAtTop: IsOverlappingAtTop = (tooltip, xOffset, yOffset, tooltipTargetWidth, tooltipTargetHeight) => { + if (typeof document.elementFromPoint !== 'function') { + return false; + } + + // Use the x center position of the target to prevent wrong element returned by elementFromPoint + // in case the target has a border radius or is a multiline text. + const targetCenterX = xOffset + tooltipTargetWidth / 2; + const elementAtTargetCenterX = document.elementFromPoint(targetCenterX, yOffset); + + // Ensure it's not the already rendered element of this very tooltip, so the tooltip doesn't try to "avoid" itself + if (!elementAtTargetCenterX || ('contains' in tooltip && tooltip.contains(elementAtTargetCenterX))) { + return false; + } + + const rectAtTargetCenterX = elementAtTargetCenterX.getBoundingClientRect(); + + // Ensure it's not overlapping with another element by checking if the yOffset is greater than the top of the element + // and less than the bottom of the element. Also ensure the tooltip target is not completely inside the elementAtTargetCenterX by vertical direction + const isOverlappingAtTargetCenterX = yOffset > rectAtTargetCenterX.top && yOffset < rectAtTargetCenterX.bottom && yOffset + tooltipTargetHeight > rectAtTargetCenterX.bottom; + + return isOverlappingAtTargetCenterX; +}; + +export default isOverlappingAtTop; diff --git a/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/types.ts b/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/types.ts new file mode 100644 index 000000000000..bdd8ff346a86 --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/types.ts @@ -0,0 +1,5 @@ +import type {View} from 'react-native'; + +type IsOverlappingAtTop = (tooltip: View | HTMLDivElement, xOffset: number, yOffset: number, tooltipTargetWidth: number, tooltipTargetHeight: number) => boolean; + +export default IsOverlappingAtTop; diff --git a/src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.native.ts b/src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.native.ts new file mode 100644 index 000000000000..17cc7200b20d --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.native.ts @@ -0,0 +1,7 @@ +import type {ViewStyle} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import positioning from '@styles/utils/positioning'; + +const tooltipPlatformStyle: ViewStyle = positioning.pAbsolute; + +export default tooltipPlatformStyle; diff --git a/src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.ts b/src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.ts new file mode 100644 index 000000000000..fd49d03b9413 --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.ts @@ -0,0 +1,7 @@ +import type {ViewStyle} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import positioning from '@styles/utils/positioning'; + +const tooltipPlatformStyle: ViewStyle = positioning.pFixed; + +export default tooltipPlatformStyle; diff --git a/src/styles/utils/generators/TooltipStyleUtils/types.ts b/src/styles/utils/generators/TooltipStyleUtils/types.ts deleted file mode 100644 index 7965ec151485..000000000000 --- a/src/styles/utils/generators/TooltipStyleUtils/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type {Animated, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; -import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment'; - -type TooltipStyles = { - animationStyle: ViewStyle; - rootWrapperStyle: ViewStyle; - textStyle: TextStyle; - pointerWrapperStyle: ViewStyle; - pointerStyle: ViewStyle; -}; - -type TooltipParams = { - tooltip: View | HTMLDivElement | null; - currentSize: Animated.Value; - windowWidth: number; - xOffset: number; - yOffset: number; - tooltipTargetWidth: number; - tooltipTargetHeight: number; - maxWidth: number; - tooltipContentWidth?: number; - tooltipWrapperHeight?: number; - manualShiftHorizontal?: number; - manualShiftVertical?: number; - shouldForceRenderingBelow?: boolean; - shouldForceRenderingLeft?: boolean; - wrapperStyle: StyleProp; - anchorAlignment?: TooltipAnchorAlignment; -}; - -type GetTooltipStylesStyleUtil = {getTooltipStyles: (props: TooltipParams) => TooltipStyles}; - -export type {TooltipStyles, TooltipParams, GetTooltipStylesStyleUtil};