diff --git a/webapp/packages/core-ui/src/DragAndDrop/DNDScrollContainer.tsx b/webapp/packages/core-ui/src/DragAndDrop/DNDScrollContainer.tsx new file mode 100644 index 00000000000..b97313e1617 --- /dev/null +++ b/webapp/packages/core-ui/src/DragAndDrop/DNDScrollContainer.tsx @@ -0,0 +1,81 @@ +/* + * 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 { forwardRef, useEffect, useRef } from 'react'; +import { observer } from 'mobx-react-lite'; + +import { useMergeRefs, useMouse } from '@cloudbeaver/core-blocks'; + +const EDGE = 60; +const MAX_SPEED = 12; + +export interface IDNDScrollContainerProps { + isDragging: boolean; + className?: string; + children: React.ReactNode; +} + +export const DNDScrollContainer = observer( + forwardRef(function DNDScrollContainer({ isDragging, className, children }, externalRef) { + const innerRef = useRef(null); + const speedRef = useRef(0); + const rafRef = useRef(null); + + const mouse = useMouse(); + + const el = innerRef.current; + + if (isDragging && el && mouse.state.position) { + const y = mouse.state.position.y; + const h = el.getBoundingClientRect().height; + + if (y < EDGE) { + speedRef.current = -MAX_SPEED * Math.min((EDGE - y) / EDGE, 1); + } else if (y > h - EDGE) { + speedRef.current = MAX_SPEED * Math.min((y - (h - EDGE)) / EDGE, 1); + } else { + speedRef.current = 0; + } + } else { + speedRef.current = 0; + } + + useEffect(() => { + if (!isDragging) { + return; + } + + // rAF loop for smooth scrolling + const loop = () => { + const node = innerRef.current; + + if (node && speedRef.current !== 0) { + node.scrollTop += speedRef.current; + } + + rafRef.current = requestAnimationFrame(loop); + }; + + rafRef.current = requestAnimationFrame(loop); + + return () => { + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + }; + }, [isDragging]); + + const ref = useMergeRefs(innerRef, mouse.reference, externalRef); + + return ( +
+ {children} +
+ ); + }), +); diff --git a/webapp/packages/core-ui/src/DragAndDrop/DNDScrollContainerLoader.ts b/webapp/packages/core-ui/src/DragAndDrop/DNDScrollContainerLoader.ts new file mode 100644 index 00000000000..e8c60005040 --- /dev/null +++ b/webapp/packages/core-ui/src/DragAndDrop/DNDScrollContainerLoader.ts @@ -0,0 +1,11 @@ +/* + * 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 { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const DNDScrollContainer = importLazyComponent(() => import('./DNDScrollContainer.js').then(m => m.DNDScrollContainer)); diff --git a/webapp/packages/core-ui/src/index.ts b/webapp/packages/core-ui/src/index.ts index 53bd6bf2208..a60932e271c 100644 --- a/webapp/packages/core-ui/src/index.ts +++ b/webapp/packages/core-ui/src/index.ts @@ -27,6 +27,7 @@ export * from './DragAndDrop/DNDPreviewLoader.js'; export * from './DragAndDrop/DNDProviderLoader.js'; export * from './DragAndDrop/useDNDBox.js'; export * from './DragAndDrop/useDNDData.js'; +export * from './DragAndDrop/DNDScrollContainerLoader.js'; export * from './Form/Components/IBaseFormProps.js'; export * from './Form/Components/BaseFormLazy.js'; diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx index f3fd978e58f..eb8c0ab687a 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx @@ -26,6 +26,7 @@ import { import { useService } from '@cloudbeaver/core-di'; import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; import { EObjectFeature, type NavNode, NavNodeInfoResource, NavTreeResource, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree'; +import { DNDScrollContainer } from '@cloudbeaver/core-ui'; import { useNavTreeDropBox } from '../useNavTreeDropBox.js'; import style from './ElementsTree.module.css'; @@ -172,7 +173,7 @@ export const ElementsTree = observer( return ( <> -
+
@@ -210,7 +211,7 @@ export const ElementsTree = observer(
-
+ ); }),