Skip to content

Commit

Permalink
Merge pull request Expensify#45390 from tienifr/feature/tooltip-for-w…
Browse files Browse the repository at this point in the history
…orkspace-chat

Feature: Tooltip for workspace chat
  • Loading branch information
deetergp committed Aug 7, 2024
2 parents d8a5aaa + b83cb82 commit 6a6c242
Show file tree
Hide file tree
Showing 39 changed files with 595 additions and 523 deletions.
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,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',

Expand Down Expand Up @@ -873,6 +876,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[];
[ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflow;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<NonNullable<OnPressHandler>>(
const onPress = useCallback<NonNullable<OnPressHandler>>(
(event) => {
event?.preventDefault();
resetSuggestions();
onPressProp();
},
[resetSuggestions],
[onPressProp],
);

const handlePointerDown = useCallback((e: PointerEvent) => {
Expand All @@ -35,7 +35,7 @@ function TransparentOverlay({resetSuggestions}: TransparentOverlayProps) {
style={styles.fullScreen}
>
<PressableWithoutFeedback
onPress={onResetSuggestions}
onPress={onPress}
style={[styles.flex1, styles.cursorDefault]}
accessibilityLabel={translate('common.close')}
role={CONST.ROLE.BUTTON}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function AutoCompleteSuggestionsPortal<TSuggestion>({left = 0, width = 0, bottom

return (
<Portal hostName="suggestions">
<TransparentOverlay resetSuggestions={resetSuggestions} />
<TransparentOverlay onPress={resetSuggestions} />
<View style={styles}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<BaseAutoCompleteSuggestions<TSuggestion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function AutoCompleteSuggestionsPortal<TSuggestion>({
bodyElement &&
ReactDOM.createPortal(
<>
<TransparentOverlay resetSuggestions={resetSuggestions} />
<TransparentOverlay onPress={resetSuggestions} />
<View style={StyleUtils.getBaseAutoCompleteSuggestionContainerStyle({left, width, bottom: bottom - getBottomSuggestionPadding()})}>{componentToRender}</View>
</>,
bodyElement,
Expand Down
22 changes: 16 additions & 6 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 @@ -299,12 +300,18 @@ 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>;

/** 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;

Expand Down Expand Up @@ -398,8 +405,10 @@ function MenuItem(
onBlur,
avatarID,
shouldRenderTooltip = false,
shouldForceRenderingTooltipLeft = false,
tooltipAnchorAlignment,
tooltipWrapperStyle = {},
tooltipShiftHorizontal = 0,
tooltipShiftVertical = 0,
renderTooltipContent,
}: MenuItemProps,
ref: PressableRef,
Expand Down Expand Up @@ -521,11 +530,12 @@ function MenuItem(
)}
<EducationalTooltip
shouldRender={shouldRenderTooltip}
shouldForceRenderingLeft={shouldForceRenderingTooltipLeft}
anchorAlignment={tooltipAnchorAlignment}
renderTooltipContent={renderTooltipContent}
wrapperStyle={tooltipWrapperStyle}
shiftHorizontal={styles.popoverMenuItem.paddingHorizontal}
shiftVertical={styles.popoverMenuItem.paddingVertical / 2}
shiftHorizontal={tooltipShiftHorizontal}
shiftVertical={tooltipShiftVertical}
shouldAutoDismiss
>
<View>
<Hoverable>
Expand Down
2 changes: 2 additions & 0 deletions src/components/Modal/BaseModal.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -256,6 +257,7 @@ function BaseModal(
customBackdrop={shouldUseCustomBackdrop ? <Overlay onPress={handleBackdropPress} /> : undefined}
>
<ModalContent onDismiss={handleDismissModal}>
<PortalHost name="modal" />
<FocusTrapForModal
active={isVisible}
initialFocus={initialFocus}
Expand Down
4 changes: 3 additions & 1 deletion src/components/PopoverMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,9 @@ function PopoverMenu({
success={item.success}
containerStyle={item.containerStyle}
shouldRenderTooltip={item.shouldRenderTooltip}
shouldForceRenderingTooltipLeft={item.shouldForceRenderingTooltipLeft}
tooltipAnchorAlignment={item.tooltipAnchorAlignment}
tooltipShiftHorizontal={item.tooltipShiftHorizontal}
tooltipShiftVertical={item.tooltipShiftVertical}
tooltipWrapperStyle={item.tooltipWrapperStyle}
renderTooltipContent={item.renderTooltipContent}
numberOfLinesTitle={item.numberOfLinesTitle}
Expand Down
85 changes: 45 additions & 40 deletions src/components/Tooltip/BaseGenericTooltip/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {Animated, View} from 'react-native';
import {Portal} from '@gorhom/portal';
import React, {useMemo, useRef, useState} from 'react';
import {Animated, InteractionManager, 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';
import type {BaseGenericTooltipProps} from './types';

// Props will change frequently.
Expand All @@ -25,8 +28,13 @@ function BaseGenericTooltip({
maxWidth = 0,
renderTooltipContent,
shouldForceRenderingBelow = false,
shouldForceRenderingLeft = false,
anchorAlignment = {
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER,
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,
Expand All @@ -35,21 +43,10 @@ function BaseGenericTooltip({

// The height of tooltip's wrapper.
const [wrapperMeasuredHeight, setWrapperMeasuredHeight] = useState<number>();
const textContentRef = useRef<RNText>(null);
const viewContentRef = useRef<RNView>(null);
const rootWrapper = useRef<RNView>(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({
Expand All @@ -66,8 +63,9 @@ function BaseGenericTooltip({
manualShiftHorizontal: shiftHorizontal,
manualShiftVertical: shiftVertical,
shouldForceRenderingBelow,
shouldForceRenderingLeft,
anchorAlignment,
wrapperStyle,
shouldAddHorizontalPadding: false,
}),
[
StyleUtils,
Expand All @@ -83,47 +81,54 @@ function BaseGenericTooltip({
shiftHorizontal,
shiftVertical,
shouldForceRenderingBelow,
shouldForceRenderingLeft,
anchorAlignment,
wrapperStyle,
],
);

let content;
if (renderTooltipContent) {
content = <View ref={viewContentRef}>{renderTooltipContent()}</View>;
content = <View>{renderTooltipContent()}</View>;
} else {
content = (
<Text
numberOfLines={numberOfLines}
style={textStyle}
>
<Text
style={textStyle}
ref={textContentRef}
>
{text}
</Text>
<Text style={textStyle}>{text}</Text>
</Text>
);
}

return (
<Animated.View
ref={rootWrapper}
style={[rootWrapperStyle, animationStyle]}
onLayout={(e) => {
const {height} = e.nativeEvent.layout;
if (height === wrapperMeasuredHeight) {
return;
}
setWrapperMeasuredHeight(height);
}}
>
{content}
<View style={pointerWrapperStyle}>
<View style={pointerStyle} />
</View>
</Animated.View>
<Portal hostName={!shouldUseOverlay ? 'modal' : undefined}>
{shouldUseOverlay && <TransparentOverlay onPress={onPressOverlay} />}
<Animated.View
ref={rootWrapper}
style={[rootWrapperStyle, animationStyle]}
onLayout={(e) => {
const {height} = e.nativeEvent.layout;
if (height === wrapperMeasuredHeight) {
return;
}
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(() => {
InteractionManager.runAfterInteractions(() => {
target.measure((x, y, width) => {
setContentMeasuredWidth(width);
});
});
}, CONST.ANIMATED_TRANSITION);
}}
>
{content}
<View style={pointerWrapperStyle}>
<View style={pointerStyle} />
</View>
</Animated.View>
</Portal>
);
}

Expand Down
34 changes: 22 additions & 12 deletions src/components/Tooltip/BaseGenericTooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
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';
import textRef from '@src/types/utils/textRef';
import viewRef from '@src/types/utils/viewRef';
import type {BaseGenericTooltipProps} from './types';
Expand All @@ -27,7 +29,12 @@ function BaseGenericTooltip({
renderTooltipContent,
shouldForceRenderingBelow = false,
wrapperStyle = {},
shouldForceRenderingLeft = false,
anchorAlignment = {
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,
Expand Down Expand Up @@ -63,7 +70,7 @@ function BaseGenericTooltip({
manualShiftHorizontal: shiftHorizontal,
manualShiftVertical: shiftVertical,
shouldForceRenderingBelow,
shouldForceRenderingLeft,
anchorAlignment,
wrapperStyle,
}),
[
Expand All @@ -80,7 +87,7 @@ function BaseGenericTooltip({
shiftHorizontal,
shiftVertical,
shouldForceRenderingBelow,
shouldForceRenderingLeft,
anchorAlignment,
wrapperStyle,
],
);
Expand Down Expand Up @@ -111,15 +118,18 @@ function BaseGenericTooltip({
}

return ReactDOM.createPortal(
<Animated.View
ref={viewRef(rootWrapper)}
style={[rootWrapperStyle, animationStyle]}
>
{content}
<View style={pointerWrapperStyle}>
<View style={pointerStyle} />
</View>
</Animated.View>,
<>
{shouldUseOverlay && <TransparentOverlay onPress={onPressOverlay} />}
<Animated.View
ref={viewRef(rootWrapper)}
style={[rootWrapperStyle, animationStyle]}
>
{content}
<View style={pointerWrapperStyle}>
<View style={pointerStyle} />
</View>
</Animated.View>
</>,
body,
);
}
Expand Down
7 changes: 5 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,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<TooltipProps, 'renderTooltipContent' | 'maxWidth' | 'numberOfLines' | 'text' | 'shouldForceRenderingBelow' | 'wrapperStyle' | 'shouldForceRenderingLeft'>;
} & Pick<
SharedTooltipProps,
'renderTooltipContent' | 'maxWidth' | 'numberOfLines' | 'text' | 'shouldForceRenderingBelow' | 'wrapperStyle' | 'anchorAlignment' | 'shouldUseOverlay' | 'onPressOverlay'
>;

// eslint-disable-next-line import/prefer-default-export
export type {BaseGenericTooltipProps};
Loading

0 comments on commit 6a6c242

Please sign in to comment.