Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,8 @@
"zoneinfo",
"zxcv",
"zxldvw",
"inputmethod",
"copyable",
"مثال"
],
"ignorePaths": [
Expand Down
28 changes: 23 additions & 5 deletions src/components/CopyTextToClipboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,37 @@ 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<TextStyle>;

urlToCopy?: string;

accessibilityRole?: AccessibilityRole;
};

function CopyTextToClipboard({text, textStyles, urlToCopy, accessibilityRole}: CopyTextToClipboardProps) {
} & Pick<PressableWithDelayToggleProps, 'iconStyles' | 'iconHeight' | 'iconWidth' | 'styles' | 'shouldUseButtonBackground' | 'shouldHaveActiveBackground'>;

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 (
Expand All @@ -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}
/>
);
}
Expand Down
25 changes: 22 additions & 3 deletions src/components/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -499,8 +503,9 @@ function MenuItem(
shouldBreakWord = false,
pressableTestID,
shouldTeleportPortalToModalLayer,
copyValue,
plaidUrl,
copyValue = title,
copyable = false,
hasSubMenuItems = false,
}: MenuItemProps,
ref: PressableRef,
Expand All @@ -512,6 +517,7 @@ function MenuItem(
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {};
const popoverAnchor = useRef<View>(null);
const deviceHasHoverSupport = hasHoverSupport();

const isCompact = viewMode === CONST.OPTION_MODE.COMPACT;
const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false;
Expand Down Expand Up @@ -964,6 +970,19 @@ function MenuItem(
additionalStyles={styles.alignSelfCenter}
/>
)}
{copyable && deviceHasHoverSupport && !interactive && isHovered && !!copyValue && (
<View style={styles.justifyContentCenter}>
<CopyTextToClipboard
urlToCopy={copyValue}
shouldHaveActiveBackground
iconHeight={variables.iconSizeExtraSmall}
iconWidth={variables.iconSizeExtraSmall}
iconStyles={styles.t0}
styles={styles.reportActionContextMenuMiniButton}
shouldUseButtonBackground
/>
</View>
)}
</View>
</View>
{!!errorText && (
Expand Down
56 changes: 42 additions & 14 deletions src/components/Pressable/PressableWithDelayToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 */
Comment thread
Krishna2323 marked this conversation as resolved.
iconWidth?: number;

/** Icon height */
iconHeight?: number;
};

function PressableWithDelayToggle({
Expand All @@ -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();
Expand All @@ -89,15 +105,17 @@ function PressableWithDelayToggle({
// of a Pressable
const PressableView = inline ? Text : PressableWithoutFeedback;
const tooltipTexts = !isActive ? tooltipTextChecked : tooltipText;
const labelText = (
<Text
suppressHighlighting
style={textStyles}
>
{!isActive && textChecked ? textChecked : text}
&nbsp;
</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 ? (
<Text
suppressHighlighting
style={textStyles}
>
{!isActive && textChecked ? textChecked : text}
&nbsp;
</Text>
) : null;

return (
<PressableView
Expand All @@ -119,7 +137,16 @@ function PressableWithDelayToggle({
tabIndex={-1}
accessible={false}
onPress={updatePressState}
style={[styles.flexRow, pressableStyle, !isActive && styles.cursorDefault]}
style={({hovered, pressed}) => [
styles.flexRow,
pressableStyle,
!isActive && styles.cursorDefault,
shouldUseButtonBackground &&
StyleUtils.getButtonBackgroundColorStyle(
getButtonState(!!shouldHaveActiveBackground || hovered, shouldHaveActiveBackground ? hovered : pressed, !shouldHaveActiveBackground && !isActive),
true,
),
]}
>
{({hovered, pressed}) => (
<>
Expand All @@ -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}
/>
)}
Expand All @@ -146,3 +173,4 @@ function PressableWithDelayToggle({
PressableWithDelayToggle.displayName = 'PressableWithDelayToggle';

export default PressableWithDelayToggle;
export type {PressableWithDelayToggleProps};
1 change: 1 addition & 0 deletions src/pages/ProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ function ProfilePage({route}: ProfilePageProps) {
copyValue={isSMSLogin ? formatPhoneNumber(phoneNumber ?? '') : login}
description={translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')}
interactive={false}
copyable
/>
</View>
) : null}
Expand Down
2 changes: 2 additions & 0 deletions src/pages/ReportDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -881,13 +881,15 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail
copyValue={base62ReportID}
interactive={false}
shouldBlockSelection
copyable
/>
<MenuItemWithTopDescription
title={report.reportID}
description={translate('common.longID')}
copyValue={report.reportID}
interactive={false}
shouldBlockSelection

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you investigate if we need shouldBlockSelection for all copiable items? This is what it was added for: #67858 (comment). I tried but could not reproduce what happened there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't reproduce it either but if it was happening before, we can add that for safety. We can destructure props like:

        copiable = false,
        shouldBlockSelection = copiable,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need. Let's see if QA could reproduce this on staging.

copyable
/>
</>
)}
Expand Down
6 changes: 5 additions & 1 deletion src/pages/settings/Wallet/WalletPage/CardDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
Expand All @@ -49,20 +49,23 @@ function CardDetails({pan = '', expiration = '', cvv = '', domain}: CardDetailsP
title={pan}
interactive={false}
copyValue={pan}
copyable
/>
)}
{expiration?.length > 0 && (
<MenuItemWithTopDescription
description={translate('cardPage.cardDetails.expiration')}
title={expiration}
interactive={false}
copyable
/>
)}
{cvv?.length > 0 && (
<MenuItemWithTopDescription
description={translate('cardPage.cardDetails.cvv')}
title={cvv}
interactive={false}
copyable
/>
)}
{pan?.length > 0 && (
Expand All @@ -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
/>
<TextLink
style={[styles.link, styles.mh5, styles.mb3]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
copyValue={isSMSLogin ? formatPhoneNumber(phoneNumber ?? '') : memberLogin}
description={translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')}
interactive={false}
copyable
/>
<MenuItemWithTopDescription
disabled={isSelectedMemberOwner || isSelectedMemberCurrentUser}
Expand Down
Loading