1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

Update account options menu design (#8654)

The `AccountDetailsDropdown` component has been rewritten to use the
new `Menu` component, and to follow the latest designs.

This should be functionally equivalent. A couple of the icons have
changed, but that's about it.

Support for a subtitle was added to `MenuItem` to support the `origin`
subtitle used for the explorer link for custom RPC endpoints.

A few adjustments were required to `test/helper.js` to accommodate
the use of `Menu` from a JSDOM context (this is the first time it's
been used in a unit test). A `popover-content` element was added to the
fake DOM, and another global was added that `react-popper` used
internally.

An additional driver method (`clickPoint`) was added to the e2e driver
to allow clicking the background behind the menu to dismiss it. This
wasn't possible using the `clickElement` method, because that method
would refuse to click an obscured element. The only non-obscured
element to click was the menu backdrop, and that didn't work either
because the center was obscured by the menu (Selenium clicks the center
of whichever element is targeted).
This commit is contained in:
Mark Stacey 2020-05-27 12:31:53 -03:00 committed by GitHub
parent c0e32b54eb
commit a6f2156386
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 200 additions and 243 deletions

View File

@ -1,5 +0,0 @@
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg">
<g fill="#fff">
<path d="M15.48.065A.846.846 0 0015.16 0h-5.047a.841.841 0 000 1.682h3.016l-4.35 4.35A.84.84 0 109.97 7.22l4.35-4.349v3.016a.84.84 0 101.681 0V.84a.846.846 0 00-.52-.776zM.52 15.935A.846.846 0 00.84 16h5.047a.841.841 0 000-1.682H2.87l4.35-4.35A.84.84 0 106.03 8.78l-4.35 4.349v-3.016a.84.84 0 10-1.681 0v5.046a.846.846 0 00.52.776z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 443 B

View File

@ -1,6 +0,0 @@
<svg height="12" width="16" xmlns="http://www.w3.org/2000/svg">
<g fill="#fff" fill-rule="evenodd">
<path d="M12.792.686L2.502 11.252l.708.726L13.5 1.412zM4.53 6.332c0-1.968 1.555-3.563 3.471-3.563.402 0 .781.085 1.14.213l.882-.905A9.25 9.25 0 008 1.838c-3.36 0-6.312 1.791-8 4.494a9.649 9.649 0 002.966 2.991l1.772-1.82a3.606 3.606 0 01-.208-1.171M13.02 3.34L11.25 5.16c.126.37.208.76.208 1.172 0 1.968-1.55 3.563-3.462 3.563-.403 0-.78-.084-1.139-.214l-.879.905c.653.146 1.324.24 2.018.24 3.351 0 6.297-1.792 7.982-4.494a9.649 9.649 0 00-2.96-2.992"/>
<path d="M8.001 8.75c1.301 0 2.354-1.082 2.354-2.418 0-.074-.014-.145-.02-.218L7.788 8.728c.071.007.14.022.212.022M8.001 3.914c-1.301 0-2.354 1.082-2.354 2.417 0 .075.014.146.02.218l2.546-2.614c-.071-.006-.14-.021-.212-.021"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 817 B

View File

@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2C11.3137 2 14 4.68629 14 8ZM16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM8 11C9.65685 11 11 9.65685 11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11Z" fill="#FFFFFF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2C11.3137 2 14 4.68629 14 8ZM16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM8 11C9.65685 11 11 9.65685 11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11Z" fill="#24292E"/>
</svg>

Before

Width:  |  Height:  |  Size: 495 B

After

Width:  |  Height:  |  Size: 495 B

View File

@ -1,8 +0,0 @@
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg">
<g fill="#fff" fill-rule="evenodd">
<path d="M13.214.643V.5H.5v12.714h.143V.643z" stroke="#fff"/>
<path d="M3.429 3.429v11.428h11.428V3.43zM2.286 2.286H16V16H2.286z" fill-rule="nonzero"/>
<rect height="5" rx="1" width="2" x="8" y="8"/>
<rect height="2" rx="1" width="2" x="8" y="5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 400 B

View File

@ -1,3 +0,0 @@
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg">
<path d="M3 0a1 1 0 000 2h9.586L.293 14.293a.999.999 0 101.414 1.414L14 3.414V13a1 1 0 002 0V0z" fill="#fff" fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 206 B

View File

@ -117,9 +117,12 @@ describe('MetaMask', function () {
describe('Show account information', function () {
it('show account details dropdown menu', async function () {
await driver.clickElement(By.css('button.menu-bar__account-options'))
const options = await driver.findElements(By.css('div.menu.account-details-dropdown div.menu__item'))
await driver.clickElement(By.css('[data-testid="account-options-menu-button"]'))
const options = await driver.findElements(By.css('.account-options-menu .menu-item'))
assert.equal(options.length, 4) // HD Wallet type does not have to show the Remove Account option
// click outside of menu to dismiss
// account menu button chosen because the menu never covers it.
await driver.clickPoint(By.css('.account-menu__icon'), 0, 0)
await driver.delay(regularDelayMs)
})
})

View File

@ -71,6 +71,15 @@ class Driver {
await element.click()
}
async clickPoint (locator, x, y) {
const element = await this.findElement(locator)
await this.driver
.actions()
.move({ origin: element, x, y })
.click()
.perform()
}
async scrollToElement (element) {
await this.driver.executeScript('arguments[0].scrollIntoView(true)', element)
}

View File

@ -57,6 +57,13 @@ global.document = window.document
// required by `react-tippy`
global.navigator = window.navigator
global.Element = window.Element
// required by `react-popper`
global.HTMLElement = window.HTMLElement
// required by any components anchored on `popover-content`
const popoverContent = window.document.createElement('div')
popoverContent.setAttribute('id', 'popover-content')
window.document.body.appendChild(popoverContent)
// delete AbortController added by jsdom so it can be polyfilled correctly below
delete window.AbortController

View File

@ -1,151 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { CONNECTED_ROUTE } from '../../../../helpers/constants/routes'
import { Menu, Item, CloseArea } from '../components/menu'
export default class AccountDetailsDropdown extends Component {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
static propTypes = {
selectedIdentity: PropTypes.object.isRequired,
network: PropTypes.string.isRequired,
keyrings: PropTypes.array.isRequired,
showAccountDetailModal: PropTypes.func.isRequired,
viewOnEtherscan: PropTypes.func.isRequired,
showRemoveAccountConfirmationModal: PropTypes.func.isRequired,
rpcPrefs: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
}
onClose = (e) => {
e.stopPropagation()
this.props.onClose()
}
render () {
const {
selectedIdentity,
network,
keyrings,
showAccountDetailModal,
viewOnEtherscan,
showRemoveAccountConfirmationModal,
rpcPrefs,
history,
} = this.props
const address = selectedIdentity.address
const keyring = keyrings.find((kr) => {
return kr.accounts.includes(address)
})
const isRemovable = keyring.type !== 'HD Key Tree'
return (
<Menu className="account-details-dropdown" isShowing>
<CloseArea onClick={this.onClose} />
<Item
onClick={(e) => {
e.stopPropagation()
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Account Options',
name: 'Clicked Expand View',
},
})
global.platform.openExtensionInBrowser()
this.props.onClose()
}}
text={this.context.t('expandView')}
icon={(
<img alt="" src="images/expand.svg" style={{ height: '15px' }} />
)}
/>
<Item
onClick={(e) => {
e.stopPropagation()
showAccountDetailModal()
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Account Options',
name: 'Viewed Account Details',
},
})
this.props.onClose()
}}
text={this.context.t('accountDetails')}
icon={(
<img src="images/info.svg" style={{ height: '15px' }} alt="" />
)}
/>
<Item
onClick={(e) => {
e.stopPropagation()
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Account Options',
name: 'Clicked View on Etherscan',
},
})
viewOnEtherscan(address, network, rpcPrefs)
this.props.onClose()
}}
text={
rpcPrefs.blockExplorerUrl
? this.context.t('viewinExplorer')
: this.context.t('viewOnEtherscan')
}
subText={
rpcPrefs.blockExplorerUrl
? rpcPrefs.blockExplorerUrl.match(/^https?:\/\/(.+)/)[1]
: null
}
icon={(
<img src="images/open-etherscan.svg" style={{ height: '15px' }} alt="" />
)}
/>
<Item
onClick={(e) => {
e.stopPropagation()
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Account Options',
name: 'Opened Connected Sites',
},
})
history.push(CONNECTED_ROUTE)
this.props.onClose()
}}
text={this.context.t('connectedSites')}
icon={(
<img src="images/icons/connected-sites-white.svg" style={{ height: '15px' }} alt="" />
)}
/>
{
isRemovable
? (
<Item
onClick={(e) => {
e.stopPropagation()
showRemoveAccountConfirmationModal(selectedIdentity)
this.props.onClose()
}}
text={this.context.t('removeAccount')}
icon={<img src="images/hide.svg" style={{ height: '15px' }} alt="" />}
/>
)
: null
}
</Menu>
)
}
}

View File

@ -1,35 +0,0 @@
import { compose } from 'redux'
import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux'
import AccountDetailsDropdown from './account-details-dropdown.component'
import * as actions from '../../../../store/actions'
import {
getSelectedIdentity,
getRpcPrefsForCurrentProvider,
} from '../../../../selectors'
import genAccountLink from '../../../../../lib/account-link'
function mapStateToProps (state) {
return {
selectedIdentity: getSelectedIdentity(state),
network: state.metamask.network,
keyrings: state.metamask.keyrings,
rpcPrefs: getRpcPrefsForCurrentProvider(state),
}
}
function mapDispatchToProps (dispatch) {
return {
showAccountDetailModal: () => {
dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
},
viewOnEtherscan: (address, network, rpcPrefs) => {
global.platform.openTab({ url: genAccountLink(address, network, rpcPrefs) })
},
showRemoveAccountConfirmationModal: (identity) => {
return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity }))
},
}
}
export default compose(withRouter, connect(mapStateToProps, mapDispatchToProps))(AccountDetailsDropdown)

View File

@ -1 +0,0 @@
export { default } from './account-details-dropdown.container'

View File

@ -1,6 +0,0 @@
.account-details-dropdown {
position: absolute;
top: 120px;
right: 24px;
z-index: 1;
}

View File

@ -102,8 +102,6 @@
@import './connected-status-indicator/index';
@import './dropdowns/account-details-dropdown/index';
@import '../ui/check-box/index';
@import '../ui/dropdown/dropdown';

View File

@ -0,0 +1,140 @@
import React from 'react'
import PropTypes from 'prop-types'
import { useHistory } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import { showModal } from '../../../store/actions'
import { CONNECTED_ROUTE } from '../../../helpers/constants/routes'
import { Menu, MenuItem } from '../../ui/menu'
import genAccountLink from '../../../../lib/account-link'
import { getCurrentKeyring, getCurrentNetwork, getRpcPrefsForCurrentProvider, getSelectedIdentity } from '../../../selectors'
import { useI18nContext } from '../../../hooks/useI18nContext'
import { useMetricEvent } from '../../../hooks/useMetricEvent'
export default function AccountOptionsMenu ({ anchorElement, onClose }) {
const t = useI18nContext()
const dispatch = useDispatch()
const history = useHistory()
const openFullscreenEvent = useMetricEvent({
eventOpts: {
category: 'Navigation',
action: 'Account Options',
name: 'Clicked Expand View',
},
})
const viewAccountDetailsEvent = useMetricEvent({
eventOpts: {
category: 'Navigation',
action: 'Account Options',
name: 'Viewed Account Details',
},
})
const viewOnEtherscanEvent = useMetricEvent({
eventOpts: {
category: 'Navigation',
action: 'Account Options',
name: 'Clicked View on Etherscan',
},
})
const openConnectedSitesEvent = useMetricEvent({
eventOpts: {
category: 'Navigation',
action: 'Account Options',
name: 'Opened Connected Sites',
},
})
const keyring = useSelector(getCurrentKeyring)
const network = useSelector(getCurrentNetwork)
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider)
const selectedIdentity = useSelector(getSelectedIdentity)
const address = selectedIdentity.address
const isRemovable = keyring.type !== 'HD Key Tree'
return (
<Menu
anchorElement={anchorElement}
className="account-options-menu"
onHide={onClose}
>
<MenuItem
onClick={() => {
openFullscreenEvent()
global.platform.openExtensionInBrowser()
onClose()
}}
iconClassName="fas fa-expand-alt"
>
{ t('expandView') }
</MenuItem>
<MenuItem
onClick={() => {
dispatch(showModal({ name: 'ACCOUNT_DETAILS' }))
viewAccountDetailsEvent()
onClose()
}}
iconClassName="fas fa-qrcode"
>
{ t('accountDetails') }
</MenuItem>
<MenuItem
onClick={() => {
viewOnEtherscanEvent()
global.platform.openTab({ url: genAccountLink(address, network, rpcPrefs) })
onClose()
}}
subtitle={
rpcPrefs.blockExplorerUrl
? (
<span className="account-options-menu__explorer-origin">
{ rpcPrefs.blockExplorerUrl.match(/^https?:\/\/(.+)/)[1] }
</span>
)
: null
}
iconClassName="fas fa-external-link-alt"
>
{
rpcPrefs.blockExplorerUrl
? t('viewinExplorer')
: t('viewOnEtherscan')
}
</MenuItem>
<MenuItem
onClick={() => {
openConnectedSitesEvent()
history.push(CONNECTED_ROUTE)
onClose()
}}
iconClassName="account-options-menu__connected-sites"
>
{ t('connectedSites') }
</MenuItem>
{
isRemovable
? (
<MenuItem
onClick={() => {
dispatch(showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', selectedIdentity }))
onClose()
}}
iconClassName="fas fa-trash-alt"
>
{ t('removeAccount') }
</MenuItem>
)
: null
}
</Menu>
)
}
AccountOptionsMenu.propTypes = {
anchorElement: PropTypes.instanceOf(window.Element),
onClose: PropTypes.func.isRequired,
}
AccountOptionsMenu.defaultProps = {
anchorElement: undefined,
}

View File

@ -11,8 +11,6 @@
background: none;
font-size: inherit;
padding: 0 8px 0 5px;
position: relative; // to allow the dropdown to position itself absolutely
place-self: center end;
}
@ -21,3 +19,18 @@
place-self: center stretch;
}
}
.account-options-menu {
&__connected-sites:before {
content: "";
background-image: url(/images/icons/connected-sites-black.svg);
background-size: contain;
background-repeat: no-repeat;
background-position: center;
padding: 8px;
}
&__explorer-origin {
color: $Grey-500;
font-size: 12px;
}
}

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react'
import SelectedAccount from '../selected-account'
import ConnectedStatusIndicator from '../connected-status-indicator'
import AccountDetailsDropdown from '../dropdowns/account-details-dropdown'
import AccountOptionsMenu from './account-options-menu'
import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
import { CONNECTED_ACCOUNTS_ROUTE } from '../../../helpers/constants/routes'
@ -19,7 +19,8 @@ export default function MenuBar () {
},
})
const history = useHistory()
const [accountDetailsMenuOpen, setAccountDetailsMenuOpen] = useState(false)
const [accountOptionsButtonElement, setAccountOptionsButtonElement] = useState(null)
const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false)
return (
<div className="menu-bar">
@ -33,17 +34,20 @@ export default function MenuBar () {
<button
className="fas fa-ellipsis-v menu-bar__account-options"
data-testid="account-options-menu-button"
ref={setAccountOptionsButtonElement}
title={t('accountOptions')}
onClick={() => {
openAccountOptionsEvent()
setAccountDetailsMenuOpen(true)
setAccountOptionsMenuOpen(true)
}}
/>
{
accountDetailsMenuOpen && (
<AccountDetailsDropdown
onClose={() => setAccountDetailsMenuOpen(false)}
accountOptionsMenuOpen && (
<AccountOptionsMenu
anchorElement={accountOptionsButtonElement}
onClose={() => setAccountOptionsMenuOpen(false)}
/>
)
}

View File

@ -36,11 +36,11 @@ describe('MenuBar', function () {
<MenuBar />
</Provider>
)
assert.ok(!wrapper.exists('AccountDetailsDropdown'))
assert.ok(!wrapper.exists('AccountOptionsMenu'))
const accountOptions = wrapper.find('.menu-bar__account-options')
accountOptions.simulate('click')
wrapper.update()
assert.ok(wrapper.exists('AccountDetailsDropdown'))
assert.ok(wrapper.exists('AccountOptionsMenu'))
})
it('sets accountDetailsMenuOpen to false when closed', function () {
@ -53,10 +53,10 @@ describe('MenuBar', function () {
const accountOptions = wrapper.find('.menu-bar__account-options')
accountOptions.simulate('click')
wrapper.update()
assert.ok(wrapper.exists('AccountDetailsDropdown'))
const accountDetailsMenu = wrapper.find('AccountDetailsDropdown')
assert.ok(wrapper.exists('AccountOptionsMenu'))
const accountDetailsMenu = wrapper.find('AccountOptionsMenu')
accountDetailsMenu.prop('onClose')()
wrapper.update()
assert.ok(!wrapper.exists('AccountDetailsDropdown'))
assert.ok(!wrapper.exists('AccountOptionsMenu'))
})
})

View File

@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
const MenuItem = ({ children, className, iconClassName, onClick }) => (
const MenuItem = ({ children, className, iconClassName, onClick, subtitle }) => (
<button className={classnames('menu-item', className)} onClick={onClick}>
{
iconClassName
@ -12,6 +12,7 @@ const MenuItem = ({ children, className, iconClassName, onClick }) => (
: null
}
<span>{children}</span>
{ subtitle }
</button>
)
@ -20,12 +21,14 @@ MenuItem.propTypes = {
className: PropTypes.string,
iconClassName: PropTypes.string,
onClick: PropTypes.func,
subtitle: PropTypes.node,
}
MenuItem.defaultProps = {
className: undefined,
iconClassName: undefined,
onClick: undefined,
subtitle: undefined,
}
export default MenuItem

View File

@ -33,8 +33,9 @@
font-family: inherit;
font-size: inherit;
display: flex;
flex-direction: row;
display: grid;
grid-template-columns: min-content auto;
text-align: start;
align-items: center;
width: 100%;
padding: 14px 0;
@ -42,6 +43,7 @@
&__icon {
margin-right: 8px;
grid-row: 1 / span 2;
}
.disconnect-icon {

View File

@ -161,13 +161,6 @@ $wallet-view-bg: $alabaster;
}
}
// account options dropdown
.account-options-menu {
align-items: center;
justify-content: flex-start;
margin: 5% 7% 0%;
}
.fiat-amount {
text-transform: uppercase;
}