Popup menu replace and styling (#6007)

* Add custom popup

* Render popup window with react portal

* Handle scroll

* Handle pointer scroll

* Refactor types

* Add useOutsideClick

* Restore button styles

* Update types

* Add tests

* Set multiple refs

* Cleanup

* Cleanup

* Move offset const to the utils

* Add useCallback in useScroll

* Remove empty lines in useElementPosition

* Change styled components to functional

* Make sure popup window stays in view

* Highlight trigger button when popup is open

* Add uesPopupWindow hook

* Make triangle larger

* Use full trigger height

* Apply new lint rules

* Group popup items into groups suggested by Jaco

Menu.Item is of SUI (semantic UI). It supports icons, but
we plan to replace SUI and thus this feature is likely to use
font awesome instead of SUI icons.

* Remove sui menu

* Improve styles

* Add translations

* Add hover

Co-authored-by: Marcin Wachulski <marcin@ethworks.io>
This commit is contained in:
MiZiet 2021-08-31 10:39:17 +02:00 committed by GitHub
parent e864cf8bda
commit b4967ca82c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 281 additions and 69 deletions

View File

@ -30,6 +30,7 @@
"Any account set as proxy will be able to perform actions in place of the proxied account": "Any account set as proxy will be able to perform actions in place of the proxied account",
"Any democracy vote performed by the delegated account will result in an additional vote from the delegating account": "Any democracy vote performed by the delegated account will result in an additional vote from the delegating account",
"Approve this call hash": "Approve this call hash",
"Backup": "Backup",
"Backup account": "Backup account",
"Cancel this call hash": "Cancel this call hash",
"Change": "Change",
@ -48,6 +49,7 @@
"Delegate": "Delegate",
"Delegate democracy votes": "Delegate democracy votes",
"Derivation for Ledger-type accounts are only available on mnemonic seeds.": "Derivation for Ledger-type accounts are only available on mnemonic seeds.",
"Derive": "Derive",
"Derive account from pair": "Derive account from pair",
"Derive account via derivation path": "Derive account via derivation path",
"Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use \"ed25519\".": "Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use \"ed25519\".",
@ -68,6 +70,7 @@
"Full Legal Name": "Full Legal Name",
"Future versions of the web-only interface will drop support for non-external accounts, much like the IPFS version.": "Future versions of the web-only interface will drop support for non-external accounts, much like the IPFS version.",
"I have saved my mnemonic seed safely": "I have saved my mnemonic seed safely",
"Identity": "Identity",
"If the delegated account is currently voting in a referendum, the delegating vote and conviction will be added.": "If the delegated account is currently voting in a referendum, the delegating vote and conviction will be added.",
"If the recipient account is new, the balance needs to be more than the existential deposit. Likewise if the sending account balance drops below the same value, the account will be removed from the state.": "If the recipient account is new, the balance needs to be more than the existential deposit. Likewise if the sending account balance drops below the same value, the account will be removed from the state.",
"If you add several proxy accounts for the same proxy type (e.g 2 accounts set as proxy for Governance), then any of those 2 accounts will be able to perfom governance actions on behalf of the proxied account": "If you add several proxy accounts for the same proxy type (e.g 2 accounts set as proxy for Governance), then any of those 2 accounts will be able to perfom governance actions on behalf of the proxied account",
@ -117,6 +120,7 @@
"Proxy overview": "Proxy overview",
"Raw seed": "Raw seed",
"Reasonable": "Reasonable",
"Recovery": "Recovery",
"Register identity": "Register identity",
"Register sub-identities": "Register sub-identities",
"Remove": "Remove",

View File

@ -18,6 +18,7 @@
"Authorize transaction": "",
"Aye": "",
"Aye: {{count}}": "",
"Backup": "",
"Blocks nominations": "",
"Bond": "",
"Bounty approval under voting": "",
@ -42,9 +43,11 @@
"Curator's fee": "",
"Dark theme (experimental, work-in-progress)": "",
"Defender": "",
"Delegate": "",
"Delegate democracy votes": "",
"Deploy": "",
"Deploy a contract": "",
"Derive": "",
"Derive account via derivation path": "",
"Developer": "",
"Distinct stash and controller accounts are recommended to ensure fund security. You will be allowed to make the transaction, but take care to not tie up all funds, only use a portion of the available funds during this period.": "",
@ -56,6 +59,7 @@
"Forget this account": "",
"Full Legal Name": "",
"Governance": "",
"Identity": "",
"If the motion is approved, Curator's deposit will be slashed and Curator will be unassigned. Bounty will return to the Funded state.": "",
"If the motion is approved, the current Curator will be unassigned and the Bounty will return to the Funded state.": "",
"If you have not claimed rewards straight after the end of the era, the validator is in the active set and you are seeing no rewards, this would mean that the reward payout transaction was made by another account on your behalf. Always check your favorite explorer to see any historic payouts made to your accounts.": "",
@ -66,6 +70,7 @@
"Locked funds (e.g. for staking) are counted.": "",
"Make recoverable": "",
"Manage proxies": "",
"Multisig": "",
"Multisig approvals": "",
"My On-Chain Name": "",
"Nay": "",
@ -92,6 +97,7 @@
"Prior locked voting": "",
"Produced blocks": "",
"Proposer": "",
"Recovery": "",
"Retrieving data": "",
"Retrieving nominators": "",
"Retrieving validators": "",

View File

@ -233,6 +233,7 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
createMenuGroup('identityGroup', [
isFunction(api.api.tx.identity?.setIdentity) && !isHardware && (
<Menu.Item
icon='link'
key='identityMain'
onClick={toggleIdentityMain}
>
@ -241,6 +242,7 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
),
isFunction(api.api.tx.identity?.setSubs) && identity?.display && !isHardware && (
<Menu.Item
icon='vector-square'
key='identitySub'
onClick={toggleIdentitySub}
>
@ -249,6 +251,7 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
),
isFunction(api.api.tx.democracy?.unlock) && democracyUnlockTx && (
<Menu.Item
icon='broom'
key='clearDemocracy'
onClick={_clearDemocracyLocks}
>
@ -257,16 +260,18 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
),
isFunction(api.api.tx.vesting?.vest) && vestingVestTx && (
<Menu.Item
icon='unlock'
key='vestingVest'
onClick={_vestingVest}
>
{t('Unlock vested amount')}
</Menu.Item>
)
]),
], t('Identity')),
createMenuGroup('deriveGroup', [
!(isExternal || isHardware || isInjected || isMultisig) && (
<Menu.Item
icon='download'
key='deriveAccount'
onClick={toggleDerive}
>
@ -275,16 +280,18 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
),
isHardware && (
<Menu.Item
icon='eye'
key='showHwAddress'
onClick={_showOnHardware}
>
{t('Show address on hardware device')}
</Menu.Item>
)
]),
], t('Derive')),
createMenuGroup('backupGroup', [
!(isExternal || isHardware || isInjected || isMultisig || isDevelopment) && (
<Menu.Item
icon='database'
key='backupJson'
onClick={toggleBackup}
>
@ -293,6 +300,7 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
),
!(isExternal || isHardware || isInjected || isMultisig || isDevelopment) && (
<Menu.Item
icon='edit'
key='changePassword'
onClick={togglePassword}
>
@ -301,16 +309,18 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
),
!(isInjected || isDevelopment) && (
<Menu.Item
icon='trash-alt'
key='forgetAccount'
onClick={toggleForget}
>
{t('Forget this account')}
</Menu.Item>
)
]),
], t('Backup')),
isFunction(api.api.tx.recovery?.createRecovery) && createMenuGroup('reoveryGroup', [
!recoveryInfo && (
<Menu.Item
icon='redo'
key='makeRecoverable'
onClick={toggleRecoverSetup}
>
@ -318,54 +328,63 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
</Menu.Item>
),
<Menu.Item
icon='screwdriver'
key='initRecovery'
onClick={toggleRecoverAccount}
>
{t('Initiate recovery for another')}
</Menu.Item>
]),
], t('Recovery')),
isFunction(api.api.tx.multisig?.asMulti) && isMultisig && createMenuGroup('multisigGroup', [
<Menu.Item
disabled={!multiInfos || !multiInfos.length}
icon='file-signature'
key='multisigApprovals'
onClick={toggleMultisig}
>
{t('Multisig approvals')}
</Menu.Item>
]),
], t('Multisig')),
isFunction(api.api.query.democracy?.votingOf) && delegation?.accountDelegated && createMenuGroup('undelegateGroup', [
<Menu.Item
icon='user-edit'
key='changeDelegate'
onClick={toggleDelegate}
>
{t('Change democracy delegation')}
</Menu.Item>,
<Menu.Item
icon='user-minus'
key='undelegate'
onClick={toggleUndelegate}
>
{t('Undelegate')}
</Menu.Item>
]),
isFunction(api.api.query.democracy?.votingOf) && !delegation?.accountDelegated && createMenuGroup('delegateGroup', [
<Menu.Item
key='delegate'
onClick={toggleDelegate}
>
{t('Delegate democracy votes')}
</Menu.Item>
]),
isFunction(api.api.query.proxy?.proxies) && createMenuGroup('proxyGroup', [
<Menu.Item
key='proxy-overview'
onClick={toggleProxyOverview}
>
{proxy?.[0].length
? t('Manage proxies')
: t('Add proxy')
}
</Menu.Item>
]),
], t('Undelegate')),
createMenuGroup('delegateGroup', [
isFunction(api.api.query.democracy?.votingOf) && !delegation?.accountDelegated && (
<Menu.Item
icon='user-plus'
key='delegate'
onClick={toggleDelegate}
>
{t('Delegate democracy votes')}
</Menu.Item>
),
isFunction(api.api.query.proxy?.proxies) && (
<Menu.Item
icon='sitemap'
key='proxy-overview'
onClick={toggleProxyOverview}
>
{proxy?.[0].length
? t('Manage proxies')
: t('Add proxy')
}
</Menu.Item>
)
], t('Delegate')),
isEditable && !api.isDevelopment && createMenuGroup('genesisGroup', [
<ChainLock
className='accounts--network-toggle'
@ -627,10 +646,7 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
className={`theme--${theme}`}
isDisabled={!menuItems.length}
value={
<Menu
text
vertical
>
<Menu>
{menuItems}
</Menu>
}

View File

@ -9,11 +9,15 @@ import React from 'react';
import { Menu } from '@polkadot/react-components';
import { keyring } from '@polkadot/ui-keyring';
export function createMenuGroup (key: string, items: (React.ReactNode | false | undefined | null)[]): React.ReactNode | null {
export function createMenuGroup (key: string, items: (React.ReactNode | false | undefined | null)[], header?: string): React.ReactNode | null {
const filtered = items.filter((item): item is React.ReactNode => !!item);
return filtered.length
? <React.Fragment key={key}><Menu.Divider />{filtered}</React.Fragment>
? <React.Fragment key={key}>
<Menu.Divider />
{header ? <Menu.Header>{header}</Menu.Header> : null}
{filtered}
</React.Fragment>
: null;
}

View File

@ -143,10 +143,7 @@ function Address ({ address, className = '', filter, isFavorite, toggleFavorite
}
const PopupDropdown = (
<Menu
text
vertical
>
<Menu>
<Menu.Item
disabled={!isEditable}
onClick={_toggleForget}

View File

@ -114,11 +114,7 @@ function Index ({ bestNumber, className, description, index, proposals, status }
}
<Popup
value={
<Menu
className='settings-menu'
text
vertical
>
<Menu className='settings-menu'>
{showCloseBounty &&
<Menu.Item
key='closeBounty'

View File

@ -282,10 +282,7 @@ function Account ({ allSlashes, className = '', info: { controllerId, destinatio
isDisabled={isDisabled}
key='settings'
value={
<Menu
text
vertical
>
<Menu>
<Menu.Item
disabled={!isOwnStash || !balancesAll?.freeBalance.gtn(0)}
onClick={toggleBondExtra}

View File

@ -11,7 +11,7 @@ import styled from 'styled-components';
interface Props {
className?: string;
color?: 'gray' | 'green' | 'normal' | 'orange' | 'red' | 'transparent' | 'white';
color?: 'gray' | 'green' | 'normal' | 'orange' | 'red' | 'transparent' | 'white' | 'darkGray';
icon: IconName;
isPadded?: boolean;
isSpinning?: boolean;
@ -75,4 +75,8 @@ export default React.memo(styled(Icon)`
&.whiteColor {
color: white;
}
&.darkGrayColor {
color: #8B8B8B;
}
`);

View File

@ -1,23 +0,0 @@
// Copyright 2017-2021 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { Divider as SUIDivider, Menu as SUIMenu } from 'semantic-ui-react';
import styled from 'styled-components';
type MenuDef = typeof SUIMenu & {
Divider: typeof SUIDivider;
};
const Menu: MenuDef = SUIMenu as MenuDef;
Menu.Divider = SUIDivider;
export default styled(Menu)`
.ui.divider {
display: none;
}
.item+.ui.divider {
display: block;
}
`;

View File

@ -0,0 +1,27 @@
// Copyright 2017-2021 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BaseProps } from './types';
import React from 'react';
import styled from 'styled-components';
function Base ({ children, className = '' }: BaseProps): React.ReactElement<BaseProps> {
return (
<div className={`ui--Menu ${className}`}>
{children}
</div>
);
}
export default React.memo(styled(Base)`
display: flex;
flex-direction: column;
min-width: 14.286rem;
margin: 1rem 0;
& > *:not(.ui--Menu__Item):not(.ui--Menu__Divider) {
margin-right: 1rem;
margin-left: 1rem;
}
`);

View File

@ -0,0 +1,22 @@
// Copyright 2017-2021 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DividerProps } from '@polkadot/react-components/Menu/types';
import React from 'react';
import styled from 'styled-components';
function Divider ({ className = '' }: DividerProps): React.ReactElement {
return (
<div className={`ui--Menu__Divider ${className}`}/>
);
}
export default React.memo(styled(Divider)`
margin: 0.25rem 0 1rem;
border-top: 1px solid var(--border-table);
&:first-child, &:last-child {
display: none
}
`);

View File

@ -0,0 +1,23 @@
// Copyright 2017-2021 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HeaderProps } from './types';
import React from 'react';
import styled from 'styled-components';
function Header ({ children, className }: HeaderProps): React.ReactElement {
return (
<div className={className}>
{children}
</div>
);
}
export default React.memo(styled(Header)`
text-transform: uppercase;
font-size: 0.714rem;
line-height: 0.857rem;
margin-bottom: 0.3rem;
`);

View File

@ -0,0 +1,59 @@
// Copyright 2017-2021 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ItemProps } from './types';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import Icon from '../Icon';
function Item ({ children, className = '', disabled, icon, onClick }: ItemProps): React.ReactElement<ItemProps> {
const _onClick = useCallback(
() => !disabled && onClick && onClick(),
[disabled, onClick]
);
return (
<div
className={`ui--Menu__Item ${className}${icon ? ' hasIcon' : ''}`}
onClick={_onClick}
>
{icon && (
<Icon
color='darkGray'
icon={icon}
/>
)}
{children}
</div>
);
}
export default React.memo(styled(Item)`
display: flex;
flex-direction: row;
align-items: center;
position: relative;
font-size: 0.93rem;
line-height: 0.93rem;
cursor: pointer;
padding: 0.5rem 1rem;
&:last-child {
margin-bottom: 0;
}
&.hasIcon {
padding-left: 2.6rem;
}
.ui--Icon {
position: absolute;
left: 1rem;
}
`);

View File

@ -0,0 +1,25 @@
// Copyright 2017-2021 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BaseProps, DividerProps, HeaderProps, ItemProps } from './types';
import React from 'react';
import MenuBase from './Base';
import Divider from './Divider';
import Header from './Header';
import Item from './Item';
type MenuDef = React.FC<BaseProps> & {
Divider: React.FC<DividerProps>;
Item: React.FC<ItemProps>;
Header: React.FC<HeaderProps>;
};
const Menu: MenuDef = MenuBase as unknown as MenuDef;
Menu.Divider = Divider;
Menu.Item = Item;
Menu.Header = Header;
export default Menu;

View File

@ -0,0 +1,27 @@
// Copyright 2017-2021 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { IconName } from '@fortawesome/fontawesome-svg-core';
import React from 'react';
export interface HeaderProps {
children?: React.ReactNode;
className?: string;
}
export interface ItemProps {
children?: React.ReactNode;
className?: string;
onClick?: () => void | Promise<void> | null;
disabled?: boolean;
icon?: IconName;
}
export interface BaseProps {
children?: React.ReactNode;
className?: string;
}
export interface DividerProps {
className?: string;
}

View File

@ -31,7 +31,7 @@ export default React.memo(styled(PopupWindow)`
z-index: -1;
margin: 0.7rem 0;
padding: 0.85rem 1rem;
padding: 0;
color: var(--color-text);
background-color: var(--bg-menu);
@ -81,4 +81,17 @@ export default React.memo(styled(PopupWindow)`
opacity: 0.3;
}
}
& > *:not(.ui--Menu) {
margin-left: 1rem;
margin-right: 1rem;
}
& > *:first-child:not(.ui--Menu) {
margin-top: 1rem;
}
& > *:last-child:not(.ui--Menu) {
margin-bottom: 1rem;
}
`);

View File

@ -265,6 +265,21 @@ export default createGlobalStyle<Props & ThemeProps>(({ theme, uiHighlight }: Pr
}
}
.ui--Menu {
.ui--Menu__Item:hover {
background: ${hexToRGB(getHighlight(uiHighlight), '.1')};
}
.ui--Toggle.isChecked .ui--Toggle-Slider {
background: ${getHighlight(uiHighlight)};
&::before {
border-color: ${getHighlight(uiHighlight)};
}
}
}
.theme--dark,
.theme--light {
.ui--Tabs .tabLinkActive .tabLinkText::after{