From b4967ca82c086ff929a252c72834cd334449d9a2 Mon Sep 17 00:00:00 2001 From: MiZiet <55240109+MiZiet@users.noreply.github.com> Date: Tue, 31 Aug 2021 10:39:17 +0200 Subject: [PATCH] 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 --- .../apps/public/locales/en/app-accounts.json | 4 + .../apps/public/locales/en/translation.json | 6 ++ .../page-accounts/src/Accounts/Account.tsx | 74 +++++++++++-------- packages/page-accounts/src/util.tsx | 8 +- .../page-addresses/src/Contacts/Address.tsx | 5 +- .../src/BountyExtraActions/index.tsx | 6 +- .../src/Actions/Account/index.tsx | 5 +- packages/react-components/src/Icon.tsx | 6 +- packages/react-components/src/Menu.tsx | 23 ------ packages/react-components/src/Menu/Base.tsx | 27 +++++++ .../react-components/src/Menu/Divider.tsx | 22 ++++++ packages/react-components/src/Menu/Header.tsx | 23 ++++++ packages/react-components/src/Menu/Item.tsx | 59 +++++++++++++++ packages/react-components/src/Menu/index.tsx | 25 +++++++ packages/react-components/src/Menu/types.ts | 27 +++++++ .../src/Popup/PopupWindow.tsx | 15 +++- packages/react-components/src/styles/index.ts | 15 ++++ 17 files changed, 281 insertions(+), 69 deletions(-) delete mode 100644 packages/react-components/src/Menu.tsx create mode 100644 packages/react-components/src/Menu/Base.tsx create mode 100644 packages/react-components/src/Menu/Divider.tsx create mode 100644 packages/react-components/src/Menu/Header.tsx create mode 100644 packages/react-components/src/Menu/Item.tsx create mode 100644 packages/react-components/src/Menu/index.tsx create mode 100644 packages/react-components/src/Menu/types.ts diff --git a/packages/apps/public/locales/en/app-accounts.json b/packages/apps/public/locales/en/app-accounts.json index c5458b3896..9d7470f305 100644 --- a/packages/apps/public/locales/en/app-accounts.json +++ b/packages/apps/public/locales/en/app-accounts.json @@ -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", diff --git a/packages/apps/public/locales/en/translation.json b/packages/apps/public/locales/en/translation.json index eda186d200..f2cd001879 100644 --- a/packages/apps/public/locales/en/translation.json +++ b/packages/apps/public/locales/en/translation.json @@ -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": "", diff --git a/packages/page-accounts/src/Accounts/Account.tsx b/packages/page-accounts/src/Accounts/Account.tsx index 2b2f535d6b..551d140f9f 100644 --- a/packages/page-accounts/src/Accounts/Account.tsx +++ b/packages/page-accounts/src/Accounts/Account.tsx @@ -233,6 +233,7 @@ function Account ({ account: { address, meta }, className = '', delegation, filt createMenuGroup('identityGroup', [ isFunction(api.api.tx.identity?.setIdentity) && !isHardware && ( @@ -241,6 +242,7 @@ function Account ({ account: { address, meta }, className = '', delegation, filt ), isFunction(api.api.tx.identity?.setSubs) && identity?.display && !isHardware && ( @@ -249,6 +251,7 @@ function Account ({ account: { address, meta }, className = '', delegation, filt ), isFunction(api.api.tx.democracy?.unlock) && democracyUnlockTx && ( @@ -257,16 +260,18 @@ function Account ({ account: { address, meta }, className = '', delegation, filt ), isFunction(api.api.tx.vesting?.vest) && vestingVestTx && ( {t('Unlock vested amount')} ) - ]), + ], t('Identity')), createMenuGroup('deriveGroup', [ !(isExternal || isHardware || isInjected || isMultisig) && ( @@ -275,16 +280,18 @@ function Account ({ account: { address, meta }, className = '', delegation, filt ), isHardware && ( {t('Show address on hardware device')} ) - ]), + ], t('Derive')), createMenuGroup('backupGroup', [ !(isExternal || isHardware || isInjected || isMultisig || isDevelopment) && ( @@ -293,6 +300,7 @@ function Account ({ account: { address, meta }, className = '', delegation, filt ), !(isExternal || isHardware || isInjected || isMultisig || isDevelopment) && ( @@ -301,16 +309,18 @@ function Account ({ account: { address, meta }, className = '', delegation, filt ), !(isInjected || isDevelopment) && ( {t('Forget this account')} ) - ]), + ], t('Backup')), isFunction(api.api.tx.recovery?.createRecovery) && createMenuGroup('reoveryGroup', [ !recoveryInfo && ( @@ -318,54 +328,63 @@ function Account ({ account: { address, meta }, className = '', delegation, filt ), {t('Initiate recovery for another')} - ]), + ], t('Recovery')), isFunction(api.api.tx.multisig?.asMulti) && isMultisig && createMenuGroup('multisigGroup', [ {t('Multisig approvals')} - ]), + ], t('Multisig')), isFunction(api.api.query.democracy?.votingOf) && delegation?.accountDelegated && createMenuGroup('undelegateGroup', [ {t('Change democracy delegation')} , {t('Undelegate')} - ]), - isFunction(api.api.query.democracy?.votingOf) && !delegation?.accountDelegated && createMenuGroup('delegateGroup', [ - - {t('Delegate democracy votes')} - - ]), - isFunction(api.api.query.proxy?.proxies) && createMenuGroup('proxyGroup', [ - - {proxy?.[0].length - ? t('Manage proxies') - : t('Add proxy') - } - - ]), + ], t('Undelegate')), + createMenuGroup('delegateGroup', [ + isFunction(api.api.query.democracy?.votingOf) && !delegation?.accountDelegated && ( + + + {t('Delegate democracy votes')} + + ), + isFunction(api.api.query.proxy?.proxies) && ( + + {proxy?.[0].length + ? t('Manage proxies') + : t('Add proxy') + } + + ) + ], t('Delegate')), isEditable && !api.isDevelopment && createMenuGroup('genesisGroup', [ + {menuItems} } diff --git a/packages/page-accounts/src/util.tsx b/packages/page-accounts/src/util.tsx index 396ed24582..a782432397 100644 --- a/packages/page-accounts/src/util.tsx +++ b/packages/page-accounts/src/util.tsx @@ -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 - ? {filtered} + ? + + {header ? {header} : null} + {filtered} + : null; } diff --git a/packages/page-addresses/src/Contacts/Address.tsx b/packages/page-addresses/src/Contacts/Address.tsx index be4593223f..7c4dfecc50 100644 --- a/packages/page-addresses/src/Contacts/Address.tsx +++ b/packages/page-addresses/src/Contacts/Address.tsx @@ -143,10 +143,7 @@ function Address ({ address, className = '', filter, isFavorite, toggleFavorite } const PopupDropdown = ( - + + {showCloseBounty && + { + return ( +
+ {children} +
+ ); +} + +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; + } +`); diff --git a/packages/react-components/src/Menu/Divider.tsx b/packages/react-components/src/Menu/Divider.tsx new file mode 100644 index 0000000000..6822be8a79 --- /dev/null +++ b/packages/react-components/src/Menu/Divider.tsx @@ -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 ( +
+ ); +} + +export default React.memo(styled(Divider)` + margin: 0.25rem 0 1rem; + border-top: 1px solid var(--border-table); + + &:first-child, &:last-child { + display: none + } +`); diff --git a/packages/react-components/src/Menu/Header.tsx b/packages/react-components/src/Menu/Header.tsx new file mode 100644 index 0000000000..ac44e4a7bd --- /dev/null +++ b/packages/react-components/src/Menu/Header.tsx @@ -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 ( +
+ {children} +
+ ); +} + +export default React.memo(styled(Header)` + text-transform: uppercase; + font-size: 0.714rem; + line-height: 0.857rem; + + margin-bottom: 0.3rem; +`); diff --git a/packages/react-components/src/Menu/Item.tsx b/packages/react-components/src/Menu/Item.tsx new file mode 100644 index 0000000000..101c506b6a --- /dev/null +++ b/packages/react-components/src/Menu/Item.tsx @@ -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 { + const _onClick = useCallback( + () => !disabled && onClick && onClick(), + [disabled, onClick] + ); + + return ( +
+ {icon && ( + + )} + {children} +
+ ); +} + +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; + } +`); diff --git a/packages/react-components/src/Menu/index.tsx b/packages/react-components/src/Menu/index.tsx new file mode 100644 index 0000000000..6d38126e58 --- /dev/null +++ b/packages/react-components/src/Menu/index.tsx @@ -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 & { + Divider: React.FC; + Item: React.FC; + Header: React.FC; +}; + +const Menu: MenuDef = MenuBase as unknown as MenuDef; + +Menu.Divider = Divider; +Menu.Item = Item; +Menu.Header = Header; + +export default Menu; diff --git a/packages/react-components/src/Menu/types.ts b/packages/react-components/src/Menu/types.ts new file mode 100644 index 0000000000..47ef6b002e --- /dev/null +++ b/packages/react-components/src/Menu/types.ts @@ -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 | null; + disabled?: boolean; + icon?: IconName; +} + +export interface BaseProps { + children?: React.ReactNode; + className?: string; +} + +export interface DividerProps { + className?: string; +} diff --git a/packages/react-components/src/Popup/PopupWindow.tsx b/packages/react-components/src/Popup/PopupWindow.tsx index 87f5de166e..efd57500a3 100644 --- a/packages/react-components/src/Popup/PopupWindow.tsx +++ b/packages/react-components/src/Popup/PopupWindow.tsx @@ -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; + } `); diff --git a/packages/react-components/src/styles/index.ts b/packages/react-components/src/styles/index.ts index 435f75cda2..769a7d37bf 100644 --- a/packages/react-components/src/styles/index.ts +++ b/packages/react-components/src/styles/index.ts @@ -265,6 +265,21 @@ export default createGlobalStyle(({ 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{