Skip to content

Commit 5b1b8a2

Browse files
Merge pull request Expensify#67305 from Krishna2323/krishna2323/issue/67111
feat: Update "copy to clipboard" pattern to be more discoverable.
2 parents 8046bb5 + 2d435c1 commit 5b1b8a2

8 files changed

Lines changed: 98 additions & 23 deletions

File tree

cspell.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,8 @@
737737
"zoneinfo",
738738
"zxcv",
739739
"zxldvw",
740+
"inputmethod",
741+
"copyable",
740742
"مثال"
741743
],
742744
"ignorePaths": [

src/components/CopyTextToClipboard.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,37 @@ import useLocalize from '@hooks/useLocalize';
44
import Clipboard from '@libs/Clipboard';
55
import * as Expensicons from './Icon/Expensicons';
66
import PressableWithDelayToggle from './Pressable/PressableWithDelayToggle';
7+
import type {PressableWithDelayToggleProps} from './Pressable/PressableWithDelayToggle';
78

89
type CopyTextToClipboardProps = {
910
/** The text to display and copy to the clipboard */
10-
text: string;
11+
text?: string;
1112

1213
/** Styles to apply to the text */
1314
textStyles?: StyleProp<TextStyle>;
1415

1516
urlToCopy?: string;
1617

1718
accessibilityRole?: AccessibilityRole;
18-
};
19-
20-
function CopyTextToClipboard({text, textStyles, urlToCopy, accessibilityRole}: CopyTextToClipboardProps) {
19+
} & Pick<PressableWithDelayToggleProps, 'iconStyles' | 'iconHeight' | 'iconWidth' | 'styles' | 'shouldUseButtonBackground' | 'shouldHaveActiveBackground'>;
20+
21+
function CopyTextToClipboard({
22+
text,
23+
textStyles,
24+
urlToCopy,
25+
accessibilityRole,
26+
iconHeight,
27+
iconStyles,
28+
iconWidth,
29+
shouldHaveActiveBackground,
30+
shouldUseButtonBackground,
31+
styles,
32+
}: CopyTextToClipboardProps) {
2133
const {translate} = useLocalize();
2234

2335
const copyToClipboard = useCallback(() => {
2436
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case
25-
Clipboard.setString(urlToCopy || text);
37+
Clipboard.setString(urlToCopy || text || '');
2638
}, [text, urlToCopy]);
2739

2840
return (
@@ -36,6 +48,12 @@ function CopyTextToClipboard({text, textStyles, urlToCopy, accessibilityRole}: C
3648
accessible
3749
accessibilityLabel={translate('reportActionContextMenu.copyToClipboard')}
3850
accessibilityRole={accessibilityRole}
51+
shouldHaveActiveBackground={shouldHaveActiveBackground}
52+
iconWidth={iconWidth}
53+
iconHeight={iconHeight}
54+
iconStyles={iconStyles}
55+
styles={styles}
56+
shouldUseButtonBackground={shouldUseButtonBackground}
3957
/>
4058
);
4159
}

src/components/MenuItem.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import useTheme from '@hooks/useTheme';
1010
import useThemeStyles from '@hooks/useThemeStyles';
1111
import ControlSelection from '@libs/ControlSelection';
1212
import convertToLTR from '@libs/convertToLTR';
13-
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
13+
import {canUseTouchScreen, hasHoverSupport} from '@libs/DeviceCapabilities';
1414
import {containsCustomEmoji, containsOnlyCustomEmoji} from '@libs/EmojiUtils';
1515
import getButtonState from '@libs/getButtonState';
1616
import mergeRefs from '@libs/mergeRefs';
@@ -26,6 +26,7 @@ import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment';
2626
import type IconAsset from '@src/types/utils/IconAsset';
2727
import Avatar from './Avatar';
2828
import Badge from './Badge';
29+
import CopyTextToClipboard from './CopyTextToClipboard';
2930
import DisplayNames from './DisplayNames';
3031
import type {DisplayNameWithTooltip} from './DisplayNames/types';
3132
import FormHelpMessage from './FormHelpMessage';
@@ -366,9 +367,12 @@ type MenuItemBaseProps = {
366367
/** Whether to teleport the portal to the modal layer */
367368
shouldTeleportPortalToModalLayer?: boolean;
368369

369-
/** The value to copy on secondary interaction */
370+
/** The value to copy in copy to clipboard action. Must be used in conjunction with `copyable=true`. Default value is `title` prop. */
370371
copyValue?: string;
371372

373+
/** Should enable copy to clipboard action */
374+
copyable?: boolean;
375+
372376
/** Plaid image for the bank */
373377
plaidUrl?: string;
374378

@@ -499,8 +503,9 @@ function MenuItem(
499503
shouldBreakWord = false,
500504
pressableTestID,
501505
shouldTeleportPortalToModalLayer,
502-
copyValue,
503506
plaidUrl,
507+
copyValue = title,
508+
copyable = false,
504509
hasSubMenuItems = false,
505510
}: MenuItemProps,
506511
ref: PressableRef,
@@ -512,6 +517,7 @@ function MenuItem(
512517
const {shouldUseNarrowLayout} = useResponsiveLayout();
513518
const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {};
514519
const popoverAnchor = useRef<View>(null);
520+
const deviceHasHoverSupport = hasHoverSupport();
515521

516522
const isCompact = viewMode === CONST.OPTION_MODE.COMPACT;
517523
const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false;
@@ -964,6 +970,19 @@ function MenuItem(
964970
additionalStyles={styles.alignSelfCenter}
965971
/>
966972
)}
973+
{copyable && deviceHasHoverSupport && !interactive && isHovered && !!copyValue && (
974+
<View style={styles.justifyContentCenter}>
975+
<CopyTextToClipboard
976+
urlToCopy={copyValue}
977+
shouldHaveActiveBackground
978+
iconHeight={variables.iconSizeExtraSmall}
979+
iconWidth={variables.iconSizeExtraSmall}
980+
iconStyles={styles.t0}
981+
styles={styles.reportActionContextMenuMiniButton}
982+
shouldUseButtonBackground
983+
/>
984+
</View>
985+
)}
967986
</View>
968987
</View>
969988
{!!errorText && (

src/components/Pressable/PressableWithDelayToggle.tsx

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import PressableWithoutFeedback from './PressableWithoutFeedback';
1717

1818
type PressableWithDelayToggleProps = PressableProps & {
1919
/** The text to display */
20-
text: string;
20+
text?: string;
2121

2222
/** The text to display once the pressable is pressed */
2323
textChecked?: string;
@@ -55,6 +55,18 @@ type PressableWithDelayToggleProps = PressableProps & {
5555
* Reference to the outer element
5656
*/
5757
ref?: PressableRef;
58+
59+
/** Whether to use background color based on button states, e.g., hovered, active, pressed... */
60+
shouldUseButtonBackground?: boolean;
61+
62+
/** Whether to always use active (hovered) background by default */
63+
shouldHaveActiveBackground?: boolean;
64+
65+
/** Icon width */
66+
iconWidth?: number;
67+
68+
/** Icon height */
69+
iconHeight?: number;
5870
};
5971

6072
function PressableWithDelayToggle({
@@ -69,8 +81,12 @@ function PressableWithDelayToggle({
6981
textStyles,
7082
iconStyles,
7183
icon,
72-
accessibilityRole,
7384
ref,
85+
accessibilityRole,
86+
shouldHaveActiveBackground,
87+
iconWidth = variables.iconSizeSmall,
88+
iconHeight = variables.iconSizeSmall,
89+
shouldUseButtonBackground = false,
7490
}: PressableWithDelayToggleProps) {
7591
const styles = useThemeStyles();
7692
const StyleUtils = useStyleUtils();
@@ -89,15 +105,17 @@ function PressableWithDelayToggle({
89105
// of a Pressable
90106
const PressableView = inline ? Text : PressableWithoutFeedback;
91107
const tooltipTexts = !isActive ? tooltipTextChecked : tooltipText;
92-
const labelText = (
93-
<Text
94-
suppressHighlighting
95-
style={textStyles}
96-
>
97-
{!isActive && textChecked ? textChecked : text}
98-
&nbsp;
99-
</Text>
100-
);
108+
const labelText =
109+
// 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
110+
text || textChecked ? (
111+
<Text
112+
suppressHighlighting
113+
style={textStyles}
114+
>
115+
{!isActive && textChecked ? textChecked : text}
116+
&nbsp;
117+
</Text>
118+
) : null;
101119

102120
return (
103121
<PressableView
@@ -119,7 +137,16 @@ function PressableWithDelayToggle({
119137
tabIndex={-1}
120138
accessible={false}
121139
onPress={updatePressState}
122-
style={[styles.flexRow, pressableStyle, !isActive && styles.cursorDefault]}
140+
style={({hovered, pressed}) => [
141+
styles.flexRow,
142+
pressableStyle,
143+
!isActive && styles.cursorDefault,
144+
shouldUseButtonBackground &&
145+
StyleUtils.getButtonBackgroundColorStyle(
146+
getButtonState(!!shouldHaveActiveBackground || hovered, shouldHaveActiveBackground ? hovered : pressed, !shouldHaveActiveBackground && !isActive),
147+
true,
148+
),
149+
]}
123150
>
124151
{({hovered, pressed}) => (
125152
<>
@@ -129,8 +156,8 @@ function PressableWithDelayToggle({
129156
src={!isActive ? iconChecked : icon}
130157
fill={StyleUtils.getIconFillColor(getButtonState(hovered, pressed, !isActive))}
131158
additionalStyles={iconStyles}
132-
width={variables.iconSizeSmall}
133-
height={variables.iconSizeSmall}
159+
width={iconWidth}
160+
height={iconHeight}
134161
inline={inline}
135162
/>
136163
)}
@@ -146,3 +173,4 @@ function PressableWithDelayToggle({
146173
PressableWithDelayToggle.displayName = 'PressableWithDelayToggle';
147174

148175
export default PressableWithDelayToggle;
176+
export type {PressableWithDelayToggleProps};

src/pages/ProfilePage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ function ProfilePage({route}: ProfilePageProps) {
231231
copyValue={isSMSLogin ? formatPhoneNumber(phoneNumber ?? '') : login}
232232
description={translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')}
233233
interactive={false}
234+
copyable
234235
/>
235236
</View>
236237
) : null}

src/pages/ReportDetailsPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,13 +881,15 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail
881881
copyValue={base62ReportID}
882882
interactive={false}
883883
shouldBlockSelection
884+
copyable
884885
/>
885886
<MenuItemWithTopDescription
886887
title={report.reportID}
887888
description={translate('common.longID')}
888889
copyValue={report.reportID}
889890
interactive={false}
890891
shouldBlockSelection
892+
copyable
891893
/>
892894
</>
893895
)}

src/pages/settings/Wallet/WalletPage/CardDetails.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ type CardDetailsProps = {
3939
function CardDetails({pan = '', expiration = '', cvv = '', domain}: CardDetailsProps) {
4040
const styles = useThemeStyles();
4141
const {translate} = useLocalize();
42-
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS);
42+
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true});
4343

4444
return (
4545
<>
@@ -49,20 +49,23 @@ function CardDetails({pan = '', expiration = '', cvv = '', domain}: CardDetailsP
4949
title={pan}
5050
interactive={false}
5151
copyValue={pan}
52+
copyable
5253
/>
5354
)}
5455
{expiration?.length > 0 && (
5556
<MenuItemWithTopDescription
5657
description={translate('cardPage.cardDetails.expiration')}
5758
title={expiration}
5859
interactive={false}
60+
copyable
5961
/>
6062
)}
6163
{cvv?.length > 0 && (
6264
<MenuItemWithTopDescription
6365
description={translate('cardPage.cardDetails.cvv')}
6466
title={cvv}
6567
interactive={false}
68+
copyable
6669
/>
6770
)}
6871
{pan?.length > 0 && (
@@ -72,6 +75,7 @@ function CardDetails({pan = '', expiration = '', cvv = '', domain}: CardDetailsP
7275
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
7376
title={getFormattedAddress(privatePersonalDetails || defaultPrivatePersonalDetails)}
7477
interactive={false}
78+
copyable
7579
/>
7680
<TextLink
7781
style={[styles.link, styles.mh5, styles.mb3]}

src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
355355
copyValue={isSMSLogin ? formatPhoneNumber(phoneNumber ?? '') : memberLogin}
356356
description={translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')}
357357
interactive={false}
358+
copyable
358359
/>
359360
<MenuItemWithTopDescription
360361
disabled={isSelectedMemberOwner || isSelectedMemberCurrentUser}

0 commit comments

Comments
 (0)