From 2c3ad5eab3aabfd0aaa5a3a299dae1e307e8edaf Mon Sep 17 00:00:00 2001 From: Clauderic Demers Date: Mon, 3 Jun 2024 22:50:04 -0400 Subject: [PATCH] Various bug fixes --- .changeset/bug-fixes.md | 1 + .../{fix-strict-modemd => fix-strict-mode.md} | 0 .changeset/restore-focus-keyboard.md | 5 + .../react/Sortable/Grid/Grid.stories.tsx | 2 +- .../Sortable/MultipleLists/MultipleLists.tsx | 2 +- .../Droppable/VanillaDroppableExample.ts | 55 +++--- .../src/core/entities/entity/entity.ts | 31 +++- .../src/core/manager/dragOperation.ts | 42 +++-- .../src/core/entities/draggable/draggable.ts | 64 ++++--- .../src/core/entities/droppable/droppable.ts | 171 +++++++++--------- .../dom/src/core/plugins/feedback/Feedback.ts | 18 +- .../src/core/sensors/pointer/PointerSensor.ts | 34 ++-- packages/dom/src/sortable/sortable.ts | 92 +++++----- .../utilities/element/createPlaceholder.ts | 12 +- packages/react/src/core/context/index.ts | 2 - .../react/src/core/draggable/useDraggable.ts | 21 +-- .../react/src/core/droppable/useDroppable.ts | 21 +-- .../src/core/hooks/useDragDropManager.ts | 7 + .../react/src/core/hooks/useDragOperation.ts | 20 ++ packages/react/src/core/hooks/useInstance.ts | 22 +++ packages/react/src/core/index.ts | 10 +- packages/react/src/sortable/useSortable.ts | 11 +- 22 files changed, 360 insertions(+), 283 deletions(-) rename .changeset/{fix-strict-modemd => fix-strict-mode.md} (100%) create mode 100644 .changeset/restore-focus-keyboard.md create mode 100644 packages/react/src/core/hooks/useDragDropManager.ts create mode 100644 packages/react/src/core/hooks/useDragOperation.ts create mode 100644 packages/react/src/core/hooks/useInstance.ts diff --git a/.changeset/bug-fixes.md b/.changeset/bug-fixes.md index 4a49d0b5..2bc5d162 100644 --- a/.changeset/bug-fixes.md +++ b/.changeset/bug-fixes.md @@ -1,4 +1,5 @@ --- +'@dnd-kit/abstract': patch '@dnd-kit/react': patch '@dnd-kit/dom': patch --- diff --git a/.changeset/fix-strict-modemd b/.changeset/fix-strict-mode.md similarity index 100% rename from .changeset/fix-strict-modemd rename to .changeset/fix-strict-mode.md diff --git a/.changeset/restore-focus-keyboard.md b/.changeset/restore-focus-keyboard.md new file mode 100644 index 00000000..7d391062 --- /dev/null +++ b/.changeset/restore-focus-keyboard.md @@ -0,0 +1,5 @@ +--- +'@dnd-kit/dom': patch +--- + +- Only restore focus after drop if the `activatorEvent` is a keyboard event. diff --git a/apps/stories/stories/react/Sortable/Grid/Grid.stories.tsx b/apps/stories/stories/react/Sortable/Grid/Grid.stories.tsx index bcb27c43..95b43907 100644 --- a/apps/stories/stories/react/Sortable/Grid/Grid.stories.tsx +++ b/apps/stories/stories/react/Sortable/Grid/Grid.stories.tsx @@ -41,7 +41,7 @@ export const VariableSizes: Story = { ...defaultArgs, itemCount: 14, collisionDetector: pointerIntersection, - getItemStyle(_, index) { + getItemStyle(_: number, index: number) { if (index === 0 || index === 10) { return { width: 320, diff --git a/apps/stories/stories/react/Sortable/MultipleLists/MultipleLists.tsx b/apps/stories/stories/react/Sortable/MultipleLists/MultipleLists.tsx index 1bf66853..32242096 100644 --- a/apps/stories/stories/react/Sortable/MultipleLists/MultipleLists.tsx +++ b/apps/stories/stories/react/Sortable/MultipleLists/MultipleLists.tsx @@ -1,4 +1,4 @@ -import React, {useRef, useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import type {PropsWithChildren} from 'react'; import {flushSync} from 'react-dom'; import {CollisionPriority} from '@dnd-kit/abstract'; diff --git a/apps/stories/stories/vanilla/Droppable/VanillaDroppableExample.ts b/apps/stories/stories/vanilla/Droppable/VanillaDroppableExample.ts index 0d8f21b8..3b01ad6c 100644 --- a/apps/stories/stories/vanilla/Droppable/VanillaDroppableExample.ts +++ b/apps/stories/stories/vanilla/Droppable/VanillaDroppableExample.ts @@ -16,24 +16,19 @@ export const DroppableExample = createVanillaStory(() => { { id: 'draggable', element: draggableElement, - effects(draggable) { - return [ - () => { - const {status} = manager.dragOperation; - - if ( - draggable.isDragSource && - (status.dragging || status.dropping) - ) { - draggableElement.setAttribute('data-shadow', 'true'); - - return () => { - draggableElement.removeAttribute('data-shadow'); - }; - } - }, - ]; - }, + effects: () => [ + () => { + const {status} = manager.dragOperation; + + if (draggable.isDragSource && (status.dragging || status.dropped)) { + draggableElement.setAttribute('data-shadow', 'true'); + + return () => { + draggableElement.removeAttribute('data-shadow'); + }; + } + }, + ], }, manager ); @@ -41,19 +36,17 @@ export const DroppableExample = createVanillaStory(() => { { id: 'droppable', element: droppableElement, - effects(droppable) { - return [ - () => { - if (droppable.isDropTarget) { - droppableElement.setAttribute('data-highlight', 'true'); - - return () => { - droppableElement.removeAttribute('data-highlight'); - }; - } - }, - ]; - }, + effects: () => [ + () => { + if (droppable.isDropTarget) { + droppableElement.setAttribute('data-highlight', 'true'); + + return () => { + droppableElement.removeAttribute('data-highlight'); + }; + } + }, + ], }, manager ); diff --git a/packages/abstract/src/core/entities/entity/entity.ts b/packages/abstract/src/core/entities/entity/entity.ts index a52efd9c..9a980f51 100644 --- a/packages/abstract/src/core/entities/entity/entity.ts +++ b/packages/abstract/src/core/entities/entity/entity.ts @@ -7,7 +7,11 @@ export interface Input = Entity> { id: UniqueIdentifier; data?: T | null; disabled?: boolean; - effects?: (instance: Instance) => Effect[]; + effects?(): Effect[]; +} + +function getDefaultEffects(): Effect[] { + return []; } /** @@ -27,25 +31,42 @@ export class Entity { input: Input, public manager: DragDropManager ) { - const {effects: getInputEffects, id, data = null, disabled = false} = input; + const { + effects: getEffects = getDefaultEffects, + id, + data = null, + disabled = false, + } = input; + + let previousId = id; this.id = id; this.data = data; this.disabled = disabled; queueMicrotask(() => { - const inputEffects = getInputEffects?.(this) ?? []; + manager.registry.register(this); - this.destroy = effects( + const cleanupEffects = effects( () => { // Re-run this effect whenever the `id` changes const {id: _} = this; + + if (id === previousId) { + return; + } + manager.registry.register(this); return () => manager.registry.unregister(this); }, - ...inputEffects + ...getEffects() ); + + this.destroy = () => { + manager.registry.unregister(this); + cleanupEffects(); + }; }); } diff --git a/packages/abstract/src/core/manager/dragOperation.ts b/packages/abstract/src/core/manager/dragOperation.ts index 6405e670..531c2df2 100644 --- a/packages/abstract/src/core/manager/dragOperation.ts +++ b/packages/abstract/src/core/manager/dragOperation.ts @@ -15,7 +15,7 @@ export enum Status { Idle = 'idle', Initializing = 'initializing', Dragging = 'dragging', - Dropping = 'dropping', + Dropped = 'dropped', } export type Serializable = { @@ -35,7 +35,8 @@ export interface DragOperation< initialized: boolean; initializing: boolean; dragging: boolean; - dropping: boolean; + dragended: boolean; + dropped: boolean; idle: boolean; }; get shape(): { @@ -77,7 +78,8 @@ export function DragOperationManager< const initialized = computed(() => status.value !== Status.Idle); const initializing = computed(() => status.value === Status.Initializing); const idle = computed(() => status.value === Status.Idle); - const dropping = computed(() => status.value === Status.Dropping); + const dropped = computed(() => status.value === Status.Dropped); + const dragended = signal(true); let previousSource: T | undefined; const source = computed(() => { const identifier = sourceIdentifier.value; @@ -116,7 +118,8 @@ export function DragOperationManager< initializing: initializing.peek(), initialized: initialized.peek(), dragging: dragging.peek(), - dropping: dropping.peek(), + dragended: dragended.peek(), + dropped: dropped.peek(), }, shape: initialShape && currentShape @@ -161,8 +164,11 @@ export function DragOperationManager< get dragging() { return dragging.value; }, - get dropping() { - return dropping.value; + get dragended() { + return dragended.value; + }, + get dropped() { + return dropped.value; }, }, get shape(): DragOperation['shape'] { @@ -218,17 +224,20 @@ export function DragOperationManager< targetIdentifier.value = id; - monitor.dispatch( - 'dragover', - defaultPreventable({ - operation: snapshot(operation), - }) - ); + if (status.peek() === Status.Dragging) { + monitor.dispatch( + 'dragover', + defaultPreventable({ + operation: snapshot(operation), + }) + ); + } return manager.renderer.rendering; }, start({event, coordinates}: {event: Event; coordinates: Coordinates}) { batch(() => { + dragended.value = false; canceled.value = false; activatorEvent.value = event; position.reset(coordinates); @@ -309,14 +318,17 @@ export function DragOperationManager< return output; }; const end = () => { + /* Wait for the renderer to finish rendering before finalizing the drag operation */ manager.renderer.rendering.then(() => { - status.value = Status.Dropping; - + status.value = Status.Dropped; manager.renderer.rendering.then(reset); }); }; - canceled.value = eventCanceled; + batch(() => { + dragended.value = true; + canceled.value = eventCanceled; + }); monitor.dispatch('dragend', { operation: snapshot(operation), diff --git a/packages/dom/src/core/entities/draggable/draggable.ts b/packages/dom/src/core/entities/draggable/draggable.ts index d7e304a8..e614fdc1 100644 --- a/packages/dom/src/core/entities/draggable/draggable.ts +++ b/packages/dom/src/core/entities/draggable/draggable.ts @@ -36,39 +36,49 @@ export class Draggable extends AbstractDraggable { public sensors: Sensors | undefined; constructor( - {element, handle, feedback = 'default', sensors, ...input}: Input, + { + element, + effects = () => [], + handle, + feedback = 'default', + sensors, + ...input + }: Input, public manager: AbstractDragDropManager ) { - super(input, manager); + super( + { + effects: () => [ + ...effects(), + () => { + const sensors = this.sensors?.map(descriptor) ?? [ + ...manager.sensors, + ]; + const unbindFunctions = sensors.map((entry) => { + const sensorInstance = + entry instanceof Sensor + ? entry + : manager.registry.register(entry.plugin); + const options = + entry instanceof Sensor ? undefined : entry.options; + + const unbind = sensorInstance.bind(this, options); + return unbind; + }); + + return function cleanup() { + unbindFunctions.forEach((unbind) => unbind()); + }; + }, + ], + ...input, + }, + manager + ); this.element = element; this.handle = handle; this.feedback = feedback; this.sensors = sensors; - - const cleanupEffect = effect(() => { - const sensors = this.sensors?.map(descriptor) ?? [...manager.sensors]; - const unbindFunctions = sensors.map((entry) => { - const sensorInstance = - entry instanceof Sensor - ? entry - : manager.registry.register(entry.plugin); - const options = entry instanceof Sensor ? undefined : entry.options; - - const unbind = sensorInstance.bind(this, options); - return unbind; - }); - - return function cleanup() { - unbindFunctions.forEach((unbind) => unbind()); - }; - }); - - const {destroy} = this; - - this.destroy = () => { - cleanupEffect(); - destroy(); - }; } } diff --git a/packages/dom/src/core/entities/droppable/droppable.ts b/packages/dom/src/core/entities/droppable/droppable.ts index fb6b44a8..4aebe578 100644 --- a/packages/dom/src/core/entities/droppable/droppable.ts +++ b/packages/dom/src/core/entities/droppable/droppable.ts @@ -6,7 +6,7 @@ import type { } from '@dnd-kit/abstract'; import {defaultCollisionDetection} from '@dnd-kit/collision'; import type {CollisionDetector} from '@dnd-kit/collision'; -import {Signal, effects, reactive, signal, untracked} from '@dnd-kit/state'; +import {Signal, reactive, signal, untracked} from '@dnd-kit/state'; import type {Shape} from '@dnd-kit/geometry'; import { DOMRectangle, @@ -25,14 +25,95 @@ export interface Input export class Droppable extends AbstractDroppable { constructor( - {element, ...input}: Input, + {element, effects = () => [], ...input}: Input, public manager: AbstractDragDropManager ) { const {collisionDetector = defaultCollisionDetection} = input; - super({...input, collisionDetector}, manager); + super( + { + ...input, + collisionDetector, + effects: () => [ + ...effects(), + () => { + const {element} = this; + const {dragOperation} = manager; + + if (element && dragOperation.status.initialized) { + const scrollableAncestor = getFirstScrollableAncestor(element); + const doc = getDocument(element); + const root = + scrollableAncestor === doc.scrollingElement + ? doc + : scrollableAncestor; + const intersectionObserver = new IntersectionObserver( + (entries) => { + const [entry] = entries.slice(-1); + const {width, height} = entry.boundingClientRect; + + if (!width && !height) { + return; + } + + this.visible = entry.isIntersecting; + }, + { + root: root ?? doc, + rootMargin: '40%', + } + ); + + const mutationObserver = new MutationObserver(() => + scheduler.schedule(this.refreshShape) + ); + + const resizeObserver = new ResizeObserver(() => + scheduler.schedule(this.refreshShape) + ); + + if (element.parentElement) { + mutationObserver.observe(element.parentElement, { + childList: true, + }); + } + + resizeObserver.observe(element); + intersectionObserver.observe(element); + + return () => { + this.shape = undefined; + this.visible = undefined; + resizeObserver.disconnect(); + mutationObserver.disconnect(); + intersectionObserver.disconnect(); + }; + } + }, + () => { + const {dragOperation} = manager; + const {status} = dragOperation; + const source = untracked(() => dragOperation.source); + + if (status.initialized) { + if (source?.type != null && !this.accepts(source)) { + return; + } - const {destroy} = this; + scheduler.schedule(this.refreshShape); + } + }, + () => { + if (manager.dragOperation.status.initialized) { + return () => { + this.shape = undefined; + }; + } + }, + ], + }, + manager + ); this.internal = { element: signal(element), @@ -46,88 +127,6 @@ export class Droppable extends AbstractDroppable { if (manager.dragOperation.status.initialized) { this.visible = true; } - - const cleanup = effects( - () => { - const {element} = this; - const {dragOperation} = manager; - - if (element && dragOperation.status.initialized) { - const scrollableAncestor = getFirstScrollableAncestor(element); - const doc = getDocument(element); - const root = - scrollableAncestor === doc.scrollingElement - ? doc - : scrollableAncestor; - const intersectionObserver = new IntersectionObserver( - (entries) => { - const [entry] = entries.slice(-1); - const {width, height} = entry.boundingClientRect; - - if (!width && !height) { - return; - } - - this.visible = entry.isIntersecting; - }, - { - root: root ?? doc, - rootMargin: '40%', - } - ); - - const mutationObserver = new MutationObserver(() => - scheduler.schedule(this.refreshShape) - ); - - const resizeObserver = new ResizeObserver(() => - scheduler.schedule(this.refreshShape) - ); - - if (element.parentElement) { - mutationObserver.observe(element.parentElement, { - childList: true, - }); - } - - resizeObserver.observe(element); - intersectionObserver.observe(element); - - return () => { - this.shape = undefined; - this.visible = undefined; - resizeObserver.disconnect(); - mutationObserver.disconnect(); - intersectionObserver.disconnect(); - }; - } - }, - () => { - const {dragOperation} = manager; - const {status} = dragOperation; - const source = untracked(() => dragOperation.source); - - if (status.initialized) { - if (source?.type != null && !this.accepts(source)) { - return; - } - - scheduler.schedule(this.refreshShape); - } - }, - () => { - if (manager.dragOperation.status.initialized) { - return () => { - this.shape = undefined; - }; - } - } - ); - - this.destroy = () => { - cleanup(); - destroy(); - }; } @reactive diff --git a/packages/dom/src/core/plugins/feedback/Feedback.ts b/packages/dom/src/core/plugins/feedback/Feedback.ts index b8e2e89c..b4456b80 100644 --- a/packages/dom/src/core/plugins/feedback/Feedback.ts +++ b/packages/dom/src/core/plugins/feedback/Feedback.ts @@ -20,7 +20,8 @@ const ATTR_PREFIX = 'data-dnd-kit-'; const CSS_PREFIX = '--dnd-kit-feedback-'; const cssRules = `[${ATTR_PREFIX}feedback] {position: fixed !important;pointer-events: none;touch-action: none;z-index: 999999;will-change: transform;top: var(${CSS_PREFIX}top, 0px) !important;left: var(${CSS_PREFIX}left, 0px) !important;width: var(${CSS_PREFIX}width, auto) !important;height: var(${CSS_PREFIX}height, auto) !important;margin: var(${CSS_PREFIX}margin, 0px) !important;padding: var(${CSS_PREFIX}padding, 0px) !important;}[${ATTR_PREFIX}feedback][style*="${CSS_PREFIX}translate"] {transition: var(${CSS_PREFIX}transition) !important;translate: var(${CSS_PREFIX}translate) !important;}[${ATTR_PREFIX}feedback][popover]{overflow:visible;}[popover]{background:unset;border:unset;}[${ATTR_PREFIX}feedback]::backdrop {display: none}`; const ATTRIBUTE = `${ATTR_PREFIX}feedback`; -const IGNORED_ATTRIBUTES = [ATTRIBUTE, 'popover']; +const PLACEHOLDER_ATTRIBUTE = `${ATTR_PREFIX}placeholder`; +const IGNORED_ATTRIBUTES = [ATTRIBUTE, PLACEHOLDER_ATTRIBUTE, 'popover']; const IGNORED_STYLES = ['view-transition-name']; export class Feedback extends Plugin { @@ -68,7 +69,9 @@ export class Feedback extends Plugin { getWindow(element).getComputedStyle(element); const droppable = manager.registry.droppables.get(source.id); const clone = feedback === 'clone'; - const placeholder = createPlaceholder(element, clone); + const placeholder = createPlaceholder(element, clone, { + [PLACEHOLDER_ATTRIBUTE]: '', + }); const isKeyboardOperation = untracked(() => isKeyboardEvent(manager.dragOperation.activatorEvent) ); @@ -112,10 +115,7 @@ export class Feedback extends Plugin { }, CSS_PREFIX ); - element.parentElement?.insertBefore( - placeholder, - element.nextElementSibling - ); + element.insertAdjacentElement('afterend', placeholder); if (supportsPopover(element)) { element.setAttribute('popover', ''); @@ -221,7 +221,7 @@ export class Feedback extends Plugin { for (const entry of entries) { if (Array.from(entry.addedNodes).includes(element)) { /* Update the position of the placeholder when the source element is moved */ - entry.target.insertBefore(placeholder, element.nextElementSibling); + element.insertAdjacentElement('afterend', placeholder); /* * Any update in DOM order that affects the source element hide the popover @@ -284,7 +284,7 @@ export class Feedback extends Plugin { const id = manager.dragOperation.source?.id; const restoreFocus = () => { - if (id == null) { + if (!isKeyboardOperation || id == null) { return; } @@ -323,7 +323,7 @@ export class Feedback extends Plugin { }; const dropEffectCleanup = effect(function dropAnimation() { - if (dragOperation.status.dropping) { + if (dragOperation.status.dropped) { const onComplete = cleanup; cleanup = undefined; diff --git a/packages/dom/src/core/sensors/pointer/PointerSensor.ts b/packages/dom/src/core/sensors/pointer/PointerSensor.ts index c6e47e16..8de5fa9d 100644 --- a/packages/dom/src/core/sensors/pointer/PointerSensor.ts +++ b/packages/dom/src/core/sensors/pointer/PointerSensor.ts @@ -51,25 +51,11 @@ export class PointerSensor extends Sensor< #clearTimeout: CleanupFunction | undefined; - #document: Document | undefined; - constructor( public manager: DragDropManager, public options?: PointerSensorOptions ) { super(manager); - - // Adding a non-capture and non-passive `touchmove` listener in order - // to force `event.preventDefault()` calls to work in dynamically added - // touchmove event handlers. This is required for iOS Safari. - this.listeners.bind(window, { - type: 'touchmove', - listener() {}, - options: { - capture: false, - passive: false, - }, - }); } public bind(source: Draggable, options = this.options) { @@ -82,6 +68,8 @@ export class PointerSensor extends Sensor< }; if (target) { + patchWindow(target.ownerDocument.defaultView); + target.addEventListener('pointerdown', listener); return () => { @@ -146,8 +134,6 @@ export class PointerSensor extends Sensor< const ownerDocument = getDocument(event.target); - this.#document = ownerDocument; - const unbindListeners = this.listeners.bind(ownerDocument, [ { type: 'pointermove', @@ -298,3 +284,19 @@ export class PointerSensor extends Sensor< function preventDefault(event: Event) { event.preventDefault(); } + +function noop() {} + +const windows = new WeakSet(); + +function patchWindow(window: Window | null) { + if (!window || windows.has(window)) { + return; + } + + window.addEventListener('touchmove', noop, { + capture: false, + passive: false, + }); + windows.add(window); +} diff --git a/packages/dom/src/sortable/sortable.ts b/packages/dom/src/sortable/sortable.ts index b316dc03..696d761d 100644 --- a/packages/dom/src/sortable/sortable.ts +++ b/packages/dom/src/sortable/sortable.ts @@ -1,4 +1,4 @@ -import {batch, effects, reactive, untracked, type Effect} from '@dnd-kit/state'; +import {batch, reactive, untracked} from '@dnd-kit/state'; import {CollisionPriority} from '@dnd-kit/abstract'; import type { Data, @@ -49,8 +49,8 @@ const defaultPlugins: PluginConstructor[] = [ ]; export interface SortableInput - extends Omit, 'effects'>, - Omit, 'effects'> { + extends DraggableInput, + DroppableInput { /** * The index of the sortable item within its group. */ @@ -63,10 +63,6 @@ export interface SortableInput * The transition configuration to use when the index of the sortable item changes. */ transition?: SortableTransition | null; - /** - * Additional effects to set up when sortable item is instantiated. - */ - effects?: (instance: Sortable) => Effect[]; /** * Plugins to register when sortable item is instantiated. * @default [SortableKeyboardPlugin, OptimisticSortingPlugin] @@ -98,7 +94,7 @@ export class Sortable { constructor( { - effects: inputEffects, + effects: inputEffects = () => [], group, index, sensors, @@ -109,12 +105,44 @@ export class Sortable { }: SortableInput, public manager: DragDropManager ) { + this.droppable = new SortableDroppable(input, manager, this); this.draggable = new SortableDraggable( - {...input, type, sensors}, + { + ...input, + effects: () => [ + () => + this.manager.monitor.addEventListener('dragstart', () => { + this.initialIndex = this.index; + this.previousIndex = this.index; + }), + () => { + const {index, previousIndex} = this; + + // Re-run this effect whenever the index changes + if (index === previousIndex) { + return; + } + + this.previousIndex = index; + + this.animate(); + }, + () => { + const {target} = this; + const {feedback, isDragSource} = this.draggable; + + if (feedback == 'move' && isDragSource) { + this.droppable.disabled = !target; + } + }, + ...inputEffects(), + ], + type, + sensors, + }, manager, this ); - this.droppable = new SortableDroppable(input, manager, this); for (const plugin of plugins) { manager.registry.register(plugin); @@ -127,44 +155,9 @@ export class Sortable { this.type = type; this.transition = transition; - const {destroy} = this; - - const unsubscribe = this.manager.monitor.addEventListener( - 'dragstart', - () => { - this.initialIndex = this.index; - this.previousIndex = this.index; - } - ); - - const cleanup = effects( - () => { - const {index, previousIndex} = this; - - // Re-run this effect whenever the index changes - if (index === previousIndex) { - return; - } - - this.previousIndex = index; - - this.animate(); - }, - () => { - const {target} = this; - const {feedback, isDragSource} = this.draggable; - - if (feedback == 'move' && isDragSource) { - this.droppable.disabled = !target; - } - }, - ...(inputEffects?.(this) ?? []) - ); - this.destroy = () => { - destroy.bind(this)(); - unsubscribe(); - cleanup(); + this.draggable.destroy(); + this.droppable.destroy(); }; } @@ -322,10 +315,7 @@ export class Sortable { return this.droppable.accepts(draggable); } - public destroy() { - this.draggable.destroy(); - this.droppable.destroy(); - } + public destroy() {} } export class SortableDraggable extends Draggable { diff --git a/packages/dom/src/utilities/element/createPlaceholder.ts b/packages/dom/src/utilities/element/createPlaceholder.ts index 053e08a2..a018214b 100644 --- a/packages/dom/src/utilities/element/createPlaceholder.ts +++ b/packages/dom/src/utilities/element/createPlaceholder.ts @@ -1,7 +1,11 @@ import {cloneElement} from './cloneElement.js'; import {supportsStyle} from '../type-guards/supportsStyle.js'; -export function createPlaceholder(element: Element, clone = false): Element { +export function createPlaceholder( + element: Element, + clone = false, + attributes?: Record +): Element { const placeholder = cloneElement(element); if (supportsStyle(placeholder)) { @@ -14,5 +18,11 @@ export function createPlaceholder(element: Element, clone = false): Element { placeholder.setAttribute('tab-index', '-1'); placeholder.setAttribute('aria-hidden', 'true'); + if (attributes) { + for (const [key, value] of Object.entries(attributes)) { + placeholder.setAttribute(key, value); + } + } + return placeholder; } diff --git a/packages/react/src/core/context/index.ts b/packages/react/src/core/context/index.ts index cdab174e..22a2058c 100644 --- a/packages/react/src/core/context/index.ts +++ b/packages/react/src/core/context/index.ts @@ -1,3 +1 @@ -export {useDragDropManager, useDragOperation} from './hooks.js'; - export {DragDropProvider} from './DragDropProvider.js'; diff --git a/packages/react/src/core/draggable/useDraggable.ts b/packages/react/src/core/draggable/useDraggable.ts index 03ace248..0bc4b5a2 100644 --- a/packages/react/src/core/draggable/useDraggable.ts +++ b/packages/react/src/core/draggable/useDraggable.ts @@ -1,11 +1,11 @@ -import {useCallback, useEffect} from 'react'; +import {useCallback} from 'react'; import type {Data} from '@dnd-kit/abstract'; import {Draggable} from '@dnd-kit/dom'; import type {DraggableInput} from '@dnd-kit/dom'; -import {useComputed, useConstant, useOnValueChange} from '@dnd-kit/react/hooks'; +import {useComputed, useOnValueChange} from '@dnd-kit/react/hooks'; import {getCurrentValue, type RefOrValue} from '@dnd-kit/react/utilities'; -import {useDragDropManager} from '../context/index.js'; +import {useInstance} from '../hooks/useInstance.js'; export interface UseDraggableInput extends Omit, 'handle' | 'element'> { @@ -17,12 +17,10 @@ export function useDraggable( input: UseDraggableInput ) { const {disabled, id, sensors} = input; - const manager = useDragDropManager(); const handle = getCurrentValue(input.handle); const element = getCurrentValue(input.element); - const draggable = useConstant( - () => new Draggable({...input, handle, element}, manager), - manager + const draggable = useInstance( + (manager) => new Draggable({...input, handle, element}, manager) ); const isDragSource = useComputed(() => draggable.isDragSource); @@ -36,15 +34,6 @@ export function useDraggable( () => (draggable.feedback = input.feedback ?? 'default') ); - useEffect(() => { - manager.registry.register(draggable); - - // Cleanup on unmount - return () => { - manager.registry.unregister(draggable); - }; - }, [manager, draggable]); - return { get isDragSource() { return isDragSource.value; diff --git a/packages/react/src/core/droppable/useDroppable.ts b/packages/react/src/core/droppable/useDroppable.ts index 8a90d60f..940f1c82 100644 --- a/packages/react/src/core/droppable/useDroppable.ts +++ b/packages/react/src/core/droppable/useDroppable.ts @@ -1,12 +1,12 @@ -import {useCallback, useEffect} from 'react'; +import {useCallback} from 'react'; import type {Data} from '@dnd-kit/abstract'; import {Droppable} from '@dnd-kit/dom'; import {deepEqual} from '@dnd-kit/state'; import type {DroppableInput} from '@dnd-kit/dom'; -import {useComputed, useConstant, useOnValueChange} from '@dnd-kit/react/hooks'; +import {useComputed, useOnValueChange} from '@dnd-kit/react/hooks'; import {getCurrentValue, type RefOrValue} from '@dnd-kit/react/utilities'; -import {useDragDropManager} from '../context/index.js'; +import {useInstance} from '../hooks/useInstance.js'; export interface UseDroppableInput extends Omit, 'element'> { @@ -16,12 +16,10 @@ export interface UseDroppableInput export function useDroppable( input: UseDroppableInput ) { - const manager = useDragDropManager(); const {collisionDetector, disabled, id, accept, type} = input; const element = getCurrentValue(input.element); - const droppable = useConstant( - () => new Droppable({...input, element}, manager), - manager + const droppable = useInstance( + (manager) => new Droppable({...input, element}, manager) ); const isDisabled = useComputed(() => droppable.disabled); const isDropTarget = useComputed(() => droppable.isDropTarget); @@ -33,15 +31,6 @@ export function useDroppable( useOnValueChange(element, () => (droppable.element = element)); useOnValueChange(type, () => (droppable.id = id)); - useEffect(() => { - manager.registry.register(droppable); - - // Cleanup on unmount - return () => { - manager.registry.unregister(droppable); - }; - }, [manager, droppable]); - return { get isDisabled() { return isDisabled.value; diff --git a/packages/react/src/core/hooks/useDragDropManager.ts b/packages/react/src/core/hooks/useDragDropManager.ts new file mode 100644 index 00000000..e92934fa --- /dev/null +++ b/packages/react/src/core/hooks/useDragDropManager.ts @@ -0,0 +1,7 @@ +import {useContext} from 'react'; + +import {DragDropContext} from '../context/context.js'; + +export function useDragDropManager() { + return useContext(DragDropContext); +} diff --git a/packages/react/src/core/hooks/useDragOperation.ts b/packages/react/src/core/hooks/useDragOperation.ts new file mode 100644 index 00000000..1fb16cf1 --- /dev/null +++ b/packages/react/src/core/hooks/useDragOperation.ts @@ -0,0 +1,20 @@ +import {useComputed} from '@dnd-kit/react/hooks'; + +import {useDragDropManager} from './useDragDropManager.js'; + +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/hooks/useInstance.ts b/packages/react/src/core/hooks/useInstance.ts new file mode 100644 index 00000000..3b5a7e32 --- /dev/null +++ b/packages/react/src/core/hooks/useInstance.ts @@ -0,0 +1,22 @@ +import {useEffect, useState} from 'react'; +import {Entity} from '@dnd-kit/abstract'; +import type {DragDropManager} from '@dnd-kit/dom'; + +import {useDragDropManager} from './useDragDropManager.js'; + +export function useInstance( + initializer: (manager: DragDropManager) => T +): T { + const manager = useDragDropManager(); + const [instance] = useState(() => initializer(manager)); + + useEffect(() => { + manager.registry.register(instance); + + return () => { + manager.registry.unregister(instance); + }; + }, []); + + return instance; +} diff --git a/packages/react/src/core/index.ts b/packages/react/src/core/index.ts index 3c54c784..a6e8587c 100644 --- a/packages/react/src/core/index.ts +++ b/packages/react/src/core/index.ts @@ -1,9 +1,9 @@ -export { - DragDropProvider, - useDragDropManager, - useDragOperation, -} from './context/index.js'; +export {DragDropProvider} from './context/index.js'; export {useDraggable} from './draggable/index.js'; export {useDroppable} from './droppable/index.js'; + +export {useDragDropManager} from './hooks/useDragDropManager.js'; + +export {useDragOperation} from './hooks/useDragOperation.js'; diff --git a/packages/react/src/sortable/useSortable.ts b/packages/react/src/sortable/useSortable.ts index 6ff2e83a..0f44c588 100644 --- a/packages/react/src/sortable/useSortable.ts +++ b/packages/react/src/sortable/useSortable.ts @@ -35,7 +35,6 @@ export function useSortable(input: UseSortableInput) { } = input; const manager = useDragDropManager(); - const handle = getCurrentValue(input.handle); const element = getCurrentValue(input.element); const sortable = useConstant( @@ -52,6 +51,16 @@ export function useSortable(input: UseSortableInput) { manager ); + useEffect(() => { + manager.registry.register(sortable.draggable); + manager.registry.register(sortable.droppable); + + return () => { + manager.registry.unregister(sortable.draggable); + manager.registry.unregister(sortable.droppable); + }; + }, [manager]); + const isDisabled = useComputed(() => sortable.disabled); const isDropTarget = useComputed(() => sortable.isDropTarget); const isDragSource = useComputed(() => sortable.isDragSource);