Skip to content

Commit

Permalink
feature: align tooltip by anchorAlignment on web
Browse files Browse the repository at this point in the history
  • Loading branch information
tienifr committed Jun 24, 2024
1 parent 2026f62 commit cc1ee1f
Show file tree
Hide file tree
Showing 11 changed files with 78 additions and 41 deletions.
9 changes: 5 additions & 4 deletions src/components/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<ViewStyle>;
Expand Down Expand Up @@ -383,7 +384,7 @@ function MenuItem(
onBlur,
avatarID,
shouldRenderTooltip = false,
shouldForceRenderingTooltipLeft = false,
tooltipAnchorAlignment,
tooltipWrapperStyle = {},
renderTooltipContent,
}: MenuItemProps,
Expand Down Expand Up @@ -490,7 +491,7 @@ function MenuItem(
)}
<EducationalTooltip
shouldRender={shouldRenderTooltip}
shouldForceRenderingLeft={shouldForceRenderingTooltipLeft}
anchorAlignment={tooltipAnchorAlignment}
renderTooltipContent={renderTooltipContent}
wrapperStyle={tooltipWrapperStyle}
shiftHorizontal={styles.popoverMenuItem.paddingHorizontal}
Expand Down
1 change: 0 additions & 1 deletion src/components/PopoverMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,6 @@ function PopoverMenu({
success={item.success}
containerStyle={item.containerStyle}
shouldRenderTooltip={item.shouldRenderTooltip}
shouldForceRenderingTooltipLeft={item.shouldForceRenderingTooltipLeft}
tooltipWrapperStyle={item.tooltipWrapperStyle}
renderTooltipContent={item.renderTooltipContent}
/>
Expand Down
3 changes: 0 additions & 3 deletions src/components/Tooltip/BaseGenericTooltip/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -66,7 +65,6 @@ function BaseGenericTooltip({
manualShiftHorizontal: shiftHorizontal,
manualShiftVertical: shiftVertical,
shouldForceRenderingBelow,
shouldForceRenderingLeft,
wrapperStyle,
}),
[
Expand All @@ -83,7 +81,6 @@ function BaseGenericTooltip({
shiftHorizontal,
shiftVertical,
shouldForceRenderingBelow,
shouldForceRenderingLeft,
wrapperStyle,
],
);
Expand Down
10 changes: 7 additions & 3 deletions src/components/Tooltip/BaseGenericTooltip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -63,7 +67,7 @@ function BaseGenericTooltip({
manualShiftHorizontal: shiftHorizontal,
manualShiftVertical: shiftVertical,
shouldForceRenderingBelow,
shouldForceRenderingLeft,
anchorAlignment,
wrapperStyle,
}),
[
Expand All @@ -80,7 +84,7 @@ function BaseGenericTooltip({
shiftHorizontal,
shiftVertical,
shouldForceRenderingBelow,
shouldForceRenderingLeft,
anchorAlignment,
wrapperStyle,
],
);
Expand Down
4 changes: 2 additions & 2 deletions src/components/Tooltip/BaseGenericTooltip/types.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -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<TooltipProps, 'renderTooltipContent' | 'maxWidth' | 'numberOfLines' | 'text' | 'shouldForceRenderingBelow' | 'wrapperStyle' | 'shouldForceRenderingLeft'>;
} & Pick<SharedTooltipProps, 'renderTooltipContent' | 'maxWidth' | 'numberOfLines' | 'text' | 'shouldForceRenderingBelow' | 'wrapperStyle' | 'anchorAlignment'>;

// eslint-disable-next-line import/prefer-default-export
export type {BaseGenericTooltipProps};
7 changes: 5 additions & 2 deletions src/components/Tooltip/GenericTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -164,7 +167,7 @@ function GenericTooltip({
key={[text, ...renderTooltipContentKey, preferredLocale].join('-')}
shouldForceRenderingBelow={shouldForceRenderingBelow}
wrapperStyle={wrapperStyle}
shouldForceRenderingLeft={shouldForceRenderingLeft}
anchorAlignment={anchorAlignment}
/>
)}

Expand Down
9 changes: 5 additions & 4 deletions src/components/Tooltip/types.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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;
Expand Down Expand Up @@ -64,12 +65,12 @@ 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 */
shouldRender?: boolean;
};

export default TooltipProps;
export type {EducationalTooltipProps, GenericTooltipProps, TooltipExtendedProps};
export type {EducationalTooltipProps, GenericTooltipProps, SharedTooltipProps, TooltipExtendedProps};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
58 changes: 38 additions & 20 deletions src/styles/utils/generators/TooltipStyleUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<GetTooltipStylesStyleUtil> = ({theme, styles}) => ({
Expand All @@ -138,7 +139,10 @@ const createTooltipStyleUtils: StyleUtilGenerator<GetTooltipStylesStyleUtil> = (
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);
Expand Down Expand Up @@ -171,7 +175,8 @@ const createTooltipStyleUtils: StyleUtilGenerator<GetTooltipStylesStyleUtil> = (
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;
Expand Down Expand Up @@ -202,22 +207,6 @@ const createTooltipStyleUtils: StyleUtilGenerator<GetTooltipStylesStyleUtil> = (
: // 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:
Expand All @@ -228,6 +217,22 @@ const createTooltipStyleUtils: StyleUtilGenerator<GetTooltipStylesStyleUtil> = (
// 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:
Expand All @@ -237,7 +242,20 @@ const createTooltipStyleUtils: StyleUtilGenerator<GetTooltipStylesStyleUtil> = (
// 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 : {};
}
Expand Down
2 changes: 2 additions & 0 deletions src/styles/utils/generators/TooltipStyleUtils/types.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -24,6 +25,7 @@ type TooltipParams = {
shouldForceRenderingBelow?: boolean;
shouldForceRenderingLeft?: boolean;
wrapperStyle: StyleProp<ViewStyle>;
anchorAlignment?: TooltipAnchorAlignment;
};

type GetTooltipStylesStyleUtil = {getTooltipStyles: (props: TooltipParams) => TooltipStyles};
Expand Down
9 changes: 9 additions & 0 deletions src/types/utils/AnchorAlignment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,13 @@ type AnchorAlignment = {
vertical: ValueOf<typeof CONST.MODAL.ANCHOR_ORIGIN_VERTICAL>;
};

type TooltipAnchorAlignment = {
/** The horizontal anchor alignment of the tooltip */
horizontal: ValueOf<typeof CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL>;

/** The vertical anchor alignment of the tooltip */
vertical: Exclude<ValueOf<typeof CONST.MODAL.ANCHOR_ORIGIN_VERTICAL>, typeof CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.CENTER>;
};

export type {TooltipAnchorAlignment};
export default AnchorAlignment;

0 comments on commit cc1ee1f

Please sign in to comment.