diff --git a/test/e2e/tests/add-account.spec.js b/test/e2e/tests/add-account.spec.js index 61a903653..cf544bb38 100644 --- a/test/e2e/tests/add-account.spec.js +++ b/test/e2e/tests/add-account.spec.js @@ -216,7 +216,7 @@ describe('Add account', function () { ); // Create 3rd account with private key - await driver.clickElement('.menu__background'); + await driver.clickElement('.mm-text-field'); await driver.clickElement({ text: 'Import account', tag: 'button' }); await driver.fill('#private-key-box', testPrivateKey); diff --git a/ui/components/multichain/account-list-item-menu/account-list-item-menu.js b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js index 2932ee01d..cb763097d 100644 --- a/ui/components/multichain/account-list-item-menu/account-list-item-menu.js +++ b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useRef, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import PropTypes from 'prop-types'; @@ -23,8 +23,15 @@ import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils ///: END:ONLY_INCLUDE_IN import { findKeyringForAddress } from '../../../ducks/metamask/metamask'; import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; -import { Menu, MenuItem } from '../../ui/menu'; -import { Text, IconName } from '../../component-library'; +import { MenuItem } from '../../ui/menu'; +import { + Text, + IconName, + Popover, + PopoverPosition, + ModalFocus, + PopoverRole, +} from '../../component-library'; import { MetaMetricsEventCategory, MetaMetricsEventLinkType, @@ -42,6 +49,7 @@ export const AccountListItemMenu = ({ closeMenu, isRemovable, identity, + isOpen, }) => { const t = useI18nContext(); const trackEvent = useContext(MetaMetricsContext); @@ -82,116 +90,178 @@ export const AccountListItemMenu = ({ }; ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + const isCustodial = keyring?.type ? /Custody/u.test(keyring.type) : false; const accounts = useSelector(getMetaMaskAccountsOrdered); - const isCustodial = /Custody/u.test(keyring.type); + const mmiActions = mmiActionsFactory(); ///: END:ONLY_INCLUDE_IN - return ( - - { - blockExplorerLinkText.firstPart === 'addBlockExplorer' - ? routeToAddBlockExplorerUrl() - : openBlockExplorer(); + // Handle Tab key press for accessibility inside the popover and will close the popover on the last MenuItem + const lastItemRef = useRef(null); + const accountDetailsItemRef = useRef(null); + const removeAccountItemRef = useRef(null); + const removeJWTItemRef = useRef(null); - trackEvent({ - event: MetaMetricsEventName.BlockExplorerLinkClicked, - category: MetaMetricsEventCategory.Accounts, - properties: { - location: 'Account Options', - chain_id: chainId, - }, - }); - }} - subtitle={blockExplorerUrlSubTitle || null} - iconName={IconName.Export} - data-testid="account-list-menu-open-explorer" - > - {t('viewOnExplorer')} - - { - dispatch(setAccountDetailsAddress(identity.address)); - trackEvent({ - event: MetaMetricsEventName.NavAccountDetailsOpened, - category: MetaMetricsEventCategory.Navigation, - properties: { - location: 'Account Options', - }, - }); - onClose(); - closeMenu?.(); - }} - iconName={IconName.ScanBarcode} - data-testid="account-list-menu-details" - > - {t('accountDetails')} - - {isRemovable ? ( - { - dispatch( - showModal({ - name: 'CONFIRM_REMOVE_ACCOUNT', - identity, - }), - ); - trackEvent({ - event: MetaMetricsEventName.AccountRemoved, - category: MetaMetricsEventCategory.Accounts, - properties: { - account_hardware_type: deviceName, - chain_id: chainId, - account_type: accountType, - }, - }); - onClose(); - closeMenu?.(); - }} - iconName={IconName.Trash} - > - {t('removeAccount')} - - ) : null} - { - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - isCustodial ? ( + // Checks the MenuItems from the bottom to top to set lastItemRef on the last MenuItem that is not disabled + useEffect(() => { + if (removeJWTItemRef.current) { + lastItemRef.current = removeJWTItemRef.current; + } else if (removeAccountItemRef.current) { + lastItemRef.current = removeAccountItemRef.current; + } else { + lastItemRef.current = accountDetailsItemRef.current; + } + }, [ + removeJWTItemRef.current, + removeAccountItemRef.current, + accountDetailsItemRef.current, + ]); + + const handleKeyDown = (event) => { + if (event.key === 'Tab' && event.target === lastItemRef.current) { + // If Tab is pressed at the last item to close popover and focus to next element in DOM + onClose(); + } + }; + + // Handle click outside of the popover to close it + const popoverDialogRef = useRef(null); + + const handleClickOutside = (event) => { + if ( + popoverDialogRef?.current && + !popoverDialogRef.current.contains(event.target) + ) { + onClose(); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( + + +
{ - const token = await dispatch(mmiActions.getCustodianToken()); - const custodyAccountDetails = await dispatch( - mmiActions.getAllCustodianAccountsWithToken( - keyring.type.split(' - ')[1], - token, - ), - ); - dispatch( - showModal({ - name: 'CONFIRM_REMOVE_JWT', - token, - custodyAccountDetails, - accounts, - selectedAddress: toChecksumHexAddress(identity.address), - }), - ); + onClick={() => { + blockExplorerLinkText.firstPart === 'addBlockExplorer' + ? routeToAddBlockExplorerUrl() + : openBlockExplorer(); + + trackEvent({ + event: MetaMetricsEventName.BlockExplorerLinkClicked, + category: MetaMetricsEventCategory.Accounts, + properties: { + location: 'Account Options', + chain_id: chainId, + }, + }); + }} + subtitle={blockExplorerUrlSubTitle || null} + iconName={IconName.Export} + data-testid="account-list-menu-open-explorer" + > + {t('viewOnExplorer')} + + { + dispatch(setAccountDetailsAddress(identity.address)); + trackEvent({ + event: MetaMetricsEventName.NavAccountDetailsOpened, + category: MetaMetricsEventCategory.Navigation, + properties: { + location: 'Account Options', + }, + }); onClose(); closeMenu?.(); }} - iconName={IconName.Trash} + iconName={IconName.ScanBarcode} + data-testid="account-list-menu-details" > - {t('removeJWT')} + {t('accountDetails')} - ) : null - ///: END:ONLY_INCLUDE_IN - } -
+ {isRemovable ? ( + { + dispatch( + showModal({ + name: 'CONFIRM_REMOVE_ACCOUNT', + identity, + }), + ); + trackEvent({ + event: MetaMetricsEventName.AccountRemoved, + category: MetaMetricsEventCategory.Accounts, + properties: { + account_hardware_type: deviceName, + chain_id: chainId, + account_type: accountType, + }, + }); + onClose(); + closeMenu?.(); + }} + iconName={IconName.Trash} + > + {t('removeAccount')} + + ) : null} + { + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + isCustodial ? ( + { + const token = await dispatch(mmiActions.getCustodianToken()); + const custodyAccountDetails = await dispatch( + mmiActions.getAllCustodianAccountsWithToken( + keyring.type.split(' - ')[1], + token, + ), + ); + dispatch( + showModal({ + name: 'CONFIRM_REMOVE_JWT', + token, + custodyAccountDetails, + accounts, + selectedAddress: toChecksumHexAddress(identity.address), + }), + ); + onClose(); + closeMenu?.(); + }} + iconName={IconName.Trash} + > + {t('removeJWT')} + + ) : null + ///: END:ONLY_INCLUDE_IN + } + + + ); }; @@ -204,6 +274,12 @@ AccountListItemMenu.propTypes = { * Function that executes when the menu is closed */ onClose: PropTypes.func.isRequired, + /** + * Represents if the menu is open or not + * + * @type {boolean} + */ + isOpen: PropTypes.bool.isRequired, /** * Function that closes the menu */ diff --git a/ui/components/multichain/account-list-item-menu/account-list-item-menu.stories.js b/ui/components/multichain/account-list-item-menu/account-list-item-menu.stories.js index 987ebf3c2..4602223f4 100644 --- a/ui/components/multichain/account-list-item-menu/account-list-item-menu.stories.js +++ b/ui/components/multichain/account-list-item-menu/account-list-item-menu.stories.js @@ -23,6 +23,9 @@ export default { identity: { control: 'object', }, + isOpen: { + control: 'boolean', + }, }, args: { anchorElement: null, @@ -34,6 +37,7 @@ export default { }, isRemovable: true, blockExplorerUrlSubTitle: 'etherscan.io', + isOpen: true, }, }; diff --git a/ui/components/multichain/account-list-item-menu/account-list-item-menu.test.js b/ui/components/multichain/account-list-item-menu/account-list-item-menu.test.js index f5b91104b..8dc2462dd 100644 --- a/ui/components/multichain/account-list-item-menu/account-list-item-menu.test.js +++ b/ui/components/multichain/account-list-item-menu/account-list-item-menu.test.js @@ -17,6 +17,7 @@ const DEFAULT_PROPS = { onClose: jest.fn(), onHide: jest.fn(), isRemovable: false, + isOpen: true, }; const render = (props = {}) => { diff --git a/ui/components/multichain/account-list-item-menu/index.scss b/ui/components/multichain/account-list-item-menu/index.scss new file mode 100644 index 000000000..5c52e4180 --- /dev/null +++ b/ui/components/multichain/account-list-item-menu/index.scss @@ -0,0 +1,6 @@ +.multichain-account-list-item-menu__popover { + z-index: $popover-in-modal-z-index; + overflow: hidden; + min-width: 225px; + max-width: 225px; +} diff --git a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap index 9dfa5527f..2afc7f4ed 100644 --- a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap +++ b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap @@ -3,7 +3,7 @@ exports[`AccountListItem renders AccountListItem component and shows account name, address, and balance 1`] = `