Skip to content

Commit 64d8b66

Browse files
committed
refactor: popup
1 parent 8d376e2 commit 64d8b66

File tree

8 files changed

+156
-135
lines changed

8 files changed

+156
-135
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@import '../../styles/mixins.scss';
2+
3+
.hover-popup {
4+
padding: var(--g-spacing-3);
5+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import React from 'react';
2+
3+
import {Popup} from '@gravity-ui/uikit';
4+
import debounce from 'lodash/debounce';
5+
6+
import {cn} from '../../utils/cn';
7+
8+
import './HoverPopup.scss';
9+
10+
const b = cn('hover-popup');
11+
12+
interface HoverPopupProps {
13+
children: React.ReactNode;
14+
popupContent: React.ReactNode;
15+
showPopup?: boolean;
16+
onShowPopup?: VoidFunction;
17+
onHidePopup?: VoidFunction;
18+
}
19+
20+
export const HoverPopup = ({
21+
children,
22+
popupContent,
23+
showPopup,
24+
onShowPopup,
25+
onHidePopup,
26+
}: HoverPopupProps) => {
27+
const [isPopupVisible, setIsPopupVisible] = React.useState(false);
28+
const anchor = React.useRef<HTMLDivElement>(null);
29+
30+
const DEBOUNCE_TIMEOUT = 100;
31+
32+
const debouncedHandleShowPopup = React.useMemo(
33+
() =>
34+
debounce(() => {
35+
setIsPopupVisible(true);
36+
onShowPopup?.();
37+
}, DEBOUNCE_TIMEOUT),
38+
[onShowPopup],
39+
);
40+
41+
const hidePopup = React.useCallback(() => {
42+
setIsPopupVisible(false);
43+
onHidePopup?.();
44+
}, [onHidePopup]);
45+
46+
const debouncedHandleHidePopup = React.useMemo(
47+
() => debounce(hidePopup, DEBOUNCE_TIMEOUT),
48+
[hidePopup],
49+
);
50+
51+
const onMouseEnter = debouncedHandleShowPopup;
52+
53+
const onMouseLeave = () => {
54+
debouncedHandleShowPopup.cancel();
55+
debouncedHandleHidePopup();
56+
};
57+
58+
const [isPopupContentHovered, setIsPopupContentHovered] = React.useState(false);
59+
const [isFocused, setIsFocused] = React.useState(false);
60+
61+
const onPopupMouseEnter = React.useCallback(() => {
62+
setIsPopupContentHovered(true);
63+
}, []);
64+
65+
const onPopupMouseLeave = React.useCallback(() => {
66+
setIsPopupContentHovered(false);
67+
}, []);
68+
69+
const onPopupContextMenu = React.useCallback(() => {
70+
setIsFocused(true);
71+
}, []);
72+
73+
const onPopupBlur = React.useCallback(() => {
74+
setIsFocused(false);
75+
}, []);
76+
77+
const onPopupEscapeKeyDown = React.useCallback(() => {
78+
setIsFocused(false);
79+
setIsPopupContentHovered(false);
80+
hidePopup();
81+
}, [hidePopup]);
82+
83+
const open = isPopupVisible || showPopup || isPopupContentHovered || isFocused;
84+
85+
return (
86+
<React.Fragment>
87+
<div ref={anchor} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
88+
{children}
89+
</div>
90+
<Popup
91+
contentClassName={b()}
92+
anchorRef={anchor}
93+
open={open}
94+
onMouseEnter={onPopupMouseEnter}
95+
onMouseLeave={onPopupMouseLeave}
96+
onEscapeKeyDown={onPopupEscapeKeyDown}
97+
onBlur={onPopupBlur}
98+
placement={['top', 'bottom']}
99+
hasArrow
100+
offset={[0, 12]}
101+
>
102+
<div onContextMenu={onPopupContextMenu}>{popupContent}</div>
103+
</Popup>
104+
</React.Fragment>
105+
);
106+
};

src/components/PDiskPopup/PDiskPopup.scss

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/components/PDiskPopup/PDiskPopup.tsx

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,15 @@
11
import React from 'react';
22

3-
import type {PopupProps} from '@gravity-ui/uikit';
4-
import {Popup} from '@gravity-ui/uikit';
5-
63
import {selectNodeHostsMap} from '../../store/reducers/nodesList';
74
import {EFlag} from '../../types/api/enums';
85
import {valueIsDefined} from '../../utils';
9-
import {cn} from '../../utils/cn';
106
import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants';
117
import {getPDiskId} from '../../utils/disks/helpers';
128
import type {PreparedPDisk} from '../../utils/disks/types';
139
import {useTypedSelector} from '../../utils/hooks';
14-
import {usePopupOpenState} from '../../utils/hooks/usePopupOpenState';
1510
import {bytesToGB} from '../../utils/utils';
16-
import type {InfoViewerItem} from '../InfoViewer';
1711
import {InfoViewer} from '../InfoViewer';
18-
19-
import './PDiskPopup.scss';
20-
21-
const b = cn('pdisk-storage-popup');
12+
import type {InfoViewerItem} from '../InfoViewer';
2213

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

@@ -62,35 +53,14 @@ export const preparePDiskData = (data: PreparedPDisk, nodeHost?: string) => {
6253
return pdiskData;
6354
};
6455

65-
interface PDiskPopupProps extends PopupProps {
56+
interface PDiskPopupProps {
6657
data: PreparedPDisk;
67-
hidePopup?: VoidFunction;
6858
}
6959

70-
export const PDiskPopup = ({data, hidePopup, ...props}: PDiskPopupProps) => {
60+
export const PDiskPopup = ({data}: PDiskPopupProps) => {
7161
const nodeHostsMap = useTypedSelector(selectNodeHostsMap);
7262
const nodeHost = valueIsDefined(data.NodeId) ? nodeHostsMap?.get(data.NodeId) : undefined;
7363
const info = React.useMemo(() => preparePDiskData(data, nodeHost), [data, nodeHost]);
7464

75-
const {open, onMouseEnter, onMouseLeave, onContextMenu, onBlur, onEscapeKeyDown} =
76-
usePopupOpenState(hidePopup);
77-
78-
return (
79-
<Popup
80-
contentClassName={b()}
81-
placement={['top', 'bottom']}
82-
hasArrow
83-
offset={[0, 12]}
84-
onMouseLeave={onMouseLeave}
85-
onMouseEnter={onMouseEnter}
86-
onEscapeKeyDown={onEscapeKeyDown}
87-
onBlur={onBlur}
88-
{...props}
89-
open={open || props.open}
90-
>
91-
<div onContextMenu={onContextMenu}>
92-
<InfoViewer title="PDisk" info={info} size="s" />
93-
</div>
94-
</Popup>
95-
);
65+
return <InfoViewer title="PDisk" info={info} size="s" />;
9666
};

src/components/VDisk/VDisk.tsx

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import React from 'react';
2-
31
import {cn} from '../../utils/cn';
42
import type {PreparedVDisk} from '../../utils/disks/types';
5-
import {usePopupAnchor} from '../../utils/hooks/usePopupAnchor';
63
import {DiskStateProgressBar} from '../DiskStateProgressBar/DiskStateProgressBar';
4+
import {HoverPopup} from '../HoverPopup/HoverPopup';
75
import {InternalLink} from '../InternalLink';
86
import {VDiskPopup} from '../VDiskPopup/VDiskPopup';
97

@@ -32,21 +30,16 @@ export const VDisk = ({
3230
onHidePopup,
3331
progressBarClassName,
3432
}: VDiskProps) => {
35-
const {isPopupVisible, anchor, onMouseEnter, onMouseLeave, hidePopup} = usePopupAnchor(
36-
onShowPopup,
37-
onHidePopup,
38-
);
39-
4033
const vDiskPath = getVDiskLink(data);
4134

4235
return (
43-
<React.Fragment>
44-
<div
45-
className={b()}
46-
ref={anchor}
47-
onMouseEnter={onMouseEnter}
48-
onMouseLeave={onMouseLeave}
49-
>
36+
<HoverPopup
37+
showPopup={showPopup}
38+
onShowPopup={onShowPopup}
39+
onHidePopup={onHidePopup}
40+
popupContent={<VDiskPopup data={data} />}
41+
>
42+
<div className={b()}>
5043
<InternalLink to={vDiskPath} className={b('content')}>
5144
<DiskStateProgressBar
5245
diskAllocatedPercent={data.AllocatedPercent}
@@ -57,12 +50,6 @@ export const VDisk = ({
5750
/>
5851
</InternalLink>
5952
</div>
60-
<VDiskPopup
61-
data={data}
62-
anchorRef={anchor}
63-
open={isPopupVisible || showPopup}
64-
hidePopup={hidePopup}
65-
/>
66-
</React.Fragment>
53+
</HoverPopup>
6754
);
6855
};

src/components/VDiskPopup/VDiskPopup.scss

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
.vdisk-storage-popup {
2-
padding: 12px;
3-
42
.info-viewer + .info-viewer {
53
margin-top: 8px;
64
padding-top: 8px;

src/components/VDiskPopup/VDiskPopup.tsx

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React from 'react';
22

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

65
import {selectNodeHostsMap} from '../../store/reducers/nodesList';
76
import {EFlag} from '../../types/api/enums';
@@ -12,7 +11,6 @@ import {stringifyVdiskId} from '../../utils/dataFormatters/dataFormatters';
1211
import {isFullVDiskData} from '../../utils/disks/helpers';
1312
import type {PreparedVDisk, UnavailableDonor} from '../../utils/disks/types';
1413
import {useTypedSelector} from '../../utils/hooks';
15-
import {usePopupOpenState} from '../../utils/hooks/usePopupOpenState';
1614
import {bytesToGB, bytesToSpeed} from '../../utils/utils';
1715
import type {InfoViewerItem} from '../InfoViewer';
1816
import {InfoViewer} from '../InfoViewer';
@@ -131,17 +129,13 @@ const prepareVDiskData = (data: PreparedVDisk) => {
131129
return vdiskData;
132130
};
133131

134-
interface VDiskPopupProps extends PopupProps {
132+
interface VDiskPopupProps {
135133
data: PreparedVDisk | UnavailableDonor;
136-
hidePopup?: VoidFunction;
137134
}
138135

139-
export const VDiskPopup = ({data, hidePopup, ...props}: VDiskPopupProps) => {
136+
export const VDiskPopup = ({data}: VDiskPopupProps) => {
140137
const isFullData = isFullVDiskData(data);
141138

142-
const {open, onMouseEnter, onMouseLeave, onContextMenu, onBlur, onEscapeKeyDown} =
143-
usePopupOpenState(hidePopup);
144-
145139
const vdiskInfo = React.useMemo(
146140
() => (isFullData ? prepareVDiskData(data) : prepareUnavailableVDiskData(data)),
147141
[data, isFullData],
@@ -179,26 +173,11 @@ export const VDiskPopup = ({data, hidePopup, ...props}: VDiskPopupProps) => {
179173
}
180174

181175
return (
182-
<Popup
183-
contentClassName={b()}
184-
placement={['top', 'bottom']}
185-
hasArrow
186-
// bigger offset for easier switching to neighbour nodes
187-
// matches the default offset for popup with arrow out of a sense of beauty
188-
offset={[0, 12]}
189-
onMouseEnter={onMouseEnter}
190-
onMouseLeave={onMouseLeave}
191-
onEscapeKeyDown={onEscapeKeyDown}
192-
onBlur={onBlur}
193-
{...props}
194-
open={open || props.open}
195-
>
196-
<div onContextMenu={onContextMenu}>
197-
{data.DonorMode && <Label className={b('donor-label')}>Donor</Label>}
198-
<InfoViewer title="VDisk" info={vdiskInfo} size="s" />
199-
{pdiskInfo && <InfoViewer title="PDisk" info={pdiskInfo} size="s" />}
200-
{donorsInfo.length > 0 && <InfoViewer title="Donors" info={donorsInfo} size="s" />}
201-
</div>
202-
</Popup>
176+
<div className={b()}>
177+
{data.DonorMode && <Label className={b('donor-label')}>Donor</Label>}
178+
<InfoViewer title="VDisk" info={vdiskInfo} size="s" />
179+
{pdiskInfo && <InfoViewer title="PDisk" info={pdiskInfo} size="s" />}
180+
{donorsInfo.length > 0 && <InfoViewer title="Donors" info={donorsInfo} size="s" />}
181+
</div>
203182
);
204183
};

0 commit comments

Comments
 (0)