Skip to content
Open
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
9 changes: 9 additions & 0 deletions src/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useGetSize } from './hooks/useGetSize';
import useHeights from './hooks/useHeights';
import useMobileTouchMove from './hooks/useMobileTouchMove';
import useOriginScroll from './hooks/useOriginScroll';
import useDragEdgeScroll from './hooks/useDragEdgeScroll';
import useScrollDrag from './hooks/useScrollDrag';
import type { ScrollOffset, ScrollOffsetInfo, ScrollPos, ScrollTarget } from './hooks/useScrollTo';
import useScrollTo from './hooks/useScrollTo';
Expand Down Expand Up @@ -470,6 +471,14 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
syncScrollTop((top) => top + offset);
});

// Native HTML5 drag (e.g. a draggable Tree node) toward the edge: the
// `mousemove` path above is not fired during a native drag, and the native
// drag-to-edge autoscroll is disabled by `overflow: hidden`, so drive it from
// the drag events here instead.
useDragEdgeScroll(inVirtual, componentRef, height, itemHeight, (offset) => {
syncScrollTop((top) => top + offset);
});

useLayoutEffect(() => {
// Firefox only
function onMozMousePixelScroll(e: WheelEvent) {
Expand Down
119 changes: 119 additions & 0 deletions src/hooks/useDragEdgeScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { raf } from '@rc-component/util';
import * as React from 'react';
import { smoothScrollOffset } from './useScrollDrag';

/**
* The mouse-drag path in `useScrollDrag` runs on `mousemove`, which the browser
* does not fire during a native HTML5 drag โ€” and it deliberately skips
* draggable targets so it never fights the native drag. So when a consumer
* (e.g. a draggable Tree) drags a node toward the edge of a virtual list, the
* container is `overflow: hidden`, the browser's native drag-to-edge autoscroll
* is disabled, and nothing scrolls.
*
* Drive the edge scrolling from the native drag events instead. `dragover` only
* fires while a drag is in progress, so no extra "is dragging" flag is needed:
* the listeners can stay mounted and simply do nothing until a drag happens.
*/
export default function useDragEdgeScroll(
inVirtual: boolean,
componentRef: React.RefObject<HTMLElement>,
height: number,
itemHeight: number,
onScrollOffset: (offset: number) => void,
) {
const onScrollOffsetRef = React.useRef(onScrollOffset);
onScrollOffsetRef.current = onScrollOffset;

React.useEffect(() => {
const ele = componentRef.current;
if (!inVirtual || !ele || !height) {
return;
}

// `height / 4` caps each band so the top and bottom never overlap on short
// containers (an idle zone always remains in the middle); `itemHeight * 1.2`
// keeps a roughly one-row band otherwise.
const edgeThreshold = Math.min(itemHeight * 1.2, height / 4);

let rafId: number | null = null;
let offset = 0;

const stopScroll = () => {
if (rafId !== null) {
raf.cancel(rafId);
rafId = null;
}
offset = 0;
};

const scrollFrame = () => {
onScrollOffsetRef.current(offset);
rafId = raf(scrollFrame);
};

const continueScroll = () => {
// `0` happens when the pointer sits exactly on the band boundary; spinning
// the loop for a no-op would fire a scroll callback on every frame.
if (offset === 0) {
stopScroll();
return;
}
if (rafId === null) {
rafId = raf(scrollFrame);
}
};

const onDragOver = (e: DragEvent) => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Skip if a nested virtual List already handled this event, the same way
// `useScrollDrag` does on `mousedown`: a nested List's `dragover` bubbles
// to both holders and only the innermost one should scroll.
const event = e as DragEvent & { _virtualHandled?: boolean };
if (event._virtualHandled) {
return;
}
// `dragover` keeps firing while the drag lasts, so mark each event: the
// innermost holder sees it first and the outer ones bail out above.
event._virtualHandled = true;

const { top, bottom } = ele.getBoundingClientRect();
const { clientY } = e;

if (clientY <= top + edgeThreshold) {
offset = -smoothScrollOffset(top + edgeThreshold - clientY);
continueScroll();
} else if (clientY >= bottom - edgeThreshold) {
offset = smoothScrollOffset(clientY - (bottom - edgeThreshold));
continueScroll();
} else {
stopScroll();
}
};

const onDragLeave = (e: DragEvent) => {
const related = e.relatedTarget as Node | null;
if (!related || !ele.contains(related)) {
stopScroll();
}
};

const ownerDocument = ele.ownerDocument;

// `dragover` / `dragleave` need the container rect, so listen on it.
// `drop` / `dragend` are listened on the document in the CAPTURE phase: a
// consumer's node may stop their propagation in the bubble phase (rc-tree's
// `TreeNode` does), so capture is the only reliable place to see the drag
// ending.
ele.addEventListener('dragover', onDragOver);
ele.addEventListener('dragleave', onDragLeave);
ownerDocument.addEventListener('drop', stopScroll, true);
ownerDocument.addEventListener('dragend', stopScroll, true);

return () => {
stopScroll();
ele.removeEventListener('dragover', onDragOver);
ele.removeEventListener('dragleave', onDragLeave);
ownerDocument.removeEventListener('drop', stopScroll, true);
ownerDocument.removeEventListener('dragend', stopScroll, true);
};
}, [inVirtual, height, itemHeight]);
}
2 changes: 1 addition & 1 deletion src/hooks/useScrollDrag.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { raf } from '@rc-component/util';
import * as React from 'react';

function smoothScrollOffset(offset: number) {
export function smoothScrollOffset(offset: number) {
return Math.floor(offset ** 0.5);
}

Expand Down
Loading
Loading