Repository: Consensys/starknet-snap
Pull request #538: feat: account list contextual menu with hide/unhide and copy address feature
Author: login: khanti42
Created: 2025-03-19T10:56:41Z | Merged: 2025-03-25T09:33:47Z

This PR enhances the Account List Modal by introducing a contextual menu that allows users to hide/unhide accounts and copy addresses.


https://github.com/user-attachments/assets/0744e41c-5bd0-4138-acd5-61e3fe1098ae


BEGIN_COMMIT_OVERRIDE

feat: add account list contextual menu

END_COMMIT_OVERRIDE



Diff:
diff --git a/packages/wallet-ui/src/assets/locales/en.json b/packages/wallet-ui/src/assets/locales/en.json
index f8131e04..c235674b 100644
--- a/packages/wallet-ui/src/assets/locales/en.json
+++ b/packages/wallet-ui/src/assets/locales/en.json
@@ -145,6 +145,12 @@
     "gotIt": {
       "message": "GOT IT!"
     },
+    "hide": {
+      "message": "Hide"
+    },
+    "unhide": {
+      "message": "Unhide"
+    },
     "hiddenAccounts": {
       "message": "Hidden accounts"
     },
diff --git a/packages/wallet-ui/src/assets/locales/fr.json b/packages/wallet-ui/src/assets/locales/fr.json
index c47a74f4..98c46ebb 100644
--- a/packages/wallet-ui/src/assets/locales/fr.json
+++ b/packages/wallet-ui/src/assets/locales/fr.json
@@ -139,6 +139,12 @@
     "gotIt": {
       "message": "COMPRIS !"
     },
+    "hide": {
+      "message": "Masquer"
+    },
+    "unhide": {
+      "message": "Afficher"
+    },
     "hiddenAccounts": {
       "message": "Comptes cachés"
     },
diff --git a/packages/wallet-ui/src/components/ui/organism/AccountListModal/AccountItem.style.ts b/packages/wallet-ui/src/components/ui/organism/AccountListModal/AccountItem.style.ts
index e2b850ee..b8241ff2 100644
--- a/packages/wallet-ui/src/components/ui/organism/AccountListModal/AccountItem.style.ts
+++ b/packages/wallet-ui/src/components/ui/organism/AccountListModal/AccountItem.style.ts
@@ -1,6 +1,7 @@
 import styled from 'styled-components';
 import { AccountImage } from 'components/ui/atom/AccountImage';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { MenuItem as MuiMenuItem } from '@mui/material';
 
 export const Wrapper = styled.div<{ selected: boolean; visible: boolean }>`
   display: flex;
@@ -27,5 +28,77 @@ export const AccountImageStyled = styled(AccountImage)`
 `;
 
 export const VisibilityIcon = styled(FontAwesomeIcon)`
+  font-size: ${(props) => props.theme.typography.p2.fontSize};
+  color: ${(props) => props.theme.palette.grey.grey2};
+`;
+
+export const AccountItemWrapper = styled.div`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: ${(props) => props.theme.spacing.small};
+  border-bottom: 1px solid ${(props) => props.theme.palette.grey.grey3};
+`;
+
+export const AccountName = styled.div`
+  font-size: ${(props) => props.theme.typography.p1.fontSize};
+  font-weight: bold;
+`;
+
+export const AccountAddress = styled.div`
+  font-size: ${(props) => props.theme.typography.p2.fontSize};
   color: ${(props) => props.theme.palette.grey.grey1};
 `;
+
+export const AccountActions = styled.div`
+  display: flex;
+  align-items: center;
+  position: relative;
+`;
+
+export const VerticalDotsIcon = styled(FontAwesomeIcon).attrs({
+  icon: 'ellipsis-v',
+})`
+  cursor: pointer;
+  font-size: ${(props) => props.theme.typography.p2.fontSize};
+  color: ${(props) => props.theme.palette.grey.grey2};
+`;
+
+export const DropdownMenu = styled.div`
+  position: absolute;
+  top: 100%;
+  right: 0;
+  background-color: ${(props) => props.theme.palette.grey.white};
+  border: 1px solid ${(props) => props.theme.palette.grey.grey3};
+  border-radius: 4px;
+  z-index: 10;
+`;
+
+export const DropdownItem = styled.div`
+  padding: ${(props) => props.theme.spacing.small};
+  cursor: pointer;
+  &:hover {
+    background-color: ${(props) => props.theme.palette.grey.grey2};
+  }
+`;
+
+export const MenuItem = styled(MuiMenuItem)`
+  display: flex;
+  align-items: center;
+  justify-content: flex-start; /* Aligne les éléments à gauche */
+  gap: ${(props) =>
+    props.theme.spacing.small}; /* Espacement entre l'icône et le texte */
+  padding: ${(props) => props.theme.spacing.small};
+  font-size: ${(props) => props.theme.typography.p2.fontSize};
+
+  & > svg {
+    flex-shrink: 0; /* Empêche l'icône de se redimensionner */
+    min-width: 30px;
+  }
+
+  & > span {
+    flex-grow: 1; /* Permet au texte de prendre l'espace restant */
+    text-align: left; /* Aligne le texte à gauche */
+    font-size: ${(props) => props.theme.typography.p2.fontSize};
+  }
+`;
diff --git a/packages/wallet-ui/src/components/ui/organism/AccountListModal/AccountItem.view.tsx b/packages/wallet-ui/src/components/ui/organism/AccountListModal/AccountItem.view.tsx
index d591e262..7fad3de1 100644
--- a/packages/wallet-ui/src/components/ui/organism/AccountListModal/AccountItem.view.tsx
+++ b/packages/wallet-ui/src/components/ui/organism/AccountListModal/AccountItem.view.tsx
@@ -1,5 +1,5 @@
-import { IconButton } from '@mui/material';
-
+import { IconButton, Menu } from '@mui/material';
+import { useState } from 'react';
 import { Account } from 'types';
 import { formatAddress } from 'utils/utils';
 import {
@@ -7,7 +7,9 @@ import {
   AccountImageStyled,
   VisibilityIcon,
   Wrapper,
+  MenuItem,
 } from './AccountItem.style';
+import { useMultiLanguage } from 'services';
 
 export interface Props {
   account: Account;
@@ -28,12 +30,13 @@ export const AccountItem = ({
   onItemClick,
   onIconButtonClick,
 }: Props) => {
+  const { translate } = useMultiLanguage();
   const { address, accountName } = account;
+  const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
 
   const preventDefaultMouseEvent = (event: React.MouseEvent) => {
     // Prevent triggering the native behaviour
     event.preventDefault();
-    // Prevent triggering the parent onClick event
     event.stopPropagation();
   };
 
@@ -42,6 +45,7 @@ export const AccountItem = ({
     if (typeof onIconButtonClick === 'function') {
       await onIconButtonClick(account);
     }
+    setMenuAnchorEl(null);
   };
 
   const onClick = async (event: React.MouseEvent) => {
@@ -51,6 +55,21 @@ export const AccountItem = ({
     }
   };
 
+  const handleCopyAddress = (event: React.MouseEvent) => {
+    preventDefaultMouseEvent(event);
+    navigator.clipboard.writeText(address);
+    setMenuAnchorEl(null);
+  };
+
+  const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
+    preventDefaultMouseEvent(event);
+    setMenuAnchorEl(event.currentTarget);
+  };
+
+  const handleMenuClose = () => {
+    setMenuAnchorEl(null);
+  };
+
   return (
     <Wrapper
       ref={scrollToRef}
@@ -65,11 +84,31 @@ export const AccountItem = ({
           <div>{formatAddress(address)}</div>
         </div>
       </AccountInfoWrapper>
-      {showIconButton && (
-        <IconButton size="small" onClick={onIconBtnClick}>
-          <VisibilityIcon icon={visible ? 'eye-slash' : 'eye'} />
+      <div>
+        <IconButton
+          size="small"
+          style={{ width: '30px' }}
+          onClick={handleMenuOpen}
+        >
+          <VisibilityIcon icon="ellipsis-vertical" />
         </IconButton>
-      )}
+        <Menu
+          anchorEl={menuAnchorEl}
+          open={Boolean(menuAnchorEl)}
+          onClose={handleMenuClose}
+        >
+          {showIconButton && (
+            <MenuItem onClick={onIconBtnClick}>
+              <VisibilityIcon icon={visible ? 'eye-slash' : 'eye'} />
+              <span>{visible ? translate('hide') : translate('unhide')}</span>
+            </MenuItem>
+          )}
+          <MenuItem onClick={handleCopyAddress}>
+            <VisibilityIcon icon="clone" />
+            <span>{translate('copyToClipboard')}</span>
+          </MenuItem>
+        </Menu>
+      </div>
     </Wrapper>
   );
 };


Reviews:
user: login: stanleyyconsensys
body: some nit comment
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T06:49:07Z
user: login: khanti42
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T09:35:15Z
user: login: stanleyyconsensys
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T09:17:29Z
user: login: stanleyyconsensys
body: LGTM
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-25T09:18:10Z

Review comments:
diff_hunk: @@ -28,12 +30,12 @@ export const AccountItem = ({
   onItemClick,
   onIconButtonClick,
 }: Props) => {
+  const { translate } = useMultiLanguage();
   const { address, accountName } = account;
+  const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
 
   const preventDefaultMouseEvent = (event: React.MouseEvent) => {
-    // Prevent triggering the native behaviour
path: packages/wallet-ui/src/components/ui/organism/AccountListModal/AccountItem.view.tsx
user: login: stanleyyconsensys
body: dont remove the comment if possible
created_at: 2025-03-24T06:46:06Z
author_association: COLLABORATOR
diff_hunk: @@ -51,6 +54,20 @@ export const AccountItem = ({
     }
   };
 
+  const handleCopyAddress = () => {
+    navigator.clipboard.writeText(address);
+    setMenuAnchorEl(null); // Ferme le menu après la copie
path: packages/wallet-ui/src/components/ui/organism/AccountListModal/AccountItem.view.tsx
user: login: stanleyyconsensys
body: please put comment in english

add toast to notify if the copy success? not sure if we have the same in account details
created_at: 2025-03-24T06:48:17Z
author_association: COLLABORATOR
diff_hunk: @@ -51,6 +54,20 @@ export const AccountItem = ({
     }
   };
 
+  const handleCopyAddress = () => {
+    navigator.clipboard.writeText(address);
+    setMenuAnchorEl(null); // Ferme le menu après la copie
path: packages/wallet-ui/src/components/ui/organism/AccountListModal/AccountItem.view.tsx
user: login: khanti42
body: The point was that in the sidebar there is just a button so its not clear what it does, this is why showing "Copied!" when clicking is good idea. But in the dropdown in the list we explicitly say "Copy to clibpboard!" which makes the intent clear. And the copy cannot fail I guess, wdyt ? 
created_at: 2025-03-24T09:35:15Z
author_association: COLLABORATOR
diff_hunk: @@ -51,6 +54,20 @@ export const AccountItem = ({
     }
   };
 
+  const handleCopyAddress = () => {
+    navigator.clipboard.writeText(address);
+    setMenuAnchorEl(null); // Ferme le menu après la copie
path: packages/wallet-ui/src/components/ui/organism/AccountListModal/AccountItem.view.tsx
user: login: stanleyyconsensys
body: agree to dismiss the fail case handle and notification for successful copy
created_at: 2025-03-25T09:17:29Z
author_association: COLLABORATOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-24T09:36:25Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=consensys_starknet-snap-wallet-ui&pullRequest=538) **Quality Gate passed for 'consensys_starknet-snap-wallet-ui'**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=consensys_starknet-snap-wallet-ui&pullRequest=538&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=consensys_starknet-snap-wallet-ui&pullRequest=538&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=consensys_starknet-snap-wallet-ui&pullRequest=538&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=consensys_starknet-snap-wallet-ui&pullRequest=538&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=consensys_starknet-snap-wallet-ui&pullRequest=538&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=consensys_starknet-snap-wallet-ui&pullRequest=538)


author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-03-24T09:36:26Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=consensys_starknet-snap-starknet-snap&pullRequest=538) **Quality Gate passed for 'consensys_starknet-snap-starknet-snap'**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=consensys_starknet-snap-starknet-snap&pullRequest=538&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=consensys_starknet-snap-starknet-snap&pullRequest=538&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=consensys_starknet-snap-starknet-snap&pullRequest=538&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=consensys_starknet-snap-starknet-snap&pullRequest=538&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=consensys_starknet-snap-starknet-snap&pullRequest=538&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=consensys_starknet-snap-starknet-snap&pullRequest=538)


author_association: NONE