diff --git a/src/components/DisplayNames/DisplayNamesTooltipItem.tsx b/src/components/DisplayNames/DisplayNamesTooltipItem.tsx index 17bc65b88e9f..440457d22965 100644 --- a/src/components/DisplayNames/DisplayNamesTooltipItem.tsx +++ b/src/components/DisplayNames/DisplayNamesTooltipItem.tsx @@ -9,7 +9,7 @@ type DisplayNamesTooltipItemProps = { index?: number; /** The function to get a distance to shift the tooltip horizontally */ - getTooltipShiftX?: (index: number) => number; + getTooltipShiftX?: (index: number) => number | undefined; /** The Account ID for the tooltip */ accountID?: number; @@ -32,7 +32,7 @@ type DisplayNamesTooltipItemProps = { function DisplayNamesTooltipItem({ index = 0, - getTooltipShiftX = () => 0, + getTooltipShiftX = () => undefined, accountID = 0, avatar = '', login = '', diff --git a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx index 06fd0574c85e..43061ada9a94 100644 --- a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx +++ b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx @@ -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 index Used to get the Ref to the node at the current index - * @returns Distance to shift the tooltip horizontally + * @param {Number} index Used to get the Ref to the node at the current index + * @returns {Number} 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 0; + return; } const {width: containerWidth, left: containerLeft} = containerRef.current.getBoundingClientRect(); diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx index 78ace63bcc35..9c641cfc19be 100644 --- a/src/components/Hoverable/index.tsx +++ b/src/components/Hoverable/index.tsx @@ -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(outerRef, () => ref.current, []); - const child = useMemo(() => React.Children.only(mapChildren(children as ReactElement, isHovered)), [children, isHovered]); + const child = useMemo(() => React.Children.only(mapChildren(children, isHovered)), [children, isHovered]); const enableHoveredOnMouseEnter = useCallback( (event: MouseEvent) => { diff --git a/src/components/Hoverable/types.ts b/src/components/Hoverable/types.ts index b4c53f2df5be..430b865f50c5 100644 --- a/src/components/Hoverable/types.ts +++ b/src/components/Hoverable/types.ts @@ -1,8 +1,8 @@ -import {ReactNode} from 'react'; +import {ReactElement} from 'react'; type HoverableProps = { /** Children to wrap with Hoverable. */ - children: ((isHovered: boolean) => ReactNode) | ReactNode; + children: ((isHovered: boolean) => ReactElement) | ReactElement; /** Whether to disable the hover action */ disabled?: boolean; diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index e16839adc16a..997e63cb022f 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -142,7 +142,7 @@ function MultipleAvatars({ if (icons.length === 1 && !shouldStackHorizontally) { return ( ( {icons.length === 2 ? ( @@ -75,7 +75,7 @@ function SubscriptAvatar({mainAvatar = {}, secondaryAvatar = {}, size = CONST.AV number); diff --git a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip.js b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip.js new file mode 100644 index 000000000000..6c611dae17fd --- /dev/null +++ b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip.js @@ -0,0 +1,19 @@ +import PropTypes from 'prop-types'; + +const propTypes = { + /** Children to wrap with Tooltip. */ + children: PropTypes.node.isRequired, +}; + +/** + * @param {propTypes} props + * @returns {ReactNodeLike} + */ +function BaseUserDetailsTooltip(props) { + return props.children; +} + +BaseUserDetailsTooltip.propTypes = propTypes; +BaseUserDetailsTooltip.displayName = 'BaseUserDetailsTooltip'; + +export default BaseUserDetailsTooltip; diff --git a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip.website.js similarity index 61% rename from src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx rename to src/components/UserDetailsTooltip/BaseUserDetailsTooltip.website.js index bebea1418342..5e88a7994e51 100644 --- a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx +++ b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip.website.js @@ -1,44 +1,45 @@ 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({accountID, fallbackUserDetails, icon, delegateAccountID, shiftHorizontal, children}: UserDetailsTooltipProps) { +function BaseUserDetailsTooltip(props) { const styles = useThemeStyles(); const {translate} = useLocalize(); const personalDetails = usePersonalDetails(); - 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) : ''; - + 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) : ''; let userAvatar = userDetails.avatar; - let userAccountID = accountID; + let userAccountID = props.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 (delegateAccountID) { - const delegateUserDetails = personalDetails?.[delegateAccountID] ?? {}; - const delegateUserDisplayName = ReportUtils.getDisplayNameForParticipant(delegateAccountID); + if (props.delegateAccountID) { + const delegateUserDetails = lodashGet(personalDetails, props.delegateAccountID, {}); + const delegateUserDisplayName = ReportUtils.getDisplayNameForParticipant(props.delegateAccountID); userDisplayName = `${delegateUserDisplayName} (${translate('reportAction.asCopilot')} ${userDisplayName})`; - userLogin = delegateUserDetails.login ?? ''; + userLogin = delegateUserDetails.login; userAvatar = delegateUserDetails.avatar; - userAccountID = delegateAccountID; + userAccountID = props.delegateAccountID; } let title = String(userDisplayName).trim() ? userDisplayName : ''; - const subtitle = userLogin.trim() && LocalePhoneNumber.formatPhoneNumber(userLogin) !== userDisplayName ? Str.removeSMSDomain(userLogin) : ''; - if (icon && (icon.type === CONST.ICON_TYPE_WORKSPACE || !title)) { - title = icon.name ?? ''; + 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 renderTooltipContent = useCallback( () => ( @@ -46,10 +47,10 @@ function BaseUserDetailsTooltip({accountID, fallbackUserDetails, icon, delegateA {title} @@ -70,7 +71,7 @@ function BaseUserDetailsTooltip({accountID, fallbackUserDetails, icon, delegateA styles.textMicro, styles.fontColorReactionLabel, styles.breakWord, - icon, + props.icon, userAvatar, userAccountID, userLogin, @@ -79,22 +80,24 @@ function BaseUserDetailsTooltip({accountID, fallbackUserDetails, icon, delegateA ], ); - if (!icon && !userDisplayName && !userLogin) { - return children; + if (!props.icon && !userDisplayName && !userLogin) { + return props.children; } return ( - {children} + {props.children} ); } +BaseUserDetailsTooltip.propTypes = propTypes; +BaseUserDetailsTooltip.defaultProps = defaultProps; BaseUserDetailsTooltip.displayName = 'BaseUserDetailsTooltip'; export default BaseUserDetailsTooltip; diff --git a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.native.tsx b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.native.tsx deleted file mode 100644 index e490cb13c66b..000000000000 --- a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.native.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import UserDetailsTooltipProps from '@components/UserDetailsTooltip/types'; - -function BaseUserDetailsTooltip({children}: UserDetailsTooltipProps) { - return children; -} - -BaseUserDetailsTooltip.displayName = 'BaseUserDetailsTooltip'; - -export default BaseUserDetailsTooltip; diff --git a/src/components/UserDetailsTooltip/index.js b/src/components/UserDetailsTooltip/index.js new file mode 100644 index 000000000000..6e256ac9ba9e --- /dev/null +++ b/src/components/UserDetailsTooltip/index.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import BaseUserDetailsTooltip from './BaseUserDetailsTooltip'; +import {defaultProps as userDetailsTooltipDefaultProps, propTypes as userDetailsTooltipPropTypes} from './userDetailsTooltipPropTypes'; + +const propTypes = { + ...userDetailsTooltipPropTypes, + + /** Whether the actual UserDetailsTooltip should be rendered. If false, it's just going to return the children */ + shouldRender: PropTypes.bool, +}; + +const defaultProps = { + ...userDetailsTooltipDefaultProps, + shouldRender: true, +}; + +function UserDetailsTooltip({shouldRender = true, children, ...props}) { + if (!shouldRender) { + return children; + } + + return ( + + {children} + + ); +} + +UserDetailsTooltip.displayName = 'UserDetailsTooltip'; +UserDetailsTooltip.propTypes = propTypes; +UserDetailsTooltip.defaultProps = defaultProps; + +export default UserDetailsTooltip; diff --git a/src/components/UserDetailsTooltip/index.tsx b/src/components/UserDetailsTooltip/index.tsx deleted file mode 100644 index 71e3a9f433cf..000000000000 --- a/src/components/UserDetailsTooltip/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -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 ( - - {children} - - ); -} - -UserDetailsTooltip.displayName = 'UserDetailsTooltip'; - -export default UserDetailsTooltip; diff --git a/src/components/UserDetailsTooltip/types.tsx b/src/components/UserDetailsTooltip/types.tsx deleted file mode 100644 index 494bb82211e2..000000000000 --- a/src/components/UserDetailsTooltip/types.tsx +++ /dev/null @@ -1,56 +0,0 @@ -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; diff --git a/src/components/UserDetailsTooltip/userDetailsTooltipPropTypes.js b/src/components/UserDetailsTooltip/userDetailsTooltipPropTypes.js new file mode 100644 index 000000000000..538e9ad5348f --- /dev/null +++ b/src/components/UserDetailsTooltip/userDetailsTooltipPropTypes.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import avatarPropTypes from '@components/avatarPropTypes'; +import personalDetailsPropType from '@pages/personalDetailsPropType'; + +const propTypes = { + /** User's Account ID */ + accountID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + /** Fallback User Details object used if no accountID */ + fallbackUserDetails: PropTypes.shape({ + /** Avatar URL */ + avatar: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + /** Display Name */ + displayName: PropTypes.string, + /** Login */ + login: PropTypes.string, + /** Whether this is a Workspace Avatar or User Avatar */ + type: PropTypes.string, + }), + /** Optionally, pass in the icon instead of calculating it. If defined, will take precedence. */ + icon: avatarPropTypes, + /** Component that displays the tooltip */ + children: PropTypes.node.isRequired, + /** List of personalDetails (keyed by accountID) */ + personalDetailsList: PropTypes.objectOf(personalDetailsPropType), + + /** The accountID of the copilot who took this action on behalf of the user */ + delegateAccountID: PropTypes.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: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), +}; + +const defaultProps = { + accountID: '', + fallbackUserDetails: {displayName: '', login: '', avatar: '', type: ''}, + personalDetailsList: {}, + delegateAccountID: 0, + icon: undefined, + shiftHorizontal: 0, +}; + +export {propTypes, defaultProps};