mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
dbeaver/pro#8355 throttle hover and menu showing
This commit is contained in:
@@ -262,4 +262,7 @@ export * from './Alert.js';
|
||||
export * from './ObjectPropertyInfo/evaluate.js';
|
||||
export * from './ObjectPropertyInfo/getObjectPropertyDefaults.js';
|
||||
export * from './useVisible.js';
|
||||
export * from './SAVED_VALUE_INDICATOR.js';
|
||||
export * from './SAVED_VALUE_INDICATOR.js';
|
||||
export * from './useDebounceValue.js';
|
||||
export * from './useThrottleValue.js';
|
||||
export * from './useThrottleCallback.js';
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable react-hooks/refs */
|
||||
|
||||
import { useMemo, useRef } from 'react';
|
||||
|
||||
type TDebouncedCallback<Params extends unknown[]> = ((...args: Params) => void) & {
|
||||
cancel: () => void;
|
||||
};
|
||||
|
||||
export function useDebounceCallback<Params extends unknown[], Return>(
|
||||
callback: (...args: Params) => Return,
|
||||
delay: number,
|
||||
): TDebouncedCallback<Params> {
|
||||
const internalCallbackRef = useRef(callback);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const delayRef = useRef(delay);
|
||||
|
||||
internalCallbackRef.current = callback;
|
||||
delayRef.current = delay;
|
||||
|
||||
const debounced = useMemo(() => {
|
||||
const cancel = () => {
|
||||
if (!timerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
};
|
||||
|
||||
const debouncedCallback = (...args: Params) => {
|
||||
cancel();
|
||||
timerRef.current = setTimeout(() => {
|
||||
internalCallbackRef.current(...args);
|
||||
}, delayRef.current);
|
||||
};
|
||||
|
||||
debouncedCallback.cancel = cancel;
|
||||
|
||||
return debouncedCallback;
|
||||
}, []);
|
||||
|
||||
return debounced;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useDebounceCallback } from './useDebounceCallback.js';
|
||||
|
||||
export function useDebounceValue<Value>(value: Value, delay: number): Value {
|
||||
const previousValueRef = useRef(value);
|
||||
const [debouncedValue, setDebounceValue] = useState(value);
|
||||
|
||||
const setState = useDebounceCallback(setDebounceValue, delay);
|
||||
|
||||
useEffect(() => {
|
||||
if (previousValueRef.current === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(value);
|
||||
|
||||
previousValueRef.current = value;
|
||||
}, [value, setState]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useThrottleCallback } from './useThrottleCallback.js';
|
||||
|
||||
interface UseHoverResult<T extends HTMLElement> {
|
||||
ref: React.RefCallback<T>;
|
||||
@@ -16,13 +17,17 @@ export function useHover<T extends HTMLElement = HTMLElement>(): UseHoverResult<
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const nodeRef = useRef<T | null>(null);
|
||||
|
||||
const handleMove = useThrottleCallback((value: boolean) => {
|
||||
setIsHovered(value);
|
||||
}, 100);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
}, []);
|
||||
handleMove(true);
|
||||
}, [handleMove]);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setIsHovered(false);
|
||||
}, []);
|
||||
handleMove(false);
|
||||
}, [handleMove]);
|
||||
|
||||
const ref = useCallback(
|
||||
(node: T | null) => {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
|
||||
type TThrottledCallback<Params extends unknown[]> = ((...args: Params) => void) & {
|
||||
cancel: () => void;
|
||||
};
|
||||
|
||||
export const useThrottleCallback = <Params extends unknown[], Return>(
|
||||
callback: (...args: Params) => Return,
|
||||
delay: number,
|
||||
): TThrottledCallback<Params> => {
|
||||
const internalCallbackRef = useRef(callback);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const isCalledRef = useRef(false);
|
||||
const delayRef = useRef(delay);
|
||||
const lastArgsRef = useRef<Params | null>(null);
|
||||
|
||||
internalCallbackRef.current = callback;
|
||||
delayRef.current = delay;
|
||||
|
||||
const throttled = useMemo(() => {
|
||||
const timer = () => {
|
||||
isCalledRef.current = false;
|
||||
|
||||
if (!lastArgsRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
internalCallbackRef.current.apply(this, lastArgsRef.current);
|
||||
lastArgsRef.current = null;
|
||||
setTimeout(timer, delayRef.current);
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
if (!timeoutRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
isCalledRef.current = false;
|
||||
};
|
||||
|
||||
const throttledCallback = (...args: Params) => {
|
||||
lastArgsRef.current = args;
|
||||
if (isCalledRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
internalCallbackRef.current.apply(this, args);
|
||||
isCalledRef.current = true;
|
||||
timeoutRef.current = setTimeout(timer, delayRef.current);
|
||||
};
|
||||
|
||||
throttledCallback.cancel = cancel;
|
||||
|
||||
cancel();
|
||||
return throttledCallback;
|
||||
}, [delay]);
|
||||
|
||||
return throttled;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useThrottleCallback } from './useThrottleCallback.js';
|
||||
|
||||
export function useThrottleValue<Value>(value: Value, delay: number) {
|
||||
const previousValueRef = useRef(value);
|
||||
const [throttledValue, setThrottledValue] = useState(value);
|
||||
|
||||
const throttledSetState = useThrottleCallback(setThrottledValue, delay);
|
||||
|
||||
useEffect(() => {
|
||||
if (previousValueRef.current === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
throttledSetState(value);
|
||||
previousValueRef.current = value;
|
||||
}, [value]);
|
||||
|
||||
return throttledValue;
|
||||
}
|
||||
+3
-1
@@ -18,6 +18,7 @@ import {
|
||||
TreeNodeIcon,
|
||||
TreeNodeName,
|
||||
useContextMenuPosition,
|
||||
useDebounceValue,
|
||||
useHover,
|
||||
useMergeRefs,
|
||||
useS,
|
||||
@@ -69,6 +70,7 @@ export const ConnectionNavNodeControl: NavTreeControlComponent = observer<NavTre
|
||||
|
||||
const temporary = node.objectFeatures.includes(EObjectFeature.dataSourceTemporary);
|
||||
const mountMenu = (selected || hoverHook.isHovered) && !dndPlaceholder;
|
||||
const mountMenuDebounced = useDebounceValue(mountMenu, mountMenu ? 0 : 200);
|
||||
|
||||
if (temporary) {
|
||||
tooltip += `\n${translate('ui_type')}: ${translate('core_connections_connection_temporary')}`;
|
||||
@@ -90,7 +92,7 @@ export const ConnectionNavNodeControl: NavTreeControlComponent = observer<NavTre
|
||||
<div className={s(styles, { nameBox: true })}>{name}</div>
|
||||
</Loader>
|
||||
</TreeNodeName>
|
||||
{mountMenu && (
|
||||
{mountMenuDebounced && (
|
||||
<div className={s(styles, { portal: true })} onClick={handlePortalClick}>
|
||||
<TreeNodeMenuLoader contextMenuPosition={contextMenuPosition} node={node} selected={selected} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user