diff --git a/apps/stories/stories/react/Sortable/Virtualized/ReactVirtualFixedGridExample.tsx b/apps/stories/stories/react/Sortable/Virtualized/ReactVirtualFixedGridExample.tsx new file mode 100644 index 00000000..1da1fc63 --- /dev/null +++ b/apps/stories/stories/react/Sortable/Virtualized/ReactVirtualFixedGridExample.tsx @@ -0,0 +1,230 @@ +import React, { useRef, useEffect, useState, useMemo, forwardRef, PropsWithChildren } from 'react'; +import type { UniqueIdentifier } from '@dnd-kit/abstract'; +import { DragDropProvider } from '@dnd-kit/react'; +import { useSortable } from '@dnd-kit/react/sortable'; +import { defaultPreset } from '@dnd-kit/dom'; +import { Debug } from '@dnd-kit/dom/plugins/debug'; +import { move } from '@dnd-kit/helpers'; +import { useVirtualizer } from '@tanstack/react-virtual'; + +import { Item, Handle } from '../../components'; +import { createRange, cloneDeep } from '../../../utilities'; + +interface Props { + debug?: boolean; +} + + +const gapCol = 20; +const gapRow = 20; +const itemWidth = 300; +const itemHeight = 62; +const paddingHorizontal = 48; +const paddingVertical = 23; + + +export function ReactVirtualFixedGridExample({ debug }: Props) { + const [items, setItems] = useState(createRange(1000).map(x => ({ id: 'n' + x }))); + const snapshot = useRef(cloneDeep(items)); + + const draggingIndex = useRef(null) + + const scrollRef = useRef(null); + const [columnCount, setColumnCount] = useState(2); + + const rowCount = useMemo( + () => Math.ceil(items.length / columnCount), + [items.length, columnCount] + ); + + const updateColumnCount = () => { + const padHorizontal = paddingHorizontal * 2; + const availableWidth = window.innerWidth - padHorizontal; + const newColumnCount = Math.floor( + availableWidth / (itemWidth + gapCol) + ); + + setColumnCount(newColumnCount); + }; + + useEffect(() => { + updateColumnCount(); + + window.addEventListener('resize', updateColumnCount) + + return () => window.removeEventListener('resize', updateColumnCount) + }, []); + + + const rowVirtualizer = useVirtualizer({ + count: rowCount, + getItemKey: (index) => { + return items[index].id + }, + getScrollElement: () => scrollRef.current, + estimateSize: () => itemHeight, + overscan: 1, + gap: gapRow, + paddingStart: paddingVertical, + paddingEnd: paddingVertical, + rangeExtractor(range) { + + const start = Math.max(range.startIndex - range.overscan, 0) + const end = Math.min(range.endIndex + range.overscan, range.count - 1) + + const arr = [] + + for (let i = start; i <= end; i++) { + arr.push(i) + } + + if (draggingIndex.current !== null) { + const includes = arr.includes(draggingIndex.current) + if (range.endIndex > draggingIndex.current) + if (!includes) arr.push(draggingIndex.current) + } + + return arr; + }, + }); + + const columnVirtualizer = useVirtualizer({ + horizontal: true, + count: columnCount, + getItemKey: (index) => { + return items[index].id + }, + getScrollElement: () => scrollRef.current, + estimateSize: () => itemWidth, + overscan: 1, + gap: gapCol, + paddingStart: paddingHorizontal, + paddingEnd: paddingHorizontal, + rangeExtractor(range) { + + const start = Math.max(range.startIndex - range.overscan, 0) + const end = Math.min(range.endIndex + range.overscan, range.count - 1) + + const arr = [] + + for (let i = start; i <= end; i++) { + arr.push(i) + } + + if (draggingIndex.current !== null) { + const includes = arr.includes(draggingIndex.current) + if (range.endIndex > draggingIndex.current) + if (!includes) arr.push(draggingIndex.current) + } + + return arr + }, + }); + + return ( + + { + + draggingIndex.current = e.operation.source.sortable.index + snapshot.current = cloneDeep(items); + }} + onDragOver={(event) => { + const { source, target } = event.operation; + + if (!source || !target) { + return; + } + + setItems((items) => move(items, source, target)); + }} + onDragEnd={(event) => { + if (event.canceled) { + setItems(snapshot.current); + } + draggingIndex.current = null + }} + > +
+ +
+ {rowVirtualizer.getVirtualItems() + .map((virtualRow) => ( + + {columnVirtualizer + .getVirtualItems() + .map((virtualColumn) => { + const index = + virtualRow.index * columnCount + + virtualColumn.index; + + return ( + + ); + })} + + ))} +
+ +
+
+ ); +} + +interface SortableProps { + id: UniqueIdentifier; + index: number; +} + +const Sortable: React.FC = ({ id, index, }) => { + const [element, setElement] = useState(null); + const handleRef = useRef(null); + + const { isDragSource } = useSortable({ + id, + index, + element, + feedback: 'clone', + handle: handleRef, + }); + + return ( + { + setElement(el); + }} + actions={} + data-index={index} + shadow={isDragSource} + > + {id} + + ); +} + diff --git a/apps/stories/stories/react/Sortable/Virtualized/Virtualized.stories.tsx b/apps/stories/stories/react/Sortable/Virtualized/Virtualized.stories.tsx index a9df9454..02f12c49 100644 --- a/apps/stories/stories/react/Sortable/Virtualized/Virtualized.stories.tsx +++ b/apps/stories/stories/react/Sortable/Virtualized/Virtualized.stories.tsx @@ -1,8 +1,9 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react'; -import {ReactWindowExample} from './ReactWindowExample'; -import {ReactVirtualExample} from './ReactVirtualExample'; -import {ReactTinyVirtualListExample} from './ReactTinyVirtualListExample'; +import { ReactWindowExample } from './ReactWindowExample'; +import { ReactVirtualExample } from './ReactVirtualExample'; +import { ReactTinyVirtualListExample } from './ReactTinyVirtualListExample'; +import { ReactVirtualFixedGridExample } from './ReactVirtualFixedGridExample'; const meta: Meta = { title: 'React/Sortable/Virtualized', @@ -25,3 +26,8 @@ export const ReactVirtual: Story = { name: 'react-virtual', render: ReactVirtualExample, }; + +export const ReactVirtualGrid: Story = { + name: 'react-virtual-fixed-grid', + render: ReactVirtualFixedGridExample, +}; \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index d938347f..0aeb9c4b 100755 Binary files a/bun.lockb and b/bun.lockb differ