Skip to content

Commit b110819

Browse files
Jony BursztynNidhiKJha
andauthored
feat: Implement ConnectedAccountsMenu buttons (#23258)
## **Description** Implements the 'Switch to this account' and the 'Disconnect' buttons on the ConnectedAccountsMenu panel. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/23258?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/2172 ## **Manual testing steps** 1. Connect two accounts to a dApp 2. Open the Connected Accounts menu panel 3. Switch between the accounts 4. Disconnect an account ## **Screenshots/Recordings** https://github.com/MetaMask/metamask-extension/assets/11148144/eca77143-c53b-4978-b698-3cb4d2e3ac95 https://github.com/MetaMask/metamask-extension/assets/11148144/13d2a4bf-042f-477a-85b6-3459bbc0eb39 ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've clearly explained what problem this PR is solving and how it is solved. - [x] I've linked related issues - [x] I've included manual testing steps - [x] I've included screenshots/recordings if applicable - [x] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. - [x] I’ve properly set the pull request status: - [ ] In case it's not yet "ready for review", I've set it to "draft". - [x] In case it's "ready for review", I've changed it from "draft" to "non-draft". ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: NidhiKJha <menidhikjha@gmail.com> Co-authored-by: Nidhi Kumari <nidhi.kumari@consensys.net>
1 parent 28fa64a commit b110819

File tree

7 files changed

+121
-37
lines changed

7 files changed

+121
-37
lines changed

ui/components/multichain/account-list-item/account-list-item.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useI18nContext } from '../../../hooks/useI18nContext';
88
import { shortenAddress } from '../../../helpers/utils/util';
99

1010
import { AccountListItemMenu, AvatarGroup } from '..';
11+
import { ConnectedAccountsMenu } from '../connected-accounts-menu';
1112
import {
1213
AvatarAccount,
1314
AvatarAccountVariant,
@@ -67,6 +68,7 @@ export const AccountListItem = ({
6768
selected = false,
6869
onClick,
6970
closeMenu,
71+
accountsCount,
7072
connectedAvatar,
7173
connectedAvatarName,
7274
isPinned = false,
@@ -117,6 +119,7 @@ export const AccountListItem = ({
117119
);
118120
const isConnected =
119121
currentTabOrigin && currentTabIsConnectedToSelectedAddress;
122+
const isSingleAccount = accountsCount === 1;
120123

121124
return (
122125
<Box
@@ -358,6 +361,16 @@ export const AccountListItem = ({
358361
isConnected={isConnected}
359362
/>
360363
)}
364+
{menuType === AccountListItemMenuTypes.Connection && (
365+
<ConnectedAccountsMenu
366+
anchorElement={accountListItemMenuElement}
367+
identity={identity}
368+
onClose={() => setAccountOptionsMenuOpen(false)}
369+
closeMenu={closeMenu}
370+
disableAccountSwitcher={isSingleAccount}
371+
isOpen={accountOptionsMenuOpen}
372+
/>
373+
)}
361374
</Box>
362375
);
363376
};
@@ -394,6 +407,10 @@ AccountListItem.propTypes = {
394407
* Function to execute when the item is clicked
395408
*/
396409
onClick: PropTypes.func,
410+
/**
411+
* Represents how many accounts are being listed
412+
*/
413+
accountsCount: PropTypes.number,
397414
/**
398415
* Function that closes the menu
399416
*/
Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,65 @@
11
import React from 'react';
2-
import { fireEvent, act } from '@testing-library/react';
3-
import { renderWithProvider } from '../../../../test/jest';
2+
import { fireEvent } from '@testing-library/react';
3+
import { renderWithProvider } from '../../../../test/lib/render-helpers';
44
import configureStore from '../../../store/store';
55
import mockState from '../../../../test/data/mock-state.json';
66
import { ConnectedAccountsMenu } from '.';
77

88
const DEFAULT_PROPS = {
99
isOpen: true,
1010
onClose: jest.fn(),
11+
identity: { address: '0x123' },
12+
anchorElement: null,
13+
disableAccountSwitcher: false,
14+
closeMenu: jest.fn(),
1115
};
1216

13-
const render = (props = {}) => {
17+
const renderComponent = (props = {}, stateChanges = {}) => {
1418
const store = configureStore({
15-
metamask: {
16-
...mockState.metamask,
19+
...mockState,
20+
...stateChanges,
21+
activeTab: {
22+
origin: 'https://example.com',
1723
},
1824
});
19-
const allProps = { ...DEFAULT_PROPS, ...props };
2025
document.body.innerHTML = '<div id="anchor"></div>';
2126
const anchorElement = document.getElementById('anchor');
2227
return renderWithProvider(
23-
<ConnectedAccountsMenu anchorElement={anchorElement} {...allProps} />,
28+
<ConnectedAccountsMenu
29+
{...DEFAULT_PROPS}
30+
{...props}
31+
anchorElement={anchorElement}
32+
/>,
2433
store,
2534
);
2635
};
2736

2837
describe('ConnectedAccountsMenu', () => {
2938
it('renders permission details menu item', async () => {
30-
await act(async () => {
31-
const { getByText } = render();
32-
expect(getByText('Permission details')).toBeInTheDocument();
33-
});
39+
const { getByTestId } = renderComponent();
40+
expect(getByTestId('permission-details-menu-item')).toBeInTheDocument();
3441
});
3542

36-
it('renders switch to this account menu item', async () => {
37-
await act(async () => {
38-
const { getByText } = render();
39-
expect(getByText('Switch to this account')).toBeInTheDocument();
40-
});
43+
it('renders switch to this account menu item if account switcher is enabled', async () => {
44+
const { getByTestId } = renderComponent();
45+
expect(getByTestId('switch-account-menu-item')).toBeInTheDocument();
46+
});
47+
48+
it('does not render switch to this account menu item if account switcher is disabled', async () => {
49+
const { queryByTestId } = renderComponent({ disableAccountSwitcher: true });
50+
expect(queryByTestId('switch-account-menu-item')).toBeNull();
4151
});
4252

4353
it('renders disconnect menu item', async () => {
44-
await act(async () => {
45-
const { getByText } = render();
46-
expect(getByText('Disconnect')).toBeInTheDocument();
47-
});
54+
const { getByTestId } = renderComponent();
55+
expect(getByTestId('disconnect-menu-item')).toBeInTheDocument();
4856
});
4957

50-
it('closes the menu on tab key down when focus is on the last menu item', async () => {
58+
it('closes the menu on tab key down when focus is within the menu', async () => {
5159
const onClose = jest.fn();
52-
await act(async () => {
53-
const { getByText } = render({ onClose });
54-
const disconnectMenuItem = getByText('Disconnect');
55-
fireEvent.keyDown(disconnectMenuItem, { key: 'Tab' });
56-
});
60+
const { getByTestId } = renderComponent({ onClose });
61+
const menu = getByTestId('permission-details-menu-item');
62+
fireEvent.keyDown(menu, { key: 'Tab' });
5763
expect(onClose).toHaveBeenCalled();
5864
});
5965
});

ui/components/multichain/connected-accounts-menu/connected-accounts-menu.tsx

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import React, { useRef, useCallback } from 'react';
1+
import React, { useRef, useCallback, useEffect } from 'react';
2+
import { useDispatch, useSelector } from 'react-redux';
23
import {
34
PopoverRole,
45
PopoverPosition,
@@ -15,21 +16,55 @@ import {
1516
TextVariant,
1617
} from '../../../helpers/constants/design-system';
1718
import { useI18nContext } from '../../../hooks/useI18nContext';
19+
import {
20+
removePermittedAccount,
21+
setSelectedAccount,
22+
} from '../../../store/actions';
23+
import { getOriginOfCurrentTab } from '../../../selectors';
24+
import { Identity } from './connected-accounts-menu.types';
1825

1926
const TsMenuItem = MenuItem as any;
2027

2128
export const ConnectedAccountsMenu = ({
2229
isOpen,
30+
identity,
2331
anchorElement,
32+
disableAccountSwitcher = false,
2433
onClose,
34+
closeMenu,
2535
}: {
2636
isOpen: boolean;
37+
identity: Identity;
2738
anchorElement: HTMLElement | null;
28-
onClose?: () => void;
39+
disableAccountSwitcher: boolean;
40+
onClose: () => void;
41+
closeMenu: () => void;
2942
}) => {
43+
const activeTabOrigin = useSelector(getOriginOfCurrentTab);
44+
const dispatch = useDispatch();
3045
const t = useI18nContext();
3146
const popoverDialogRef = useRef<HTMLDivElement | null>(null);
3247

48+
const handleClickOutside = useCallback(
49+
(event) => {
50+
if (
51+
popoverDialogRef?.current &&
52+
!popoverDialogRef.current.contains(event.target)
53+
) {
54+
onClose();
55+
}
56+
},
57+
[onClose],
58+
);
59+
60+
useEffect(() => {
61+
document.addEventListener('mousedown', handleClickOutside);
62+
63+
return () => {
64+
document.removeEventListener('mousedown', handleClickOutside);
65+
};
66+
}, [handleClickOutside]);
67+
3368
const handleKeyDown = useCallback(
3469
(event) => {
3570
if (
@@ -64,16 +99,30 @@ export const ConnectedAccountsMenu = ({
6499
>
65100
<Text variant={TextVariant.bodyMd}>{t('permissionDetails')}</Text>
66101
</TsMenuItem>
67-
<TsMenuItem
68-
iconName={IconName.SwapHorizontal}
69-
data-testid="switch-account-menu-item"
70-
>
71-
<Text variant={TextVariant.bodyMd}>{t('switchToThisAccount')}</Text>
72-
</TsMenuItem>
102+
{disableAccountSwitcher ? null : (
103+
<TsMenuItem
104+
iconName={IconName.SwapHorizontal}
105+
data-testid="switch-account-menu-item"
106+
onClick={() => {
107+
dispatch(setSelectedAccount(identity.address));
108+
onClose();
109+
closeMenu();
110+
}}
111+
>
112+
<Text variant={TextVariant.bodyMd}>
113+
{t('switchToThisAccount')}
114+
</Text>
115+
</TsMenuItem>
116+
)}
73117
<TsMenuItem
74118
iconName={IconName.Logout}
75119
iconColor={IconColor.errorDefault}
76120
data-testid="disconnect-menu-item"
121+
onClick={() => {
122+
dispatch(
123+
removePermittedAccount(activeTabOrigin, identity.address),
124+
);
125+
}}
77126
>
78127
<Text color={TextColor.errorDefault} variant={TextVariant.bodyMd}>
79128
{t('disconnect')}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface Identity {
2+
address: string;
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@use "design-system";
2+
3+
.multichain-connected-accounts-menu__popover {
4+
z-index: design-system.$popover-in-modal-z-index;
5+
overflow: hidden;
6+
min-width: 225px;
7+
max-width: 225px;
8+
}

ui/components/multichain/multichain-components.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
@import 'account-picker';
1515
@import 'activity-list-item';
1616
@import 'app-header';
17+
@import 'connected-accounts-menu';
1718
@import 'connected-site-menu';
18-
@import 'account-list-menu';
1919
@import 'token-list-item';
2020
@import 'network-list-item';
2121
@import 'network-list-menu';

ui/components/multichain/pages/connections/connections.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export const Connections = () => {
6060
);
6161
const selectedAccount = useSelector(getSelectedAccount);
6262
const internalAccounts = useSelector(getInternalAccounts);
63-
const mergedAccount = mergeAccounts(connectedAccounts, internalAccounts);
63+
const mergedAccounts = mergeAccounts(connectedAccounts, internalAccounts);
6464
return (
6565
<Page data-testid="connections-page" className="connections-page">
6666
<Header
@@ -117,7 +117,7 @@ export const Connections = () => {
117117
name={t('connectedaccountsTabKey')}
118118
padding={4}
119119
>
120-
{mergedAccount.map((account: AccountType, index: number) => {
120+
{mergedAccounts.map((account: AccountType, index: number) => {
121121
const connectedSites: ConnectedSites = {};
122122

123123
const connectedSite = connectedSites[account.address]?.find(
@@ -127,6 +127,7 @@ export const Connections = () => {
127127
<AccountListItem
128128
identity={account}
129129
key={account.address}
130+
accountsCount={mergedAccounts.length}
130131
selected={selectedAccount.address === account.address}
131132
connectedAvatar={connectedSite?.iconUrl}
132133
connectedAvatarName={connectedSite?.name}

0 commit comments

Comments
 (0)