Remove sui popup (#5924)

* 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

Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
MiZiet 2021-08-31 09:35:33 +02:00 committed by GitHub
parent ead840ea74
commit b6c827a908
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 645 additions and 248 deletions

View File

@ -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
)}
<Popup
className={`theme--${theme}`}
isOpen={isSettingsOpen}
onClose={toggleSettings}
trigger={
<Button
icon='ellipsis-v'
isDisabled={!menuItems.length}
onClick={toggleSettings}
/>
isDisabled={!menuItems.length}
value={
<Menu
text
vertical
>
{menuItems}
</Menu>
}
>
<Menu
onClick={toggleSettings}
text
vertical
>
{menuItems}
</Menu>
</Popup>
/>
</td>
<td className='links media--1400'>
<LinkExternal
@ -654,9 +645,11 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
/>
</td>
<td className='button'>
<div className='table-column-icon'
<div
className='table-column-icon'
data-testid='row-toggle'
onClick={toggleIsExpanded}>
onClick={toggleIsExpanded}
>
<Icon icon={
isExpanded
? 'caret-up'
@ -669,8 +662,10 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
<tr className={`${className} ${isExpanded ? 'isExpanded' : 'isCollapsed'} ${isEven ? 'isEven' : 'isOdd'}`}>
<td colSpan={2} />
<td>
<div className='tags'
data-testid='tags'>
<div
className='tags'
data-testid='tags'
>
<Tags
value={tags}
withTitle

View File

@ -40,7 +40,6 @@ function Address ({ address, className = '', filter, isFavorite, toggleFavorite
const [current, setCurrent] = useState<KeyringAddress | null>(null);
const [genesisHash, setGenesisHash] = useState<string | null>(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 = (
<Menu
text
vertical
>
<Menu.Item
disabled={!isEditable}
onClick={_toggleForget}
>
{t<string>('Forget this address')}
</Menu.Item>
{isEditable && !api.isDevelopment && (
<>
<Menu.Divider />
<ChainLock
className='addresses--network-toggle'
genesisHash={genesisHash}
onChange={_onGenesisChange}
/>
</>
)}
</Menu>
);
return (
<tr className={className}>
<td className='favorite'>
@ -207,38 +225,8 @@ function Address ({ address, className = '', filter, isFavorite, toggleFavorite
)}
<Popup
className={`theme--${theme}`}
isOpen={isSettingPopupOpen}
onClose={_toggleSettingPopup}
trigger={
<Button
icon='ellipsis-v'
onClick={_toggleSettingPopup}
/>
}
>
<Menu
onClick={_toggleSettingPopup}
text
vertical
>
<Menu.Item
disabled={!isEditable}
onClick={_toggleForget}
>
{t<string>('Forget this address')}
</Menu.Item>
{isEditable && !api.isDevelopment && (
<>
<Menu.Divider />
<ChainLock
className='addresses--network-toggle'
genesisHash={genesisHash}
onChange={_onGenesisChange}
/>
</>
)}
</Menu>
</Popup>
value={PopupDropdown}
/>
</td>
<td className='links media--1400'>
<LinkExternal

View File

@ -28,7 +28,6 @@ interface Props {
}
function Index ({ bestNumber, className, description, index, proposals, status }: Props): React.ReactElement<Props> | 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 }
/>
}
<Popup
isOpen={isPopupOpen}
onClose={togglePopupOpen}
trigger={
<Button
dataTestId='extra-actions'
icon='ellipsis-v'
onClick={togglePopupOpen}
/>
}
>
<Menu
className='settings-menu'
onClick={togglePopupOpen}
text
vertical
>
{showCloseBounty &&
value={
<Menu
className='settings-menu'
text
vertical
>
{showCloseBounty &&
<Menu.Item
key='closeBounty'
onClick={toggleCloseBounty}
>
{t<string>('Close')}
</Menu.Item>
}
{showRejectCurator &&
}
{showRejectCurator &&
<Menu.Item
key='rejectCurator'
onClick={toggleRejectCurator}
>
{t<string>('Reject curator')}
</Menu.Item>
}
{showExtendExpiry &&
}
{showExtendExpiry &&
<Menu.Item
key='extendExpiry'
onClick={toggleExtendExpiry}
>
{t<string>('Extend expiry')}
</Menu.Item>
}
{showGiveUpCurator &&
}
{showGiveUpCurator &&
<Menu.Item
key='giveUpCurator'
onClick={toggleGiveUpCurator}
>
{t<string>('Give up')}
</Menu.Item>
}
{showSlashCurator && availableSlashActions.map((actionName) =>
<Menu.Item
key={actionName}
onClick={slashCurator(actionName)}
>
{slashCuratorActionNames.current[actionName]}
</Menu.Item>
)}
</Menu>
}
{showSlashCurator && availableSlashActions.map((actionName) =>
<Menu.Item
key={actionName}
onClick={slashCurator(actionName)}
>
{slashCuratorActionNames.current[actionName]}
</Menu.Item>
)}
</Menu>
}
>
<Button
dataTestId='extra-actions'
icon='ellipsis-v'
isReadOnly={false}
/>
</Popup>
</div>
)

View File

@ -20,7 +20,6 @@ interface Props {
function ActionButtons ({ className = '', isCustomExample, isRunning, removeSnippet, runJs, saveSnippet, stopJs }: Props): React.ReactElement<Props> {
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
: (
<Popup
className='popup-local'
isOpen={isOpen}
on='click'
onClose={_onPopupClose}
trigger={
<Button
icon='save'
onClick={_onPopupOpen}
/>
onCloseAction={_onPopupClose}
value={
<>
<Input
autoFocus
maxLength={50}
min={1}
onChange={_onChangeName}
onEnter={_saveSnippet}
placeholder={t<string>('Name your example')}
value={snippetName}
withLabel={false}
/>
<Button
icon='save'
isDisabled={!snippetName.length}
label={t<string>('Save snippet to local storage')}
onClick={_saveSnippet}
/>
</>
}
>
<Input
autoFocus
maxLength={50}
min={1}
onChange={_onChangeName}
onEnter={_saveSnippet}
placeholder={t<string>('Name your example')}
value={snippetName}
withLabel={false}
/>
<Button
icon='save'
isDisabled={!snippetName.length}
label={t<string>('Save snippet to local storage')}
onClick={_saveSnippet}
isReadOnly={false}
/>
</Popup>
)

View File

@ -79,7 +79,6 @@ function Account ({ allSlashes, className = '', info: { controllerId, destinatio
const [isRewardDestinationOpen, toggleRewardDestination] = useToggle();
const [isSetControllerOpen, toggleSetController] = useToggle();
const [isSetSessionOpen, toggleSetSession] = useToggle();
const [isSettingsOpen, toggleSettings] = useToggle();
const [isUnbondOpen, toggleUnbond] = useToggle();
const [isValidateOpen, toggleValidate] = useToggle();
const { balancesAll, spanCount, stakingAccount } = useStashCalls(api, stashId);
@ -280,95 +279,87 @@ function Account ({ allSlashes, className = '', info: { controllerId, destinatio
)
}
<Popup
isOpen={isSettingsOpen}
isDisabled={isDisabled}
key='settings'
onClose={toggleSettings}
trigger={
<Button
icon='ellipsis-v'
isDisabled={isDisabled}
onClick={toggleSettings}
/>
}
>
<Menu
onClick={toggleSettings}
text
vertical
>
<Menu.Item
disabled={!isOwnStash || !balancesAll?.freeBalance.gtn(0)}
onClick={toggleBondExtra}
value={
<Menu
text
vertical
>
{t<string>('Bond more funds')}
</Menu.Item>
<Menu.Item
disabled={!isOwnController || !stakingAccount || !stakingAccount.stakingLedger || stakingAccount.stakingLedger.active?.isEmpty}
onClick={toggleUnbond}
>
{t<string>('Unbond funds')}
</Menu.Item>
<Menu.Item
disabled={!isOwnController || !stakingAccount || !stakingAccount.redeemable || !stakingAccount.redeemable.gtn(0)}
onClick={withdrawFunds}
>
{t<string>('Withdraw unbonded funds')}
</Menu.Item>
<Menu.Divider />
<Menu.Item
disabled={!isOwnStash}
onClick={toggleSetController}
>
{t<string>('Change controller account')}
</Menu.Item>
<Menu.Item
disabled={!isOwnController}
onClick={toggleRewardDestination}
>
{t<string>('Change reward destination')}
</Menu.Item>
{isStashValidating && (
<>
<Menu.Item
disabled={!isOwnController}
onClick={toggleValidate}
>
{t<string>('Change validator preferences')}
</Menu.Item>
{isFunction(api.tx.staking.kick) && (
<Menu.Item
disabled={!isOwnStash || !balancesAll?.freeBalance.gtn(0)}
onClick={toggleBondExtra}
>
{t<string>('Bond more funds')}
</Menu.Item>
<Menu.Item
disabled={!isOwnController || !stakingAccount || !stakingAccount.stakingLedger || stakingAccount.stakingLedger.active?.isEmpty}
onClick={toggleUnbond}
>
{t<string>('Unbond funds')}
</Menu.Item>
<Menu.Item
disabled={!isOwnController || !stakingAccount || !stakingAccount.redeemable || !stakingAccount.redeemable.gtn(0)}
onClick={withdrawFunds}
>
{t<string>('Withdraw unbonded funds')}
</Menu.Item>
<Menu.Divider />
<Menu.Item
disabled={!isOwnStash}
onClick={toggleSetController}
>
{t<string>('Change controller account')}
</Menu.Item>
<Menu.Item
disabled={!isOwnController}
onClick={toggleRewardDestination}
>
{t<string>('Change reward destination')}
</Menu.Item>
{isStashValidating && (
<>
<Menu.Item
disabled={!isOwnController}
onClick={toggleKick}
onClick={toggleValidate}
>
{t<string>('Remove nominees')}
{t<string>('Change validator preferences')}
</Menu.Item>
)}
</>
)}
<Menu.Divider />
{!isStashNominating &&
{isFunction(api.tx.staking.kick) && (
<Menu.Item
disabled={!isOwnController}
onClick={toggleKick}
>
{t<string>('Remove nominees')}
</Menu.Item>
)}
</>
)}
<Menu.Divider />
{!isStashNominating &&
<Menu.Item
disabled={!isOwnController}
onClick={toggleSetSession}
>
{t<string>('Change session keys')}
</Menu.Item>
}
{isStashNominating &&
}
{isStashNominating &&
<Menu.Item
disabled={!isOwnController || !targets.validators?.length}
onClick={toggleNominate}
>
{t<string>('Set nominees')}
</Menu.Item>
}
{!isStashNominating &&
}
{!isStashNominating &&
<Menu.Item onClick={toggleInject}>
{t<string>('Inject session keys (advanced)')}
</Menu.Item>
}
</Menu>
</Popup>
}
</Menu>
}
/>
</>
)}
</td>

View File

@ -9,7 +9,7 @@ import styled from 'styled-components';
import Icon from '../Icon';
import Spinner from '../Spinner';
function Button ({ children, className = '', dataTestId = '', icon, isBasic, isBusy, isCircular, isDisabled, isFull, isIcon, isSelected, isToplevel, label, onClick, onMouseEnter, onMouseLeave, tabIndex, withoutLink }: ButtonProps): React.ReactElement<ButtonProps> {
function Button ({ children, className = '', dataTestId = '', icon, isBasic, isBusy, isCircular, isDisabled, isFull, isIcon, isSelected, isToplevel, label, onClick, isReadOnly = !onClick, onMouseEnter, onMouseLeave, tabIndex, withoutLink }: ButtonProps): React.ReactElement<ButtonProps> {
const _onClick = useCallback(
() => !(isBusy || isDisabled) && onClick && onClick(),
[isBusy, isDisabled, onClick]
@ -17,7 +17,7 @@ function Button ({ children, className = '', dataTestId = '', icon, isBasic, isB
return (
<button
className={`ui--Button${label ? ' hasLabel' : ''}${isBasic ? ' isBasic' : ''}${isCircular ? ' isCircular' : ''}${isFull ? ' isFull' : ''}${isIcon ? ' isIcon' : ''}${(isBusy || isDisabled) ? ' isDisabled' : ''}${isBusy ? ' isBusy' : ''}${!onClick ? ' isReadOnly' : ''}${isSelected ? ' isSelected' : ''}${isToplevel ? ' isToplevel' : ''}${withoutLink ? ' withoutLink' : ''} ${className}`}
className={`ui--Button${label ? ' hasLabel' : ''}${isBasic ? ' isBasic' : ''}${isCircular ? ' isCircular' : ''}${isFull ? ' isFull' : ''}${isIcon ? ' isIcon' : ''}${(isBusy || isDisabled) ? ' isDisabled' : ''}${isBusy ? ' isBusy' : ''}${isReadOnly ? ' isReadOnly' : ''}${isSelected ? ' isSelected' : ''}${isToplevel ? ' isToplevel' : ''}${withoutLink ? ' withoutLink' : ''} ${className}`}
data-testid={dataTestId}
onClick={_onClick}
onMouseEnter={onMouseEnter}

View File

@ -17,6 +17,7 @@ export interface ButtonProps {
isDisabled?: boolean;
isFull?: boolean;
isIcon?: boolean;
isReadOnly?: boolean;
isSelected?: boolean;
isToplevel?: boolean;
label?: React.ReactNode;

View File

@ -14,10 +14,14 @@ interface ParentAccountProps {
function ParentAccount ({ address, className }: ParentAccountProps): React.ReactElement<ParentAccountProps> {
return (
<div className={className}
data-testid='parent'>
<Icon className='parent-icon'
icon='code-branch'/>
<div
className={className}
data-testid='parent'
>
<Icon
className='parent-icon'
icon='code-branch'
/>
<AccountName
value={address}

View File

@ -1,34 +0,0 @@
// Copyright 2017-2021 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PopupProps } from 'semantic-ui-react';
import React from 'react';
import { Popup as SUIPopup } from 'semantic-ui-react';
interface Props {
children: React.ReactNode;
className?: string;
isOpen?: boolean;
on?: PopupProps['on'];
onClose?: () => void;
position?: PopupProps['position'];
trigger?: React.ReactNode;
}
function Popup ({ children, className = '', isOpen, on = 'click', onClose, position = 'bottom right', trigger }: Props): React.ReactElement<Props> {
return (
<SUIPopup
className={className}
on={on}
onClose={onClose}
open={isOpen}
position={position}
trigger={trigger}
>
{children}
</SUIPopup>
);
}
export default React.memo(Popup);

View File

@ -0,0 +1,67 @@
// Copyright 2017-2021 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { Popup } from '@polkadot/react-components';
function TestPopup () {
return (
<>
<h1>Test outside text</h1>
<Popup
value={
<div>
Test popup content
</div>
}
/>
</>
);
}
function renderPopup () {
return render(
<TestPopup/>
);
}
describe('Popup Component', () => {
it('opens and closes', async () => {
renderPopup();
await expectPopupToBeClosed();
await togglePopup();
await expectPopupToBeOpen();
await togglePopup();
await expectPopupToBeClosed();
});
it('closes popup with outside click', async () => {
renderPopup();
await expectPopupToBeClosed();
await togglePopup();
await expectPopupToBeOpen();
await clickOutside();
await expectPopupToBeClosed();
});
});
async function expectPopupToBeClosed () {
await screen.findByRole('button');
expect(screen.queryAllByText('Test popup content')).toHaveLength(0);
}
async function expectPopupToBeOpen () {
await screen.findByText('Test popup content');
}
async function togglePopup () {
fireEvent.click(await screen.findByRole('button'));
}
async function clickOutside () {
fireEvent.click(await screen.findByText('Test outside text'));
}

View File

@ -0,0 +1,84 @@
// Copyright 2017-2021 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PopupWindowProps } from './types';
import React from 'react';
import { createPortal } from 'react-dom';
import styled, { css } from 'styled-components';
import { usePopupWindow } from '@polkadot/react-hooks/usePopupWindow';
function PopupWindow ({ children, className = '', position, triggerRef, windowRef }: PopupWindowProps) {
const { renderWindowPosition, verticalPosition } = usePopupWindow(windowRef, triggerRef, position);
return createPortal(
<div
className={`${className}${verticalPosition === 'top' ? ' pointerTop' : ' pointerBottom'}`}
ref={windowRef}
style={renderWindowPosition && { transform: `translate3d(${renderWindowPosition.x}px, ${renderWindowPosition.y}px, 0)`, zIndex: 300 }}
>
{children}
</div>,
document.body
);
}
export default React.memo(styled(PopupWindow)`
position: absolute;
top:0;
left:0;
z-index: -1;
margin: 0.7rem 0;
padding: 0.85rem 1rem;
color: var(--color-text);
background-color: var(--bg-menu);
border-radius: 4px;
border: 1px solid #d4d4d5;
box-shadow: 0 2px 4px 0 rgb(34 36 38 / 12%), 0 2px 10px 0 rgb(34 36 38 / 15%);
&::before {
position: absolute;
right: 50%;
top: unset;
bottom: -0.45rem;
box-shadow: 1px 1px 0 0 #bababc;
${({ position }) => position === 'left' && css`
left: unset;
right: 0.8rem;
`}
${({ position }) => position === 'right' && css`
left: 0.8rem;
right: unset;
`}
content: '';
background-color: var(--bg-menu);
width: 1rem;
height: 1rem;
transform: rotate(45deg);
z-index: 2;
}
&.pointerBottom::before {
box-shadow: -1px -1px 0 0 #bababc;
top: -0.45rem;
bottom: unset;
}
.ui.text.menu .item {
color: var(--color-text) !important;
text-align: left;
&.disabled {
opacity: 0.3;
}
}
`);

View File

@ -0,0 +1,61 @@
// Copyright 2017-2021 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PopupProps } from './types';
import React, { useEffect, useRef } from 'react';
import styled from 'styled-components';
import { Button } from '@polkadot/react-components/index';
import { useOutsideClick, useToggle } from '@polkadot/react-hooks';
import PopupWindow from './PopupWindow';
function Popup ({ children, className = '', isDisabled = false, onCloseAction, position = 'left', value }: PopupProps) {
const [isOpen, toggleIsOpen, setIsOpen] = useToggle(false);
const triggerRef = useRef<HTMLDivElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
useOutsideClick([triggerRef, dropdownRef], () => setIsOpen(false));
useEffect(() => {
if (!isOpen && onCloseAction) {
onCloseAction();
}
}, [isOpen, onCloseAction]);
return (
<div className={`ui--Popup ${className}`}>
{isOpen && (
<PopupWindow
position={position}
triggerRef={triggerRef}
windowRef={dropdownRef}
>
{value}
</PopupWindow>
)}
<div
onClick={toggleIsOpen}
ref={triggerRef}
>
{children ?? (
<Button
className={isOpen ? 'isOpen' : ''}
icon='ellipsis-v'
isDisabled={isDisabled}
isReadOnly={false}
/>
)}
</div>
</div>
);
}
export default React.memo(styled(Popup)`
display: inline-flex;
flex-direction: column;
justify-content: center;
position: relative;
`);

View File

@ -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<HTMLDivElement>;
triggerRef: React.RefObject<HTMLDivElement>;
position: HorizontalPosition;
}
export interface PopupProps {
isDisabled?: boolean;
className?: string;
value?: React.ReactNode;
children?: React.ReactNode;
position?: HorizontalPosition;
onCloseAction?: () => void;
}

View File

@ -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;
}

View File

@ -255,6 +255,16 @@ export default createGlobalStyle<Props & ThemeProps>(({ 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{

View File

@ -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;
}

View File

@ -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';

View File

@ -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<HTMLElement | undefined | null>): ElementPosition | undefined {
const [elementPosition, setElementPosition] = useState<ElementPosition>();
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;
}

View File

@ -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<HTMLDivElement>[], e: MouseEvent) {
return refs.find((ref) => ref.current && ref.current.contains(e.target as HTMLElement));
}
export const useOutsideClick = (elements: RefObject<HTMLDivElement>[], 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]);
};

View File

@ -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<HTMLDivElement>,
triggerRef: React.RefObject<HTMLDivElement>,
position: HorizontalPosition
): {renderWindowPosition: {x: number, y: number} | undefined, verticalPosition: VerticalPosition | undefined} {
const [renderWindowPosition, setRenderWindowPosition] = useState<{x: number, y: number}>();
const [verticalPosition, setVerticalPosition] = useState<VerticalPosition>();
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 };
}

View File

@ -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;
}

View File

@ -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<WindowSize>({
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;
}