Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions webapp/packages/core-ui/src/DragAndDrop/DNDScrollContainer.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement, IDNDScrollContainerProps>(function DNDScrollContainer({ isDragging, className, children }, externalRef) {
const innerRef = useRef<HTMLDivElement>(null);
const speedRef = useRef(0);
const rafRef = useRef<number>(null);

const mouse = useMouse<HTMLDivElement>();

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 (
<div ref={ref} className={className}>
{children}
</div>
);
}),
);
Original file line number Diff line number Diff line change
@@ -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));
1 change: 1 addition & 0 deletions webapp/packages/core-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
export * from './ContextMenu/IContextMenuItemProps.js';
export * from './ContextMenu/MenuBar/MenuBarLazy.js';
export * from './ContextMenu/MenuBar/MenuBarItemLoader.js';
export * from './ContextMenu/MenuActionElement.js';

Check failure on line 17 in webapp/packages/core-ui/src/index.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Don't import/export .tsx files from .ts files directly, use React.lazy()
export * from './ContextMenu/MenuContext.js';
export { default as MenuBarStyles } from './ContextMenu/MenuBar/MenuBar.module.css';
export { default as MenuBarItemStyles } from './ContextMenu/MenuBar/MenuBarItem.module.css';
Expand All @@ -27,6 +27,7 @@
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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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';
Expand All @@ -44,7 +45,7 @@
import { type IElementsTree, type IElementsTreeOptions, useElementsTree } from './useElementsTree.js';
import { useElementsTreeFolderExplorer } from './useElementsTreeFolderExplorer.js';

export interface ElementsTreeProps extends IElementsTreeOptions, React.PropsWithChildren {

Check failure on line 48 in webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Interface name `ElementsTreeProps` must match the RegExp: /^I[A-Z]/u
/** Specifies the root path for the tree. ROOT_NODE_PATH will be used if not defined */
root?: string;
selectionTree?: boolean;
Expand Down Expand Up @@ -172,7 +173,7 @@
return (
<>
<ElementsTreeTools tree={tree} settingsElements={settingsElements} />
<div ref={treeMergedRef} className={s(styles, { treeBox: true })}>
<DNDScrollContainer ref={treeMergedRef} className={s(styles, { treeBox: true })} isDragging={!!dndBox.state.context}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The best solution here is to invent something tricky with html/css so it triggers scroll. Cause it already works fine, but this zone is too small
I can see why you want JS to handle that. But feel like there is a native html/css solution, it's just tricky to find

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I couldn't find any native solution. Everyone mentions that the native solution has a very small scrollable area. If you know of any solutions, feel free to suggest them.

<ElementsTreeContext.Provider value={context}>
<div className={s(styles, { box: true }, className)}>
<FolderExplorer state={folderExplorer}>
Expand Down Expand Up @@ -210,7 +211,7 @@
</FolderExplorer>
</div>
</ElementsTreeContext.Provider>
</div>
</DNDScrollContainer>
</>
);
}),
Expand Down
Loading