diff --git a/packages/page-accounts/src/Accounts/Account.tsx b/packages/page-accounts/src/Accounts/Account.tsx index b69424042e..2b2f535d6b 100644 --- a/packages/page-accounts/src/Accounts/Account.tsx +++ b/packages/page-accounts/src/Accounts/Account.tsx @@ -127,7 +127,6 @@ function Account ({ account: { address, meta }, className = '', delegation, filt const [isPasswordOpen, togglePassword] = useToggle(); const [isRecoverAccountOpen, toggleRecoverAccount] = useToggle(); const [isRecoverSetupOpen, toggleRecoverSetup] = useToggle(); - const [isSettingsOpen, toggleSettings] = useToggle(); const [isTransferOpen, toggleTransfer] = useToggle(); const [isDelegateOpen, toggleDelegate] = useToggle(); const [isUndelegateOpen, toggleUndelegate] = useToggle(); @@ -626,24 +625,16 @@ function Account ({ account: { address, meta }, className = '', delegation, filt )} + isDisabled={!menuItems.length} + value={ + + {menuItems} + } - > - - {menuItems} - - + /> -
+ onClick={toggleIsExpanded} + > -
+
(null); const [genesisHash, setGenesisHash] = useState(null); const [isForgetOpen, setIsForgetOpen] = useState(false); - const [isSettingPopupOpen, setIsSettingPopupOpen] = useState(false); const [isTransferOpen, setIsTransferOpen] = useState(false); const [isVisible, setIsVisible] = useState(true); @@ -113,11 +112,6 @@ function Address ({ address, className = '', filter, isFavorite, toggleFavorite [isForgetOpen] ); - const _toggleSettingPopup = useCallback( - (): void => setIsSettingPopupOpen(!isSettingPopupOpen), - [isSettingPopupOpen] - ); - const _toggleTransfer = useCallback( (): void => setIsTransferOpen(!isTransferOpen), [isTransferOpen] @@ -148,6 +142,30 @@ function Address ({ address, className = '', filter, isFavorite, toggleFavorite return null; } + const PopupDropdown = ( + + + {t('Forget this address')} + + {isEditable && !api.isDevelopment && ( + <> + + + + )} + + ); + return ( @@ -207,38 +225,8 @@ function Address ({ address, className = '', filter, isFavorite, toggleFavorite )} - } - > - - - {t('Forget this address')} - - {isEditable && !api.isDevelopment && ( - <> - - - - )} - - + value={PopupDropdown} + /> | null { - const [isPopupOpen, togglePopupOpen] = useToggle(); const [isCloseBountyOpen, toggleCloseBounty] = useToggle(); const [isRejectCuratorOpen, toggleRejectCurator] = useToggle(); const [isSlashCuratorOpen, toggleSlashCurator] = useToggle(); @@ -114,63 +113,60 @@ function Index ({ bestNumber, className, description, index, proposals, status } /> } - } - > - - {showCloseBounty && + value={ + + {showCloseBounty && {t('Close')} - } - {showRejectCurator && + } + {showRejectCurator && {t('Reject curator')} - } - {showExtendExpiry && + } + {showExtendExpiry && {t('Extend expiry')} - } - {showGiveUpCurator && + } + {showGiveUpCurator && {t('Give up')} - } - {showSlashCurator && availableSlashActions.map((actionName) => - - {slashCuratorActionNames.current[actionName]} - - )} - + } + {showSlashCurator && availableSlashActions.map((actionName) => + + {slashCuratorActionNames.current[actionName]} + + )} + + } + > +
) diff --git a/packages/page-js/src/ActionButtons.tsx b/packages/page-js/src/ActionButtons.tsx index f762aaae8e..02cfc8a773 100644 --- a/packages/page-js/src/ActionButtons.tsx +++ b/packages/page-js/src/ActionButtons.tsx @@ -20,7 +20,6 @@ interface Props { function ActionButtons ({ className = '', isCustomExample, isRunning, removeSnippet, runJs, saveSnippet, stopJs }: Props): React.ReactElement { const { t } = useTranslation(); - const [isOpen, setIsOpen] = useState(false); const [snippetName, setSnippetName] = useState(''); const _onChangeName = useCallback( @@ -28,15 +27,9 @@ function ActionButtons ({ className = '', isCustomExample, isRunning, removeSnip [] ); - const _onPopupOpen = useCallback( - () => setIsOpen(true), - [] - ); - const _onPopupClose = useCallback( (): void => { setSnippetName(''); - setIsOpen(false); }, [] ); @@ -61,31 +54,31 @@ function ActionButtons ({ className = '', isCustomExample, isRunning, removeSnip : ( + onCloseAction={_onPopupClose} + value={ + <> + ('Name your example')} + value={snippetName} + withLabel={false} + /> +
+
+ ); +} + +export default React.memo(styled(Popup)` + display: inline-flex; + flex-direction: column; + justify-content: center; + position: relative; +`); diff --git a/packages/react-components/src/Popup/types.ts b/packages/react-components/src/Popup/types.ts new file mode 100644 index 0000000000..ab9c69b018 --- /dev/null +++ b/packages/react-components/src/Popup/types.ts @@ -0,0 +1,24 @@ +// Copyright 2017-2021 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +export type HorizontalPosition = 'left' | 'middle' | 'right' +export type VerticalPosition = 'top' | 'bottom' + +export interface PopupWindowProps { + className?: string; + children?: React.ReactNode; + windowRef: React.RefObject; + triggerRef: React.RefObject; + position: HorizontalPosition; +} + +export interface PopupProps { + isDisabled?: boolean; + className?: string; + value?: React.ReactNode; + children?: React.ReactNode; + position?: HorizontalPosition; + onCloseAction?: () => void; +} diff --git a/packages/react-components/src/Popup/utils.ts b/packages/react-components/src/Popup/utils.ts new file mode 100644 index 0000000000..7f2df6d4e9 --- /dev/null +++ b/packages/react-components/src/Popup/utils.ts @@ -0,0 +1,76 @@ +// Copyright 2017-2021 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ElementPosition } from '@polkadot/react-hooks/useElementPosition'; + +import { WindowSize } from '@polkadot/react-hooks/useWindowSize'; + +import { HorizontalPosition, VerticalPosition } from './types'; + +// 0.8rem +const POINTER_OFFSET = 14 * 0.8; +// we don't want our popup window to cover apps menu +const MENU_BAR_HEIGHT = 56; +// adds 1rem margin between browsers window and popup +const FIXED_VERTICAL_OFFSET_MARGIN = 14; + +export function getPosition ( + triggerPosition: ElementPosition, + positionX: HorizontalPosition, + positionY: VerticalPosition, + windowPosition: ElementPosition, + scrollY: number, + windowSize: WindowSize +): {x: number, y: number} { + const globalX = triggerPosition.x + (triggerPosition.width / 2); + const globalY = triggerPosition.y + scrollY + (triggerPosition.height / 2); + + return { + x: globalX - getHorizontalOffset(windowPosition.width, positionX), + y: fitsInView(positionY, triggerPosition, windowPosition.height, windowSize, scrollY) + ? globalY + getVerticalOffset(triggerPosition.height, positionY, windowPosition.height) + : getFixedVerticalPosition(scrollY, positionY, windowSize, windowPosition.height) + }; +} + +function getHorizontalOffset (popupWindowWidth: number, position: HorizontalPosition): number { + if (position === 'left') { + return popupWindowWidth - POINTER_OFFSET; + } + + if (position === 'right') { + return POINTER_OFFSET; + } + + return (popupWindowWidth / 2); +} + +function fitsInView (positionY: VerticalPosition, trigger: ElementPosition, popupWindowHeight: number, windowSize: WindowSize, scrollY: number): boolean { + const { height: triggerHeight, y: triggerY } = trigger; + + if (positionY === 'bottom') { + return windowSize.height - triggerHeight - triggerY - FIXED_VERTICAL_OFFSET_MARGIN > popupWindowHeight; + } + + return scrollY < MENU_BAR_HEIGHT + ? triggerY - (MENU_BAR_HEIGHT - scrollY) > popupWindowHeight + : triggerY > popupWindowHeight; +} + +function getVerticalOffset (triggerHeight: number, position: VerticalPosition, windowHeight: number): number { + if (position === 'bottom') { + return triggerHeight / 2; + } + + return (triggerHeight / 2 + windowHeight + POINTER_OFFSET) * -1; +} + +function getFixedVerticalPosition (scrollY: number, position: VerticalPosition, windowSize: WindowSize, popupWindowHeight: number): number { + if (position === 'bottom') { + return scrollY + windowSize.height - popupWindowHeight - FIXED_VERTICAL_OFFSET_MARGIN; + } + + return scrollY < MENU_BAR_HEIGHT + ? MENU_BAR_HEIGHT + : scrollY; +} diff --git a/packages/react-components/src/styles/index.ts b/packages/react-components/src/styles/index.ts index f63d583ac0..435f75cda2 100644 --- a/packages/react-components/src/styles/index.ts +++ b/packages/react-components/src/styles/index.ts @@ -255,6 +255,16 @@ export default createGlobalStyle(({ theme, uiHighlight }: Pr } } + .ui--Popup .ui--Button.isOpen:not(.isDisabled):not(.isReadOnly) { + background: ${getHighlight(uiHighlight)} !important; + color: ${getContrast(uiHighlight)} !important; + + .ui--Icon { + background: transparent !important; + color: ${getContrast(uiHighlight)} !important; + } + } + .theme--dark, .theme--light { .ui--Tabs .tabLinkActive .tabLinkText::after{ diff --git a/packages/react-components/src/styles/semantic.ts b/packages/react-components/src/styles/semantic.ts index 405900c9d0..644e581c0b 100644 --- a/packages/react-components/src/styles/semantic.ts +++ b/packages/react-components/src/styles/semantic.ts @@ -127,23 +127,6 @@ export default (_theme: ThemeDef): string => ` display: flex !important; } - .ui.popup { - background: var(--bg-menu); - color: var(--color-text); - - .ui.text.menu .item { - color: var(--color-text) !important; - - &.disabled { - opacity: 0.3; - } - } - - &&::before { - background: var(--bg-menu); - } - } - .ui.secondary.vertical.menu > .item { margin: 0; } diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 5c1ad2444c..eac7462487 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -52,3 +52,8 @@ export { useTxBatch } from './useTxBatch'; export { useVotingStatus } from './useVotingStatus'; export { useWeight } from './useWeight'; export { useWeightFee } from './useWeightFee'; +export { useElementPosition } from './useElementPosition'; +export { useScroll } from './useScroll'; +export { useWindowSize } from './useWindowSize'; +export { useOutsideClick } from './useOutsideClick'; +export { usePopupWindow } from './usePopupWindow'; diff --git a/packages/react-hooks/src/useElementPosition.ts b/packages/react-hooks/src/useElementPosition.ts new file mode 100644 index 0000000000..a0ac353e6f --- /dev/null +++ b/packages/react-hooks/src/useElementPosition.ts @@ -0,0 +1,35 @@ +// Copyright 2017-2021 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React, { useEffect, useState } from 'react'; + +import { useScroll } from '@polkadot/react-hooks/useScroll'; +import { useWindowSize } from '@polkadot/react-hooks/useWindowSize'; + +export interface ElementPosition { + x: number, + y: number, + width: number, + height: number, +} + +export function useElementPosition (ref: React.MutableRefObject): ElementPosition | undefined { + const [elementPosition, setElementPosition] = useState(); + const windowSize = useWindowSize(); + const scrollY = useScroll(); + + useEffect(() => { + if (ref && ref.current) { + const { height, width, x, y } = ref.current?.getBoundingClientRect(); + + setElementPosition({ + height, + width, + x, + y + }); + } + }, [ref, scrollY, windowSize]); + + return elementPosition; +} diff --git a/packages/react-hooks/src/useOutsideClick.ts b/packages/react-hooks/src/useOutsideClick.ts new file mode 100644 index 0000000000..417b02d45a --- /dev/null +++ b/packages/react-hooks/src/useOutsideClick.ts @@ -0,0 +1,24 @@ +// Copyright 2017-2021 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { RefObject, useCallback, useEffect } from 'react'; + +function getClickedElement (refs: React.RefObject[], e: MouseEvent) { + return refs.find((ref) => ref.current && ref.current.contains(e.target as HTMLElement)); +} + +export const useOutsideClick = (elements: RefObject[], callback: () => void): void => { + const handleClick = useCallback((e: MouseEvent) => { + if (elements.length && !getClickedElement(elements, e)) { + callback(); + } + }, [elements, callback]); + + useEffect(() => { + document.addEventListener('click', handleClick); + + return () => { + document.removeEventListener('click', handleClick); + }; + }, [handleClick, callback]); +}; diff --git a/packages/react-hooks/src/usePopupWindow.ts b/packages/react-hooks/src/usePopupWindow.ts new file mode 100644 index 0000000000..e50a619352 --- /dev/null +++ b/packages/react-hooks/src/usePopupWindow.ts @@ -0,0 +1,39 @@ +// Copyright 2017-2021 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React, { useEffect, useState } from 'react'; + +import { HorizontalPosition, VerticalPosition } from '@polkadot/react-components/Popup/types'; +import { getPosition } from '@polkadot/react-components/Popup/utils'; + +import { useElementPosition } from './useElementPosition'; +import { useScroll } from './useScroll'; +import { useWindowSize } from './useWindowSize'; + +export function usePopupWindow ( + windowRef: React.RefObject, + triggerRef: React.RefObject, + position: HorizontalPosition +): {renderWindowPosition: {x: number, y: number} | undefined, verticalPosition: VerticalPosition | undefined} { + const [renderWindowPosition, setRenderWindowPosition] = useState<{x: number, y: number}>(); + const [verticalPosition, setVerticalPosition] = useState(); + const windowPosition = useElementPosition(windowRef); + const triggerPosition = useElementPosition(triggerRef); + + const scrollY = useScroll(); + const windowSize = useWindowSize(); + + useEffect(() => { + if (windowSize && triggerPosition) { + setVerticalPosition((triggerPosition.y > windowSize.height / 2) ? 'top' : 'bottom'); + } + }, [triggerPosition, windowSize]); + + useEffect(() => { + if (windowPosition && triggerPosition && verticalPosition) { + setRenderWindowPosition(getPosition(triggerPosition, position, verticalPosition, windowPosition, scrollY, windowSize)); + } + }, [position, scrollY, triggerPosition, verticalPosition, windowPosition, windowSize]); + + return { renderWindowPosition, verticalPosition }; +} diff --git a/packages/react-hooks/src/useScroll.ts b/packages/react-hooks/src/useScroll.ts new file mode 100644 index 0000000000..15bc81a0f6 --- /dev/null +++ b/packages/react-hooks/src/useScroll.ts @@ -0,0 +1,23 @@ +// Copyright 2017-2021 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useCallback, useEffect, useState } from 'react'; + +export function useScroll (): number { + const [scrollY, setScrollY] = useState(window.pageYOffset); + const setYOffset = useCallback((): void => setScrollY(window.pageYOffset), []); + + useEffect(() => { + function watchScroll () { + window.addEventListener('scroll', setYOffset); + } + + watchScroll(); + + return () => { + window.removeEventListener('scroll', setYOffset); + }; + }, [setYOffset]); + + return scrollY; +} diff --git a/packages/react-hooks/src/useWindowSize.ts b/packages/react-hooks/src/useWindowSize.ts new file mode 100644 index 0000000000..e0812f3c3e --- /dev/null +++ b/packages/react-hooks/src/useWindowSize.ts @@ -0,0 +1,32 @@ +// Copyright 2017-2021 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useEffect, useState } from 'react'; + +export interface WindowSize { + width: number; + height: number; +} + +export function useWindowSize (): WindowSize { + const [windowSize, setWindowSize] = useState({ + height: 0, + width: 0 + }); + + useEffect(() => { + function handleResize () { + setWindowSize({ + height: window.innerHeight, + width: window.innerWidth + }); + } + + window.addEventListener('resize', handleResize); + handleResize(); + + return () => window.removeEventListener('resize', handleResize); + }, []); + + return windowSize; +}