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 fa58b5cd5f5..2667eca0833 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 e5a9b873dbc..c4ce198cf5a 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 bb02e17f07d..e41e4eeea26 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 662905fc1ec..35624e54d78 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 2b48fa91141..4be9c142238 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 cf2218abf5b..aba8567b212 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 ff5cfa05b57..3fe4fa6c2aa 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 846848ab25b..8e98c5b2321 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 1907309e1bf..7965ec15148 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 899e3d9e277..5ed043d36b1 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 2667eca0833..b2834bac5de 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 b9cff08d020..cb280d5b3aa 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 c4ce198cf5a..2dafbecf84d 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 3fe4fa6c2aa..3d5f712623a 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 00000000000..61c10170a9b --- /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 00000000000..339ddf30619 --- /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 00000000000..983155e811a --- /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 fa4264f45b1..00000000000 --- 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 8e98c5b2321..588b054b715 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 00000000000..fa80f447187 --- /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 00000000000..081d1a0a693 --- /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 00000000000..bdd8ff346a8 --- /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 00000000000..17cc7200b20 --- /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 00000000000..fd49d03b941 --- /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 7965ec15148..00000000000 --- 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 7703b804611..fe625bc6a52 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 1398d74bbd6..f440f672ee0 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 2b45cecbc47..4fd5a03d7af 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 c7b5125d02f..98b2245a276 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 075903d0f32..43021e936cd 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 4fd5a03d7af..f4634327640 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 0cd134d1e63..0874369c893 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 0874369c893..832b533c039 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 f4634327640..431a12d0010 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 07af25321d8..f7bf2ae12a5 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 97938aa48df..7f146c586b8 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 9fede8068e6..d6dcff6a583 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 00f37508612..c8a0d0e04b7 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 65c1a357d8f..ea3a894d034 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 e8fd0a238be..8fc15636b66 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 3019e3dfbb6..8abc2526ead 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 9fc4102bc87..d08eee6c508 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 5912649c6dc..c83d0eea693 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 8fc15636b66..13f7cf280be 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 8abc2526ead..56affbab9de 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 5d2145a0c3d..72620468ea8 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 ebffe5f9964..b2c9a92beb0 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 9bf9eb74071..39950606e3b 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 c83d0eea693..cb158150fc8 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 d43ff64d7e8..00552805a2e 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 b2c9a92beb0..f78c95960f2 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 472a94d970c..86b2e44788a 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 39950606e3b..c47c5b53544 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 c47c5b53544..ee8b7cde197 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 86b2e44788a..da16af5f5a0 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 61c10170a9b..377cb3dbdfc 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 339ddf30619..dbbdc52d5b9 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 983155e811a..bc82a9b4fbe 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 693c7fbe7d5..5af4e30f07f 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 00552805a2e..d43ff64d7e8 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 da16af5f5a0..70bdf7b2bd6 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 c761faccad3..c2081fa33bd 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 d26dd042236..4d322fe15c4 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 b53e208137e..5990ab92a2f 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 2dafbecf84d..5a6e81fa84e 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 e41e4eeea26..41f3e97c808 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 35624e54d78..41e68961890 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 cb158150fc8..f425aa0f4bd 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 aba8567b212..4165b960f32 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 5af4e30f07f..502708e1bac 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 40a9f5fe8dc..893aeab1ad1 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 5a6e81fa84e..b663875c8de 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 f425aa0f4bd..125b245d022 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 44e34ba5ff2..00000000000 --- 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 d9494927774..00000000000 --- 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 081962166ff..00000000000 --- 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 fcf5d383fa6..07dba25844e 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 b663875c8de..cf021cf074b 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 125b245d022..e291a9ab2c1 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 cf021cf074b..09c58837e4c 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 09c58837e4c..132260a93ec 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 e291a9ab2c1..e42f6b52119 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 132260a93ec..09c58837e4c 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 e42f6b52119..7a09789b17c 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 88ad2f6d5e0..a797f83b6c3 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 f1adfac3581..48e90dc7df2 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 48e90dc7df2..b5a3d290979 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 b5a3d290979..b7cd39e637c 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 7a09789b17c..6330c39caf7 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 c281c7819f1..93d3f35e25a 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 893aeab1ad1..dbfe684ea06 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 93d3f35e25a..c41bc5650f1 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 b7cd39e637c..e42f95874b4 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 6330c39caf7..08e51ad2b93 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 08e51ad2b93..b5e93a1ce59 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';