From cc1ee1fe7f44e2035f256536bc2569f51cf4885e Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 24 Jun 2024 22:34:02 +0700 Subject: [PATCH 01/34] feature: align tooltip by anchorAlignment on web --- src/components/MenuItem.tsx | 9 +-- src/components/PopoverMenu.tsx | 1 - .../BaseGenericTooltip/index.native.tsx | 3 - .../Tooltip/BaseGenericTooltip/index.tsx | 10 +++- .../Tooltip/BaseGenericTooltip/types.ts | 4 +- src/components/Tooltip/GenericTooltip.tsx | 7 ++- src/components/Tooltip/types.ts | 9 +-- .../FloatingActionButtonAndPopover.tsx | 7 ++- .../generators/TooltipStyleUtils/index.ts | 58 ++++++++++++------- .../generators/TooltipStyleUtils/types.ts | 2 + src/types/utils/AnchorAlignment.ts | 9 +++ 11 files changed, 78 insertions(+), 41 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index fa58b5cd5f5f..2667eca0833c 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -19,6 +19,7 @@ import variables from '@styles/variables'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; +import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment'; import type IconAsset from '@src/types/utils/IconAsset'; import Avatar from './Avatar'; import Badge from './Badge'; @@ -290,8 +291,8 @@ type MenuItemBaseProps = { /** Whether to show the tooltip */ shouldRenderTooltip?: boolean; - /** Whether to align the tooltip left */ - shouldForceRenderingTooltipLeft?: boolean; + /** Anchor alignment of the tooltip */ + tooltipAnchorAlignment?: TooltipAnchorAlignment; /** Additional styles for tooltip wrapper */ tooltipWrapperStyle?: StyleProp; @@ -383,7 +384,7 @@ function MenuItem( onBlur, avatarID, shouldRenderTooltip = false, - shouldForceRenderingTooltipLeft = false, + tooltipAnchorAlignment, tooltipWrapperStyle = {}, renderTooltipContent, }: MenuItemProps, @@ -490,7 +491,7 @@ function MenuItem( )} diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index e5a9b873dbc0..c4ce198cf5ae 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -25,7 +25,6 @@ function BaseGenericTooltip({ maxWidth = 0, renderTooltipContent, shouldForceRenderingBelow = false, - shouldForceRenderingLeft = false, wrapperStyle = {}, }: BaseGenericTooltipProps) { // The width of tooltip's inner content. Has to be undefined in the beginning @@ -66,7 +65,6 @@ function BaseGenericTooltip({ manualShiftHorizontal: shiftHorizontal, manualShiftVertical: shiftVertical, shouldForceRenderingBelow, - shouldForceRenderingLeft, wrapperStyle, }), [ @@ -83,7 +81,6 @@ function BaseGenericTooltip({ shiftHorizontal, shiftVertical, shouldForceRenderingBelow, - shouldForceRenderingLeft, wrapperStyle, ], ); diff --git a/src/components/Tooltip/BaseGenericTooltip/index.tsx b/src/components/Tooltip/BaseGenericTooltip/index.tsx index bb02e17f07d9..e41e4eeea26f 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'; import {Animated, View} from 'react-native'; import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; +import CONST from '@src/CONST'; import textRef from '@src/types/utils/textRef'; import viewRef from '@src/types/utils/viewRef'; import type {BaseGenericTooltipProps} from './types'; @@ -27,7 +28,10 @@ function BaseGenericTooltip({ renderTooltipContent, shouldForceRenderingBelow = false, wrapperStyle = {}, - shouldForceRenderingLeft = false, + anchorAlignment = { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + }, }: BaseGenericTooltipProps) { // The width of tooltip's inner content. Has to be undefined in the beginning // as a width of 0 will cause the content to be rendered of a width of 0, @@ -63,7 +67,7 @@ function BaseGenericTooltip({ manualShiftHorizontal: shiftHorizontal, manualShiftVertical: shiftVertical, shouldForceRenderingBelow, - shouldForceRenderingLeft, + anchorAlignment, wrapperStyle, }), [ @@ -80,7 +84,7 @@ function BaseGenericTooltip({ shiftHorizontal, shiftVertical, shouldForceRenderingBelow, - shouldForceRenderingLeft, + anchorAlignment, wrapperStyle, ], ); diff --git a/src/components/Tooltip/BaseGenericTooltip/types.ts b/src/components/Tooltip/BaseGenericTooltip/types.ts index 662905fc1ec6..35624e54d78f 100644 --- a/src/components/Tooltip/BaseGenericTooltip/types.ts +++ b/src/components/Tooltip/BaseGenericTooltip/types.ts @@ -1,5 +1,5 @@ import type {Animated} from 'react-native'; -import type TooltipProps from '@components/Tooltip/types'; +import type {SharedTooltipProps} from '@components/Tooltip/types'; type BaseGenericTooltipProps = { /** Window width */ @@ -27,7 +27,7 @@ type BaseGenericTooltipProps = { /** Any additional amount to manually adjust the vertical position of the tooltip. A positive value shifts the tooltip down, and a negative value shifts it up. */ shiftVertical?: number; -} & Pick; +} & Pick; // eslint-disable-next-line import/prefer-default-export export type {BaseGenericTooltipProps}; diff --git a/src/components/Tooltip/GenericTooltip.tsx b/src/components/Tooltip/GenericTooltip.tsx index 2b48fa91141f..4be9c1422380 100644 --- a/src/components/Tooltip/GenericTooltip.tsx +++ b/src/components/Tooltip/GenericTooltip.tsx @@ -29,7 +29,10 @@ function GenericTooltip({ shiftVertical = 0, shouldForceRenderingBelow = false, wrapperStyle = {}, - shouldForceRenderingLeft = false, + anchorAlignment = { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + }, shouldForceAnimate = false, }: GenericTooltipProps) { const {preferredLocale} = useLocalize(); @@ -164,7 +167,7 @@ function GenericTooltip({ key={[text, ...renderTooltipContentKey, preferredLocale].join('-')} shouldForceRenderingBelow={shouldForceRenderingBelow} wrapperStyle={wrapperStyle} - shouldForceRenderingLeft={shouldForceRenderingLeft} + anchorAlignment={anchorAlignment} /> )} diff --git a/src/components/Tooltip/types.ts b/src/components/Tooltip/types.ts index cf2218abf5b3..aba8567b2125 100644 --- a/src/components/Tooltip/types.ts +++ b/src/components/Tooltip/types.ts @@ -1,6 +1,7 @@ import type {ReactNode} from 'react'; import type React from 'react'; import type {LayoutRectangle, StyleProp, ViewStyle} from 'react-native'; +import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; type SharedTooltipProps = { @@ -27,8 +28,8 @@ type SharedTooltipProps = { /** Unique key of renderTooltipContent to rerender the tooltip when one of the key changes */ renderTooltipContentKey?: string[]; - /** Whether to left align the tooltip relative to wrapped component */ - shouldForceRenderingLeft?: boolean; + /** The anchor alignment of the tooltip */ + anchorAlignment?: TooltipAnchorAlignment; /** Whether to display tooltip below the wrapped component */ shouldForceRenderingBelow?: boolean; @@ -64,7 +65,7 @@ type TooltipProps = ChildrenProps & shouldHandleScroll?: boolean; }; -type EducationalTooltipProps = ChildrenProps & TooltipProps; +type EducationalTooltipProps = ChildrenProps & SharedTooltipProps; type TooltipExtendedProps = (EducationalTooltipProps | TooltipProps) & { /** Whether the actual Tooltip should be rendered. If false, it's just going to return the children */ @@ -72,4 +73,4 @@ type TooltipExtendedProps = (EducationalTooltipProps | TooltipProps) & { }; export default TooltipProps; -export type {EducationalTooltipProps, GenericTooltipProps, TooltipExtendedProps}; +export type {EducationalTooltipProps, GenericTooltipProps, SharedTooltipProps, TooltipExtendedProps}; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index ff5cfa05b57b..3fe4fa6c2aa6 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -471,8 +471,11 @@ function FloatingActionButtonAndPopover( numberOfLinesDescription: 1, onSelected: () => interceptAnonymousUser(() => navigateToQuickAction()), shouldShowSubscriptRightAvatar: ReportUtils.isPolicyExpenseChat(quickActionReport), - shouldRenderTooltip: quickAction?.isFirstQuickAction, - shouldForceRenderingTooltipLeft: true, + shouldRenderTooltip: true, + tooltipAnchorAlignment: { + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, + }, renderTooltipContent: renderQuickActionTooltip, tooltipWrapperStyle: styles.quickActionTooltipWrapper, }, diff --git a/src/styles/utils/generators/TooltipStyleUtils/index.ts b/src/styles/utils/generators/TooltipStyleUtils/index.ts index 846848ab25bd..8e98c5b23218 100644 --- a/src/styles/utils/generators/TooltipStyleUtils/index.ts +++ b/src/styles/utils/generators/TooltipStyleUtils/index.ts @@ -11,6 +11,7 @@ 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. @@ -120,7 +121,7 @@ function isOverlappingAtTop(tooltip: View | HTMLDivElement, xOffset: number, yOf * @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 [anchorAlignment] - Align tooltip anchor horizontally and vertically. * @param [wrapperStyle] - Any additional styles for the root wrapper. */ const createTooltipStyleUtils: StyleUtilGenerator = ({theme, styles}) => ({ @@ -138,7 +139,10 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( manualShiftHorizontal = 0, manualShiftVertical = 0, shouldForceRenderingBelow = false, - shouldForceRenderingLeft = false, + anchorAlignment = { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + }, wrapperStyle = {}, }) => { const customWrapperStyle = StyleSheet.flatten(wrapperStyle); @@ -171,7 +175,8 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( shouldShowBelow = shouldForceRenderingBelow || yOffset - tooltipHeight - POINTER_HEIGHT < GUTTER_WIDTH + titleBarHeight || - !!(tooltip && isOverlappingAtTop(tooltip, xOffset, yOffset, tooltipTargetWidth, tooltipTargetHeight)); + !!(tooltip && isOverlappingAtTop(tooltip, xOffset, yOffset, tooltipTargetWidth, tooltipTargetHeight)) || + anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP; // When the tooltip size is ready, we can start animating the scale. scale = currentSize; @@ -202,22 +207,6 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( : // We need to shift the tooltip up above the component. So shift the tooltip up (-) by... 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 horizontal shift (left or right) computed above to keep it out of the gutters. - // 3) Lastly, add the manual horizontal shift passed in as a parameter. - rootWrapperLeft = xOffset + (shouldForceRenderingLeft ? 0 : tooltipTargetWidth / 2 - tooltipWidth / 2) + horizontalShift + manualShiftHorizontal; - // By default, the pointer's top-left will align with the top-left of the tooltip wrapper. // // To align it vertically, we'll: @@ -228,6 +217,22 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( // so that the bottom of the pointer lines up with the top of the tooltip pointerWrapperTop = shouldShowBelow ? -POINTER_HEIGHT : tooltipHeight; + // Horizontal tooltip position: + // 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: + // 1) Add the horizontal shift (left or right) computed above to keep it out of the gutters. + // 2) Add the manual horizontal shift passed in as a parameter. + // 3a) Horizontally align left: No need for shifting. + // 3b) 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. + + // Horizontal pointer position: // 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: @@ -237,7 +242,20 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( // so the pointer's center lines up with the tooltipWidth's center. // - Remove the wrapper's horizontalShift to maintain the pointer // at the center of the hovered component. - pointerWrapperLeft = shouldForceRenderingLeft ? POINTER_WIDTH / 2 : horizontalShiftPointer + (tooltipWidth / 2 - POINTER_WIDTH / 2); + rootWrapperLeft = xOffset + horizontalShift + manualShiftHorizontal; + switch (anchorAlignment.horizontal) { + case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT: + pointerWrapperLeft = POINTER_WIDTH / 2; + break; + case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT: + pointerWrapperLeft = horizontalShiftPointer + (tooltipWidth - POINTER_WIDTH * 1.5); + rootWrapperLeft += tooltipTargetWidth - tooltipWidth; + break; + case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER: + default: + pointerWrapperLeft = horizontalShiftPointer + (tooltipWidth / 2 - POINTER_WIDTH / 2); + rootWrapperLeft += tooltipTargetWidth / 2 - tooltipWidth / 2; + } pointerAdditionalStyle = shouldShowBelow ? styles.flipUpsideDown : {}; } diff --git a/src/styles/utils/generators/TooltipStyleUtils/types.ts b/src/styles/utils/generators/TooltipStyleUtils/types.ts index 1907309e1bf5..7965ec151485 100644 --- a/src/styles/utils/generators/TooltipStyleUtils/types.ts +++ b/src/styles/utils/generators/TooltipStyleUtils/types.ts @@ -1,4 +1,5 @@ import type {Animated, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment'; type TooltipStyles = { animationStyle: ViewStyle; @@ -24,6 +25,7 @@ type TooltipParams = { shouldForceRenderingBelow?: boolean; shouldForceRenderingLeft?: boolean; wrapperStyle: StyleProp; + anchorAlignment?: TooltipAnchorAlignment; }; type GetTooltipStylesStyleUtil = {getTooltipStyles: (props: TooltipParams) => TooltipStyles}; diff --git a/src/types/utils/AnchorAlignment.ts b/src/types/utils/AnchorAlignment.ts index 899e3d9e277b..5ed043d36b18 100644 --- a/src/types/utils/AnchorAlignment.ts +++ b/src/types/utils/AnchorAlignment.ts @@ -9,4 +9,13 @@ type AnchorAlignment = { vertical: ValueOf; }; +type TooltipAnchorAlignment = { + /** The horizontal anchor alignment of the tooltip */ + horizontal: ValueOf; + + /** The vertical anchor alignment of the tooltip */ + vertical: Exclude, typeof CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.CENTER>; +}; + +export type {TooltipAnchorAlignment}; export default AnchorAlignment; From f6c3290613c3c6cf321b648ac9c0e8bdedacce47 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 24 Jun 2024 23:42:35 +0700 Subject: [PATCH 02/34] 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}; From cfd72f13feaaf1b66d7347f26a00a03ead656b49 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 3 Jul 2024 17:18:36 +0700 Subject: [PATCH 03/34] apply tooltip to lhn row --- .../LHNOptionsList/OptionRowLHN.tsx | 348 ++++++++++-------- .../BaseEducationalTooltip.tsx | 22 +- 2 files changed, 197 insertions(+), 173 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 7703b804611a..fe625bc6a52d 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -14,6 +14,7 @@ import PressableWithSecondaryInteraction from '@components/PressableWithSecondar import SubscriptAvatar from '@components/SubscriptAvatar'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; +import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -56,6 +57,16 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti }, []), ); + const renderGBRTooltip = useCallback( + () => ( + + {translate('quickAction.tooltip.title')} + {translate('quickAction.tooltip.subtitle')} + + ), + [styles.quickActionTooltipTitle, styles.quickActionTooltipSubtitle, translate], + ); + const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT; const sidebarInnerRowStyle = StyleSheet.flatten( isInFocusMode @@ -140,176 +151,189 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti shouldShowErrorMessages={false} needsOffscreenAlphaCompositing > - - {(hovered) => ( - { - Performance.markStart(CONST.TIMING.OPEN_REPORT); + + + + {(hovered) => ( + { + Performance.markStart(CONST.TIMING.OPEN_REPORT); - event?.preventDefault(); - // Enable Composer to focus on clicking the same chat after opening the context menu. - ReportActionComposeFocusManager.focus(); - onSelectRow(optionItem, popoverAnchor); - }} - onMouseDown={(event) => { - // Allow composer blur on right click - if (!event) { - return; - } + event?.preventDefault(); + // Enable Composer to focus on clicking the same chat after opening the context menu. + ReportActionComposeFocusManager.focus(); + onSelectRow(optionItem, popoverAnchor); + }} + onMouseDown={(event) => { + // Allow composer blur on right click + if (!event) { + return; + } - // Prevent composer blur on left click - event.preventDefault(); - }} - testID={optionItem.reportID} - onSecondaryInteraction={(event) => { - showPopover(event); - // Ensure that we blur the composer when opening context menu, so that only one component is focused at a time - if (DomUtils.getActiveElement()) { - (DomUtils.getActiveElement() as HTMLElement | null)?.blur(); - } - }} - withoutFocusOnSecondaryInteraction - activeOpacity={0.8} - style={[ - styles.flexRow, - styles.alignItemsCenter, - styles.justifyContentBetween, - styles.sidebarLink, - styles.sidebarLinkInnerLHN, - StyleUtils.getBackgroundColorStyle(theme.sidebar), - isFocused ? styles.sidebarLinkActive : null, - (hovered || isContextMenuActive) && !isFocused ? styles.sidebarLinkHover : null, - ]} - role={CONST.ROLE.BUTTON} - accessibilityLabel={translate('accessibilityHints.navigatesToChat')} - onLayout={onLayout} - needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} - > - - - {!!optionItem.icons?.length && - (optionItem.shouldShowSubscript ? ( - - ) : ( - - ))} - - - - {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( - - )} - {isStatusVisible && ( - - {emojiCode} - + // Prevent composer blur on left click + event.preventDefault(); + }} + testID={optionItem.reportID} + onSecondaryInteraction={(event) => { + showPopover(event); + // Ensure that we blur the composer when opening context menu, so that only one component is focused at a time + if (DomUtils.getActiveElement()) { + (DomUtils.getActiveElement() as HTMLElement | null)?.blur(); + } + }} + withoutFocusOnSecondaryInteraction + activeOpacity={0.8} + style={[ + styles.flexRow, + styles.alignItemsCenter, + styles.justifyContentBetween, + styles.sidebarLink, + styles.sidebarLinkInnerLHN, + StyleUtils.getBackgroundColorStyle(theme.sidebar), + isFocused ? styles.sidebarLinkActive : null, + (hovered || isContextMenuActive) && !isFocused ? styles.sidebarLinkHover : null, + ]} + role={CONST.ROLE.BUTTON} + accessibilityLabel={translate('accessibilityHints.navigatesToChat')} + onLayout={onLayout} + needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} + > + + + {!!optionItem.icons?.length && + (optionItem.shouldShowSubscript ? ( + + ) : ( + + ))} + + + + {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( + + )} + {isStatusVisible && ( + + {emojiCode} + + )} + + {optionItem.alternateText ? ( + + {parseHtmlToText(optionItem.alternateText)} + + ) : null} + + {optionItem?.descriptiveText ? ( + + {optionItem.descriptiveText} + + ) : null} + {hasBrickError && ( + + + )} - {optionItem.alternateText ? ( - - {parseHtmlToText(optionItem.alternateText)} - - ) : null} - - {optionItem?.descriptiveText ? ( - - {optionItem.descriptiveText} - - ) : null} - {hasBrickError && ( - - - - )} - - - - {shouldShowGreenDotIndicator && ( - - - )} - {hasDraftComment && optionItem.isAllowedToComment && ( - - - )} - {!shouldShowGreenDotIndicator && !hasBrickError && optionItem.isPinned && ( - - + {shouldShowGreenDotIndicator && ( + + + + )} + {hasDraftComment && optionItem.isAllowedToComment && ( + + + + )} + {!shouldShowGreenDotIndicator && !hasBrickError && optionItem.isPinned && ( + + + + )} - )} - - - )} - + + )} + + + ); } diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index 1398d74bbd67..f440f672ee04 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -22,17 +22,17 @@ function BaseEducationalTooltip({children, ...props}: TooltipProps) { [], ); - // Automatically hide tooltip after 5 seconds - useEffect(() => { - if (!hideTooltipRef.current) { - return; - } - - const intervalID = setInterval(hideTooltipRef.current, 5000); - return () => { - clearInterval(intervalID); - }; - }, []); + // // Automatically hide tooltip after 5 seconds + // useEffect(() => { + // if (!hideTooltipRef.current) { + // return; + // } + + // const intervalID = setInterval(hideTooltipRef.current, 5000); + // return () => { + // clearInterval(intervalID); + // }; + // }, []); return ( Date: Fri, 12 Jul 2024 11:27:06 +0700 Subject: [PATCH 04/34] add copy and icon --- .../LHNOptionsList/OptionRowLHN.tsx | 25 +++++++++++++++---- src/languages/en.ts | 1 + src/languages/es.ts | 1 + 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 2b45cecbc477..4fd5a03d7afb 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -59,12 +59,26 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const renderGBRTooltip = useCallback( () => ( - - {translate('quickAction.tooltip.title')} - {translate('quickAction.tooltip.subtitle')} - + + + {translate('sidebarScreen.tooltip')} + ), - [styles.quickActionTooltipTitle, styles.quickActionTooltipSubtitle, translate], + [ + styles.alignItemsCenter, + styles.flexRow, + styles.justifyContentCenter, + styles.flexWrap, + styles.textAlignCenter, + styles.gap1, + styles.quickActionTooltipSubtitle, + theme.tooltipHighlightText, + translate, + ], ); const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT; @@ -159,6 +173,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, }} shiftHorizontal={-20} + shiftVertical={-50} wrapperStyle={styles.quickActionTooltipWrapper} > diff --git a/src/languages/en.ts b/src/languages/en.ts index c7b5125d02fa..98b2245a276c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -604,6 +604,7 @@ export default { listOfChatMessages: 'List of chat messages', listOfChats: 'List of chats', saveTheWorld: 'Save the world', + tooltip: 'Get started here!', }, allSettingsScreen: { subscriptions: 'Subscriptions', diff --git a/src/languages/es.ts b/src/languages/es.ts index 075903d0f324..43021e936cda 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -597,6 +597,7 @@ export default { listOfChatMessages: 'Lista de mensajes del chat', listOfChats: 'lista de chats', saveTheWorld: 'Salvar el mundo', + tooltip: '¡Comienza aquí!', }, allSettingsScreen: { subscriptions: 'Suscripciones', From e26e63c636e7f2846d1233c97af6f27e3cb78a26 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 15 Jul 2024 15:06:30 +0700 Subject: [PATCH 05/34] hide tooltip when navigate --- .../LHNOptionsList/OptionRowLHN.tsx | 5 +-- .../BaseEducationalTooltip.tsx | 33 ++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 4fd5a03d7afb..f46343276400 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -44,6 +44,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID || -1}`); + const [isFirstTimeNewExpensifyUser] = useOnyx(ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER); const {translate} = useLocalize(); const [isContextMenuActive, setIsContextMenuActive] = useState(false); @@ -166,14 +167,14 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti needsOffscreenAlphaCompositing > diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index 0cd134d1e63c..0874369c8935 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -1,4 +1,5 @@ -import React, {memo, useEffect, useRef} from 'react'; +import {useNavigation} from '@react-navigation/native'; +import React, {memo, useCallback, useEffect, useRef} from 'react'; import type {LayoutEvent} from 'react-native'; import GenericTooltip from '@components/Tooltip/GenericTooltip'; import type TooltipProps from '@components/Tooltip/types'; @@ -9,26 +10,28 @@ import getBounds from './getBounds'; * This tooltip would show immediately without user's interaction and hide after 5 seconds. */ function BaseEducationalTooltip({children, ...props}: TooltipProps) { + const navigation = useNavigation(); const hideTooltipRef = useRef<() => void>(); - useEffect( - () => () => { - if (!hideTooltipRef.current) { - return; - } + const triggerHideTooltip = useCallback(() => { + if (!hideTooltipRef.current) { + return; + } - hideTooltipRef.current(); - }, - [], - ); + hideTooltipRef.current(); + }, []); + + useEffect(() => { + const unsubscribeBlur = navigation.addListener('blur', triggerHideTooltip); + return () => { + unsubscribeBlur(); + triggerHideTooltip(); + }; + }, [navigation, triggerHideTooltip]); // // Automatically hide tooltip after 5 seconds // useEffect(() => { - // if (!hideTooltipRef.current) { - // return; - // } - - // const intervalID = setInterval(hideTooltipRef.current, 5000); + // const intervalID = setInterval(triggerHideTooltip, 5000); // return () => { // clearInterval(intervalID); // }; From da051d6f2b2b9102bfef501ec59bbe6cce2e6e54 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 15 Jul 2024 16:04:36 +0700 Subject: [PATCH 06/34] fix test & perf --- .../Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index 0874369c8935..832b533c0392 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -24,7 +24,7 @@ function BaseEducationalTooltip({children, ...props}: TooltipProps) { useEffect(() => { const unsubscribeBlur = navigation.addListener('blur', triggerHideTooltip); return () => { - unsubscribeBlur(); + unsubscribeBlur?.(); triggerHideTooltip(); }; }, [navigation, triggerHideTooltip]); From c8829de06e6e4407bcd07553a764f95322f5de41 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 15 Jul 2024 16:09:39 +0700 Subject: [PATCH 07/34] revert: apply tooltip to lhn row --- .../LHNOptionsList/OptionRowLHN.tsx | 364 ++++++++---------- 1 file changed, 162 insertions(+), 202 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index f46343276400..431a12d00106 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -14,7 +14,6 @@ import PressableWithSecondaryInteraction from '@components/PressableWithSecondar import SubscriptAvatar from '@components/SubscriptAvatar'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; -import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -44,7 +43,6 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID || -1}`); - const [isFirstTimeNewExpensifyUser] = useOnyx(ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER); const {translate} = useLocalize(); const [isContextMenuActive, setIsContextMenuActive] = useState(false); @@ -58,30 +56,6 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti }, []), ); - const renderGBRTooltip = useCallback( - () => ( - - - {translate('sidebarScreen.tooltip')} - - ), - [ - styles.alignItemsCenter, - styles.flexRow, - styles.justifyContentCenter, - styles.flexWrap, - styles.textAlignCenter, - styles.gap1, - styles.quickActionTooltipSubtitle, - theme.tooltipHighlightText, - translate, - ], - ); - const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT; const sidebarInnerRowStyle = StyleSheet.flatten( isInFocusMode @@ -166,190 +140,176 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti shouldShowErrorMessages={false} needsOffscreenAlphaCompositing > - - - - {(hovered) => ( - { - Performance.markStart(CONST.TIMING.OPEN_REPORT); + + {(hovered) => ( + { + Performance.markStart(CONST.TIMING.OPEN_REPORT); - event?.preventDefault(); - // Enable Composer to focus on clicking the same chat after opening the context menu. - ReportActionComposeFocusManager.focus(); - onSelectRow(optionItem, popoverAnchor); - }} - onMouseDown={(event) => { - // Allow composer blur on right click - if (!event) { - return; - } + event?.preventDefault(); + // Enable Composer to focus on clicking the same chat after opening the context menu. + ReportActionComposeFocusManager.focus(); + onSelectRow(optionItem, popoverAnchor); + }} + onMouseDown={(event) => { + // Allow composer blur on right click + if (!event) { + return; + } - // Prevent composer blur on left click - event.preventDefault(); - }} - testID={optionItem.reportID} - onSecondaryInteraction={(event) => { - showPopover(event); - // Ensure that we blur the composer when opening context menu, so that only one component is focused at a time - if (DomUtils.getActiveElement()) { - (DomUtils.getActiveElement() as HTMLElement | null)?.blur(); - } - }} - withoutFocusOnSecondaryInteraction - activeOpacity={0.8} - style={[ - styles.flexRow, - styles.alignItemsCenter, - styles.justifyContentBetween, - styles.sidebarLink, - styles.sidebarLinkInnerLHN, - StyleUtils.getBackgroundColorStyle(theme.sidebar), - isFocused ? styles.sidebarLinkActive : null, - (hovered || isContextMenuActive) && !isFocused ? styles.sidebarLinkHover : null, - ]} - role={CONST.ROLE.BUTTON} - accessibilityLabel={translate('accessibilityHints.navigatesToChat')} - onLayout={onLayout} - needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} - > - - - {!!optionItem.icons?.length && - (optionItem.shouldShowSubscript ? ( - - ) : ( - - ))} - - - - {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( - - )} - {isStatusVisible && ( - - {emojiCode} - - )} - - {optionItem.alternateText ? ( - - {Parser.htmlToText(optionItem.alternateText)} - - ) : null} - - {optionItem?.descriptiveText ? ( - - {optionItem.descriptiveText} - - ) : null} - {hasBrickError && ( - - - + // Prevent composer blur on left click + event.preventDefault(); + }} + testID={optionItem.reportID} + onSecondaryInteraction={(event) => { + showPopover(event); + // Ensure that we blur the composer when opening context menu, so that only one component is focused at a time + if (DomUtils.getActiveElement()) { + (DomUtils.getActiveElement() as HTMLElement | null)?.blur(); + } + }} + withoutFocusOnSecondaryInteraction + activeOpacity={0.8} + style={[ + styles.flexRow, + styles.alignItemsCenter, + styles.justifyContentBetween, + styles.sidebarLink, + styles.sidebarLinkInnerLHN, + StyleUtils.getBackgroundColorStyle(theme.sidebar), + isFocused ? styles.sidebarLinkActive : null, + (hovered || isContextMenuActive) && !isFocused ? styles.sidebarLinkHover : null, + ]} + role={CONST.ROLE.BUTTON} + accessibilityLabel={translate('accessibilityHints.navigatesToChat')} + onLayout={onLayout} + needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} + > + + + {!!optionItem.icons?.length && + (optionItem.shouldShowSubscript ? ( + + ) : ( + + ))} + + + + {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( + + )} + {isStatusVisible && ( + + {emojiCode} + )} + {optionItem.alternateText ? ( + + {Parser.htmlToText(optionItem.alternateText)} + + ) : null} + + {optionItem?.descriptiveText ? ( + + {optionItem.descriptiveText} + + ) : null} + {hasBrickError && ( + + + + )} + + + + {shouldShowGreenDotIndicator && ( + + + )} + {hasDraftComment && optionItem.isAllowedToComment && ( - {shouldShowGreenDotIndicator && ( - - - - )} - {hasDraftComment && optionItem.isAllowedToComment && ( - - - - )} - {!shouldShowGreenDotIndicator && !hasBrickError && optionItem.isPinned && ( - - - - )} + + + )} + {!shouldShowGreenDotIndicator && !hasBrickError && optionItem.isPinned && ( + + - - )} - - - + )} + + + )} + ); } From 5de4ac771b0374226c8d5c16885819bec87e48e2 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 15 Jul 2024 16:27:45 +0700 Subject: [PATCH 08/34] implement tooltip ui --- src/languages/en.ts | 4 + src/languages/es.ts | 4 + .../ReportActionCompose.tsx | 246 +++++++++++------- src/styles/index.ts | 6 + 4 files changed, 160 insertions(+), 100 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 07af25321d8e..f7bf2ae12a56 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -506,6 +506,10 @@ export default { emoji: 'Emoji', collapse: 'Collapse', expand: 'Expand', + tooltip: { + title: 'Get started!', + subtitle: ' Submit your first expense', + }, }, reportActionContextMenu: { copyToClipboard: 'Copy to clipboard', diff --git a/src/languages/es.ts b/src/languages/es.ts index 97938aa48dfd..7f146c586b8f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -497,6 +497,10 @@ export default { emoji: 'Emoji', collapse: 'Colapsar', expand: 'Expandir', + tooltip: { + title: 'Get started!', + subtitle: ' Submit your first expense', + }, }, reportActionContextMenu: { copyToClipboard: 'Copiar al portapapeles', diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 9fede8068e64..d6dcff6a5831 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -10,16 +10,21 @@ import type {FileObject} from '@components/AttachmentModal'; import AttachmentModal from '@components/AttachmentModal'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; import type {Mention} from '@components/MentionSuggestions'; import OfflineIndicator from '@components/OfflineIndicator'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails} from '@components/OnyxProvider'; +import Text from '@components/Text'; +import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useDebounce from '@hooks/useDebounce'; import useHandleExceedMaxCommentLength from '@hooks/useHandleExceedMaxCommentLength'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; @@ -118,6 +123,7 @@ function ReportActionCompose({ onComposerFocus, onComposerBlur, }: ReportActionComposeProps) { + const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); const {isMediumScreenWidth, isSmallScreenWidth} = useWindowDimensions(); @@ -377,6 +383,34 @@ function ReportActionCompose({ return reportActionComposeHeight - emojiOffsetWithComposeBox - CONST.MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM; }, [styles]); + const renderWorkspaceChatTooltip = useCallback( + () => ( + + + + {translate('reportActionCompose.tooltip.title')} + {translate('reportActionCompose.tooltip.subtitle')} + + + ), + [ + styles.alignItemsCenter, + styles.flexRow, + styles.justifyContentCenter, + styles.flexWrap, + styles.textAlignCenter, + styles.gap1, + styles.quickActionTooltipTitle, + styles.quickActionTooltipSubtitle, + theme.tooltipHighlightText, + translate, + ], + ); + return ( @@ -388,110 +422,122 @@ function ReportActionCompose({ style={isComposerFullSize ? styles.chatItemFullComposeRow : {}} contentContainerStyle={isComposerFullSize ? styles.flex1 : {}} > - - setIsAttachmentPreviewActive(true)} - onModalHide={onAttachmentPreviewClose} + - {({displayFileInModal}) => ( - <> - { - isNextModalWillOpenRef.current = false; - restoreKeyboardState(); - }} - onMenuClosed={restoreKeyboardState} - onAddActionPressed={onAddActionPressed} - onItemSelected={onItemSelected} - actionButtonRef={actionButtonRef} - /> - { - if (value.length === 0 && isComposerFullSize) { - Report.setIsComposerFullSize(reportID, false); - } - validateCommentMaxLength(value, {reportID}); - }} - /> - { - if (isAttachmentPreviewActive) { - return; - } - const data = event.dataTransfer?.items[0]; - displayFileInModal(data as unknown as FileObject); - }} - /> - + setIsAttachmentPreviewActive(true)} + onModalHide={onAttachmentPreviewClose} + > + {({displayFileInModal}) => ( + <> + { + isNextModalWillOpenRef.current = false; + restoreKeyboardState(); + }} + onMenuClosed={restoreKeyboardState} + onAddActionPressed={onAddActionPressed} + onItemSelected={onItemSelected} + actionButtonRef={actionButtonRef} + /> + { + if (value.length === 0 && isComposerFullSize) { + Report.setIsComposerFullSize(reportID, false); + } + validateCommentMaxLength(value, {reportID}); + }} + /> + { + if (isAttachmentPreviewActive) { + return; + } + const data = event.dataTransfer?.items[0]; + displayFileInModal(data as unknown as FileObject); + }} + /> + + )} + + {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( + composerRef.current?.replaceSelectionWithText(...args)} + emojiPickerID={report?.reportID} + shiftVertical={emojiShiftVertical} + /> )} - - {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( - composerRef.current?.replaceSelectionWithText(...args)} - emojiPickerID={report?.reportID} - shiftVertical={emojiShiftVertical} + - )} - - + + ...wordBreak.breakWord, }, + reportActionComposeTooltipWrapper: { + backgroundColor: theme.tooltipHighlightBG, + paddingVertical: 8, + borderRadius: variables.componentBorderRadiusMedium, + }, + quickActionTooltipWrapper: { backgroundColor: theme.tooltipHighlightBG, }, From 578834f8c2a998cd52ee40a6e5504b84c2bd68f9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 26 Jul 2024 16:22:33 +0700 Subject: [PATCH 09/34] add nvp --- src/ONYXKEYS.ts | 4 ++++ src/languages/en.ts | 1 - src/languages/es.ts | 3 +-- src/libs/actions/User.ts | 20 +++++++++++++++++++ .../ReportActionCompose.tsx | 11 +++++++++- src/types/onyx/WorkspaceTooltip.ts | 9 +++++++++ src/types/onyx/index.ts | 2 ++ 7 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 src/types/onyx/WorkspaceTooltip.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 00f37508612d..c8a0d0e04b7d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -201,6 +201,9 @@ const ONYXKEYS = { /** The end date (epoch timestamp) of the workspace owner’s grace period after the free trial ends. */ NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END: 'nvp_private_billingGracePeriodEnd', + /** The NVP containing all information related to educational tooltip in workspace chat */ + NVP_WORKSPACE_TOOLTIP: 'workspaceTooltip', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -855,6 +858,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_BILLING_FUND_ID]: number; [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; + [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip; [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; }; diff --git a/src/languages/en.ts b/src/languages/en.ts index 65c1a357d8f2..ea3a894d034f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -617,7 +617,6 @@ export default { listOfChatMessages: 'List of chat messages', listOfChats: 'List of chats', saveTheWorld: 'Save the world', - tooltip: 'Get started here!', }, allSettingsScreen: { subscription: 'Subscription', diff --git a/src/languages/es.ts b/src/languages/es.ts index e8fd0a238be9..8fc15636b662 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -506,7 +506,7 @@ export default { collapse: 'Colapsar', expand: 'Expandir', tooltip: { - title: 'Get started!', + title: 'Comenzar!', subtitle: ' Submit your first expense', }, }, @@ -610,7 +610,6 @@ export default { listOfChatMessages: 'Lista de mensajes del chat', listOfChats: 'lista de chats', saveTheWorld: 'Salvar el mundo', - tooltip: '¡Comienza aquí!', }, allSettingsScreen: { subscription: 'Suscripcion', diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 3019e3dfbb6c..8abc2526ead4 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1033,6 +1033,25 @@ function dismissTrackTrainingModal() { }); } +function dismissWorkspaceTooltip() { + const parameters: SetNameValuePairParams = { + name: ONYXKEYS.NVP_WORKSPACE_TOOLTIP, + value: false, + }; + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_WORKSPACE_TOOLTIP, + value: {shouldShow: false}, + }, + ]; + + API.write(WRITE_COMMANDS.SET_NAME_VALUE_PAIR, parameters, { + optimisticData, + }); +} + function requestRefund() { API.write(WRITE_COMMANDS.REQUEST_REFUND, null); } @@ -1042,6 +1061,7 @@ export { closeAccount, dismissReferralBanner, dismissTrackTrainingModal, + dismissWorkspaceTooltip, resendValidateCode, requestContactMethodValidateCode, updateNewsletterSubscription, diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 9fc4102bc879..d08eee6c508a 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -74,6 +74,9 @@ type ReportActionComposeOnyxProps = { /** Whether the composer input should be shown */ shouldShowComposeInput: OnyxEntry; + + /** Whether to show educational tooltip in workspace chat for first-time user */ + workspaceTooltip: OnyxEntry; }; type ReportActionComposeProps = ReportActionComposeOnyxProps & @@ -110,6 +113,7 @@ const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); function ReportActionCompose({ blockedFromConcierge, currentUserPersonalDetails, + workspaceTooltip, disabled = false, isComposerFullSize = false, onSubmit, @@ -383,6 +387,8 @@ function ReportActionCompose({ return reportActionComposeHeight - emojiOffsetWithComposeBox - CONST.MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM; }, [styles]); + const shouldShowEducationalTooltip = !!workspaceTooltip?.shouldShow; + const renderWorkspaceChatTooltip = useCallback( () => ( @@ -424,7 +430,7 @@ function ReportActionCompose({ contentContainerStyle={isComposerFullSize ? styles.flex1 : {}} > Date: Fri, 26 Jul 2024 18:11:09 +0700 Subject: [PATCH 10/34] hide tooltip when press add or navigate to other pages --- .../BaseEducationalTooltip.tsx | 26 +++++-------------- .../ReportActionCompose.tsx | 13 +++++++++- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index 5912649c6dcd..c83d0eea6935 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -1,5 +1,4 @@ -import {useNavigation} from '@react-navigation/native'; -import React, {memo, useCallback, useEffect, useRef} from 'react'; +import React, {memo, useEffect, useRef} from 'react'; import type {LayoutChangeEvent} from 'react-native'; import GenericTooltip from '@components/Tooltip/GenericTooltip'; import type TooltipProps from '@components/Tooltip/types'; @@ -10,32 +9,19 @@ import getBounds from './getBounds'; * This tooltip would show immediately without user's interaction and hide after 5 seconds. */ function BaseEducationalTooltip({children, ...props}: TooltipProps) { - const navigation = useNavigation(); const hideTooltipRef = useRef<() => void>(); - const triggerHideTooltip = useCallback(() => { + // Automatically hide tooltip after 5 seconds + useEffect(() => { if (!hideTooltipRef.current) { return; } - hideTooltipRef.current(); - }, []); - - useEffect(() => { - const unsubscribeBlur = navigation.addListener('blur', triggerHideTooltip); + const intervalID = setInterval(hideTooltipRef.current, 5000); return () => { - unsubscribeBlur?.(); - triggerHideTooltip(); + clearInterval(intervalID); }; - }, [navigation, triggerHideTooltip]); - - // // Automatically hide tooltip after 5 seconds - // useEffect(() => { - // const intervalID = setInterval(triggerHideTooltip, 5000); - // return () => { - // clearInterval(intervalID); - // }; - // }, []); + }, []); return ( { + setShouldShowEducationalTooltip(false); if (!willBlurTextInputOnTapOutside) { isKeyboardVisibleWhenShowingModalRef.current = !!composerRef.current?.isFocused(); } @@ -387,7 +391,14 @@ function ReportActionCompose({ return reportActionComposeHeight - emojiOffsetWithComposeBox - CONST.MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM; }, [styles]); - const shouldShowEducationalTooltip = !!workspaceTooltip?.shouldShow; + useEffect(() => { + const unsubscribeBlur = navigation.addListener('blur', () => setShouldShowEducationalTooltip(false)); + return unsubscribeBlur; + }, [navigation]); + + useEffect(() => { + setShouldShowEducationalTooltip(!!workspaceTooltip?.shouldShow); + }, [workspaceTooltip?.shouldShow]); const renderWorkspaceChatTooltip = useCallback( () => ( From 74406413e141ea3f13d462c244fb2b88483d5460 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 26 Jul 2024 18:11:22 +0700 Subject: [PATCH 11/34] update Spanish copy --- src/languages/es.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 8fc15636b662..13f7cf280be8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -506,8 +506,8 @@ export default { collapse: 'Colapsar', expand: 'Expandir', tooltip: { - title: 'Comenzar!', - subtitle: ' Submit your first expense', + title: '¡Empecemos!', + subtitle: ' Presenta tu primer gasto', }, }, reportActionContextMenu: { From 0051bf1f23d6c8043960ad268a54873af3ceaf68 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 30 Jul 2024 02:31:25 +0700 Subject: [PATCH 12/34] dismiss tooltip --- src/libs/actions/User.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 8abc2526ead4..56affbab9de6 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1034,22 +1034,7 @@ function dismissTrackTrainingModal() { } function dismissWorkspaceTooltip() { - const parameters: SetNameValuePairParams = { - name: ONYXKEYS.NVP_WORKSPACE_TOOLTIP, - value: false, - }; - - const optimisticData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.NVP_WORKSPACE_TOOLTIP, - value: {shouldShow: false}, - }, - ]; - - API.write(WRITE_COMMANDS.SET_NAME_VALUE_PAIR, parameters, { - optimisticData, - }); + Onyx.merge(ONYXKEYS.NVP_WORKSPACE_TOOLTIP, {shouldShow: false}); } function requestRefund() { From 1c9b6c818fdf420b6720b00c61dc29b8cc25354e Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 30 Jul 2024 04:43:40 +0700 Subject: [PATCH 13/34] refactor shouldShow logic --- src/pages/home/ReportScreen.tsx | 8 +++++ .../ReportActionCompose.tsx | 33 ++++++++----------- src/pages/home/report/ReportFooter.tsx | 6 ++++ 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 5d2145a0c3d4..72620468ea8f 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -67,6 +67,9 @@ type ReportScreenOnyxProps = { /** The report metadata loading states */ reportMetadata: OnyxEntry; + + /** Whether to show educational tooltip in workspace chat for first-time user */ + workspaceTooltip: OnyxEntry; }; type OnyxHOCProps = { @@ -118,6 +121,7 @@ function ReportScreen({ }, markReadyForHydration, policies = {}, + workspaceTooltip, isSidebarLoaded = false, currentReportID = '', navigation, @@ -834,6 +838,7 @@ function ReportScreen({ isComposerFullSize={!!isComposerFullSize} isEmptyChat={isEmptyChat} lastReportAction={lastReportAction} + workspaceTooltip={workspaceTooltip} /> ) : null} @@ -871,6 +876,9 @@ export default withCurrentReportID( key: ONYXKEYS.COLLECTION.POLICY, allowStaleData: true, }, + workspaceTooltip: { + key: ONYXKEYS.NVP_WORKSPACE_TOOLTIP, + }, }, true, )( diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index ebffe5f99649..b2c9a92beb0d 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -75,9 +75,6 @@ type ReportActionComposeOnyxProps = { /** Whether the composer input should be shown */ shouldShowComposeInput: OnyxEntry; - - /** Whether to show educational tooltip in workspace chat for first-time user */ - workspaceTooltip: OnyxEntry; }; type ReportActionComposeProps = ReportActionComposeOnyxProps & @@ -103,6 +100,9 @@ type ReportActionComposeProps = ReportActionComposeOnyxProps & /** Should the input be disabled */ disabled?: boolean; + + /** Should show educational tooltip */ + shouldShowEducationalTooltip?: boolean; }; // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will @@ -114,7 +114,6 @@ const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); function ReportActionCompose({ blockedFromConcierge, currentUserPersonalDetails, - workspaceTooltip, disabled = false, isComposerFullSize = false, onSubmit, @@ -125,6 +124,7 @@ function ReportActionCompose({ isReportReadyForDisplay = true, isEmptyChat, lastReportAction, + shouldShowEducationalTooltip, onComposerFocus, onComposerBlur, }: ReportActionComposeProps) { @@ -146,7 +146,7 @@ function ReportActionCompose({ return shouldFocusInputOnScreenFocus && shouldShowComposeInput && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible; }); const [isFullComposerAvailable, setIsFullComposerAvailable] = useState(isComposerFullSize); - const [shouldShowEducationalTooltip, setShouldShowEducationalTooltip] = useState(!!workspaceTooltip?.shouldShow); + const [shouldHideEducationalTooltip, setShouldHideEducationalTooltip] = useState(!shouldShowEducationalTooltip); // A flag to indicate whether the onScroll callback is likely triggered by a layout change (caused by text change) or not const isScrollLikelyLayoutTriggered = useRef(false); @@ -259,7 +259,7 @@ function ReportActionCompose({ ); const onAddActionPressed = useCallback(() => { - setShouldShowEducationalTooltip(false); + setShouldHideEducationalTooltip(true); if (!willBlurTextInputOnTapOutside) { isKeyboardVisibleWhenShowingModalRef.current = !!composerRef.current?.isFocused(); } @@ -392,14 +392,10 @@ function ReportActionCompose({ }, [styles]); useEffect(() => { - const unsubscribeBlur = navigation.addListener('blur', () => setShouldShowEducationalTooltip(false)); + const unsubscribeBlur = navigation.addListener('blur', () => setShouldHideEducationalTooltip(true)); return unsubscribeBlur; }, [navigation]); - useEffect(() => { - setShouldShowEducationalTooltip(!!workspaceTooltip?.shouldShow); - }, [workspaceTooltip?.shouldShow]); - const renderWorkspaceChatTooltip = useCallback( () => ( @@ -441,7 +437,7 @@ function ReportActionCompose({ contentContainerStyle={isComposerFullSize ? styles.flex1 : {}} > { - if (isNavigating) { - return; - } - focus(); - }} + if (isNavigating) { + return; + } + focus(); + }} onEmojiSelected={(...args) => composerRef.current?.replaceSelectionWithText(...args)} emojiPickerID={report?.reportID} shiftVertical={emojiShiftVertical} @@ -589,9 +585,6 @@ export default withCurrentUserPersonalDetails( shouldShowComposeInput: { key: ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, }, - workspaceTooltip: { - key: ONYXKEYS.NVP_WORKSPACE_TOOLTIP, - }, })(memo(ReportActionCompose)), ); diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 9bf9eb74071f..39950606e3b8 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -43,6 +43,9 @@ type ReportFooterProps = { /** The last report action */ lastReportAction?: OnyxEntry; + /** Whether to show educational tooltip in workspace chat for first-time user */ + workspaceTooltip: OnyxEntry; + /** Whether the chat is empty */ isEmptyChat?: boolean; @@ -71,6 +74,7 @@ function ReportFooter({ isEmptyChat = true, isReportReadyForDisplay = true, isComposerFullSize = false, + workspaceTooltip, onComposerBlur, onComposerFocus, }: ReportFooterProps) { @@ -110,6 +114,7 @@ function ReportFooter({ const isSystemChat = ReportUtils.isSystemChat(report); const isAdminsOnlyPostingRoom = ReportUtils.isAdminsOnlyPostingRoom(report); const isUserPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); + const shouldShowEducationalTooltip = !!workspaceTooltip?.shouldShow && !isUserPolicyAdmin; const allPersonalDetails = usePersonalDetails(); @@ -210,6 +215,7 @@ function ReportFooter({ pendingAction={pendingAction} isComposerFullSize={isComposerFullSize} isReportReadyForDisplay={isReportReadyForDisplay} + shouldShowEducationalTooltip={shouldShowEducationalTooltip} /> From 7b204cba9c69d9c284ac8d21cdd97ea957c31e90 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 30 Jul 2024 16:50:39 +0700 Subject: [PATCH 14/34] hide tooltip on navigating --- .../EducationalTooltip/BaseEducationalTooltip.tsx | 11 +++++++++++ .../Tooltip/EducationalTooltip/index.tsx | 15 +++++++++++++-- .../ReportActionCompose/ReportActionCompose.tsx | 7 ------- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index c83d0eea6935..cb158150fc88 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -11,6 +11,17 @@ import getBounds from './getBounds'; function BaseEducationalTooltip({children, ...props}: TooltipProps) { const hideTooltipRef = useRef<() => void>(); + useEffect( + () => () => { + if (!hideTooltipRef.current) { + return; + } + + hideTooltipRef.current(); + }, + [], + ); + // Automatically hide tooltip after 5 seconds useEffect(() => { if (!hideTooltipRef.current) { diff --git a/src/components/Tooltip/EducationalTooltip/index.tsx b/src/components/Tooltip/EducationalTooltip/index.tsx index d43ff64d7e8e..00552805a2e6 100644 --- a/src/components/Tooltip/EducationalTooltip/index.tsx +++ b/src/components/Tooltip/EducationalTooltip/index.tsx @@ -1,9 +1,20 @@ -import React from 'react'; +import {useNavigation} from '@react-navigation/native'; +import React, {useEffect, useState} from 'react'; import type {TooltipExtendedProps} from '@components/Tooltip/types'; import BaseEducationalTooltip from './BaseEducationalTooltip'; function EducationalTooltip({shouldRender = true, children, ...props}: TooltipExtendedProps) { - if (!shouldRender) { + const navigation = useNavigation(); + const [shouldHide, setShouldHide] = useState(false); + + useEffect(() => { + const unsubscribe = navigation.addListener('blur', () => { + setShouldHide(true); + }); + return unsubscribe; + }, [navigation]); + + if (!shouldRender || shouldHide) { return children; } diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index b2c9a92beb0d..f78c95960f23 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -1,4 +1,3 @@ -import {useNavigation} from '@react-navigation/native'; import type {SyntheticEvent} from 'react'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputFocusEventData, TextInputSelectionChangeEventData} from 'react-native'; @@ -131,7 +130,6 @@ function ReportActionCompose({ const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); - const navigation = useNavigation(); const {isMediumScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const {isOffline} = useNetwork(); const animatedRef = useAnimatedRef(); @@ -391,11 +389,6 @@ function ReportActionCompose({ return reportActionComposeHeight - emojiOffsetWithComposeBox - CONST.MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM; }, [styles]); - useEffect(() => { - const unsubscribeBlur = navigation.addListener('blur', () => setShouldHideEducationalTooltip(true)); - return unsubscribeBlur; - }, [navigation]); - const renderWorkspaceChatTooltip = useCallback( () => ( From 1d1599e7d73043bdfac8ce3e9466e96320814f12 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 31 Jul 2024 17:30:31 +0700 Subject: [PATCH 15/34] fix: tooltip position on safari is incorrect when the page is transitioning --- .../home/report/ReportActionCompose/ReportActionCompose.tsx | 2 +- src/pages/home/report/ReportFooter.tsx | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 472a94d970c9..86b2e44788ab 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -147,7 +147,7 @@ function ReportActionCompose({ return shouldFocusInputOnScreenFocus && shouldShowComposeInput && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible; }); const [isFullComposerAvailable, setIsFullComposerAvailable] = useState(isComposerFullSize); - const [shouldHideEducationalTooltip, setShouldHideEducationalTooltip] = useState(!shouldShowEducationalTooltip); + const [shouldHideEducationalTooltip, setShouldHideEducationalTooltip] = useState(false); // A flag to indicate whether the onScroll callback is likely triggered by a layout change (caused by text change) or not const isScrollLikelyLayoutTriggered = useRef(false); diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 39950606e3b8..c47c5b53544d 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -29,6 +29,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import ReportActionCompose from './ReportActionCompose/ReportActionCompose'; import SystemChatReportFooterMessage from './SystemChatReportFooterMessage'; +import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; type ReportFooterProps = { /** Report object for the current report */ @@ -83,6 +84,7 @@ function ReportFooter({ const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus(); const [shouldShowComposeInput] = useOnyx(ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, {initialValue: false}); const [isAnonymousUser = false] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.authTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS}); @@ -215,7 +217,7 @@ function ReportFooter({ pendingAction={pendingAction} isComposerFullSize={isComposerFullSize} isReportReadyForDisplay={isReportReadyForDisplay} - shouldShowEducationalTooltip={shouldShowEducationalTooltip} + shouldShowEducationalTooltip={didScreenTransitionEnd && shouldShowEducationalTooltip} /> @@ -235,5 +237,6 @@ export default memo( prevProps.isEmptyChat === nextProps.isEmptyChat && prevProps.lastReportAction === nextProps.lastReportAction && prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay && + prevProps.workspaceTooltip?.shouldShow === nextProps.workspaceTooltip?.shouldShow && lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata), ); From 99fc8bc95f91e31c7fb360ad41c525bbd991458a Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 31 Jul 2024 17:31:15 +0700 Subject: [PATCH 16/34] fix lint --- src/pages/home/report/ReportFooter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index c47c5b53544d..ee8b7cde1971 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -14,6 +14,7 @@ import SwipeableView from '@components/SwipeableView'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Log from '@libs/Log'; @@ -29,7 +30,6 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import ReportActionCompose from './ReportActionCompose/ReportActionCompose'; import SystemChatReportFooterMessage from './SystemChatReportFooterMessage'; -import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; type ReportFooterProps = { /** Report object for the current report */ From 4a799809929381b8765864fa76032b9e2376f81b Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 31 Jul 2024 17:36:27 +0700 Subject: [PATCH 17/34] define shift in variables --- .../home/report/ReportActionCompose/ReportActionCompose.tsx | 5 +++-- src/styles/variables.ts | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 86b2e44788ab..da16af5f5a0a 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -36,6 +36,7 @@ import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutsi import ParticipantLocalTime from '@pages/home/report/ParticipantLocalTime'; import ReportDropUI from '@pages/home/report/ReportDropUI'; import ReportTypingIndicator from '@pages/home/report/ReportTypingIndicator'; +import variables from '@styles/variables'; import * as EmojiPickerActions from '@userActions/EmojiPickerAction'; import * as Report from '@userActions/Report'; import * as User from '@userActions/User'; @@ -436,8 +437,8 @@ function ReportActionCompose({ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }} wrapperStyle={styles.reportActionComposeTooltipWrapper} - shiftHorizontal={10} - shiftVertical={-10} + shiftHorizontal={variables.composerTooltipShiftHorizontal} + shiftVertical={variables.composerTooltipShiftVertical} > Date: Wed, 31 Jul 2024 17:39:36 +0700 Subject: [PATCH 18/34] export type --- .../TooltipStyleUtils/computeHorizontalShift/index.native.ts | 2 +- .../TooltipStyleUtils/computeHorizontalShift/index.ts | 2 +- .../TooltipStyleUtils/computeHorizontalShift/types.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.native.ts b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.native.ts index 61c10170a9b7..377cb3dbdfc0 100644 --- a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.native.ts +++ b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.native.ts @@ -1,4 +1,4 @@ -import type ComputeHorizontalShift from './types'; +import type {ComputeHorizontalShift} from './types'; const computeHorizontalShift: ComputeHorizontalShift = () => 0; diff --git a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts index 339ddf306197..dbbdc52d5b9a 100644 --- a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts +++ b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts @@ -1,6 +1,6 @@ import roundToNearestMultipleOfFour from '@libs/roundToNearestMultipleOfFour'; import variables from '@styles/variables'; -import type ComputeHorizontalShift from './types'; +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. */ diff --git a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts index 983155e811aa..bc82a9b4fbe4 100644 --- a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts +++ b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts @@ -1,3 +1,4 @@ type ComputeHorizontalShift = (windowWidth: number, xOffset: number, componentWidth: number, tooltipWidth: number, manualShiftHorizontal: number) => number; -export default ComputeHorizontalShift; +// eslint-disable-next-line import/prefer-default-export +export type {ComputeHorizontalShift}; From 6bf01c9209d61f2b9576400f2a232d33e03384ff Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 1 Aug 2024 14:41:23 +0700 Subject: [PATCH 19/34] get onyx workspaceTooltip --- src/pages/home/ReportScreen.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 693c7fbe7d5d..5af4e30f07f1 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -125,6 +125,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro selector: (parentReportActions) => getParentReportAction(parentReportActions, reportOnyx?.parentReportActionID ?? ''), }); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP); + const [workspaceTooltip] = useOnyx(ONYXKEYS.NVP_WORKSPACE_TOOLTIP); const wasLoadingApp = usePrevious(isLoadingApp); const finishedLoadingApp = wasLoadingApp && !isLoadingApp; const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(parentReportAction); From ed6c7edfa8f7590c2bbfc7cb8292118107f7161d Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 1 Aug 2024 15:39:54 +0700 Subject: [PATCH 20/34] fix: navigation object couldn't be found --- .../Tooltip/EducationalTooltip/index.tsx | 15 ++------------- .../ReportActionCompose/ReportActionCompose.tsx | 9 +++++++++ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/Tooltip/EducationalTooltip/index.tsx b/src/components/Tooltip/EducationalTooltip/index.tsx index 00552805a2e6..d43ff64d7e8e 100644 --- a/src/components/Tooltip/EducationalTooltip/index.tsx +++ b/src/components/Tooltip/EducationalTooltip/index.tsx @@ -1,20 +1,9 @@ -import {useNavigation} from '@react-navigation/native'; -import React, {useEffect, useState} from 'react'; +import React from 'react'; import type {TooltipExtendedProps} from '@components/Tooltip/types'; import BaseEducationalTooltip from './BaseEducationalTooltip'; function EducationalTooltip({shouldRender = true, children, ...props}: TooltipExtendedProps) { - const navigation = useNavigation(); - const [shouldHide, setShouldHide] = useState(false); - - useEffect(() => { - const unsubscribe = navigation.addListener('blur', () => { - setShouldHide(true); - }); - return unsubscribe; - }, [navigation]); - - if (!shouldRender || shouldHide) { + if (!shouldRender) { return children; } diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index da16af5f5a0a..70bdf7b2bd6b 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -1,3 +1,4 @@ +import {useNavigation} from '@react-navigation/native'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputFocusEventData, TextInputSelectionChangeEventData} from 'react-native'; import {View} from 'react-native'; @@ -139,6 +140,7 @@ function ReportActionCompose({ const {isOffline} = useNetwork(); const actionButtonRef = useRef(null); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; + const navigation = useNavigation(); /** * Updates the Highlight state of the composer @@ -351,6 +353,13 @@ function ReportActionCompose({ [], ); + useEffect(() => { + const unsubscribe = navigation.addListener('blur', () => { + setShouldHideEducationalTooltip(true); + }); + return unsubscribe; + }, [navigation]); + // When we invite someone to a room they don't have the policy object, but we still want them to be able to mention other reports they are members of, so we only check if the policyID in the report is from a workspace const isGroupPolicyReport = useMemo(() => !!report?.policyID && report.policyID !== CONST.POLICY.ID_FAKE, [report]); const reportRecipientAcountIDs = ReportUtils.getReportRecipientAccountIDs(report, currentUserPersonalDetails.accountID); From fdacac4bfdb18ba3ea74636805e996f2fe113ccb Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 2 Aug 2024 10:43:34 +0700 Subject: [PATCH 21/34] rename resetSuggestions to onPress --- .../TransparentOverlay/TransparentOverlay.tsx | 12 ++++++------ .../AutoCompleteSuggestionsPortal/index.native.tsx | 2 +- .../AutoCompleteSuggestionsPortal/index.tsx | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx index c761faccad39..c2081fa33bd1 100644 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx @@ -8,21 +8,21 @@ import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; type TransparentOverlayProps = { - resetSuggestions: () => void; + onPress: () => void; }; type OnPressHandler = PressableProps['onPress']; -function TransparentOverlay({resetSuggestions}: TransparentOverlayProps) { +function TransparentOverlay({onPress: onPressProp}: TransparentOverlayProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const onResetSuggestions = useCallback>( + const onPress = useCallback>( (event) => { event?.preventDefault(); - resetSuggestions(); + onPressProp(); }, - [resetSuggestions], + [onPressProp], ); const handlePointerDown = useCallback((e: PointerEvent) => { @@ -35,7 +35,7 @@ function TransparentOverlay({resetSuggestions}: TransparentOverlayProps) { style={styles.fullScreen} > ({left = 0, width = 0, bottom return ( - + {/* eslint-disable-next-line react/jsx-props-no-spreading */} diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx index d26dd0422368..4d322fe15c4e 100644 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx @@ -39,7 +39,7 @@ function AutoCompleteSuggestionsPortal({ bodyElement && ReactDOM.createPortal( <> - + {componentToRender} , bodyElement, From 27fd8ddb2b9dcf35b36178cf4ec739fefce0c9f2 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 2 Aug 2024 11:14:25 +0700 Subject: [PATCH 22/34] implement press anywhere to dismiss tooltip --- src/components/MenuItem.tsx | 1 + .../BaseGenericTooltip/index.native.tsx | 70 +++++++++---------- .../Tooltip/BaseGenericTooltip/index.tsx | 24 ++++--- .../Tooltip/BaseGenericTooltip/types.ts | 5 +- .../BaseEducationalTooltip.tsx | 8 +-- src/components/Tooltip/GenericTooltip.tsx | 4 ++ src/components/Tooltip/types.ts | 12 +++- src/pages/home/ReportScreen.tsx | 1 + .../ReportActionCompose.tsx | 2 + 9 files changed, 74 insertions(+), 53 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index b53e208137eb..5990ab92a2ff 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -531,6 +531,7 @@ function MenuItem( wrapperStyle={tooltipWrapperStyle} shiftHorizontal={tooltipShiftHorizontal} shiftVertical={tooltipShiftVertical} + shouldAutoDismiss > diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index 2dafbecf84d0..5a6e81fa84e8 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -1,7 +1,9 @@ -import React, {useEffect, useMemo, useRef, useState} from 'react'; +import {Portal} from '@gorhom/portal'; +import React, {useMemo, useRef, useState} from 'react'; import {Animated, View} from 'react-native'; // eslint-disable-next-line no-restricted-imports -import type {Text as RNText, View as RNView} from 'react-native'; +import type {View as RNView} from 'react-native'; +import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay'; import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; import CONST from '@src/CONST'; @@ -31,6 +33,8 @@ function BaseGenericTooltip({ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }, wrapperStyle = {}, + shouldUseOverlay = false, + onPressOverlay = () => {}, }: BaseGenericTooltipProps) { // The width of tooltip's inner content. Has to be undefined in the beginning // as a width of 0 will cause the content to be rendered of a width of 0, @@ -39,29 +43,18 @@ function BaseGenericTooltip({ // The height of tooltip's wrapper. const [wrapperMeasuredHeight, setWrapperMeasuredHeight] = useState(); - const textContentRef = useRef(null); - const viewContentRef = useRef(null); const rootWrapper = useRef(null); const StyleUtils = useStyleUtils(); - // Measure content width - useEffect(() => { - if (!textContentRef.current && !viewContentRef.current) { - return; - } - const contentRef = viewContentRef.current ?? textContentRef.current; - contentRef?.measure((x, y, width) => setContentMeasuredWidth(width)); - }, []); - const {animationStyle, rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo( () => StyleUtils.getTooltipStyles({ tooltip: rootWrapper.current, currentSize: animation, windowWidth, - xOffset, - yOffset, + xOffset: xOffset - windowWidth, + yOffset: yOffset - targetHeight - 12, tooltipTargetWidth: targetWidth, tooltipTargetHeight: targetHeight, maxWidth, @@ -94,40 +87,41 @@ function BaseGenericTooltip({ let content; if (renderTooltipContent) { - content = {renderTooltipContent()}; + content = {renderTooltipContent()}; } else { content = ( - - {text} - + {text} ); } return ( - { - const {height} = e.nativeEvent.layout; - if (height === wrapperMeasuredHeight) { - return; - } - setWrapperMeasuredHeight(height); - }} - > - {content} - - - - + + {shouldUseOverlay && } + { + const {height} = e.nativeEvent.layout; + if (height === wrapperMeasuredHeight) { + return; + } + setWrapperMeasuredHeight(height); + e.target.measure((x, y, width) => { + setContentMeasuredWidth(width); + }); + }} + > + {content} + + + + + ); } diff --git a/src/components/Tooltip/BaseGenericTooltip/index.tsx b/src/components/Tooltip/BaseGenericTooltip/index.tsx index e41e4eeea26f..41f3e97c8087 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.tsx @@ -1,6 +1,7 @@ import React, {useLayoutEffect, useMemo, useRef, useState} from 'react'; import ReactDOM from 'react-dom'; import {Animated, View} from 'react-native'; +import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay'; import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; import CONST from '@src/CONST'; @@ -32,6 +33,8 @@ function BaseGenericTooltip({ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }, + shouldUseOverlay = false, + onPressOverlay = () => {}, }: BaseGenericTooltipProps) { // The width of tooltip's inner content. Has to be undefined in the beginning // as a width of 0 will cause the content to be rendered of a width of 0, @@ -115,15 +118,18 @@ function BaseGenericTooltip({ } return ReactDOM.createPortal( - - {content} - - - - , + <> + {shouldUseOverlay && } + + {content} + + + + + , body, ); } diff --git a/src/components/Tooltip/BaseGenericTooltip/types.ts b/src/components/Tooltip/BaseGenericTooltip/types.ts index 35624e54d78f..41e689618902 100644 --- a/src/components/Tooltip/BaseGenericTooltip/types.ts +++ b/src/components/Tooltip/BaseGenericTooltip/types.ts @@ -27,7 +27,10 @@ type BaseGenericTooltipProps = { /** Any additional amount to manually adjust the vertical position of the tooltip. A positive value shifts the tooltip down, and a negative value shifts it up. */ shiftVertical?: number; -} & Pick; +} & Pick< + SharedTooltipProps, + 'renderTooltipContent' | 'maxWidth' | 'numberOfLines' | 'text' | 'shouldForceRenderingBelow' | 'wrapperStyle' | 'anchorAlignment' | 'shouldUseOverlay' | 'onPressOverlay' +>; // eslint-disable-next-line import/prefer-default-export export type {BaseGenericTooltipProps}; diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index cb158150fc88..f425aa0f4bdb 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -1,14 +1,14 @@ import React, {memo, useEffect, useRef} from 'react'; import type {LayoutChangeEvent} from 'react-native'; import GenericTooltip from '@components/Tooltip/GenericTooltip'; -import type TooltipProps from '@components/Tooltip/types'; +import type {EducationalTooltipProps} from '@components/Tooltip/types'; import getBounds from './getBounds'; /** * A component used to wrap an element intended for displaying a tooltip. * This tooltip would show immediately without user's interaction and hide after 5 seconds. */ -function BaseEducationalTooltip({children, ...props}: TooltipProps) { +function BaseEducationalTooltip({children, shouldAutoDismiss = false, ...props}: EducationalTooltipProps) { const hideTooltipRef = useRef<() => void>(); useEffect( @@ -24,7 +24,7 @@ function BaseEducationalTooltip({children, ...props}: TooltipProps) { // Automatically hide tooltip after 5 seconds useEffect(() => { - if (!hideTooltipRef.current) { + if (!hideTooltipRef.current || !shouldAutoDismiss) { return; } @@ -32,7 +32,7 @@ function BaseEducationalTooltip({children, ...props}: TooltipProps) { return () => { clearInterval(intervalID); }; - }, []); + }, [shouldAutoDismiss]); return ( {}, }: GenericTooltipProps) { const {preferredLocale} = useLocalize(); const {windowWidth} = useWindowDimensions(); @@ -168,6 +170,8 @@ function GenericTooltip({ shouldForceRenderingBelow={shouldForceRenderingBelow} wrapperStyle={wrapperStyle} anchorAlignment={anchorAlignment} + shouldUseOverlay={shouldUseOverlay} + onPressOverlay={onPressOverlay} /> )} diff --git a/src/components/Tooltip/types.ts b/src/components/Tooltip/types.ts index aba8567b2125..4165b960f322 100644 --- a/src/components/Tooltip/types.ts +++ b/src/components/Tooltip/types.ts @@ -36,6 +36,12 @@ type SharedTooltipProps = { /** Additional styles for tooltip wrapper view */ wrapperStyle?: StyleProp; + + /** Should render a fullscreen transparent overlay */ + shouldUseOverlay?: boolean; + + /** Callback to fire when the transparent overlay is pressed */ + onPressOverlay?: () => void; }; type GenericTooltipState = { @@ -65,7 +71,11 @@ type TooltipProps = ChildrenProps & shouldHandleScroll?: boolean; }; -type EducationalTooltipProps = ChildrenProps & SharedTooltipProps; +type EducationalTooltipProps = ChildrenProps & + SharedTooltipProps & { + /** Whether to automatically dismiss the tooltip after 5 seconds */ + shouldAutoDismiss?: boolean; + }; type TooltipExtendedProps = (EducationalTooltipProps | TooltipProps) & { /** Whether the actual Tooltip should be rendered. If false, it's just going to return the children */ diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 5af4e30f07f1..502708e1bac9 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -815,6 +815,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro ) : null} + diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 40a9f5fe8dc2..893aeab1ad17 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -442,6 +442,8 @@ function ReportActionCompose({ setShouldHideEducationalTooltip(true)} anchorAlignment={{ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, From 5c4b81299af694c63d280e6e875f497e7255e74f Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 2 Aug 2024 14:36:33 +0700 Subject: [PATCH 23/34] fix tooltip position on native --- .../Tooltip/BaseGenericTooltip/index.native.tsx | 6 ++++-- .../EducationalTooltip/BaseEducationalTooltip.tsx | 13 ++++++++++--- .../EducationalTooltip/getBounds/index.native.ts | 6 ------ .../Tooltip/EducationalTooltip/getBounds/index.ts | 6 ------ .../Tooltip/EducationalTooltip/getBounds/types.ts | 5 ----- .../utils/generators/TooltipStyleUtils/index.ts | 5 ++++- 6 files changed, 18 insertions(+), 23 deletions(-) delete mode 100644 src/components/Tooltip/EducationalTooltip/getBounds/index.native.ts delete mode 100644 src/components/Tooltip/EducationalTooltip/getBounds/index.ts delete mode 100644 src/components/Tooltip/EducationalTooltip/getBounds/types.ts diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index 5a6e81fa84e8..b663875c8de8 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -6,6 +6,7 @@ import type {View as RNView} from 'react-native'; import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay'; import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {BaseGenericTooltipProps} from './types'; @@ -53,8 +54,8 @@ function BaseGenericTooltip({ tooltip: rootWrapper.current, currentSize: animation, windowWidth, - xOffset: xOffset - windowWidth, - yOffset: yOffset - targetHeight - 12, + xOffset, + yOffset: yOffset - targetHeight - variables.gutterWidth, tooltipTargetWidth: targetWidth, tooltipTargetHeight: targetHeight, maxWidth, @@ -65,6 +66,7 @@ function BaseGenericTooltip({ shouldForceRenderingBelow, anchorAlignment, wrapperStyle, + shouldAddHorizontalPadding: false, }), [ StyleUtils, diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index f425aa0f4bdb..125b245d0221 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -2,7 +2,6 @@ import React, {memo, useEffect, useRef} from 'react'; import type {LayoutChangeEvent} from 'react-native'; import GenericTooltip from '@components/Tooltip/GenericTooltip'; import type {EducationalTooltipProps} from '@components/Tooltip/types'; -import getBounds from './getBounds'; /** * A component used to wrap an element intended for displaying a tooltip. @@ -45,8 +44,16 @@ function BaseEducationalTooltip({children, shouldAutoDismiss = false, ...props}: hideTooltipRef.current = hideTooltip; return React.cloneElement(children as React.ReactElement, { onLayout: (e: LayoutChangeEvent) => { - updateTargetBounds(getBounds(e)); - showTooltip(); + const target = e.target || e.nativeEvent.target; + target?.measure((fx, fy, width, height, px, py) => { + updateTargetBounds({ + height, + width, + x: px, + y: py, + }); + showTooltip(); + }); }, }); }} diff --git a/src/components/Tooltip/EducationalTooltip/getBounds/index.native.ts b/src/components/Tooltip/EducationalTooltip/getBounds/index.native.ts deleted file mode 100644 index 44e34ba5ff21..000000000000 --- a/src/components/Tooltip/EducationalTooltip/getBounds/index.native.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type {LayoutChangeEvent} from 'react-native'; -import type GetBounds from './types'; - -const getBounds: GetBounds = (event: LayoutChangeEvent) => event.nativeEvent.layout; - -export default getBounds; diff --git a/src/components/Tooltip/EducationalTooltip/getBounds/index.ts b/src/components/Tooltip/EducationalTooltip/getBounds/index.ts deleted file mode 100644 index d94949277740..000000000000 --- a/src/components/Tooltip/EducationalTooltip/getBounds/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type {LayoutChangeEvent} from 'react-native'; -import type GetBounds from './types'; - -const getBounds: GetBounds = (event: LayoutChangeEvent) => (event.nativeEvent.target as HTMLElement).getBoundingClientRect(); - -export default getBounds; diff --git a/src/components/Tooltip/EducationalTooltip/getBounds/types.ts b/src/components/Tooltip/EducationalTooltip/getBounds/types.ts deleted file mode 100644 index 081962166ff1..000000000000 --- a/src/components/Tooltip/EducationalTooltip/getBounds/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type {LayoutChangeEvent, LayoutRectangle} from 'react-native'; - -type GetBounds = (event: LayoutChangeEvent) => LayoutRectangle; - -export default GetBounds; diff --git a/src/styles/utils/generators/TooltipStyleUtils/index.ts b/src/styles/utils/generators/TooltipStyleUtils/index.ts index fcf5d383fa69..07dba25844ea 100644 --- a/src/styles/utils/generators/TooltipStyleUtils/index.ts +++ b/src/styles/utils/generators/TooltipStyleUtils/index.ts @@ -44,6 +44,7 @@ type TooltipParams = { shouldForceRenderingBelow?: boolean; wrapperStyle: StyleProp; anchorAlignment?: TooltipAnchorAlignment; + shouldAddHorizontalPadding?: boolean; }; type GetTooltipStylesStyleUtil = {getTooltipStyles: (props: TooltipParams) => TooltipStyles}; @@ -86,6 +87,7 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( tooltipWrapperHeight, manualShiftHorizontal = 0, manualShiftVertical = 0, + shouldAddHorizontalPadding = true, shouldForceRenderingBelow = false, anchorAlignment = { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER, @@ -95,11 +97,12 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( }) => { const customWrapperStyle = StyleSheet.flatten(wrapperStyle); const tooltipVerticalPadding = spacing.pv1; + const tooltipHorizontalPadding = shouldAddHorizontalPadding ? spacing.ph2.paddingHorizontal * 2 : 0; // 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 tooltipWidth = tooltipContentWidth && tooltipContentWidth + tooltipHorizontalPadding + 1; const tooltipHeight = tooltipWrapperHeight; const isTooltipSizeReady = tooltipWidth !== undefined && tooltipHeight !== undefined; From 0591c4c5db63155aab42c6a5f427e181129a8bcc Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 2 Aug 2024 16:06:46 +0700 Subject: [PATCH 24/34] add comments --- src/components/Tooltip/BaseGenericTooltip/index.native.tsx | 2 ++ .../Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx | 1 + 2 files changed, 3 insertions(+) diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index b663875c8de8..cf021cf074ba 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -55,6 +55,8 @@ function BaseGenericTooltip({ currentSize: animation, windowWidth, xOffset, + // On native, yOffset is calculated from bottom edge of element to the top of screen + // so we need to exclude targetHeight and gutterWidth yOffset: yOffset - targetHeight - variables.gutterWidth, tooltipTargetWidth: targetWidth, tooltipTargetHeight: targetHeight, diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index 125b245d0221..e291a9ab2c19 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -44,6 +44,7 @@ function BaseEducationalTooltip({children, shouldAutoDismiss = false, ...props}: hideTooltipRef.current = hideTooltip; return React.cloneElement(children as React.ReactElement, { onLayout: (e: LayoutChangeEvent) => { + // e.target is specific to native, use e.nativeEvent.target on web instead const target = e.target || e.nativeEvent.target; target?.measure((fx, fy, width, height, px, py) => { updateTargetBounds({ From 04c4751b1e477f5e387190ff911ebf7ef6e3dbd5 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 6 Aug 2024 14:41:29 +0700 Subject: [PATCH 25/34] fix: android tooltip broken --- src/components/Tooltip/BaseGenericTooltip/index.native.tsx | 7 ++----- src/pages/home/ReportScreen.tsx | 1 - 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index cf021cf074ba..09c58837e4c6 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -6,7 +6,6 @@ import type {View as RNView} from 'react-native'; import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay'; import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; -import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {BaseGenericTooltipProps} from './types'; @@ -55,9 +54,7 @@ function BaseGenericTooltip({ currentSize: animation, windowWidth, xOffset, - // On native, yOffset is calculated from bottom edge of element to the top of screen - // so we need to exclude targetHeight and gutterWidth - yOffset: yOffset - targetHeight - variables.gutterWidth, + yOffset, tooltipTargetWidth: targetWidth, tooltipTargetHeight: targetHeight, maxWidth, @@ -104,7 +101,7 @@ function BaseGenericTooltip({ } return ( - + {shouldUseOverlay && } - From 63be42dd9f7de0a21b9df8cc76ef3ce34dad2175 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 6 Aug 2024 14:46:13 +0700 Subject: [PATCH 26/34] fix: wrong tooltip position for animated parents --- .../BaseGenericTooltip/index.native.tsx | 41 ++++++++++--------- .../BaseEducationalTooltip.tsx | 19 +++++---- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index 09c58837e4c6..132260a93ec1 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -3,6 +3,7 @@ import React, {useMemo, useRef, useState} from 'react'; import {Animated, View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {View as RNView} from 'react-native'; +import {FullWindowOverlay} from 'react-native-screens'; import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay'; import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -103,25 +104,27 @@ function BaseGenericTooltip({ return ( {shouldUseOverlay && } - { - const {height} = e.nativeEvent.layout; - if (height === wrapperMeasuredHeight) { - return; - } - setWrapperMeasuredHeight(height); - e.target.measure((x, y, width) => { - setContentMeasuredWidth(width); - }); - }} - > - {content} - - - - + + { + const {height} = e.nativeEvent.layout; + if (height === wrapperMeasuredHeight) { + return; + } + setWrapperMeasuredHeight(height); + e.target.measure((x, y, width) => { + setContentMeasuredWidth(width); + }); + }} + > + {content} + + + + + ); } diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index e291a9ab2c19..e42f6b521195 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -46,15 +46,18 @@ function BaseEducationalTooltip({children, shouldAutoDismiss = false, ...props}: onLayout: (e: LayoutChangeEvent) => { // e.target is specific to native, use e.nativeEvent.target on web instead const target = e.target || e.nativeEvent.target; - target?.measure((fx, fy, width, height, px, py) => { - updateTargetBounds({ - height, - width, - x: px, - y: py, + // When tooltip is used inside an animated view (e.g. popover), we need to wait for the animation to finish beforem measuring content. + setTimeout(() => { + target?.measure((fx, fy, width, height, px, py) => { + updateTargetBounds({ + height, + width, + x: px, + y: py, + }); + showTooltip(); }); - showTooltip(); - }); + }, 500); }, }); }} From def069f2a1fc9cce9d9f31734517e0c0527ab135 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 6 Aug 2024 15:00:12 +0700 Subject: [PATCH 27/34] revert the FullWindowOverlay approach --- .../BaseGenericTooltip/index.native.tsx | 41 +++++++++---------- .../BaseEducationalTooltip.tsx | 2 +- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index 132260a93ec1..09c58837e4c6 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -3,7 +3,6 @@ import React, {useMemo, useRef, useState} from 'react'; import {Animated, View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {View as RNView} from 'react-native'; -import {FullWindowOverlay} from 'react-native-screens'; import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay'; import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -104,27 +103,25 @@ function BaseGenericTooltip({ return ( {shouldUseOverlay && } - - { - const {height} = e.nativeEvent.layout; - if (height === wrapperMeasuredHeight) { - return; - } - setWrapperMeasuredHeight(height); - e.target.measure((x, y, width) => { - setContentMeasuredWidth(width); - }); - }} - > - {content} - - - - - + { + const {height} = e.nativeEvent.layout; + if (height === wrapperMeasuredHeight) { + return; + } + setWrapperMeasuredHeight(height); + e.target.measure((x, y, width) => { + setContentMeasuredWidth(width); + }); + }} + > + {content} + + + + ); } diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index e42f6b521195..7a09789b17c7 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -46,7 +46,7 @@ function BaseEducationalTooltip({children, shouldAutoDismiss = false, ...props}: onLayout: (e: LayoutChangeEvent) => { // e.target is specific to native, use e.nativeEvent.target on web instead const target = e.target || e.nativeEvent.target; - // When tooltip is used inside an animated view (e.g. popover), we need to wait for the animation to finish beforem measuring content. + // When tooltip is used inside an animated view (e.g. popover), we need to wait for the animation to finish before measuring content. setTimeout(() => { target?.measure((fx, fy, width, height, px, py) => { updateTargetBounds({ From a5d35b0e26aa0866f7aa288604bd022bb6bfba9a Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 6 Aug 2024 16:42:22 +0700 Subject: [PATCH 28/34] QAB --- src/components/Modal/BaseModal.tsx | 2 ++ src/components/Tooltip/BaseGenericTooltip/index.native.tsx | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 88ad2f6d5e00..a797f83b6c3b 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -1,3 +1,4 @@ +import {PortalHost} from '@gorhom/portal'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import ReactNativeModal from 'react-native-modal'; @@ -256,6 +257,7 @@ function BaseModal( customBackdrop={shouldUseCustomBackdrop ? : undefined} > + + {shouldUseOverlay && } Date: Tue, 6 Aug 2024 17:29:54 +0700 Subject: [PATCH 29/34] fix lint --- src/components/Tooltip/BaseGenericTooltip/index.native.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index f1adfac35813..48e90dc7df2a 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -3,7 +3,6 @@ import React, {useMemo, useRef, useState} from 'react'; import {Animated, View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {View as RNView} from 'react-native'; -import {FullWindowOverlay} from 'react-native-screens'; import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay'; import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; From 602bec71733bb646e14954f13189c3e7899cf66a Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 7 Aug 2024 14:22:45 +0700 Subject: [PATCH 30/34] delay measure --- .../Tooltip/BaseGenericTooltip/index.native.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index 48e90dc7df2a..b5a3d2909791 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -112,9 +112,12 @@ function BaseGenericTooltip({ return; } setWrapperMeasuredHeight(height); - e.target.measure((x, y, width) => { - setContentMeasuredWidth(width); - }); + // When tooltip is used inside an animated view (e.g. popover), we need to wait for the animation to finish before measuring content. + setTimeout(() => { + e.target.measure((x, y, width) => { + setContentMeasuredWidth(width); + }); + }, 500); }} > {content} From bef2353185bee21fcb02bb292b5e190780916f41 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 7 Aug 2024 14:45:42 +0700 Subject: [PATCH 31/34] fix: composer flickers when tap to hide tooltip --- .../Tooltip/BaseGenericTooltip/index.native.tsx | 3 ++- .../BaseEducationalTooltip.tsx | 4 ++-- src/components/Tooltip/GenericTooltip.tsx | 16 ++++++++++++++-- .../ReportActionCompose/ReportActionCompose.tsx | 3 +-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index b5a3d2909791..b7cd39e637cf 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -113,8 +113,9 @@ function BaseGenericTooltip({ } setWrapperMeasuredHeight(height); // When tooltip is used inside an animated view (e.g. popover), we need to wait for the animation to finish before measuring content. + const target = e.target; setTimeout(() => { - e.target.measure((x, y, width) => { + target.measure((x, y, width) => { setContentMeasuredWidth(width); }); }, 500); diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index 7a09789b17c7..6330c39caf78 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -27,9 +27,9 @@ function BaseEducationalTooltip({children, shouldAutoDismiss = false, ...props}: return; } - const intervalID = setInterval(hideTooltipRef.current, 5000); + const timerID = setTimeout(hideTooltipRef.current, 5000); return () => { - clearInterval(intervalID); + clearTimeout(timerID); }; }, [shouldAutoDismiss]); diff --git a/src/components/Tooltip/GenericTooltip.tsx b/src/components/Tooltip/GenericTooltip.tsx index c281c7819f1d..93d3f35e25a5 100644 --- a/src/components/Tooltip/GenericTooltip.tsx +++ b/src/components/Tooltip/GenericTooltip.tsx @@ -34,8 +34,8 @@ function GenericTooltip({ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }, shouldForceAnimate = false, - shouldUseOverlay = false, - onPressOverlay = () => {}, + shouldUseOverlay: shouldUseOverlayProp = false, + onPressOverlay: onPressOverlayProp = () => {}, }: GenericTooltipProps) { const {preferredLocale} = useLocalize(); const {windowWidth} = useWindowDimensions(); @@ -56,6 +56,9 @@ function GenericTooltip({ const [wrapperWidth, setWrapperWidth] = useState(0); const [wrapperHeight, setWrapperHeight] = useState(0); + // Transparent overlay should disappear once user taps it + const [shouldUseOverlay, setShouldUseOverlay] = useState(shouldUseOverlayProp); + // Whether the tooltip is first tooltip to activate the TooltipSense const isTooltipSenseInitiator = useRef(false); const animation = useRef(new Animated.Value(0)); @@ -141,6 +144,15 @@ function GenericTooltip({ setIsVisible(false); }, []); + const onPressOverlay = useCallback(() => { + if (!shouldUseOverlay) { + return; + } + setShouldUseOverlay(false); + hideTooltip(); + onPressOverlayProp(); + }, [shouldUseOverlay, onPressOverlayProp]); + useImperativeHandle(TooltipRefManager.ref, () => ({hideTooltip}), [hideTooltip]); // Skip the tooltip and return the children if the text is empty, we don't have a render function. diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 893aeab1ad17..dbfe684ea062 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -260,7 +260,6 @@ function ReportActionCompose({ ); const onAddActionPressed = useCallback(() => { - setShouldHideEducationalTooltip(true); if (!willBlurTextInputOnTapOutside) { isKeyboardVisibleWhenShowingModalRef.current = !!composerRef.current?.isFocused(); } @@ -443,7 +442,7 @@ function ReportActionCompose({ shouldRender={!shouldHideEducationalTooltip && shouldShowEducationalTooltip} renderTooltipContent={renderWorkspaceChatTooltip} shouldUseOverlay - onPressOverlay={() => setShouldHideEducationalTooltip(true)} + onPressOverlay={() => User.dismissWorkspaceTooltip()} anchorAlignment={{ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, From 4b6d7e482a0e3cb079764f000426edb928804134 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 7 Aug 2024 15:27:06 +0700 Subject: [PATCH 32/34] fix lint --- src/components/Tooltip/GenericTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Tooltip/GenericTooltip.tsx b/src/components/Tooltip/GenericTooltip.tsx index 93d3f35e25a5..c41bc5650f1d 100644 --- a/src/components/Tooltip/GenericTooltip.tsx +++ b/src/components/Tooltip/GenericTooltip.tsx @@ -151,7 +151,7 @@ function GenericTooltip({ setShouldUseOverlay(false); hideTooltip(); onPressOverlayProp(); - }, [shouldUseOverlay, onPressOverlayProp]); + }, [shouldUseOverlay, onPressOverlayProp, hideTooltip]); useImperativeHandle(TooltipRefManager.ref, () => ({hideTooltip}), [hideTooltip]); From d106777c29ef3ecd1fdeaae85c378080eb6e9963 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 7 Aug 2024 17:50:14 +0700 Subject: [PATCH 33/34] polish settimeout callback --- .../BaseGenericTooltip/index.native.tsx | 10 +++++---- .../BaseEducationalTooltip.tsx | 21 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index b7cd39e637cf..e42f95874b42 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -1,6 +1,6 @@ import {Portal} from '@gorhom/portal'; import React, {useMemo, useRef, useState} from 'react'; -import {Animated, View} from 'react-native'; +import {Animated, InteractionManager, View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {View as RNView} from 'react-native'; import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay'; @@ -115,10 +115,12 @@ function BaseGenericTooltip({ // When tooltip is used inside an animated view (e.g. popover), we need to wait for the animation to finish before measuring content. const target = e.target; setTimeout(() => { - target.measure((x, y, width) => { - setContentMeasuredWidth(width); + InteractionManager.runAfterInteractions(() => { + target.measure((x, y, width) => { + setContentMeasuredWidth(width); + }); }); - }, 500); + }, CONST.ANIMATED_TRANSITION); }} > {content} diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index 6330c39caf78..08e51ad2b93a 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -1,7 +1,8 @@ import React, {memo, useEffect, useRef} from 'react'; -import type {LayoutChangeEvent} from 'react-native'; +import {InteractionManager, type LayoutChangeEvent} from 'react-native'; import GenericTooltip from '@components/Tooltip/GenericTooltip'; import type {EducationalTooltipProps} from '@components/Tooltip/types'; +import CONST from '@src/CONST'; /** * A component used to wrap an element intended for displaying a tooltip. @@ -48,16 +49,18 @@ function BaseEducationalTooltip({children, shouldAutoDismiss = false, ...props}: const target = e.target || e.nativeEvent.target; // When tooltip is used inside an animated view (e.g. popover), we need to wait for the animation to finish before measuring content. setTimeout(() => { - target?.measure((fx, fy, width, height, px, py) => { - updateTargetBounds({ - height, - width, - x: px, - y: py, + InteractionManager.runAfterInteractions(() => { + target?.measure((fx, fy, width, height, px, py) => { + updateTargetBounds({ + height, + width, + x: px, + y: py, + }); + showTooltip(); }); - showTooltip(); }); - }, 500); + }, CONST.ANIMATED_TRANSITION); }, }); }} From b83cb8233c5f9e88d41c2daf7a56239fbc0a829c Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 7 Aug 2024 18:00:57 +0700 Subject: [PATCH 34/34] fix lint --- .../Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index 08e51ad2b93a..b5e93a1ce595 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -1,5 +1,6 @@ import React, {memo, useEffect, useRef} from 'react'; -import {InteractionManager, type LayoutChangeEvent} from 'react-native'; +import {InteractionManager} from 'react-native'; +import type {LayoutChangeEvent} from 'react-native'; import GenericTooltip from '@components/Tooltip/GenericTooltip'; import type {EducationalTooltipProps} from '@components/Tooltip/types'; import CONST from '@src/CONST';