Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: popup closes on context menu copy #1453

Merged
merged 11 commits into from
Oct 16, 2024
3 changes: 3 additions & 0 deletions src/components/HoverPopup/HoverPopup.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.hover-popup {
padding: var(--g-spacing-3);
}
112 changes: 112 additions & 0 deletions src/components/HoverPopup/HoverPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from 'react';

import {Popup} from '@gravity-ui/uikit';
import debounce from 'lodash/debounce';

import {cn} from '../../utils/cn';

import './HoverPopup.scss';

const b = cn('hover-popup');

const DEBOUNCE_TIMEOUT = 100;

interface HoverPopupProps {
children: React.ReactNode;
popupContent: React.ReactNode;
showPopup?: boolean;
offset?: [number, number];
anchorRef?: React.RefObject<HTMLElement>;
onShowPopup?: VoidFunction;
onHidePopup?: VoidFunction;
}

export const HoverPopup = ({
children,
popupContent,
showPopup,
offset,
anchorRef,
onShowPopup,
onHidePopup,
}: HoverPopupProps) => {
const [isPopupVisible, setIsPopupVisible] = React.useState(false);
const anchor = React.useRef<HTMLDivElement>(null);

const debouncedHandleShowPopup = React.useMemo(
() =>
debounce(() => {
setIsPopupVisible(true);
onShowPopup?.();
}, DEBOUNCE_TIMEOUT),
[onShowPopup],
);

const hidePopup = React.useCallback(() => {
setIsPopupVisible(false);
onHidePopup?.();
}, [onHidePopup]);

const debouncedHandleHidePopup = React.useMemo(
() => debounce(hidePopup, DEBOUNCE_TIMEOUT),
[hidePopup],
);

const onMouseEnter = debouncedHandleShowPopup;

const onMouseLeave = () => {
debouncedHandleShowPopup.cancel();
debouncedHandleHidePopup();
};

const [isPopupContentHovered, setIsPopupContentHovered] = React.useState(false);
const [isFocused, setIsFocused] = React.useState(false);

const onPopupMouseEnter = React.useCallback(() => {
setIsPopupContentHovered(true);
}, []);

const onPopupMouseLeave = React.useCallback(() => {
setIsPopupContentHovered(false);
}, []);

const onPopupContextMenu = React.useCallback(() => {
setIsFocused(true);
}, []);

const onPopupBlur = React.useCallback(() => {
setIsFocused(false);
}, []);

const onPopupEscapeKeyDown = React.useCallback(() => {
setIsFocused(false);
setIsPopupContentHovered(false);
hidePopup();
}, [hidePopup]);
Comment on lines +65 to +85
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need all these useCallback?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes
because we pass this function as prop to external component which may use this function as prop


const open = isPopupVisible || showPopup || isPopupContentHovered || isFocused;

return (
<React.Fragment>
<div ref={anchor} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{children}
</div>
<Popup
contentClassName={b()}
anchorRef={anchorRef || anchor}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we allow to pass anchorRef? Won't it break behaviour set by us?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We allow to pass anchorRef for custom anchors if they are needed
if anchorRef is not passed - default anchor from component is used

open={open}
onMouseEnter={onPopupMouseEnter}
onMouseLeave={onPopupMouseLeave}
onEscapeKeyDown={onPopupEscapeKeyDown}
onBlur={onPopupBlur}
placement={['top', 'bottom']}
hasArrow
// bigger offset for easier switching to neighbour nodes
// matches the default offset for popup with arrow out of a sense of beauty
offset={offset || [0, 12]}
>
<div onContextMenu={onPopupContextMenu}>{popupContent}</div>
</Popup>
</React.Fragment>
);
};
3 changes: 0 additions & 3 deletions src/components/PDiskPopup/PDiskPopup.scss

This file was deleted.

39 changes: 4 additions & 35 deletions src/components/PDiskPopup/PDiskPopup.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
import React from 'react';

import type {PopupProps} from '@gravity-ui/uikit';
import {Popup} from '@gravity-ui/uikit';

import {selectNodeHostsMap} from '../../store/reducers/nodesList';
import {EFlag} from '../../types/api/enums';
import {valueIsDefined} from '../../utils';
import {cn} from '../../utils/cn';
import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants';
import {getPDiskId} from '../../utils/disks/helpers';
import type {PreparedPDisk} from '../../utils/disks/types';
import {useTypedSelector} from '../../utils/hooks';
import {bytesToGB} from '../../utils/utils';
import type {InfoViewerItem} from '../InfoViewer';
import {InfoViewer} from '../InfoViewer';

import './PDiskPopup.scss';

const b = cn('pdisk-storage-popup');
import type {InfoViewerItem} from '../InfoViewer';

const errorColors = [EFlag.Orange, EFlag.Red, EFlag.Yellow];

Expand Down Expand Up @@ -61,37 +53,14 @@ export const preparePDiskData = (data: PreparedPDisk, nodeHost?: string) => {
return pdiskData;
};

interface PDiskPopupProps extends PopupProps {
interface PDiskPopupProps {
data: PreparedPDisk;
}

export const PDiskPopup = ({data, ...props}: PDiskPopupProps) => {
export const PDiskPopup = ({data}: PDiskPopupProps) => {
const nodeHostsMap = useTypedSelector(selectNodeHostsMap);
const nodeHost = valueIsDefined(data.NodeId) ? nodeHostsMap?.get(data.NodeId) : undefined;
const info = React.useMemo(() => preparePDiskData(data, nodeHost), [data, nodeHost]);

const [isPopupContentHovered, setIsPopupContentHovered] = React.useState(false);
const onMouseLeave = React.useCallback(() => {
setIsPopupContentHovered(false);
}, []);
const onMouseEnter = React.useCallback(() => {
setIsPopupContentHovered(true);
}, []);

return (
<Popup
contentClassName={b()}
placement={['top', 'bottom']}
hasArrow
// bigger offset for easier switching to neighbour nodes
// matches the default offset for popup with arrow out of a sense of beauty
offset={[0, 12]}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
{...props}
open={isPopupContentHovered || props.open}
>
<InfoViewer title="PDisk" info={info} size="s" />
</Popup>
);
return <InfoViewer title="PDisk" info={info} size="s" />;
};
41 changes: 9 additions & 32 deletions src/components/VDisk/VDisk.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import React from 'react';

import {debounce} from 'lodash';

import {cn} from '../../utils/cn';
import type {PreparedVDisk} from '../../utils/disks/types';
import {DiskStateProgressBar} from '../DiskStateProgressBar/DiskStateProgressBar';
import {HoverPopup} from '../HoverPopup/HoverPopup';
import {InternalLink} from '../InternalLink';
import {VDiskPopup} from '../VDiskPopup/VDiskPopup';

Expand All @@ -14,8 +11,6 @@ import './VDisk.scss';

const b = cn('ydb-vdisk-component');

const DEBOUNCE_TIMEOUT = 100;

export interface VDiskProps {
data?: PreparedVDisk;
compact?: boolean;
Expand All @@ -35,33 +30,16 @@ export const VDisk = ({
onHidePopup,
progressBarClassName,
}: VDiskProps) => {
const [isPopupVisible, setIsPopupVisible] = React.useState(false);

const anchor = React.useRef(null);

const debouncedHandleShowPopup = debounce(() => {
setIsPopupVisible(true);
onShowPopup?.();
}, DEBOUNCE_TIMEOUT);

const debouncedHandleHidePopup = debounce(() => {
setIsPopupVisible(false);
onHidePopup?.();
}, DEBOUNCE_TIMEOUT);

const vDiskPath = getVDiskLink(data);

return (
<React.Fragment>
<div
className={b()}
ref={anchor}
onMouseEnter={debouncedHandleShowPopup}
onMouseLeave={() => {
debouncedHandleShowPopup.cancel();
debouncedHandleHidePopup();
}}
>
<HoverPopup
showPopup={showPopup}
onShowPopup={onShowPopup}
onHidePopup={onHidePopup}
popupContent={<VDiskPopup data={data} />}
>
<div className={b()}>
<InternalLink to={vDiskPath} className={b('content')}>
<DiskStateProgressBar
diskAllocatedPercent={data.AllocatedPercent}
Expand All @@ -72,7 +50,6 @@ export const VDisk = ({
/>
</InternalLink>
</div>
<VDiskPopup data={data} anchorRef={anchor} open={isPopupVisible || showPopup} />
</React.Fragment>
</HoverPopup>
);
};
2 changes: 0 additions & 2 deletions src/components/VDiskPopup/VDiskPopup.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
.vdisk-storage-popup {
padding: 12px;

.info-viewer + .info-viewer {
margin-top: 8px;
padding-top: 8px;
Expand Down
30 changes: 5 additions & 25 deletions src/components/VDiskPopup/VDiskPopup.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';

import type {PopupProps} from '@gravity-ui/uikit';
import {Label, Popup} from '@gravity-ui/uikit';
import {Label} from '@gravity-ui/uikit';

import {selectNodeHostsMap} from '../../store/reducers/nodesList';
import {EFlag} from '../../types/api/enums';
Expand Down Expand Up @@ -130,21 +129,13 @@ const prepareVDiskData = (data: PreparedVDisk) => {
return vdiskData;
};

interface VDiskPopupProps extends PopupProps {
interface VDiskPopupProps {
data: PreparedVDisk | UnavailableDonor;
}

export const VDiskPopup = ({data, ...props}: VDiskPopupProps) => {
export const VDiskPopup = ({data}: VDiskPopupProps) => {
const isFullData = isFullVDiskData(data);

const [isPopupContentHovered, setIsPopupContentHovered] = React.useState(false);
const onMouseLeave = React.useCallback(() => {
setIsPopupContentHovered(false);
}, []);
const onMouseEnter = React.useCallback(() => {
setIsPopupContentHovered(true);
}, []);

const vdiskInfo = React.useMemo(
() => (isFullData ? prepareVDiskData(data) : prepareUnavailableVDiskData(data)),
[data, isFullData],
Expand Down Expand Up @@ -182,22 +173,11 @@ export const VDiskPopup = ({data, ...props}: VDiskPopupProps) => {
}

return (
<Popup
contentClassName={b()}
placement={['top', 'bottom']}
hasArrow
// bigger offset for easier switching to neighbour nodes
// matches the default offset for popup with arrow out of a sense of beauty
offset={[0, 12]}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
{...props}
open={isPopupContentHovered || props.open}
>
<div className={b()}>
{data.DonorMode && <Label className={b('donor-label')}>Donor</Label>}
<InfoViewer title="VDisk" info={vdiskInfo} size="s" />
{pdiskInfo && <InfoViewer title="PDisk" info={pdiskInfo} size="s" />}
{donorsInfo.length > 0 && <InfoViewer title="Donors" info={donorsInfo} size="s" />}
</Popup>
</div>
);
};
Loading
Loading