diff --git a/cspell.json b/cspell.json index 47b18a41a4b6..1bc661349281 100644 --- a/cspell.json +++ b/cspell.json @@ -737,6 +737,8 @@ "zoneinfo", "zxcv", "zxldvw", + "inputmethod", + "copyable", "مثال" ], "ignorePaths": [ diff --git a/src/components/CopyTextToClipboard.tsx b/src/components/CopyTextToClipboard.tsx index 3c99a85fd0e7..5e1159941bfb 100644 --- a/src/components/CopyTextToClipboard.tsx +++ b/src/components/CopyTextToClipboard.tsx @@ -4,10 +4,11 @@ import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import * as Expensicons from './Icon/Expensicons'; import PressableWithDelayToggle from './Pressable/PressableWithDelayToggle'; +import type {PressableWithDelayToggleProps} from './Pressable/PressableWithDelayToggle'; type CopyTextToClipboardProps = { /** The text to display and copy to the clipboard */ - text: string; + text?: string; /** Styles to apply to the text */ textStyles?: StyleProp; @@ -15,14 +16,25 @@ type CopyTextToClipboardProps = { urlToCopy?: string; accessibilityRole?: AccessibilityRole; -}; - -function CopyTextToClipboard({text, textStyles, urlToCopy, accessibilityRole}: CopyTextToClipboardProps) { +} & Pick; + +function CopyTextToClipboard({ + text, + textStyles, + urlToCopy, + accessibilityRole, + iconHeight, + iconStyles, + iconWidth, + shouldHaveActiveBackground, + shouldUseButtonBackground, + styles, +}: CopyTextToClipboardProps) { const {translate} = useLocalize(); const copyToClipboard = useCallback(() => { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case - Clipboard.setString(urlToCopy || text); + Clipboard.setString(urlToCopy || text || ''); }, [text, urlToCopy]); return ( @@ -36,6 +48,12 @@ function CopyTextToClipboard({text, textStyles, urlToCopy, accessibilityRole}: C accessible accessibilityLabel={translate('reportActionContextMenu.copyToClipboard')} accessibilityRole={accessibilityRole} + shouldHaveActiveBackground={shouldHaveActiveBackground} + iconWidth={iconWidth} + iconHeight={iconHeight} + iconStyles={iconStyles} + styles={styles} + shouldUseButtonBackground={shouldUseButtonBackground} /> ); } diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 6bf8f20c18f5..c9a7afff13cf 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -10,7 +10,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; import convertToLTR from '@libs/convertToLTR'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; +import {canUseTouchScreen, hasHoverSupport} from '@libs/DeviceCapabilities'; import {containsCustomEmoji, containsOnlyCustomEmoji} from '@libs/EmojiUtils'; import getButtonState from '@libs/getButtonState'; import mergeRefs from '@libs/mergeRefs'; @@ -26,6 +26,7 @@ import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment'; import type IconAsset from '@src/types/utils/IconAsset'; import Avatar from './Avatar'; import Badge from './Badge'; +import CopyTextToClipboard from './CopyTextToClipboard'; import DisplayNames from './DisplayNames'; import type {DisplayNameWithTooltip} from './DisplayNames/types'; import FormHelpMessage from './FormHelpMessage'; @@ -366,9 +367,12 @@ type MenuItemBaseProps = { /** Whether to teleport the portal to the modal layer */ shouldTeleportPortalToModalLayer?: boolean; - /** The value to copy on secondary interaction */ + /** The value to copy in copy to clipboard action. Must be used in conjunction with `copyable=true`. Default value is `title` prop. */ copyValue?: string; + /** Should enable copy to clipboard action */ + copyable?: boolean; + /** Plaid image for the bank */ plaidUrl?: string; @@ -499,8 +503,9 @@ function MenuItem( shouldBreakWord = false, pressableTestID, shouldTeleportPortalToModalLayer, - copyValue, plaidUrl, + copyValue = title, + copyable = false, hasSubMenuItems = false, }: MenuItemProps, ref: PressableRef, @@ -512,6 +517,7 @@ function MenuItem( const {shouldUseNarrowLayout} = useResponsiveLayout(); const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; const popoverAnchor = useRef(null); + const deviceHasHoverSupport = hasHoverSupport(); const isCompact = viewMode === CONST.OPTION_MODE.COMPACT; const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; @@ -964,6 +970,19 @@ function MenuItem( additionalStyles={styles.alignSelfCenter} /> )} + {copyable && deviceHasHoverSupport && !interactive && isHovered && !!copyValue && ( + + + + )} {!!errorText && ( diff --git a/src/components/Pressable/PressableWithDelayToggle.tsx b/src/components/Pressable/PressableWithDelayToggle.tsx index ace8b25ecc02..3e8914df5fde 100644 --- a/src/components/Pressable/PressableWithDelayToggle.tsx +++ b/src/components/Pressable/PressableWithDelayToggle.tsx @@ -17,7 +17,7 @@ import PressableWithoutFeedback from './PressableWithoutFeedback'; type PressableWithDelayToggleProps = PressableProps & { /** The text to display */ - text: string; + text?: string; /** The text to display once the pressable is pressed */ textChecked?: string; @@ -55,6 +55,18 @@ type PressableWithDelayToggleProps = PressableProps & { * Reference to the outer element */ ref?: PressableRef; + + /** Whether to use background color based on button states, e.g., hovered, active, pressed... */ + shouldUseButtonBackground?: boolean; + + /** Whether to always use active (hovered) background by default */ + shouldHaveActiveBackground?: boolean; + + /** Icon width */ + iconWidth?: number; + + /** Icon height */ + iconHeight?: number; }; function PressableWithDelayToggle({ @@ -69,8 +81,12 @@ function PressableWithDelayToggle({ textStyles, iconStyles, icon, - accessibilityRole, ref, + accessibilityRole, + shouldHaveActiveBackground, + iconWidth = variables.iconSizeSmall, + iconHeight = variables.iconSizeSmall, + shouldUseButtonBackground = false, }: PressableWithDelayToggleProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -89,15 +105,17 @@ function PressableWithDelayToggle({ // of a Pressable const PressableView = inline ? Text : PressableWithoutFeedback; const tooltipTexts = !isActive ? tooltipTextChecked : tooltipText; - const labelText = ( - - {!isActive && textChecked ? textChecked : text} -   - - ); + const labelText = + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null + text || textChecked ? ( + + {!isActive && textChecked ? textChecked : text} +   + + ) : null; return ( [ + styles.flexRow, + pressableStyle, + !isActive && styles.cursorDefault, + shouldUseButtonBackground && + StyleUtils.getButtonBackgroundColorStyle( + getButtonState(!!shouldHaveActiveBackground || hovered, shouldHaveActiveBackground ? hovered : pressed, !shouldHaveActiveBackground && !isActive), + true, + ), + ]} > {({hovered, pressed}) => ( <> @@ -129,8 +156,8 @@ function PressableWithDelayToggle({ src={!isActive ? iconChecked : icon} fill={StyleUtils.getIconFillColor(getButtonState(hovered, pressed, !isActive))} additionalStyles={iconStyles} - width={variables.iconSizeSmall} - height={variables.iconSizeSmall} + width={iconWidth} + height={iconHeight} inline={inline} /> )} @@ -146,3 +173,4 @@ function PressableWithDelayToggle({ PressableWithDelayToggle.displayName = 'PressableWithDelayToggle'; export default PressableWithDelayToggle; +export type {PressableWithDelayToggleProps}; diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index d920cb8bb8a0..ce1caebff9a5 100755 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -231,6 +231,7 @@ function ProfilePage({route}: ProfilePageProps) { copyValue={isSMSLogin ? formatPhoneNumber(phoneNumber ?? '') : login} description={translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')} interactive={false} + copyable /> ) : null} diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index e22e313fd2eb..b87d1bfa4bab 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -881,6 +881,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail copyValue={base62ReportID} interactive={false} shouldBlockSelection + copyable /> )} diff --git a/src/pages/settings/Wallet/WalletPage/CardDetails.tsx b/src/pages/settings/Wallet/WalletPage/CardDetails.tsx index 65d281242995..eb12d0c14c6e 100644 --- a/src/pages/settings/Wallet/WalletPage/CardDetails.tsx +++ b/src/pages/settings/Wallet/WalletPage/CardDetails.tsx @@ -39,7 +39,7 @@ type CardDetailsProps = { function CardDetails({pan = '', expiration = '', cvv = '', domain}: CardDetailsProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); return ( <> @@ -49,6 +49,7 @@ function CardDetails({pan = '', expiration = '', cvv = '', domain}: CardDetailsP title={pan} interactive={false} copyValue={pan} + copyable /> )} {expiration?.length > 0 && ( @@ -56,6 +57,7 @@ function CardDetails({pan = '', expiration = '', cvv = '', domain}: CardDetailsP description={translate('cardPage.cardDetails.expiration')} title={expiration} interactive={false} + copyable /> )} {cvv?.length > 0 && ( @@ -63,6 +65,7 @@ function CardDetails({pan = '', expiration = '', cvv = '', domain}: CardDetailsP description={translate('cardPage.cardDetails.cvv')} title={cvv} interactive={false} + copyable /> )} {pan?.length > 0 && ( @@ -72,6 +75,7 @@ function CardDetails({pan = '', expiration = '', cvv = '', domain}: CardDetailsP // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing title={getFormattedAddress(privatePersonalDetails || defaultPrivatePersonalDetails)} interactive={false} + copyable />