diff --git a/src/components/HoverPopup/HoverPopup.scss b/src/components/HoverPopup/HoverPopup.scss new file mode 100644 index 0000000000..3ee3acfbb5 --- /dev/null +++ b/src/components/HoverPopup/HoverPopup.scss @@ -0,0 +1,3 @@ +.hover-popup { + padding: var(--g-spacing-3); +} diff --git a/src/components/HoverPopup/HoverPopup.tsx b/src/components/HoverPopup/HoverPopup.tsx new file mode 100644 index 0000000000..6b6c541fba --- /dev/null +++ b/src/components/HoverPopup/HoverPopup.tsx @@ -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; + 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(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]); + + const open = isPopupVisible || showPopup || isPopupContentHovered || isFocused; + + return ( + +
+ {children} +
+ +
{popupContent}
+
+
+ ); +}; diff --git a/src/components/PDiskPopup/PDiskPopup.scss b/src/components/PDiskPopup/PDiskPopup.scss deleted file mode 100644 index 103f255f78..0000000000 --- a/src/components/PDiskPopup/PDiskPopup.scss +++ /dev/null @@ -1,3 +0,0 @@ -.pdisk-storage-popup { - padding: 12px; -} diff --git a/src/components/PDiskPopup/PDiskPopup.tsx b/src/components/PDiskPopup/PDiskPopup.tsx index 0bca72c7f9..62900598bd 100644 --- a/src/components/PDiskPopup/PDiskPopup.tsx +++ b/src/components/PDiskPopup/PDiskPopup.tsx @@ -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]; @@ -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 ( - - - - ); + return ; }; diff --git a/src/components/VDisk/VDisk.tsx b/src/components/VDisk/VDisk.tsx index c369719c67..436cfa9b06 100644 --- a/src/components/VDisk/VDisk.tsx +++ b/src/components/VDisk/VDisk.tsx @@ -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'; @@ -14,8 +11,6 @@ import './VDisk.scss'; const b = cn('ydb-vdisk-component'); -const DEBOUNCE_TIMEOUT = 100; - export interface VDiskProps { data?: PreparedVDisk; compact?: boolean; @@ -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 ( - -
{ - debouncedHandleShowPopup.cancel(); - debouncedHandleHidePopup(); - }} - > + } + > +
- - +
); }; diff --git a/src/components/VDiskPopup/VDiskPopup.scss b/src/components/VDiskPopup/VDiskPopup.scss index d1a622519f..c98fcc9fcc 100644 --- a/src/components/VDiskPopup/VDiskPopup.scss +++ b/src/components/VDiskPopup/VDiskPopup.scss @@ -1,6 +1,4 @@ .vdisk-storage-popup { - padding: 12px; - .info-viewer + .info-viewer { margin-top: 8px; padding-top: 8px; diff --git a/src/components/VDiskPopup/VDiskPopup.tsx b/src/components/VDiskPopup/VDiskPopup.tsx index 45b63c8eee..ca09c77703 100644 --- a/src/components/VDiskPopup/VDiskPopup.tsx +++ b/src/components/VDiskPopup/VDiskPopup.tsx @@ -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'; @@ -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], @@ -182,22 +173,11 @@ export const VDiskPopup = ({data, ...props}: VDiskPopupProps) => { } return ( - +
{data.DonorMode && } {pdiskInfo && } {donorsInfo.length > 0 && } - +
); }; diff --git a/src/containers/Storage/PDisk/PDisk.tsx b/src/containers/Storage/PDisk/PDisk.tsx index a48162925a..b37b5bbf40 100644 --- a/src/containers/Storage/PDisk/PDisk.tsx +++ b/src/containers/Storage/PDisk/PDisk.tsx @@ -1,16 +1,14 @@ import React from 'react'; -import {debounce} from 'lodash'; - import {DiskStateProgressBar} from '../../../components/DiskStateProgressBar/DiskStateProgressBar'; +import {HoverPopup} from '../../../components/HoverPopup/HoverPopup'; import {InternalLink} from '../../../components/InternalLink'; import {PDiskPopup} from '../../../components/PDiskPopup/PDiskPopup'; import {VDisk} from '../../../components/VDisk/VDisk'; -import routes, {createHref, getPDiskPagePath} from '../../../routes'; +import {getPDiskPagePath} from '../../../routes'; import {valueIsDefined} from '../../../utils'; import {cn} from '../../../utils/cn'; import type {PreparedPDisk, PreparedVDisk} from '../../../utils/disks/types'; -import {STRUCTURE} from '../../Node/NodePages'; import type {StorageViewContext} from '../types'; import {isVdiskActive} from '../utils'; @@ -18,8 +16,6 @@ import './PDisk.scss'; const b = cn('pdisk-storage'); -const DEBOUNCE_TIMEOUT = 100; - interface PDiskProps { data?: PreparedPDisk; vDisks?: PreparedVDisk[]; @@ -41,22 +37,10 @@ export const PDisk = ({ progressBarClassName, viewContext, }: PDiskProps) => { - const [isPopupVisible, setIsPopupVisible] = React.useState(false); - - const anchor = React.useRef(null); - const {NodeId, PDiskId} = data; const pDiskIdsDefined = valueIsDefined(NodeId) && valueIsDefined(PDiskId); - const debouncedHandleShowPopup = debounce(() => { - setIsPopupVisible(true); - onShowPopup?.(); - }, DEBOUNCE_TIMEOUT); - - const debouncedHandleHidePopup = debounce(() => { - setIsPopupVisible(false); - onHidePopup?.(); - }, DEBOUNCE_TIMEOUT); + const anchorRef = React.useRef(null); const renderVDisks = () => { if (!vDisks?.length) { @@ -65,52 +49,40 @@ export const PDisk = ({ return (
- {vDisks.map((vdisk) => { - return ( -
- -
- ); - })} + {vDisks.map((vdisk) => ( +
+ +
+ ))}
); }; let pDiskPath: string | undefined; - if (pDiskIdsDefined) { - pDiskPath = createHref(routes.node, {id: NodeId, activeTab: STRUCTURE}, {pdiskId: PDiskId}); - } - if (pDiskIdsDefined) { pDiskPath = getPDiskPagePath(PDiskId, NodeId); } return ( - -
- {renderVDisks()} - { - debouncedHandleShowPopup.cancel(); - debouncedHandleHidePopup(); - }} - > +
+ {renderVDisks()} + } + > +
{data.Type}
-
- - + +
); };