Skip to content

Commit

Permalink
fix: use custom autoscroll with virtualization
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelarbibe committed Jul 2, 2024
1 parent 182b5b3 commit e0e747b
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 6 deletions.
27 changes: 24 additions & 3 deletions examples/virtual/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import "./index.css";
import { type Active, DragOverlay, MeasuringStrategy } from "@dnd-kit/core";
import { endOfDay, startOfDay } from "date-fns";
import type { DragEndEvent, Range, ResizeEndEvent } from "dnd-timeline";
import type {
DragEndEvent,
DragStartEvent,
Range,
ResizeEndEvent,
} from "dnd-timeline";
import { TimelineContext } from "dnd-timeline";
import React, { useCallback, useState } from "react";
import { useCallback, useState } from "react";
import Timeline from "./Timeline";
import { generateItems, generateRows } from "./utils";

Expand All @@ -12,6 +18,7 @@ const DEFAULT_RANGE: Range = {
};

function App() {
const [active, setActive] = useState<Active | null>(null);
const [range, setRange] = useState(DEFAULT_RANGE);

const [rows] = useState(generateRows(1000));
Expand All @@ -36,7 +43,9 @@ function App() {
}),
);
}, []);

const onDragEnd = useCallback((event: DragEndEvent) => {
setActive(null);
const activeRowId = event.over?.id as string;
const updatedSpan = event.active.data.current.getSpanFromDragEvent?.(event);

Expand All @@ -57,14 +66,26 @@ function App() {
);
}, []);

const onDragStart = useCallback(
(event: DragStartEvent) => setActive(event.active),
[],
);

const onDragCancel = useCallback(() => setActive(null), []);

return (
<TimelineContext
range={range}
onDragEnd={onDragEnd}
onDragStart={onDragStart}
onResizeEnd={onResizeEnd}
onDragCancel={onDragCancel}
onRangeChanged={setRange}
autoScroll={{ enabled: false }}
overlayed
>
<Timeline items={items} rows={rows} />
<Timeline items={items} rows={rows} activeItem={active} />
<DragOverlay>{active && <div>{active.id}</div>}</DragOverlay>
</TimelineContext>
);
}
Expand Down
4 changes: 4 additions & 0 deletions examples/virtual/src/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type React from "react";
interface ItemProps {
id: string;
span: Span;
rowId: string;
children: React.ReactNode;
}

Expand All @@ -13,6 +14,9 @@ function Item(props: ItemProps) {
useItem({
id: props.id,
span: props.span,
data: {
rowId: props.rowId,
},
});

return (
Expand Down
43 changes: 40 additions & 3 deletions examples/virtual/src/Timeline.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,63 @@
import { useVirtualizer } from "@tanstack/react-virtual";
import { type Active, useDndContext } from "@dnd-kit/core";
import {
type Range,
defaultRangeExtractor,
useVirtualizer,
} from "@tanstack/react-virtual";
import type { ItemDefinition, RowDefinition } from "dnd-timeline";
import { groupItemsToSubrows, useTimelineContext } from "dnd-timeline";
import { useMemo } from "react";
import { useCallback, useMemo } from "react";
import Item from "./Item";
import Row from "./Row";
import Sidebar from "./Sidebar";
import Subrow from "./Subrow";
import { useAutoscroll } from "./useAutoscroll";

interface TimelineProps {
activeItem: Active | null;
rows: RowDefinition[];
items: ItemDefinition[];
}

function Timeline(props: TimelineProps) {
const { dragOverlay } = useDndContext();
const { setTimelineRef, timelineRef, style, range } = useTimelineContext();

useAutoscroll({
containerRef: timelineRef,
dragOverlay: dragOverlay,
});

const groupedSubrows = useMemo(
() => groupItemsToSubrows(props.items, range),
[props.items, range],
);

const activeItemIndex = useMemo(
() =>
props.rows.findIndex(
(row) => row.id === props.activeItem?.data.current?.rowId,
),
[props.rows, props.activeItem],
);

const rowVirtualizer = useVirtualizer({
count: props.rows.length,
getItemKey: (index) => props.rows[index].id,
getScrollElement: () => timelineRef.current,
estimateSize: (index) =>
(groupedSubrows[props.rows[index].id]?.length || 1) * 50,
rangeExtractor: useCallback(
(range: Range) => {
const next = new Set([
...(activeItemIndex === -1 ? [] : [activeItemIndex]),
...defaultRangeExtractor(range),
]);

return [...next].sort((a, b) => a - b);
},
[activeItemIndex],
),
});

return (
Expand Down Expand Up @@ -66,7 +98,12 @@ function Timeline(props: TimelineProps) {
{groupedSubrows[virtualRow.key]?.map((subrow, index) => (
<Subrow key={`${virtualRow.key}-${index}`}>
{subrow.map((item) => (
<Item id={item.id} key={item.id} span={item.span}>
<Item
id={item.id}
rowId={item.rowId}
key={item.id}
span={item.span}
>
{`Item ${item.id}`}
</Item>
))}
Expand Down
86 changes: 86 additions & 0 deletions examples/virtual/src/useAutoscroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useEffect, useState } from "react";

interface UseAutoScrollOptions {
acceleration: number;
threshold: number;
}

interface DragOverlay {
nodeRef: React.MutableRefObject<HTMLElement | null>;
}

interface UseAutoScrollProps {
containerRef: React.MutableRefObject<HTMLElement | null>;
dragOverlay: DragOverlay;
options?: Partial<UseAutoScrollOptions>;
}

const DEFAULT_OPTIONS: UseAutoScrollOptions = {
acceleration: 10,
threshold: 100,
};

export const useAutoscroll = (props: UseAutoScrollProps) => {
const [scroll, setScroll] = useState<number>(0);

const options = {
...DEFAULT_OPTIONS,
...props.options,
};

useEffect(() => {
const node = props.dragOverlay.nodeRef.current;
const container = props.containerRef.current;

if (!node || !container) return;

const handleMouseMove = (event: MouseEvent) => {
const { clientY } = event;

const { top, bottom } = container.getBoundingClientRect();

const distanceFromTopThreshold = top + options.threshold - clientY;
const distanceFromBottomThreshold =
clientY - (bottom - options.threshold);

if (distanceFromTopThreshold > 0) {
setScroll(
-(distanceFromTopThreshold / options.threshold) *
options.acceleration,
);
} else if (distanceFromBottomThreshold > 0) {
setScroll(
(distanceFromBottomThreshold / options.threshold) *
options.acceleration,
);
} else {
setScroll(0);
}
};

node.addEventListener("mousemove", handleMouseMove);

return () => {
setScroll(0);
node.removeEventListener("mousemove", handleMouseMove);
};
}, [
props.dragOverlay,
props.containerRef.current,
options.acceleration,
options.threshold,
]);

useEffect(() => {
const container = props.containerRef.current;
if (!container) return;

const scrollContainer = () => {
container.scrollTop += scroll;
};

const scrollInterval = setInterval(scrollContainer, 10);

return () => clearInterval(scrollInterval);
}, [props.containerRef, scroll]);
};

0 comments on commit e0e747b

Please sign in to comment.