Skip to content

Commit

Permalink
Merge pull request #32731 from software-mansion-labs/ts/UserDetailsTo…
Browse files Browse the repository at this point in the history
…oltip

[TS migration] Migrate 'UserDetailsTooltip' component to TypeScript
  • Loading branch information
robertjchen authored Dec 18, 2023
2 parents 8c0dd06 + 7ebcf24 commit e011f88
Show file tree
Hide file tree
Showing 14 changed files with 128 additions and 145 deletions.
4 changes: 2 additions & 2 deletions src/components/DisplayNames/DisplayNamesTooltipItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type DisplayNamesTooltipItemProps = {
index?: number;

/** The function to get a distance to shift the tooltip horizontally */
getTooltipShiftX?: (index: number) => number | undefined;
getTooltipShiftX?: (index: number) => number;

/** The Account ID for the tooltip */
accountID?: number;
Expand All @@ -32,7 +32,7 @@ type DisplayNamesTooltipItemProps = {

function DisplayNamesTooltipItem({
index = 0,
getTooltipShiftX = () => undefined,
getTooltipShiftX = () => 0,
accountID = 0,
avatar = '',
login = '',
Expand Down
6 changes: 3 additions & 3 deletions src/components/DisplayNames/DisplayNamesWithTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ function DisplayNamesWithToolTip({shouldUseFullTitle, fullTitle, displayNamesWit
* 2. Now we get the tooltip original position.
* 3. If inline node's right edge is overflowing the container's right edge, we set the tooltip to the center
* of the distance between the left edge of the inline node and right edge of the container.
* @param {Number} index Used to get the Ref to the node at the current index
* @returns {Number} Distance to shift the tooltip horizontally
* @param index Used to get the Ref to the node at the current index
* @returns Distance to shift the tooltip horizontally
*/
const getTooltipShiftX = useCallback((index: number) => {
// Only shift the tooltip in case the containerLayout or Refs to the text node are available
if (!containerRef.current || !childRefs.current[index]) {
return;
return 0;
}
const {width: containerWidth, left: containerLeft} = containerRef.current.getBoundingClientRect();

Expand Down
2 changes: 1 addition & 1 deletion src/components/Hoverable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ function Hoverable(
// Expose inner ref to parent through outerRef. This enable us to use ref both in parent and child.
useImperativeHandle<HTMLElement | null, HTMLElement | null>(outerRef, () => ref.current, []);

const child = useMemo(() => React.Children.only(mapChildren(children, isHovered)), [children, isHovered]);
const child = useMemo(() => React.Children.only(mapChildren(children as ReactElement, isHovered)), [children, isHovered]);

const enableHoveredOnMouseEnter = useCallback(
(event: MouseEvent) => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/Hoverable/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {ReactElement} from 'react';
import {ReactNode} from 'react';

type HoverableProps = {
/** Children to wrap with Hoverable. */
children: ((isHovered: boolean) => ReactElement) | ReactElement;
children: ((isHovered: boolean) => ReactNode) | ReactNode;

/** Whether to disable the hover action */
disabled?: boolean;
Expand Down
8 changes: 4 additions & 4 deletions src/components/MultipleAvatars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ function MultipleAvatars({
if (icons.length === 1 && !shouldStackHorizontally) {
return (
<UserDetailsTooltip
accountID={icons[0].id}
accountID={Number(icons[0].id)}
icon={icons[0]}
fallbackUserDetails={{
displayName: icons[0].name,
Expand Down Expand Up @@ -184,7 +184,7 @@ function MultipleAvatars({
{[...avatars].splice(0, maxAvatarsInRow).map((icon, index) => (
<UserDetailsTooltip
key={`stackedAvatars-${icon.id}`}
accountID={icon.id}
accountID={Number(icon.id)}
icon={icon}
fallbackUserDetails={{
displayName: icon.name,
Expand Down Expand Up @@ -257,7 +257,7 @@ function MultipleAvatars({
<View style={avatarContainerStyles}>
<View style={[singleAvatarStyle, icons[0].type === CONST.ICON_TYPE_WORKSPACE ? StyleUtils.getAvatarBorderRadius(size, icons[0].type) : {}]}>
<UserDetailsTooltip
accountID={icons[0].id}
accountID={Number(icons[0].id)}
icon={icons[0]}
fallbackUserDetails={{
displayName: icons[0].name,
Expand All @@ -279,7 +279,7 @@ function MultipleAvatars({
<View style={[secondAvatarStyles, secondAvatarStyle, icons[1].type === CONST.ICON_TYPE_WORKSPACE ? StyleUtils.getAvatarBorderRadius(size, icons[1].type) : {}]}>
{icons.length === 2 ? (
<UserDetailsTooltip
accountID={icons[1].id}
accountID={Number(icons[1].id)}
icon={icons[1]}
fallbackUserDetails={{
displayName: icons[1].name,
Expand Down
4 changes: 2 additions & 2 deletions src/components/SubscriptAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function SubscriptAvatar({mainAvatar = {}, secondaryAvatar = {}, size = CONST.AV
<View style={[containerStyle, noMargin ? styles.mr0 : {}]}>
<UserDetailsTooltip
shouldRender={showTooltip}
accountID={mainAvatar.id ?? -1}
accountID={Number(mainAvatar.id ?? -1)}
icon={mainAvatar}
>
<View>
Expand All @@ -75,7 +75,7 @@ function SubscriptAvatar({mainAvatar = {}, secondaryAvatar = {}, size = CONST.AV
</UserDetailsTooltip>
<UserDetailsTooltip
shouldRender={showTooltip}
accountID={secondaryAvatar.id ?? -1}
accountID={Number(secondaryAvatar.id ?? -1)}
icon={secondaryAvatar}
>
<View
Expand Down
8 changes: 3 additions & 5 deletions src/components/Tooltip/types.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import {ReactElement, ReactNode} from 'react';
import {ReactNode} from 'react';
import ChildrenProps from '@src/types/utils/ChildrenProps';

type TooltipProps = {
type TooltipProps = ChildrenProps & {
/** The text to display in the tooltip. If text is ommitted, only children will be rendered. */
text?: string;

/** Maximum number of lines to show in tooltip */
numberOfLines?: number;

/** Children to wrap with Tooltip. */
children: ReactElement;

/** Any additional amount to manually adjust the horizontal position of the tooltip.
A positive value shifts the tooltip to the right, and a negative value shifts it to the left. */
shiftHorizontal?: number | (() => number);
Expand Down
19 changes: 0 additions & 19 deletions src/components/UserDetailsTooltip/BaseUserDetailsTooltip.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import UserDetailsTooltipProps from '@components/UserDetailsTooltip/types';

function BaseUserDetailsTooltip({children}: UserDetailsTooltipProps) {
return children;
}

BaseUserDetailsTooltip.displayName = 'BaseUserDetailsTooltip';

export default BaseUserDetailsTooltip;
Original file line number Diff line number Diff line change
@@ -1,56 +1,55 @@
import Str from 'expensify-common/lib/str';
import lodashGet from 'lodash/get';
import React, {useCallback} from 'react';
import {Text, View} from 'react-native';
import _ from 'underscore';
import Avatar from '@components/Avatar';
import {usePersonalDetails} from '@components/OnyxProvider';
import Tooltip from '@components/Tooltip';
import UserDetailsTooltipProps from '@components/UserDetailsTooltip/types';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
import * as ReportUtils from '@libs/ReportUtils';
import * as UserUtils from '@libs/UserUtils';
import CONST from '@src/CONST';
import {defaultProps, propTypes} from './userDetailsTooltipPropTypes';

function BaseUserDetailsTooltip(props) {
function BaseUserDetailsTooltip({accountID, fallbackUserDetails, icon, delegateAccountID, shiftHorizontal, children}: UserDetailsTooltipProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const personalDetails = usePersonalDetails();

const userDetails = lodashGet(personalDetails, props.accountID, props.fallbackUserDetails);
let userDisplayName = ReportUtils.getDisplayNameForParticipant(props.accountID) || (userDetails.displayName ? userDetails.displayName.trim() : '');
let userLogin = (userDetails.login || '').trim() && !_.isEqual(userDetails.login, userDetails.displayName) ? Str.removeSMSDomain(userDetails.login) : '';
const userDetails = personalDetails?.[accountID] ?? fallbackUserDetails ?? {};
let userDisplayName = ReportUtils.getDisplayNameForParticipant(accountID) ?? (userDetails.displayName ? userDetails.displayName.trim() : '');
let userLogin = userDetails.login?.trim() && userDetails.login !== userDetails.displayName ? Str.removeSMSDomain(userDetails.login) : '';

let userAvatar = userDetails.avatar;
let userAccountID = props.accountID;
let userAccountID = accountID;

// We replace the actor's email, name, and avatar with the Copilot manually for now. This will be improved upon when
// the Copilot feature is implemented.
if (props.delegateAccountID) {
const delegateUserDetails = lodashGet(personalDetails, props.delegateAccountID, {});
const delegateUserDisplayName = ReportUtils.getDisplayNameForParticipant(props.delegateAccountID);
if (delegateAccountID) {
const delegateUserDetails = personalDetails?.[delegateAccountID] ?? {};
const delegateUserDisplayName = ReportUtils.getDisplayNameForParticipant(delegateAccountID);
userDisplayName = `${delegateUserDisplayName} (${translate('reportAction.asCopilot')} ${userDisplayName})`;
userLogin = delegateUserDetails.login;
userLogin = delegateUserDetails.login ?? '';

Check failure on line 33 in src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx

View workflow job for this annotation

GitHub Actions / typecheck / typecheck

Property 'login' does not exist on type '{}'.
userAvatar = delegateUserDetails.avatar;

Check failure on line 34 in src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx

View workflow job for this annotation

GitHub Actions / typecheck / typecheck

Property 'avatar' does not exist on type '{}'.
userAccountID = props.delegateAccountID;
userAccountID = delegateAccountID;
}

let title = String(userDisplayName).trim() ? userDisplayName : '';
const subtitle = (userLogin || '').trim() && !_.isEqual(LocalePhoneNumber.formatPhoneNumber(userLogin || ''), userDisplayName) ? Str.removeSMSDomain(userLogin) : '';
if (props.icon && (props.icon.type === CONST.ICON_TYPE_WORKSPACE || !title)) {
title = props.icon.name;
const subtitle = userLogin.trim() && LocalePhoneNumber.formatPhoneNumber(userLogin) !== userDisplayName ? Str.removeSMSDomain(userLogin) : '';
if (icon && (icon.type === CONST.ICON_TYPE_WORKSPACE || !title)) {
title = icon.name ?? '';
}
const renderTooltipContent = useCallback(
() => (
<View style={[styles.alignItemsCenter, styles.ph2, styles.pv2]}>
<View style={styles.emptyAvatar}>
<Avatar
containerStyles={[styles.actionAvatar]}
source={props.icon ? props.icon.source : UserUtils.getAvatar(userAvatar, userAccountID)}
type={props.icon ? props.icon.type : CONST.ICON_TYPE_AVATAR}
name={props.icon ? props.icon.name : userLogin}
fallbackIcon={lodashGet(props.icon, 'fallbackIcon')}
source={icon ? icon.source : UserUtils.getAvatar(userAvatar, userAccountID)}

Check failure on line 49 in src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx

View workflow job for this annotation

GitHub Actions / typecheck / typecheck

Argument of type 'AvatarSource | undefined' is not assignable to parameter of type 'AvatarSource'.
type={icon ? icon.type : CONST.ICON_TYPE_AVATAR}
name={icon ? icon.name : userLogin}
fallbackIcon={icon?.fallbackIcon}
/>
</View>
<Text style={[styles.mt2, styles.textMicroBold, styles.textReactionSenders, styles.textAlignCenter]}>{title}</Text>
Expand All @@ -71,7 +70,7 @@ function BaseUserDetailsTooltip(props) {
styles.textMicro,
styles.fontColorReactionLabel,
styles.breakWord,
props.icon,
icon,
userAvatar,
userAccountID,
userLogin,
Expand All @@ -80,24 +79,22 @@ function BaseUserDetailsTooltip(props) {
],
);

if (!props.icon && !userDisplayName && !userLogin) {
return props.children;
if (!icon && !userDisplayName && !userLogin) {
return children;
}

return (
<Tooltip
shiftHorizontal={props.shiftHorizontal}
shiftHorizontal={shiftHorizontal}
renderTooltipContent={renderTooltipContent}
renderTooltipContentKey={[userDisplayName, userLogin]}
shouldHandleScroll
>
{props.children}
{children}
</Tooltip>
);
}

BaseUserDetailsTooltip.propTypes = propTypes;
BaseUserDetailsTooltip.defaultProps = defaultProps;
BaseUserDetailsTooltip.displayName = 'BaseUserDetailsTooltip';

export default BaseUserDetailsTooltip;
37 changes: 0 additions & 37 deletions src/components/UserDetailsTooltip/index.js

This file was deleted.

22 changes: 22 additions & 0 deletions src/components/UserDetailsTooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import BaseUserDetailsTooltip from './BaseUserDetailsTooltip/index.native';
import UserDetailsTooltipProps from './types';

function UserDetailsTooltip({shouldRender = true, children, ...props}: UserDetailsTooltipProps) {
if (!shouldRender) {
return children;
}

return (
<BaseUserDetailsTooltip
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{children}
</BaseUserDetailsTooltip>
);
}

UserDetailsTooltip.displayName = 'UserDetailsTooltip';

export default UserDetailsTooltip;
56 changes: 56 additions & 0 deletions src/components/UserDetailsTooltip/types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {AvatarSource} from '@libs/UserUtils';
import {AvatarType} from '@src/types/onyx/OnyxCommon';
import ChildrenProps from '@src/types/utils/ChildrenProps';

type FallbackUserDetails = {
/** The name to display in bold */
displayName?: string;

/** The login for the tooltip fallback */
login?: string;

/** The avatar for the tooltip fallback */
avatar?: AvatarSource;

/** Denotes whether it is an avatar or a workspace avatar */
type?: AvatarType;
};

type Icon = {
/** Source for the avatar. Can be a URL or an icon. */
source?: AvatarSource;

/** A fallback avatar icon to display when there is an error on loading avatar from remote URL.
* If the avatar is type === workspace, this fallback icon will be ignored and decided based on the name prop.
*/
fallbackIcon?: AvatarSource;

/** Denotes whether it is an avatar or a workspace avatar */
type?: AvatarType;

/** Owner of the avatar. If user, displayName. If workspace, policy name */
name?: string;
};

type UserDetailsTooltipProps = ChildrenProps & {
/** User's Account ID */
accountID: number;

/** Fallback User Details object used if no accountID */
fallbackUserDetails?: FallbackUserDetails;

/** Optionally, pass in the icon instead of calculating it. If defined, will take precedence. */
icon?: Icon;

/** The accountID of the copilot who took this action on behalf of the user */
delegateAccountID?: number;

/** Any additional amount to manually adjust the horizontal position of the tooltip.
A positive value shifts the tooltip to the right, and a negative value shifts it to the left. */
shiftHorizontal?: number | (() => number);

/** Whether the actual UserDetailsTooltip should be rendered. If false, it's just going to return the children */
shouldRender?: boolean;
};

export default UserDetailsTooltipProps;
Loading

0 comments on commit e011f88

Please sign in to comment.