From 14cf1088dc7e578e03537db27a1af975fe18d369 Mon Sep 17 00:00:00 2001 From: Clauderic Demers Date: Sat, 1 Jun 2024 22:25:54 -0400 Subject: [PATCH] Bug fixes --- .../Sortable/MultipleLists/MultipleLists.tsx | 113 ++++++++++-------- .../react/Sortable/SortableExample.tsx | 63 +++++----- .../Sortable/Vertical/Vertical.stories.tsx | 14 +++ .../abstract/src/core/collision/observer.ts | 7 +- packages/abstract/src/core/collision/types.ts | 2 +- .../src/core/entities/draggable/draggable.ts | 4 +- .../src/core/entities/droppable/droppable.ts | 38 +++--- .../src/core/entities/entity/entity.ts | 28 ++--- .../src/core/manager/dragOperation.ts | 20 ++-- .../src/algorithms/pointerIntersection.ts | 7 +- .../src/core/entities/droppable/droppable.ts | 2 +- .../src/sortable/SortableKeyboardPlugin.ts | 4 +- packages/dom/src/sortable/sortable.ts | 11 +- packages/react/src/core/context/hook.ts | 7 -- packages/react/src/core/context/hooks.ts | 25 ++++ packages/react/src/core/context/index.ts | 2 +- packages/react/src/core/index.ts | 6 +- packages/react/src/sortable/useSortable.ts | 5 +- 18 files changed, 212 insertions(+), 146 deletions(-) delete mode 100644 packages/react/src/core/context/hook.ts create mode 100644 packages/react/src/core/context/hooks.ts diff --git a/apps/docs/stories/react/Sortable/MultipleLists/MultipleLists.tsx b/apps/docs/stories/react/Sortable/MultipleLists/MultipleLists.tsx index e39db519..ec4810be 100644 --- a/apps/docs/stories/react/Sortable/MultipleLists/MultipleLists.tsx +++ b/apps/docs/stories/react/Sortable/MultipleLists/MultipleLists.tsx @@ -1,15 +1,22 @@ import React, {useRef, useState} from 'react'; import type {PropsWithChildren} from 'react'; import {CollisionPriority} from '@dnd-kit/abstract'; -import {DragDropProvider} from '@dnd-kit/react'; +import {DragDropProvider, useDragOperation} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {move} from '@dnd-kit/helpers'; import {DragDropManager, defaultPreset} from '@dnd-kit/dom'; import {Debug} from '@dnd-kit/dom/plugins/debug'; import {supportsViewTransition} from '@dnd-kit/dom/utilities'; -import {Actions, Container, Item, Handle, Remove} from '../../components'; -import {createRange, cloneDeep} from '../../../utilities'; +import { + Actions, + Container, + Item, + Handle, + Remove, +} from '../../components/index.js'; +import {createRange} from '../../../utilities/createRange.js'; +import {cloneDeep} from '../../../utilities/cloneDeep.js'; import {flushSync} from 'react-dom'; interface Props { @@ -21,14 +28,9 @@ interface Props { vertical?: boolean; } -export function MultipleLists({ - debug, - defaultItems, - grid, - itemCount, - scrollable, - vertical, -}: Props) { +export function MultipleLists( + {debug, defaultItems, grid, itemCount, scrollable, vertical}: Props +) { const [items, setItems] = useState( defaultItems ?? { A: createRange(itemCount).map((id) => `A${id}`), @@ -79,26 +81,34 @@ export function MultipleLists({ gap: 20, }} > - {columns.map((column, columnIndex) => ( - - {items[column].map((id, index) => ( - - ))} - - ))} + {columns.map((column, columnIndex) => { + const rows = items[column]; + const children = + rows.length > 0 + ? rows.map((id, index) => ( + + )) + : null; + + return ( + + {children} + + ); + })} ); @@ -111,9 +121,7 @@ export function MultipleLists({ })); if (supportsViewTransition(document)) { - document.startViewTransition(() => { - flushSync(remove); - }); + document.startViewTransition(() => flushSync(remove)); } else { remove(); } @@ -128,20 +136,16 @@ interface SortableItemProps { onRemove?: (id: string, column: string) => void; } -const COLORS = { +const COLORS: Record = { A: '#7193f1', B: '#FF851B', C: '#2ECC40', D: '#ff3680', }; -function SortableItem({ - id, - column, - index, - style, - onRemove, -}: PropsWithChildren) { +function SortableItem( + {id, column, index, style, onRemove}: PropsWithChildren +) { const {handleRef, ref, isDragSource} = useSortable({ id, accept: 'item', @@ -178,18 +182,25 @@ interface SortableColumnProps { scrollable?: boolean; } -function SortableColumn({ - children, - columns, - id, - index, - scrollable, -}: PropsWithChildren) { +function SortableColumn( + { + children, + columns, + id, + index, + scrollable, + }: PropsWithChildren +) { + const empty = !children; + const {source} = useDragOperation(); const {handleRef, isDragSource, ref} = useSortable({ id, accept: ['column', 'item'], - /* Prioritize item collisions over column collisions. */ - collisionPriority: CollisionPriority.Lowest, + collisionPriority: + empty || source?.type === 'column' + ? CollisionPriority.Normal + : /* Prioritize item collisions over column collisions when the column has children. */ + CollisionPriority.Lowest, type: 'column', index, }); diff --git a/apps/docs/stories/react/Sortable/SortableExample.tsx b/apps/docs/stories/react/Sortable/SortableExample.tsx index db34a40e..fcb71f3a 100644 --- a/apps/docs/stories/react/Sortable/SortableExample.tsx +++ b/apps/docs/stories/react/Sortable/SortableExample.tsx @@ -13,8 +13,9 @@ import {directionBiased} from '@dnd-kit/collision'; import {move} from '@dnd-kit/helpers'; import {Debug} from '@dnd-kit/dom/plugins/debug'; -import {Item, Handle} from '../components'; -import {createRange, cloneDeep} from '../../utilities'; +import {Item, Handle} from '../components/index.js'; +import {createRange} from '../../utilities/createRange.js'; +import {cloneDeep} from '../../utilities/cloneDeep.js'; interface Props { debug?: boolean; @@ -29,18 +30,20 @@ interface Props { getItemStyle?(id: UniqueIdentifier, index: number): CSSProperties; } -export function SortableExample({ - debug, - itemCount = 15, - collisionDetector, - disabled, - dragHandle, - feedback, - layout = 'vertical', - modifiers, - transition, - getItemStyle, -}: Props) { +export function SortableExample( + { + debug, + itemCount = 15, + collisionDetector, + disabled, + dragHandle, + feedback, + layout = 'vertical', + modifiers, + transition, + getItemStyle, + }: Props +) { const [items, setItems] = useState(createRange(itemCount)); const snapshot = useRef(cloneDeep(items)); @@ -96,16 +99,18 @@ interface SortableProps { style?: React.CSSProperties; } -function SortableItem({ - id, - index, - collisionDetector = directionBiased, - disabled, - dragHandle, - feedback, - transition, - style, -}: PropsWithChildren) { +function SortableItem( + { + id, + index, + collisionDetector = directionBiased, + disabled, + dragHandle, + feedback, + transition, + style, + }: PropsWithChildren +) { const [element, setElement] = useState(null); const handleRef = useRef(null); @@ -132,10 +137,12 @@ function SortableItem({ ); } -function Wrapper({ - layout, - children, -}: PropsWithChildren<{layout: 'vertical' | 'horizontal' | 'grid'}>) { +function Wrapper( + { + layout, + children, + }: PropsWithChildren<{layout: 'vertical' | 'horizontal' | 'grid'}> +) { return
{children}
; } diff --git a/apps/docs/stories/react/Sortable/Vertical/Vertical.stories.tsx b/apps/docs/stories/react/Sortable/Vertical/Vertical.stories.tsx index 05eb5023..5da9e1b9 100644 --- a/apps/docs/stories/react/Sortable/Vertical/Vertical.stories.tsx +++ b/apps/docs/stories/react/Sortable/Vertical/Vertical.stories.tsx @@ -42,6 +42,20 @@ export const VariableHeights: Story = { }, }; +export const DynamicHeights: Story = { + name: 'Dynamic heights', + args: { + debug: false, + getItemStyle(_, index) { + const heights = {1: 100, 3: 150, 5: 200, 8: 100, 12: 150}; + + return { + height: heights[index], + }; + }, + }, +}; + export const Clone: Story = { name: 'Clone feedback', args: { diff --git a/packages/abstract/src/core/collision/observer.ts b/packages/abstract/src/core/collision/observer.ts index f6308327..2270d2a4 100644 --- a/packages/abstract/src/core/collision/observer.ts +++ b/packages/abstract/src/core/collision/observer.ts @@ -40,12 +40,12 @@ export class CollisionObserver< public forceUpdate(refresh = true) { untracked(() => { - const type = this.manager.dragOperation.source?.type; + const {source} = this.manager.dragOperation; batch(() => { if (refresh) { for (const droppable of this.manager.registry.droppables) { - if (type != null && !droppable.accepts(type)) { + if (source && !droppable.accepts(source)) { continue; } @@ -69,7 +69,6 @@ export class CollisionObserver< return DEFAULT_VALUE; } - const type = source?.type; const collisions: Collision[] = []; this.forceUpdateCount.value; @@ -79,7 +78,7 @@ export class CollisionObserver< continue; } - if (type != null && !entry.accepts(type)) { + if (source && !entry.accepts(source)) { continue; } diff --git a/packages/abstract/src/core/collision/types.ts b/packages/abstract/src/core/collision/types.ts index 5337ef0f..e3a78134 100644 --- a/packages/abstract/src/core/collision/types.ts +++ b/packages/abstract/src/core/collision/types.ts @@ -8,7 +8,7 @@ import type { export enum CollisionPriority { Lowest, Low, - Medium, + Normal, High, Highest, } diff --git a/packages/abstract/src/core/entities/draggable/draggable.ts b/packages/abstract/src/core/entities/draggable/draggable.ts index 6a4b9516..da26fcbd 100644 --- a/packages/abstract/src/core/entities/draggable/draggable.ts +++ b/packages/abstract/src/core/entities/draggable/draggable.ts @@ -17,11 +17,13 @@ export interface Input< export class Draggable extends Entity { constructor( - {modifiers, ...input}: Input, + {modifiers, type, ...input}: Input, public manager: DragDropManager ) { super(input, manager); + this.type = type; + if (modifiers?.length) { this.modifiers = modifiers.map((modifier) => { const {plugin, options} = descriptor(modifier); diff --git a/packages/abstract/src/core/entities/droppable/droppable.ts b/packages/abstract/src/core/entities/droppable/droppable.ts index 7f41a007..6be17f46 100644 --- a/packages/abstract/src/core/entities/droppable/droppable.ts +++ b/packages/abstract/src/core/entities/droppable/droppable.ts @@ -8,12 +8,13 @@ import { type CollisionDetector, } from '../../collision/index.js'; import type {DragDropManager} from '../../manager/index.js'; +import {Draggable} from '../draggable/draggable.js'; export interface Input< T extends Data = Data, U extends Droppable = Droppable, > extends EntityInput { - accept?: Type | Type[]; + accept?: Type | Type[] | ((source: Draggable) => boolean); collisionPriority?: CollisionPriority | number; collisionDetector: CollisionDetector; type?: Type; @@ -22,28 +23,31 @@ export interface Input< export class Droppable extends Entity { constructor( { + accept, collisionDetector, collisionPriority = CollisionPriority.Normal, + type, ...input }: Input, public manager: DragDropManager ) { super(input, manager); - const {destroy} = this; + this.accept = accept; this.collisionDetector = collisionDetector; this.collisionPriority = collisionPriority; - - this.destroy = () => { - destroy(); - }; + this.type = type; } /** * An array of types that are compatible with the droppable. */ @reactive - public accept: Type | Type[] | undefined; + public accept: + | Type + | Type[] + | ((draggable: Draggable) => boolean) + | undefined; /** * The type of the droppable. @@ -52,25 +56,31 @@ export class Droppable extends Entity { public type: Type | undefined; /** - * Checks whether or not the droppable accepts a given type. + * Checks whether or not the droppable accepts a given draggable. * - * @param {Type|Type[]} types + * @param {Draggable} draggable * @returns {boolean} */ - public accepts(types: Type | Type[]): boolean { + public accepts(draggable: Draggable): boolean { const {accept} = this; if (!accept) { return true; } - const acceptedTypes = Array.isArray(accept) ? accept : [accept]; + if (!draggable.type) { + return false; + } + + if (Array.isArray(accept)) { + return accept.includes(draggable.type); + } - if (Array.isArray(types)) { - return types.some((type) => acceptedTypes.includes(type)); + if (typeof accept === 'function') { + return accept(draggable); } - return acceptedTypes.includes(types); + return draggable.type === accept; } @reactive diff --git a/packages/abstract/src/core/entities/entity/entity.ts b/packages/abstract/src/core/entities/entity/entity.ts index 7715e632..a52efd9c 100644 --- a/packages/abstract/src/core/entities/entity/entity.ts +++ b/packages/abstract/src/core/entities/entity/entity.ts @@ -33,24 +33,20 @@ export class Entity { this.data = data; this.disabled = disabled; - // Make sure all input properties are set on the instance before registering it - for (const [key, value] of Object.entries(input)) { - if (value === undefined) continue; - this[key as keyof this] = value; - } + queueMicrotask(() => { + const inputEffects = getInputEffects?.(this) ?? []; - const inputEffects = getInputEffects?.(this) ?? []; + this.destroy = effects( + () => { + // Re-run this effect whenever the `id` changes + const {id: _} = this; + manager.registry.register(this); - this.destroy = effects( - () => { - // Re-run this effect whenever the `id` changes - const {id: _} = this; - manager.registry.register(this); - - return () => manager.registry.unregister(this); - }, - ...inputEffects - ); + return () => manager.registry.unregister(this); + }, + ...inputEffects + ); + }); } /** diff --git a/packages/abstract/src/core/manager/dragOperation.ts b/packages/abstract/src/core/manager/dragOperation.ts index d0a2296a..d10eba8c 100644 --- a/packages/abstract/src/core/manager/dragOperation.ts +++ b/packages/abstract/src/core/manager/dragOperation.ts @@ -267,16 +267,18 @@ export function DragOperationManager< }, }); - if (defaultPrevented) { - return; - } - - const coordinates = to ?? { - x: position.current.x + by.x, - y: position.current.y + by.y, - }; + queueMicrotask(() => { + if (defaultPrevented) { + return; + } + + const coordinates = to ?? { + x: position.current.x + by.x, + y: position.current.y + by.y, + }; - position.update(coordinates); + position.update(coordinates); + }); }, stop({canceled = false}: {canceled?: boolean} = {}) { let promise: Promise | undefined; diff --git a/packages/collision/src/algorithms/pointerIntersection.ts b/packages/collision/src/algorithms/pointerIntersection.ts index 2d0961f3..d72a35a1 100644 --- a/packages/collision/src/algorithms/pointerIntersection.ts +++ b/packages/collision/src/algorithms/pointerIntersection.ts @@ -11,10 +11,9 @@ import {Point, Rectangle} from '@dnd-kit/geometry'; * * Returns null if the pointer is outside of the droppable element. */ -export const pointerIntersection: CollisionDetector = ({ - dragOperation, - droppable, -}) => { +export const pointerIntersection: CollisionDetector = ( + {dragOperation, droppable} +) => { const pointerCoordinates = dragOperation.position.current; if (!pointerCoordinates) { diff --git a/packages/dom/src/core/entities/droppable/droppable.ts b/packages/dom/src/core/entities/droppable/droppable.ts index 68313088..93e70fb4 100644 --- a/packages/dom/src/core/entities/droppable/droppable.ts +++ b/packages/dom/src/core/entities/droppable/droppable.ts @@ -116,7 +116,7 @@ export class Droppable extends AbstractDroppable { const source = untracked(() => dragOperation.source); if (status.initialized) { - if (source?.type != null && !this.accepts(source.type)) { + if (source?.type != null && !this.accepts(source)) { return; } diff --git a/packages/dom/src/sortable/SortableKeyboardPlugin.ts b/packages/dom/src/sortable/SortableKeyboardPlugin.ts index ced92bae..73882845 100644 --- a/packages/dom/src/sortable/SortableKeyboardPlugin.ts +++ b/packages/dom/src/sortable/SortableKeyboardPlugin.ts @@ -81,7 +81,7 @@ export class SortableKeyboardPlugin extends Plugin { if ( !shape || (id === source?.id && isSortable(droppable)) || - (source?.type != null && !droppable.accepts(source.type)) + (source?.type != null && !droppable.accepts(source)) ) { continue; } @@ -154,7 +154,9 @@ export class SortableKeyboardPlugin extends Plugin { y: shape.center.y, }, }); + actions.setDropTarget(source.id).then(() => { + dragOperation.shape = shape; collisionObserver.enable(); }); }); diff --git a/packages/dom/src/sortable/sortable.ts b/packages/dom/src/sortable/sortable.ts index 3ecde116..93f6ea95 100644 --- a/packages/dom/src/sortable/sortable.ts +++ b/packages/dom/src/sortable/sortable.ts @@ -1,4 +1,5 @@ import {batch, effects, reactive, untracked, type Effect} from '@dnd-kit/state'; +import {CollisionPriority} from '@dnd-kit/abstract'; import type { Data, DragDropManager as AbstractDragDropManager, @@ -228,8 +229,8 @@ export class Sortable { this.draggable.sensors = value; } - public set collisionPriority(value: number | undefined) { - this.droppable.collisionPriority = value; + public set collisionPriority(value: CollisionPriority | number | undefined) { + this.droppable.collisionPriority = value ?? CollisionPriority.Normal; } public set collisionDetector(value: CollisionDetector | undefined) { @@ -241,7 +242,7 @@ export class Sortable { this.droppable.type = type; } - public set accept(value: Type | Type[] | undefined) { + public set accept(value: Droppable['accept']) { this.droppable.accept = value; } @@ -260,8 +261,8 @@ export class Sortable { this.droppable.refreshShape(); } - public accepts(types: Type | Type[]): boolean { - return this.droppable.accepts(types); + public accepts(draggable: Draggable): boolean { + return this.droppable.accepts(draggable); } public destroy() { diff --git a/packages/react/src/core/context/hook.ts b/packages/react/src/core/context/hook.ts deleted file mode 100644 index 5c4d8844..00000000 --- a/packages/react/src/core/context/hook.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {useContext} from 'react'; - -import {DragDropContext} from './context.js'; - -export function useDragDropManager() { - return useContext(DragDropContext); -} diff --git a/packages/react/src/core/context/hooks.ts b/packages/react/src/core/context/hooks.ts new file mode 100644 index 00000000..e6398b1a --- /dev/null +++ b/packages/react/src/core/context/hooks.ts @@ -0,0 +1,25 @@ +import {useContext} from 'react'; + +import {DragDropContext} from './context.js'; +import {useComputed} from '../../hooks/useComputed.js'; + +export function useDragDropManager() { + return useContext(DragDropContext); +} + +export function useDragOperation() { + const manager = useDragDropManager(); + const {dragOperation} = manager; + + const source = useComputed(() => dragOperation.source); + const target = useComputed(() => dragOperation.target); + + return { + get source() { + return source.value; + }, + get target() { + return target.value; + }, + }; +} diff --git a/packages/react/src/core/context/index.ts b/packages/react/src/core/context/index.ts index 2c9b8a79..cdab174e 100644 --- a/packages/react/src/core/context/index.ts +++ b/packages/react/src/core/context/index.ts @@ -1,3 +1,3 @@ -export {useDragDropManager} from './hook.js'; +export {useDragDropManager, useDragOperation} from './hooks.js'; export {DragDropProvider} from './DragDropProvider.js'; diff --git a/packages/react/src/core/index.ts b/packages/react/src/core/index.ts index ac51c732..3c54c784 100644 --- a/packages/react/src/core/index.ts +++ b/packages/react/src/core/index.ts @@ -1,4 +1,8 @@ -export {DragDropProvider, useDragDropManager} from './context/index.js'; +export { + DragDropProvider, + useDragDropManager, + useDragOperation, +} from './context/index.js'; export {useDraggable} from './draggable/index.js'; diff --git a/packages/react/src/sortable/useSortable.ts b/packages/react/src/sortable/useSortable.ts index ca4b54b7..a6db5dad 100644 --- a/packages/react/src/sortable/useSortable.ts +++ b/packages/react/src/sortable/useSortable.ts @@ -1,6 +1,6 @@ import {useCallback, useEffect} from 'react'; import {deepEqual} from '@dnd-kit/state'; -import type {Data} from '@dnd-kit/abstract'; +import {type Data} from '@dnd-kit/abstract'; import {Sortable, defaultSortableTransition} from '@dnd-kit/dom/sortable'; import type {SortableInput} from '@dnd-kit/dom/sortable'; import {useDragDropManager} from '@dnd-kit/react'; @@ -12,12 +12,13 @@ import { useIsomorphicLayoutEffect as layoutEffect, } from '@dnd-kit/react/hooks'; import {getCurrentValue, type RefOrValue} from '@dnd-kit/react/utilities'; +import {FeedbackType} from '@dnd-kit/dom'; export interface UseSortableInput extends Omit, 'handle' | 'element' | 'feedback'> { handle?: RefOrValue; element?: RefOrValue; - feedback?: 'move' | 'clone' | 'none' | (() => React.ReactNode); + feedback?: FeedbackType | (() => React.ReactNode); } export function useSortable(input: UseSortableInput) {