diff --git a/packages/core/src/components/DndContext/DndContext.tsx b/packages/core/src/components/DndContext/DndContext.tsx index 8e73837a..33a27294 100644 --- a/packages/core/src/components/DndContext/DndContext.tsx +++ b/packages/core/src/components/DndContext/DndContext.tsx @@ -24,7 +24,6 @@ import { getInitialState, reducer, } from '../../store'; -import type {ViewRect} from '../../types'; import {DndMonitorContext, DndMonitorState} from '../../hooks/monitor'; import { useAutoScroller, @@ -36,6 +35,7 @@ import { useSensorSetup, useClientRect, useClientRects, + useWindowRect, useRect, useScrollOffsets, } from '../../hooks/utilities'; @@ -59,12 +59,13 @@ import { defaultCoordinates, getAdjustedRect, getRectDelta, - getViewRect, rectIntersection, } from '../../utilities'; +import {getTransformAgnosticClientRect} from '../../utilities/rect'; import {applyModifiers, Modifiers} from '../../modifiers'; import type {Active, DataRef, Over} from '../../store/types'; import type { + ClientRect, DragStartEvent, DragCancelEvent, DragEndEvent, @@ -97,13 +98,18 @@ export interface Props { onDragCancel?(event: DragCancelEvent): void; } -export interface DraggableMeasuring { - measure(node: HTMLElement): ViewRect; +interface Measuring { + measure(node: HTMLElement): ClientRect; } +export interface DraggableMeasuring extends Measuring {} + +export interface DragOverlayMeasuring extends Measuring {} + export interface MeasuringConfiguration { draggable?: Partial; droppable?: Partial; + dragOverlay?: Partial; } export interface CancelDropArguments extends DragEndEvent {} @@ -180,7 +186,7 @@ export const DndContext = memo(function DndContext({ return droppableContainers.getEnabled(); }, [droppableContainers]); const { - layoutRectMap: droppableRects, + rectMap: droppableRects, recomputeLayouts, willRecomputeLayouts, } = useDroppableMeasuring(enabledDroppableContainers, { @@ -194,45 +200,37 @@ export const DndContext = memo(function DndContext({ : null; const activeNodeRect = useRect( activeNode, - measuring?.draggable?.measure ?? getViewRect + measuring?.draggable?.measure ?? getTransformAgnosticClientRect + ); + const containerNodeRect = useClientRect( + activeNode ? activeNode.parentElement : null ); - const activeNodeClientRect = useClientRect(activeNode); - const initialActiveNodeRectRef = useRef(null); - const initialActiveNodeRect = initialActiveNodeRectRef.current; const sensorContext = useRef({ active: null, activeNode, collisionRect: null, droppableRects, draggableNodes, + draggingNode: null, draggingNodeRect: null, droppableContainers, over: null, scrollableAncestors: [], scrollAdjustedTranslate: null, - translatedRect: null, }); const overNode = droppableContainers.getNodeFor( sensorContext.current.over?.id ); - const windowRect = useClientRect( - activeNode ? activeNode.ownerDocument.defaultView : null - ); - const containerNodeRect = useClientRect( - activeNode ? activeNode.parentElement : null - ); - const scrollableAncestors = useScrollableAncestors( - activeId ? overNode ?? activeNode : null - ); - const scrollableAncestorRects = useClientRects(scrollableAncestors); const dragOverlay = useDragOverlayMeasuring({ - disabled: activeId == null, - forceRecompute: willRecomputeLayouts, + measure: measuring?.dragOverlay?.measure, }); // Use the rect of the drag overlay if it is mounted + const draggingNode = dragOverlay.nodeRef.current ?? activeNode; const draggingNodeRect = dragOverlay.rect ?? activeNodeRect; + const initialActiveNodeRectRef = useRef(null); + const initialActiveNodeRect = initialActiveNodeRectRef.current; // The delta between the previous and new position of the draggable node // is only relevant when there is no drag overlay @@ -241,6 +239,18 @@ export const DndContext = memo(function DndContext({ ? getRectDelta(activeNodeRect, initialActiveNodeRect) : defaultCoordinates; + // Get the window rect of the dragging node + const windowRect = useWindowRect( + draggingNode ? draggingNode.ownerDocument.defaultView : null + ); + + // Get scrollable ancestors of the dragging node + const scrollableAncestors = useScrollableAncestors( + activeId ? overNode ?? draggingNode : null + ); + const scrollableAncestorRects = useClientRects(scrollableAncestors as any); + + // Apply modifiers const modifiedTranslate = applyModifiers(modifiers, { transform: { x: translate.x - nodeRectDelta.x, @@ -250,7 +260,7 @@ export const DndContext = memo(function DndContext({ }, activatorEvent, active, - activeNodeRect: activeNodeClientRect, + activeNodeRect, containerNodeRect, draggingNodeRect, over: sensorContext.current.over, @@ -268,13 +278,10 @@ export const DndContext = memo(function DndContext({ const scrollAdjustedTranslate = add(modifiedTranslate, scrollAdjustment); - const translatedRect = draggingNodeRect + const collisionRect = draggingNodeRect ? getAdjustedRect(draggingNodeRect, modifiedTranslate) : null; - const collisionRect = translatedRect - ? getAdjustedRect(translatedRect, scrollAdjustment) - : null; const overId = active && collisionRect ? collisionDetection({ @@ -393,14 +400,15 @@ export const DndContext = memo(function DndContext({ if (event) { setMonitorState({type, event}); } - }); - if (event) { - const {onDragCancel, onDragEnd} = latestProps.current; - const handler = type === Action.DragEnd ? onDragEnd : onDragCancel; + if (event) { + const {onDragCancel, onDragEnd} = latestProps.current; + const handler = + type === Action.DragEnd ? onDragEnd : onDragCancel; - handler?.(event); - } + handler?.(event); + } + }); }; } }, @@ -539,35 +547,35 @@ export const DndContext = memo(function DndContext({ collisionRect, droppableRects, draggableNodes, + draggingNode, draggingNodeRect, droppableContainers, over, scrollableAncestors, scrollAdjustedTranslate: scrollAdjustedTranslate, - translatedRect, }; activeRects.current = { initial: draggingNodeRect, - translated: translatedRect, + translated: collisionRect, }; }, [ active, activeNode, collisionRect, draggableNodes, + draggingNode, draggingNodeRect, droppableRects, droppableContainers, over, scrollableAncestors, scrollAdjustedTranslate, - translatedRect, ]); useAutoScroller({ ...getAutoScrollerOptions(), - draggingRect: translatedRect, + draggingRect: collisionRect, pointerCoordinates, scrollableAncestors, scrollableAncestorRects, @@ -578,7 +586,6 @@ export const DndContext = memo(function DndContext({ active, activeNode, activeNodeRect, - activeNodeClientRect, activatorEvent, activators, ariaDescribedById: { @@ -602,7 +609,6 @@ export const DndContext = memo(function DndContext({ }, [ active, activeNode, - activeNodeClientRect, activeNodeRect, activatorEvent, activators, diff --git a/packages/core/src/components/DragOverlay/DragOverlay.tsx b/packages/core/src/components/DragOverlay/DragOverlay.tsx index 50e6f342..1457a5b1 100644 --- a/packages/core/src/components/DragOverlay/DragOverlay.tsx +++ b/packages/core/src/components/DragOverlay/DragOverlay.tsx @@ -5,8 +5,8 @@ import {getRelativeTransformOrigin} from '../../utilities'; import {applyModifiers, Modifiers} from '../../modifiers'; import {ActiveDraggableContext} from '../DndContext'; import {useDndContext} from '../../hooks'; -import type {ViewRect} from '../../types'; -import {useDropAnimation, DropAnimation} from './hooks'; +import type {ClientRect} from '../../types'; +import {useDropAnimation, defaultDropAnimation, DropAnimation} from './hooks'; type TransitionGetter = ( activatorEvent: Event | null @@ -30,12 +30,6 @@ const defaultTransition: TransitionGetter = (activatorEvent) => { return isKeyboardActivator ? 'transform 250ms ease' : undefined; }; -export const defaultDropAnimation: DropAnimation = { - duration: 250, - easing: 'ease', - dragSourceOpacity: 0, -}; - export const DragOverlay = React.memo( ({ adjustScale = false, @@ -51,7 +45,6 @@ export const DragOverlay = React.memo( const { active, activeNodeRect, - activeNodeClientRect, containerNodeRect, draggableNodes, activatorEvent, @@ -65,7 +58,7 @@ export const DragOverlay = React.memo( const modifiedTransform = applyModifiers(modifiers, { activatorEvent, active, - activeNodeRect: activeNodeClientRect, + activeNodeRect, containerNodeRect, draggingNodeRect: dragOverlay.rect, over, @@ -84,23 +77,33 @@ export const DragOverlay = React.memo( scaleY: 1, }; - const initialNodeRect = useLazyMemo( + const initialRect = useLazyMemo( (previousValue) => { if (isDragging) { - return previousValue ?? activeNodeRect; + if (previousValue) { + return previousValue; + } + + if (!activeNodeRect) { + return null; + } + + return { + ...activeNodeRect, + }; } return null; }, [isDragging, activeNodeRect] ); - const style: React.CSSProperties | undefined = initialNodeRect + const style: React.CSSProperties | undefined = initialRect ? { position: 'fixed', - width: initialNodeRect.width, - height: initialNodeRect.height, - top: initialNodeRect.top, - left: initialNodeRect.left, + width: initialRect.width, + height: initialRect.height, + top: initialRect.top, + left: initialRect.left, zIndex, transform: CSS.Transform.toString(finalTransform), touchAction: 'none', @@ -108,7 +111,7 @@ export const DragOverlay = React.memo( adjustScale && activatorEvent ? getRelativeTransformOrigin( activatorEvent as MouseEvent | KeyboardEvent | TouchEvent, - initialNodeRect as any + initialRect ) : undefined, transition: diff --git a/packages/core/src/components/DragOverlay/hooks/index.ts b/packages/core/src/components/DragOverlay/hooks/index.ts index 21a81cdd..ce813865 100644 --- a/packages/core/src/components/DragOverlay/hooks/index.ts +++ b/packages/core/src/components/DragOverlay/hooks/index.ts @@ -1,2 +1,2 @@ -export {useDropAnimation} from './useDropAnimation'; +export {useDropAnimation, defaultDropAnimation} from './useDropAnimation'; export type {DropAnimation} from './useDropAnimation'; diff --git a/packages/core/src/components/DragOverlay/hooks/useDropAnimation.ts b/packages/core/src/components/DragOverlay/hooks/useDropAnimation.ts index 74c6f6b5..893bf39b 100644 --- a/packages/core/src/components/DragOverlay/hooks/useDropAnimation.ts +++ b/packages/core/src/components/DragOverlay/hooks/useDropAnimation.ts @@ -1,10 +1,10 @@ -import {useEffect, useState} from 'react'; +import {useState} from 'react'; import {CSS, Transform, useIsomorphicLayoutEffect} from '@dnd-kit/utilities'; import type {UniqueIdentifier} from '../../../types'; import type {DraggableNodes} from '../../../store'; -import {getViewRect} from '../../../utilities'; import {getMeasurableNode} from '../../../utilities/nodes'; +import {getTransformAgnosticClientRect} from '../../../utilities/rect'; export interface DropAnimation { duration: number; @@ -24,20 +24,26 @@ interface Arguments { transform: Transform | undefined; } +export const defaultDropAnimation: DropAnimation = { + duration: 250, + easing: 'ease', + dragSourceOpacity: 0, +}; + export function useDropAnimation({ animate, adjustScale, activeId, draggableNodes, duration, - easing, dragSourceOpacity, + easing, node, transform, }: Arguments) { const [dropAnimationComplete, setDropAnimationComplete] = useState(false); - useEffect(() => { + useIsomorphicLayoutEffect(() => { if (!animate || !activeId || !easing || !duration) { if (animate) { setDropAnimationComplete(true); @@ -46,71 +52,70 @@ export function useDropAnimation({ return; } - requestAnimationFrame(() => { - const finalNode = draggableNodes[activeId]?.node.current; + const finalNode = draggableNodes[activeId]?.node.current; - if (transform && node && finalNode && finalNode.parentNode !== null) { - const fromNode = getMeasurableNode(node); + if (transform && node && finalNode && finalNode.parentNode !== null) { + const fromNode = getMeasurableNode(node); - if (fromNode) { - const from = fromNode.getBoundingClientRect(); - const to = getViewRect(finalNode); - const delta = { - x: from.left - to.left, - y: from.top - to.top, - }; + if (fromNode) { + const from = fromNode.getBoundingClientRect(); + const to = getTransformAgnosticClientRect(finalNode); - if (Math.abs(delta.x) || Math.abs(delta.y)) { - const scaleDelta = { - scaleX: adjustScale - ? (to.width * transform.scaleX) / from.width - : 1, - scaleY: adjustScale - ? (to.height * transform.scaleY) / from.height - : 1, - }; - const finalTransform = CSS.Transform.toString({ - x: transform.x - delta.x, - y: transform.y - delta.y, - ...scaleDelta, - }); - const originalOpacity = finalNode.style.opacity; - - if (dragSourceOpacity != null) { - finalNode.style.opacity = `${dragSourceOpacity}`; - } + const delta = { + x: from.left - to.left, + y: from.top - to.top, + }; - const nodeAnimation = node.animate( - [ - { - transform: CSS.Transform.toString(transform), - }, - { - transform: finalTransform, - }, - ], + if (Math.abs(delta.x) || Math.abs(delta.y)) { + const scaleDelta = { + scaleX: adjustScale + ? (to.width * transform.scaleX) / from.width + : 1, + scaleY: adjustScale + ? (to.height * transform.scaleY) / from.height + : 1, + }; + const finalTransform = CSS.Transform.toString({ + x: transform.x - delta.x, + y: transform.y - delta.y, + ...scaleDelta, + }); + const originalOpacity = finalNode.style.opacity; + + if (dragSourceOpacity != null) { + finalNode.style.opacity = `${dragSourceOpacity}`; + } + + const nodeAnimation = node.animate( + [ + { + transform: CSS.Transform.toString(transform), + }, { - easing, - duration, - } - ); + transform: finalTransform, + }, + ], + { + easing, + duration, + } + ); - nodeAnimation.onfinish = () => { - node.style.display = 'none'; + nodeAnimation.onfinish = () => { + node.style.display = 'none'; - setDropAnimationComplete(true); + setDropAnimationComplete(true); - if (finalNode && dragSourceOpacity != null) { - finalNode.style.opacity = originalOpacity; - } - }; - return; - } + if (finalNode && dragSourceOpacity != null) { + finalNode.style.opacity = originalOpacity; + } + }; + return; } } + } - setDropAnimationComplete(true); - }); + setDropAnimationComplete(true); }, [ animate, activeId, diff --git a/packages/core/src/components/DragOverlay/index.ts b/packages/core/src/components/DragOverlay/index.ts index 4e7e0955..38767374 100644 --- a/packages/core/src/components/DragOverlay/index.ts +++ b/packages/core/src/components/DragOverlay/index.ts @@ -1,3 +1,4 @@ -export {DragOverlay, defaultDropAnimation} from './DragOverlay'; +export {DragOverlay} from './DragOverlay'; export type {Props} from './DragOverlay'; +export {defaultDropAnimation} from './hooks'; export type {DropAnimation} from './hooks'; diff --git a/packages/core/src/hooks/useDraggable.ts b/packages/core/src/hooks/useDraggable.ts index 35a401c6..7efab94a 100644 --- a/packages/core/src/hooks/useDraggable.ts +++ b/packages/core/src/hooks/useDraggable.ts @@ -1,5 +1,10 @@ -import {createContext, useContext, useEffect, useMemo} from 'react'; -import {Transform, useNodeRef, useUniqueId} from '@dnd-kit/utilities'; +import {createContext, useContext, useMemo} from 'react'; +import { + Transform, + useNodeRef, + useUniqueId, + useIsomorphicLayoutEffect, +} from '@dnd-kit/utilities'; import {Context, Data} from '../store'; import {ActiveDraggableContext} from '../components/DndContext'; @@ -55,7 +60,7 @@ export function useDraggable({ const listeners = useSyntheticListeners(activators, id); const dataRef = useData(data); - useEffect( + useIsomorphicLayoutEffect( () => { draggableNodes[id] = {id, key, node, data: dataRef}; diff --git a/packages/core/src/hooks/useDroppable.ts b/packages/core/src/hooks/useDroppable.ts index 4e6cbd92..db4f3961 100644 --- a/packages/core/src/hooks/useDroppable.ts +++ b/packages/core/src/hooks/useDroppable.ts @@ -6,7 +6,7 @@ import { } from '@dnd-kit/utilities'; import {Context, Action, Data} from '../store'; -import type {LayoutRect} from '../types'; +import type {ClientRect} from '../types'; import {useData} from './utilities'; export interface UseDroppableArguments { @@ -24,7 +24,7 @@ export function useDroppable({ }: UseDroppableArguments) { const key = useUniqueId(ID_PREFIX); const {active, dispatch, over} = useContext(Context); - const rect = useRef(null); + const rect = useRef(null); const [nodeRef, setNodeRef] = useNodeRef(); const dataRef = useData(data); diff --git a/packages/core/src/hooks/utilities/index.ts b/packages/core/src/hooks/utilities/index.ts index 39083abd..64b88f35 100644 --- a/packages/core/src/hooks/utilities/index.ts +++ b/packages/core/src/hooks/utilities/index.ts @@ -22,5 +22,5 @@ export type { SyntheticListeners, SyntheticListenerMap, } from './useSyntheticListeners'; -export {useRect, useClientRect, useClientRects, useViewRect} from './useRect'; +export {useRect, useClientRect, useClientRects, useWindowRect} from './useRect'; export {useDragOverlayMeasuring} from './useDragOverlayMeasuring'; diff --git a/packages/core/src/hooks/utilities/useAutoScroller.ts b/packages/core/src/hooks/utilities/useAutoScroller.ts index 4e1f0600..86483318 100644 --- a/packages/core/src/hooks/utilities/useAutoScroller.ts +++ b/packages/core/src/hooks/utilities/useAutoScroller.ts @@ -2,7 +2,7 @@ import {useCallback, useEffect, useMemo, useRef} from 'react'; import {useInterval} from '@dnd-kit/utilities'; import {getScrollDirectionAndSpeed, defaultCoordinates} from '../../utilities'; -import type {Coordinates, Direction, ViewRect} from '../../types'; +import type {Coordinates, Direction, ClientRect} from '../../types'; export type ScrollAncestorSortingFn = (ancestors: Element[]) => Element[]; @@ -25,11 +25,11 @@ export interface Options { } interface Arguments extends Options { - draggingRect: ViewRect | null; + draggingRect: ClientRect | null; enabled: boolean; pointerCoordinates: Coordinates | null; scrollableAncestors: Element[]; - scrollableAncestorRects: ViewRect[]; + scrollableAncestorRects: ClientRect[]; } export type CanScroll = (element: Element) => boolean; diff --git a/packages/core/src/hooks/utilities/useDragOverlayMeasuring.ts b/packages/core/src/hooks/utilities/useDragOverlayMeasuring.ts index b9b3d1da..abc63473 100644 --- a/packages/core/src/hooks/utilities/useDragOverlayMeasuring.ts +++ b/packages/core/src/hooks/utilities/useDragOverlayMeasuring.ts @@ -1,44 +1,63 @@ -import {useMemo} from 'react'; -import {useNodeRef} from '@dnd-kit/utilities'; +import {useMemo, useCallback, useState, useRef} from 'react'; +import { + isHTMLElement, + useIsomorphicLayoutEffect, + useNodeRef, +} from '@dnd-kit/utilities'; import {getMeasurableNode} from '../../utilities/nodes'; -import {getLayoutRect} from '../../utilities/rect'; +import {getClientRect} from '../../utilities/rect'; import type {DndContextDescriptor} from '../../store'; -import type {ViewRect} from '../../types'; - -import {createUseRectFn} from './useRect'; +import type {ClientRect} from '../../types'; interface Arguments { - disabled: boolean; - forceRecompute: boolean; -} - -// To-do: Delete and replace with `getViewRect` when https://github.com/clauderic/dnd-kit/pull/415 is merged -function getDragOverlayRect(element: HTMLElement): ViewRect { - const {width, height, offsetLeft, offsetTop} = getLayoutRect(element); - - return { - top: offsetTop, - bottom: offsetTop + height, - left: offsetLeft, - right: offsetLeft + width, - width, - height, - offsetTop, - offsetLeft, - }; + measure?(element: HTMLElement): ClientRect; } -const useDragOverlayRect = createUseRectFn(getDragOverlayRect); export function useDragOverlayMeasuring({ - disabled, - forceRecompute, + measure = getClientRect, }: Arguments): DndContextDescriptor['dragOverlay'] { - const [nodeRef, setRef] = useNodeRef(); - const rect = useDragOverlayRect( - disabled ? null : getMeasurableNode(nodeRef.current), - forceRecompute + const [rect, setRect] = useState(null); + const measureRef = useRef(measure); + const handleResize = useCallback( + (entries: ResizeObserverEntry[]) => { + for (const {target} of entries) { + if (isHTMLElement(target)) { + setRect((rect) => { + const newRect = measure(target); + + return rect + ? {...rect, width: newRect.width, height: newRect.height} + : newRect; + }); + break; + } + } + }, + [measure] ); + const resizeObserver = useMemo(() => new ResizeObserver(handleResize), [ + handleResize, + ]); + const handleNodeChange = useCallback( + (element) => { + const node = getMeasurableNode(element); + + resizeObserver.disconnect(); + + if (node) { + resizeObserver.observe(node); + } + + setRect(node ? measure(node) : null); + }, + [measure, resizeObserver] + ); + const [nodeRef, setRef] = useNodeRef(handleNodeChange); + + useIsomorphicLayoutEffect(() => { + measureRef.current = measure; + }, [measure]); return useMemo( () => ({ diff --git a/packages/core/src/hooks/utilities/useDroppableMeasuring.ts b/packages/core/src/hooks/utilities/useDroppableMeasuring.ts index 3572b690..ef9690d9 100644 --- a/packages/core/src/hooks/utilities/useDroppableMeasuring.ts +++ b/packages/core/src/hooks/utilities/useDroppableMeasuring.ts @@ -1,9 +1,9 @@ import {useCallback, useEffect, useRef, useState} from 'react'; import {useLazyMemo} from '@dnd-kit/utilities'; -import {getLayoutRect} from '../../utilities'; -import type {DroppableContainer, LayoutRectMap} from '../../store/types'; -import type {LayoutRect} from '../../types'; +import {Rect, getTransformAgnosticClientRect} from '../../utilities/rect'; +import type {DroppableContainer, RectMap} from '../../store/types'; +import type {ClientRect} from '../../types'; interface Arguments { dragging: boolean; @@ -21,7 +21,7 @@ export enum MeasuringFrequency { Optimized = 'optimized', } -type MeasuringFunction = (element: HTMLElement) => LayoutRect; +type MeasuringFunction = (element: HTMLElement) => ClientRect; export interface DroppableMeasuring { measure: MeasuringFunction; @@ -29,10 +29,10 @@ export interface DroppableMeasuring { frequency: MeasuringFrequency | number; } -const defaultValue: LayoutRectMap = new Map(); +const defaultValue: RectMap = new Map(); const defaultConfig: DroppableMeasuring = { - measure: getLayoutRect, + measure: getTransformAgnosticClientRect, strategy: MeasuringStrategy.WhileDragging, frequency: MeasuringFrequency.Optimized, }; @@ -50,7 +50,7 @@ export function useDroppableMeasuring( const recomputeLayouts = useCallback(() => setWillRecomputeLayouts(true), []); const recomputeLayoutsTimeoutId = useRef(null); const disabled = isDisabled(); - const layoutRectMap = useLazyMemo( + const rectMap = useLazyMemo( (previousValue) => { if (disabled && !dragging) { return defaultValue; @@ -66,13 +66,12 @@ export function useDroppableMeasuring( if (!container) { continue; } + const node = container.node.current; - container.rect.current = container.node.current - ? measure(container.node.current) - : null; + container.rect.current = node ? new Rect(measure(node), node) : null; } - return createLayoutRectMap(containers); + return createRectMap(containers); } return previousValue; @@ -122,7 +121,7 @@ export function useDroppableMeasuring( ); return { - layoutRectMap, + rectMap, recomputeLayouts, willRecomputeLayouts, }; @@ -139,10 +138,8 @@ export function useDroppableMeasuring( } } -function createLayoutRectMap( - containers: DroppableContainer[] | null -): LayoutRectMap { - const layoutRectMap: LayoutRectMap = new Map(); +function createRectMap(containers: DroppableContainer[] | null): RectMap { + const rectMap: RectMap = new Map(); if (containers) { for (const container of containers) { @@ -156,9 +153,9 @@ function createLayoutRectMap( continue; } - layoutRectMap.set(id, rect.current); + rectMap.set(id, rect.current); } } - return layoutRectMap; + return rectMap; } diff --git a/packages/core/src/hooks/utilities/useRect.ts b/packages/core/src/hooks/utilities/useRect.ts index 58ce1b1c..74ca33c0 100644 --- a/packages/core/src/hooks/utilities/useRect.ts +++ b/packages/core/src/hooks/utilities/useRect.ts @@ -1,26 +1,26 @@ -import {useRef} from 'react'; +import {useMemo, useRef} from 'react'; import {isHTMLElement, useLazyMemo} from '@dnd-kit/utilities'; -import {getBoundingClientRect, getViewRect} from '../../utilities'; -import type {LayoutRect} from '../../types'; +import { + Rect, + getWindowClientRect, + getTransformAgnosticClientRect, +} from '../../utilities/rect'; +import type {ClientRect} from '../../types'; -type RectFn = (element: U) => T; +type RectFn = (element: T) => ClientRect; -export const useViewRect = createUseRectFn(getViewRect); -export const useClientRect = createUseRectFn(getBoundingClientRect); -export const useClientRects = createUseRectsFn(getBoundingClientRect); +export const useClientRect = createUseRectFn(getTransformAgnosticClientRect); +export const useClientRects = createUseRectsFn(getTransformAgnosticClientRect); -export function useRect< - T = LayoutRect, - U extends Element | Window = HTMLElement ->( - element: U | null, - getRect: (element: U) => T, +export function useRect( + element: T | null, + getRect: (element: T) => ClientRect, forceRecompute?: boolean -): T | null { +): Rect | null { const previousElement = useRef(element); - return useLazyMemo( + return useLazyMemo( (previousValue) => { if (!element) { return null; @@ -35,7 +35,7 @@ export function useRect< return null; } - return getRect(element as U); + return new Rect(getRect(element), element); } return previousValue ?? null; @@ -44,21 +44,21 @@ export function useRect< ); } -export function createUseRectFn< - T = LayoutRect, - U extends Element | Window = HTMLElement ->(getRect: RectFn) { - return (element: U | null, forceRecompute?: boolean) => +export function createUseRectFn(getRect: RectFn) { + return (element: T | null, forceRecompute?: boolean) => useRect(element, getRect, forceRecompute); } -function createUseRectsFn(getRect: RectFn) { - const defaultValue: T[] = []; +function createUseRectsFn(getRect: RectFn) { + const defaultValue: Rect[] = []; - return function useRects(elements: Element[], forceRecompute?: boolean): T[] { + return function useRects( + elements: HTMLElement[], + forceRecompute?: boolean + ): Rect[] { const previousElements = useRef(elements); - return useLazyMemo( + return useLazyMemo( (previousValue) => { if (!elements.length) { return defaultValue; @@ -69,7 +69,7 @@ function createUseRectsFn(getRect: RectFn) { (!previousValue && elements.length) || elements !== previousElements.current ) { - return elements.map((element) => getRect(element as HTMLElement)); + return elements.map((element) => new Rect(getRect(element), element)); } return previousValue ?? defaultValue; @@ -78,3 +78,9 @@ function createUseRectsFn(getRect: RectFn) { ); }; } + +export function useWindowRect(element: typeof window | null) { + return useMemo(() => (element ? getWindowClientRect(element) : null), [ + element, + ]); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d3f51deb..1eef0e99 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -80,24 +80,20 @@ export type { } from './store'; export type { + ClientRect, DistanceMeasurement, DragEndEvent, DragMoveEvent, DragOverEvent, DragStartEvent, DragCancelEvent, - LayoutRect, Translate, UniqueIdentifier, - ViewRect, } from './types'; export { defaultCoordinates, - getBoundingClientRect, - getViewRect, - getLayoutRect, - getViewportLayoutRect, + getClientRect, getScrollableAncestors, closestCenter, closestCorners, diff --git a/packages/core/src/modifiers/types.ts b/packages/core/src/modifiers/types.ts index 68d983b7..f980d7cc 100644 --- a/packages/core/src/modifiers/types.ts +++ b/packages/core/src/modifiers/types.ts @@ -1,17 +1,17 @@ import type {Transform} from '@dnd-kit/utilities'; import type {Active, Over} from '../store'; -import type {ClientRect, ViewRect} from '../types'; +import type {ClientRect} from '../types'; export type Modifier = (args: { activatorEvent: Event | null; active: Active | null; - activeNodeRect: ViewRect | null; - draggingNodeRect: ViewRect | null; - containerNodeRect: ViewRect | null; + activeNodeRect: ClientRect | null; + draggingNodeRect: ClientRect | null; + containerNodeRect: ClientRect | null; over: Over | null; - overlayNodeRect: ViewRect | null; + overlayNodeRect: ClientRect | null; scrollableAncestors: Element[]; - scrollableAncestorRects: ViewRect[]; + scrollableAncestorRects: ClientRect[]; transform: Transform; windowRect: ClientRect | null; }) => Transform; diff --git a/packages/core/src/sensors/keyboard/KeyboardSensor.ts b/packages/core/src/sensors/keyboard/KeyboardSensor.ts index 5dc4eab1..5883c4f0 100644 --- a/packages/core/src/sensors/keyboard/KeyboardSensor.ts +++ b/packages/core/src/sensors/keyboard/KeyboardSensor.ts @@ -9,7 +9,7 @@ import { import type {Coordinates} from '../../types'; import { defaultCoordinates, - getBoundingClientRect, + getTransformAgnosticClientRect, getScrollPosition, getScrollElementRect, } from '../../utilities'; @@ -68,7 +68,9 @@ export class KeyboardSensor implements SensorInstance { throw new Error('Active draggable node is undefined'); } - const activeNodeRect = getBoundingClientRect(activeNode.node.current); + const activeNodeRect = getTransformAgnosticClientRect( + activeNode.node.current + ); const coordinates = { x: activeNodeRect.left, y: activeNodeRect.top, diff --git a/packages/core/src/sensors/types.ts b/packages/core/src/sensors/types.ts index 3e8d9960..38206c6a 100644 --- a/packages/core/src/sensors/types.ts +++ b/packages/core/src/sensors/types.ts @@ -5,15 +5,14 @@ import type { DraggableNode, DraggableNodes, DroppableContainers, - LayoutRectMap, + RectMap, } from '../store'; import type { Coordinates, - LayoutRect, SyntheticEventName, Translate, UniqueIdentifier, - ViewRect, + ClientRect, } from '../types'; export enum Response { @@ -25,15 +24,15 @@ export enum Response { export type SensorContext = { active: Active | null; activeNode: HTMLElement | null; - collisionRect: ViewRect | null; + collisionRect: ClientRect | null; draggableNodes: DraggableNodes; - draggingNodeRect: LayoutRect | null; - droppableRects: LayoutRectMap; + draggingNode: HTMLElement | null; + draggingNodeRect: ClientRect | null; + droppableRects: RectMap; droppableContainers: DroppableContainers; over: Over | null; scrollableAncestors: Element[]; scrollAdjustedTranslate: Translate | null; - translatedRect: ViewRect | null; }; export type SensorOptions = {}; diff --git a/packages/core/src/store/context.ts b/packages/core/src/store/context.ts index e6f052b9..7e278501 100644 --- a/packages/core/src/store/context.ts +++ b/packages/core/src/store/context.ts @@ -9,7 +9,6 @@ export const Context = createContext({ active: null, activeNode: null, activeNodeRect: null, - activeNodeClientRect: null, activators: [], ariaDescribedById: { draggable: '', diff --git a/packages/core/src/store/index.ts b/packages/core/src/store/index.ts index 2ee43932..31c5f1ac 100644 --- a/packages/core/src/store/index.ts +++ b/packages/core/src/store/index.ts @@ -11,7 +11,7 @@ export type { DroppableContainer, DroppableContainers, DndContextDescriptor, - LayoutRectMap, + RectMap, Over, State, } from './types'; diff --git a/packages/core/src/store/types.ts b/packages/core/src/store/types.ts index e0bcfa5e..3d9bd0af 100644 --- a/packages/core/src/store/types.ts +++ b/packages/core/src/store/types.ts @@ -1,12 +1,6 @@ import type {MutableRefObject} from 'react'; -import type { - Coordinates, - ViewRect, - ClientRect, - LayoutRect, - UniqueIdentifier, -} from '../types'; +import type {Coordinates, ClientRect, UniqueIdentifier} from '../types'; import type {SyntheticListeners} from '../hooks/utilities'; import type {Actions} from './actions'; import type {DroppableContainersMap} from './constructors'; @@ -29,21 +23,21 @@ export interface DroppableContainer { data: DataRef; disabled: boolean; node: MutableRefObject; - rect: MutableRefObject; + rect: MutableRefObject; } export interface Active { id: UniqueIdentifier; data: DataRef; rect: MutableRefObject<{ - initial: ViewRect | null; - translated: ViewRect | null; + initial: ClientRect | null; + translated: ClientRect | null; }>; } export interface Over { id: UniqueIdentifier; - rect: LayoutRect; + rect: ClientRect; disabled: boolean; data: DataRef; } @@ -62,7 +56,7 @@ export type DraggableNodes = Record< export type DroppableContainers = DroppableContainersMap; -export type LayoutRectMap = Map; +export type RectMap = Map; export interface State { droppable: { @@ -82,23 +76,22 @@ export interface DndContextDescriptor { activatorEvent: Event | null; active: Active | null; activeNode: HTMLElement | null; - activeNodeRect: ViewRect | null; - activeNodeClientRect: ClientRect | null; + activeNodeRect: ClientRect | null; ariaDescribedById: { draggable: UniqueIdentifier; }; - containerNodeRect: ViewRect | null; + containerNodeRect: ClientRect | null; draggableNodes: DraggableNodes; droppableContainers: DroppableContainers; - droppableRects: LayoutRectMap; + droppableRects: RectMap; over: Over | null; dragOverlay: { nodeRef: MutableRefObject; - rect: ViewRect | null; + rect: ClientRect | null; setRef: (element: HTMLElement | null) => void; }; scrollableAncestors: Element[]; - scrollableAncestorRects: ViewRect[]; + scrollableAncestorRects: ClientRect[]; recomputeLayouts(): void; willRecomputeLayouts: boolean; windowRect: ClientRect | null; diff --git a/packages/core/src/types/coordinates.ts b/packages/core/src/types/coordinates.ts index 4d4655ad..bea5dc21 100644 --- a/packages/core/src/types/coordinates.ts +++ b/packages/core/src/types/coordinates.ts @@ -10,22 +10,6 @@ export type DistanceMeasurement = export type Translate = Coordinates; -export interface LayoutRect { - width: number; - height: number; - offsetLeft: number; - offsetTop: number; -} - -export interface ViewRect extends LayoutRect { - top: number; - left: number; - right: number; - bottom: number; -} - -export interface ClientRect extends ViewRect {} - export interface ScrollCoordinates { initial: Coordinates; current: Coordinates; diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 2c4a7245..ecd775ba 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -1,9 +1,6 @@ export type { Coordinates, - ClientRect, DistanceMeasurement, - LayoutRect, - ViewRect, Translate, ScrollCoordinates, } from './coordinates'; @@ -17,3 +14,4 @@ export type { } from './events'; export type {UniqueIdentifier} from './other'; export type {SyntheticEventName} from './react'; +export type {ClientRect} from './rect'; diff --git a/packages/core/src/types/rect.ts b/packages/core/src/types/rect.ts new file mode 100644 index 00000000..115a7c48 --- /dev/null +++ b/packages/core/src/types/rect.ts @@ -0,0 +1,8 @@ +export interface ClientRect { + width: number; + height: number; + top: number; + left: number; + right: number; + bottom: number; +} diff --git a/packages/core/src/utilities/algorithms/closestCenter.ts b/packages/core/src/utilities/algorithms/closestCenter.ts index fd029639..268f9562 100644 --- a/packages/core/src/utilities/algorithms/closestCenter.ts +++ b/packages/core/src/utilities/algorithms/closestCenter.ts @@ -1,5 +1,5 @@ import {distanceBetween} from '../coordinates'; -import type {Coordinates, LayoutRect, UniqueIdentifier} from '../../types'; +import type {Coordinates, ClientRect, UniqueIdentifier} from '../../types'; import type {CollisionDetection} from './types'; @@ -7,9 +7,9 @@ import type {CollisionDetection} from './types'; * Returns the coordinates of the center of a given ClientRect */ function centerOfRectangle( - rect: LayoutRect, - left = rect.offsetLeft, - top = rect.offsetTop + rect: ClientRect, + left = rect.left, + top = rect.top ): Coordinates { return { x: left + rect.width * 0.5, diff --git a/packages/core/src/utilities/algorithms/closestCorners.ts b/packages/core/src/utilities/algorithms/closestCorners.ts index 3d3569af..a7a00272 100644 --- a/packages/core/src/utilities/algorithms/closestCorners.ts +++ b/packages/core/src/utilities/algorithms/closestCorners.ts @@ -1,6 +1,5 @@ -import type {LayoutRect, UniqueIdentifier} from '../../types'; +import type {ClientRect, UniqueIdentifier} from '../../types'; import {distanceBetween} from '../coordinates'; -import {isViewRect} from '../rect'; import type {CollisionDetection} from './types'; @@ -10,9 +9,9 @@ import type {CollisionDetection} from './types'; */ function cornersOfRectangle( - rect: LayoutRect, - left = rect.offsetLeft, - top = rect.offsetTop + rect: ClientRect, + left = rect.left, + top = rect.top ) { return [ { @@ -56,11 +55,7 @@ export const closestCorners: CollisionDetection = ({ } = droppableContainer; if (rect) { - const rectCorners = cornersOfRectangle( - rect, - isViewRect(rect) ? rect.left : undefined, - isViewRect(rect) ? rect.top : undefined - ); + const rectCorners = cornersOfRectangle(rect, rect.left, rect.top); const distances = corners.reduce((accumulator, corner, index) => { return accumulator + distanceBetween(rectCorners[index], corner); }, 0); diff --git a/packages/core/src/utilities/algorithms/rectIntersection.ts b/packages/core/src/utilities/algorithms/rectIntersection.ts index bf1c98d7..d41d7008 100644 --- a/packages/core/src/utilities/algorithms/rectIntersection.ts +++ b/packages/core/src/utilities/algorithms/rectIntersection.ts @@ -1,21 +1,15 @@ -import type {LayoutRect, UniqueIdentifier, ViewRect} from '../../types'; +import type {ClientRect, UniqueIdentifier} from '../../types'; import type {CollisionDetection} from './types'; /** * Returns the intersecting rectangle area between two rectangles */ -function getIntersectionRatio(entry: LayoutRect, target: ViewRect): number { - const top = Math.max(target.top, entry.offsetTop); - const left = Math.max(target.left, entry.offsetLeft); - const right = Math.min( - target.left + target.width, - entry.offsetLeft + entry.width - ); - const bottom = Math.min( - target.top + target.height, - entry.offsetTop + entry.height - ); +function getIntersectionRatio(entry: ClientRect, target: ClientRect): number { + const top = Math.max(target.top, entry.top); + const left = Math.max(target.left, entry.left); + const right = Math.min(target.left + target.width, entry.left + entry.width); + const bottom = Math.min(target.top + target.height, entry.top + entry.height); const width = right - left; const height = bottom - top; diff --git a/packages/core/src/utilities/algorithms/types.ts b/packages/core/src/utilities/algorithms/types.ts index 6420efba..f3b68203 100644 --- a/packages/core/src/utilities/algorithms/types.ts +++ b/packages/core/src/utilities/algorithms/types.ts @@ -1,8 +1,8 @@ import type {Active, DroppableContainer} from '../../store'; -import type {UniqueIdentifier, ViewRect} from '../../types'; +import type {UniqueIdentifier, ClientRect} from '../../types'; export type CollisionDetection = (args: { active: Active; - collisionRect: ViewRect; + collisionRect: ClientRect; droppableContainers: DroppableContainer[]; }) => UniqueIdentifier | null; diff --git a/packages/core/src/utilities/coordinates/getRelativeTransformOrigin.ts b/packages/core/src/utilities/coordinates/getRelativeTransformOrigin.ts index 8dc4cec9..53a7cb45 100644 --- a/packages/core/src/utilities/coordinates/getRelativeTransformOrigin.ts +++ b/packages/core/src/utilities/coordinates/getRelativeTransformOrigin.ts @@ -1,4 +1,5 @@ import {getEventCoordinates, isKeyboardEvent} from '@dnd-kit/utilities'; +import type {ClientRect} from '../../types'; export function getRelativeTransformOrigin( event: MouseEvent | TouchEvent | KeyboardEvent, diff --git a/packages/core/src/utilities/index.ts b/packages/core/src/utilities/index.ts index ba743647..b9a6abb2 100644 --- a/packages/core/src/utilities/index.ts +++ b/packages/core/src/utilities/index.ts @@ -8,14 +8,13 @@ export { } from './coordinates'; export { + Rect, adjustScale, getAdjustedRect, + getClientRect, + getTransformAgnosticClientRect, + getWindowClientRect, getRectDelta, - getLayoutRect, - getViewportLayoutRect, - getBoundingClientRect, - getViewRect, - isViewRect, } from './rect'; export {noop} from './other'; diff --git a/packages/core/src/utilities/rect/Rect.ts b/packages/core/src/utilities/rect/Rect.ts new file mode 100644 index 00000000..5cf683a2 --- /dev/null +++ b/packages/core/src/utilities/rect/Rect.ts @@ -0,0 +1,55 @@ +import type {ClientRect} from '../../types/rect'; +import { + getScrollableAncestors, + getScrollOffsets, + getScrollXOffset, + getScrollYOffset, +} from '../scroll'; + +const properties = [ + ['x', ['left', 'right'], getScrollXOffset], + ['y', ['top', 'bottom'], getScrollYOffset], +] as const; + +export class Rect { + constructor(rect: ClientRect, element: HTMLElement) { + const scrollableAncestors = getScrollableAncestors(element); + const scrollOffsets = getScrollOffsets(scrollableAncestors); + + this.rect = {...rect}; + this.width = rect.width; + this.height = rect.height; + + for (const [axis, keys, getScrollOffset] of properties) { + for (const key of keys) { + Object.defineProperty(this, key, { + get: () => { + const currentOffsets = getScrollOffset(scrollableAncestors); + const scrollOffsetsDeltla = scrollOffsets[axis] - currentOffsets; + + return this.rect[key] + scrollOffsetsDeltla; + }, + enumerable: true, + }); + } + } + + Object.defineProperty(this, 'rect', {enumerable: false}); + } + + private rect: ClientRect; + + public width: number; + + public height: number; + + // The below properties are set by the `Object.defineProperty` calls in the constructor + // @ts-ignore + public top: number; + // @ts-ignore + public bottom: number; + // @ts-ignore + public right: number; + // @ts-ignore + public left: number; +} diff --git a/packages/core/src/utilities/rect/adjustScale.ts b/packages/core/src/utilities/rect/adjustScale.ts index 825075e6..2c7945c0 100644 --- a/packages/core/src/utilities/rect/adjustScale.ts +++ b/packages/core/src/utilities/rect/adjustScale.ts @@ -1,10 +1,10 @@ import type {Transform} from '@dnd-kit/utilities'; -import type {LayoutRect} from '../../types'; +import type {ClientRect} from '../../types'; export function adjustScale( transform: Transform, - rect1: LayoutRect | null, - rect2: LayoutRect | null + rect1: ClientRect | null, + rect2: ClientRect | null ): Transform { return { ...transform, diff --git a/packages/core/src/utilities/rect/getRect.ts b/packages/core/src/utilities/rect/getRect.ts index 162663cf..25d9c259 100644 --- a/packages/core/src/utilities/rect/getRect.ts +++ b/packages/core/src/utilities/rect/getRect.ts @@ -1,112 +1,54 @@ -import {isHTMLElement, isWindow} from '@dnd-kit/utilities'; +import {getWindow} from '@dnd-kit/utilities'; -import type {Coordinates, ClientRect, LayoutRect, ViewRect} from '../../types'; -import {getScrollableAncestors, getScrollOffsets} from '../scroll'; -import {defaultCoordinates} from '../coordinates'; +import type {ClientRect} from '../../types'; +import {inverseTransform} from '../transform'; -function getEdgeOffset( - node: HTMLElement | null, - parent: (Node & ParentNode) | null, - offset = defaultCoordinates -): Coordinates { - if (!node || !isHTMLElement(node)) { - return offset; - } - - const nodeOffset = { - x: offset.x + node.offsetLeft, - y: offset.y + node.offsetTop, - }; - - if (node.offsetParent === parent) { - return nodeOffset; - } - - return getEdgeOffset(node.offsetParent as HTMLElement, parent, nodeOffset); +interface Options { + ignoreTransform?: boolean; } -export function getLayoutRect(element: HTMLElement): LayoutRect { - const {offsetWidth: width, offsetHeight: height} = element; - const {x: offsetLeft, y: offsetTop} = getEdgeOffset(element, null); +const defaultOptions: Options = {ignoreTransform: false}; - return { - width, - height, - offsetTop, - offsetLeft, - }; -} - -export function getViewportLayoutRect(element: HTMLElement): LayoutRect { - const {width, height, top, left} = element.getBoundingClientRect(); - const scrollableAncestors = getScrollableAncestors(element); - const scrollOffsets = getScrollOffsets(scrollableAncestors); +/** + * Returns the bounding client rect of an element relative to the viewport. + */ +export function getClientRect( + element: HTMLElement, + options: Options = defaultOptions +) { + let rect: ClientRect = element.getBoundingClientRect(); - return { - width, - height, - offsetTop: top + scrollOffsets.y, - offsetLeft: left + scrollOffsets.x, - }; -} - -export function getBoundingClientRect( - element: HTMLElement | typeof window -): ClientRect { - if (isWindow(element)) { - const width = window.innerWidth; - const height = window.innerHeight; + if (options.ignoreTransform) { + const {getComputedStyle} = getWindow(element); + const {transform, transformOrigin} = getComputedStyle(element); - return { - top: 0, - left: 0, - right: width, - bottom: height, - width, - height, - offsetTop: 0, - offsetLeft: 0, - }; + if (transform) { + rect = inverseTransform(rect, transform, transformOrigin); + } } - const {offsetTop, offsetLeft} = getLayoutRect(element); - const { - width, - height, - top, - bottom, - left, - right, - } = element.getBoundingClientRect(); + const {top, left, width, height, bottom, right} = rect; return { + top, + left, width, height, - top, bottom, right, - left, - offsetTop, - offsetLeft, }; } -export function getViewRect(element: HTMLElement): ViewRect { - const {width, height, offsetTop, offsetLeft} = getLayoutRect(element); - const scrollableAncestors = getScrollableAncestors(element); - const scrollOffsets = getScrollOffsets(scrollableAncestors); - - const top = offsetTop - scrollOffsets.y; - const left = offsetLeft - scrollOffsets.x; - - return { - width, - height, - top, - bottom: top + height, - right: left + width, - left, - offsetTop, - offsetLeft, - }; +/** + * Returns the bounding client rect of an element relative to the viewport. + * + * @remarks + * The ClientRect returned by this method does not take into account transforms + * applied to the element it measures. + * + */ +export function getTransformAgnosticClientRect( + element: HTMLElement +): ClientRect { + return getClientRect(element, {ignoreTransform: true}); } diff --git a/packages/core/src/utilities/rect/getRectDelta.ts b/packages/core/src/utilities/rect/getRectDelta.ts index c32010ad..3298bc53 100644 --- a/packages/core/src/utilities/rect/getRectDelta.ts +++ b/packages/core/src/utilities/rect/getRectDelta.ts @@ -1,9 +1,9 @@ -import type {Coordinates, ViewRect} from '../../types'; +import type {Coordinates, ClientRect} from '../../types'; import {defaultCoordinates} from '../coordinates'; export function getRectDelta( - rect1: ViewRect | null, - rect2: ViewRect | null + rect1: ClientRect | null, + rect2: ClientRect | null ): Coordinates { return rect1 && rect2 ? { diff --git a/packages/core/src/utilities/rect/getWindowClientRect.ts b/packages/core/src/utilities/rect/getWindowClientRect.ts new file mode 100644 index 00000000..fd806d6a --- /dev/null +++ b/packages/core/src/utilities/rect/getWindowClientRect.ts @@ -0,0 +1,15 @@ +import type {ClientRect} from '../../types'; + +export function getWindowClientRect(element: typeof window): ClientRect { + const width = element.innerWidth; + const height = element.innerHeight; + + return { + top: 0, + left: 0, + right: width, + bottom: height, + width, + height, + }; +} diff --git a/packages/core/src/utilities/rect/index.ts b/packages/core/src/utilities/rect/index.ts index 3f43c432..e73eb967 100644 --- a/packages/core/src/utilities/rect/index.ts +++ b/packages/core/src/utilities/rect/index.ts @@ -4,11 +4,8 @@ export {getRectDelta} from './getRectDelta'; export {getAdjustedRect} from './rectAdjustment'; -export { - getBoundingClientRect, - getLayoutRect, - getViewportLayoutRect, - getViewRect, -} from './getRect'; - -export {isViewRect} from './isViewRect'; +export {getClientRect, getTransformAgnosticClientRect} from './getRect'; + +export {getWindowClientRect} from './getWindowClientRect'; + +export {Rect} from './Rect'; diff --git a/packages/core/src/utilities/rect/isViewRect.ts b/packages/core/src/utilities/rect/isViewRect.ts deleted file mode 100644 index bf403548..00000000 --- a/packages/core/src/utilities/rect/isViewRect.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type {LayoutRect, ViewRect} from '../../types'; - -export function isViewRect(entry: LayoutRect | ViewRect): entry is ViewRect { - return 'top' in entry; -} diff --git a/packages/core/src/utilities/rect/rectAdjustment.ts b/packages/core/src/utilities/rect/rectAdjustment.ts index 9a2d4df5..d421403a 100644 --- a/packages/core/src/utilities/rect/rectAdjustment.ts +++ b/packages/core/src/utilities/rect/rectAdjustment.ts @@ -1,21 +1,19 @@ -import type {Coordinates, ViewRect} from '../../types'; +import type {Coordinates, ClientRect} from '../../types'; export function createRectAdjustmentFn(modifier: number) { - return function adjustViewRect( - viewRect: ViewRect, + return function adjustClientRect( + rect: ClientRect, ...adjustments: Coordinates[] - ): ViewRect { - return adjustments.reduce( + ): ClientRect { + return adjustments.reduce( (acc, adjustment) => ({ ...acc, top: acc.top + modifier * adjustment.y, bottom: acc.bottom + modifier * adjustment.y, left: acc.left + modifier * adjustment.x, right: acc.right + modifier * adjustment.x, - offsetLeft: acc.offsetLeft + modifier * adjustment.x, - offsetTop: acc.offsetTop + modifier * adjustment.y, }), - {...viewRect} + {...rect} ); }; } diff --git a/packages/core/src/utilities/scroll/getScrollCoordinates.ts b/packages/core/src/utilities/scroll/getScrollCoordinates.ts index 210b6901..a1ff4f0c 100644 --- a/packages/core/src/utilities/scroll/getScrollCoordinates.ts +++ b/packages/core/src/utilities/scroll/getScrollCoordinates.ts @@ -2,18 +2,27 @@ import {isWindow} from '@dnd-kit/utilities'; import type {Coordinates} from '../../types'; -export function getScrollCoordinates( - element: Element | typeof window -): Coordinates { +export function getScrollXCoordinate(element: Element | typeof window): number { if (isWindow(element)) { - return { - x: element.scrollX, - y: element.scrollY, - }; + return element.scrollX; } + return element.scrollLeft; +} + +export function getScrollYCoordinate(element: Element | typeof window): number { + if (isWindow(element)) { + return element.scrollY; + } + + return element.scrollTop; +} + +export function getScrollCoordinates( + element: Element | typeof window +): Coordinates { return { - x: element.scrollLeft, - y: element.scrollTop, + x: getScrollXCoordinate(element), + y: getScrollYCoordinate(element), }; } diff --git a/packages/core/src/utilities/scroll/getScrollDirectionAndSpeed.ts b/packages/core/src/utilities/scroll/getScrollDirectionAndSpeed.ts index 917da787..6d7395f5 100644 --- a/packages/core/src/utilities/scroll/getScrollDirectionAndSpeed.ts +++ b/packages/core/src/utilities/scroll/getScrollDirectionAndSpeed.ts @@ -1,8 +1,9 @@ -import {Direction, ViewRect} from '../../types'; +import {Direction, ClientRect} from '../../types'; import {getScrollPosition} from './getScrollPosition'; import {isDocumentScrollingElement} from './documentScrollingElement'; -interface Rect extends Pick {} +interface PositionalCoordinates + extends Pick {} const defaultThreshold = { x: 0.2, @@ -11,8 +12,8 @@ const defaultThreshold = { export function getScrollDirectionAndSpeed( scrollContainer: Element, - scrollContainerRect: ViewRect, - {top, left, right, bottom}: Rect, + scrollContainerRect: ClientRect, + {top, left, right, bottom}: PositionalCoordinates, acceleration = 10, thresholdPercentage = defaultThreshold ) { diff --git a/packages/core/src/utilities/scroll/getScrollOffsets.ts b/packages/core/src/utilities/scroll/getScrollOffsets.ts index 7a3cb283..ffec40ac 100644 --- a/packages/core/src/utilities/scroll/getScrollOffsets.ts +++ b/packages/core/src/utilities/scroll/getScrollOffsets.ts @@ -1,7 +1,11 @@ import {add} from '@dnd-kit/utilities'; import type {Coordinates} from '../../types'; -import {getScrollCoordinates} from './getScrollCoordinates'; +import { + getScrollCoordinates, + getScrollXCoordinate, + getScrollYCoordinate, +} from './getScrollCoordinates'; import {defaultCoordinates} from '../coordinates'; export function getScrollOffsets(scrollableAncestors: Element[]): Coordinates { @@ -9,3 +13,15 @@ export function getScrollOffsets(scrollableAncestors: Element[]): Coordinates { return add(acc, getScrollCoordinates(node)); }, defaultCoordinates); } + +export function getScrollXOffset(scrollableAncestors: Element[]): number { + return scrollableAncestors.reduce((acc, node) => { + return acc + getScrollXCoordinate(node); + }, 0); +} + +export function getScrollYOffset(scrollableAncestors: Element[]): number { + return scrollableAncestors.reduce((acc, node) => { + return acc + getScrollYCoordinate(node); + }, 0); +} diff --git a/packages/core/src/utilities/scroll/getScrollableAncestors.ts b/packages/core/src/utilities/scroll/getScrollableAncestors.ts index 1c4b2f17..cc5a73d4 100644 --- a/packages/core/src/utilities/scroll/getScrollableAncestors.ts +++ b/packages/core/src/utilities/scroll/getScrollableAncestors.ts @@ -1,4 +1,9 @@ -import {isDocument, isHTMLElement, isSVGElement} from '@dnd-kit/utilities'; +import { + getWindow, + isDocument, + isHTMLElement, + isSVGElement, +} from '@dnd-kit/utilities'; import {isFixed} from './isFixed'; import {isScrollable} from './isScrollable'; @@ -29,10 +34,13 @@ export function getScrollableAncestors(element: Node | null): Element[] { return scrollParents; } - const computedStyle = window.getComputedStyle(node); + const {getComputedStyle} = getWindow(node); + const computedStyle = getComputedStyle(node); - if (isScrollable(node, computedStyle)) { - scrollParents.push(node); + if (node !== element) { + if (isScrollable(node, computedStyle)) { + scrollParents.push(node); + } } if (isFixed(node, computedStyle)) { @@ -42,5 +50,9 @@ export function getScrollableAncestors(element: Node | null): Element[] { return findScrollableAncestors(node.parentNode); } - return element ? findScrollableAncestors(element.parentNode) : scrollParents; + if (!element) { + return scrollParents; + } + + return findScrollableAncestors(element); } diff --git a/packages/core/src/utilities/scroll/index.ts b/packages/core/src/utilities/scroll/index.ts index dded5a1a..d68f30eb 100644 --- a/packages/core/src/utilities/scroll/index.ts +++ b/packages/core/src/utilities/scroll/index.ts @@ -3,7 +3,11 @@ export {getScrollableElement} from './getScrollableElement'; export {getScrollCoordinates} from './getScrollCoordinates'; export {getScrollDirectionAndSpeed} from './getScrollDirectionAndSpeed'; export {getScrollElementRect} from './getScrollElementRect'; -export {getScrollOffsets} from './getScrollOffsets'; +export { + getScrollOffsets, + getScrollXOffset, + getScrollYOffset, +} from './getScrollOffsets'; export {getScrollPosition} from './getScrollPosition'; export {isDocumentScrollingElement} from './documentScrollingElement'; export {isScrollable} from './isScrollable'; diff --git a/packages/core/src/utilities/scroll/isFixed.ts b/packages/core/src/utilities/scroll/isFixed.ts index f536f805..c6ecfaff 100644 --- a/packages/core/src/utilities/scroll/isFixed.ts +++ b/packages/core/src/utilities/scroll/isFixed.ts @@ -1,6 +1,8 @@ +import {getWindow} from '@dnd-kit/utilities'; + export function isFixed( node: HTMLElement, - computedStyle: CSSStyleDeclaration = window.getComputedStyle(node) + computedStyle: CSSStyleDeclaration = getWindow(node).getComputedStyle(node) ): boolean { return computedStyle.position === 'fixed'; } diff --git a/packages/core/src/utilities/scroll/isScrollable.ts b/packages/core/src/utilities/scroll/isScrollable.ts index 0529e760..574bcae4 100644 --- a/packages/core/src/utilities/scroll/isScrollable.ts +++ b/packages/core/src/utilities/scroll/isScrollable.ts @@ -1,6 +1,10 @@ +import {getWindow} from '@dnd-kit/utilities'; + export function isScrollable( - node: HTMLElement, - computedStyle: CSSStyleDeclaration = window.getComputedStyle(node) + element: HTMLElement, + computedStyle: CSSStyleDeclaration = getWindow(element).getComputedStyle( + element + ) ): boolean { const overflowRegex = /(auto|scroll|overlay)/; const properties = ['overflow', 'overflowX', 'overflowY']; diff --git a/packages/core/src/utilities/transform/index.ts b/packages/core/src/utilities/transform/index.ts new file mode 100644 index 00000000..330ceafa --- /dev/null +++ b/packages/core/src/utilities/transform/index.ts @@ -0,0 +1 @@ +export {inverseTransform} from './inverseTransform'; diff --git a/packages/core/src/utilities/transform/inverseTransform.ts b/packages/core/src/utilities/transform/inverseTransform.ts new file mode 100644 index 00000000..6095ce2d --- /dev/null +++ b/packages/core/src/utilities/transform/inverseTransform.ts @@ -0,0 +1,43 @@ +import type {ClientRect} from '../../types'; + +export function inverseTransform( + rect: ClientRect, + transform: string, + transformOrigin: string +): ClientRect { + let ta, sx, sy, dx, dy; + + if (transform.startsWith('matrix3d(')) { + ta = transform.slice(9, -1).split(/, /); + sx = +ta[0]; + sy = +ta[5]; + dx = +ta[12]; + dy = +ta[13]; + } else if (transform.startsWith('matrix(')) { + ta = transform.slice(7, -1).split(/, /); + sx = +ta[0]; + sy = +ta[3]; + dx = +ta[4]; + dy = +ta[5]; + } else { + return rect; + } + + const x = rect.left - dx - (1 - sx) * parseFloat(transformOrigin); + const y = + rect.top - + dy - + (1 - sy) * + parseFloat(transformOrigin.slice(transformOrigin.indexOf(' ') + 1)); + const w = sx ? rect.width / sx : rect.width; + const h = sy ? rect.height / sy : rect.height; + + return { + width: w, + height: h, + top: y, + right: x + w, + bottom: y + h, + left: x, + }; +} diff --git a/packages/modifiers/src/utilities/restrictToBoundingRect.ts b/packages/modifiers/src/utilities/restrictToBoundingRect.ts index c152f878..4505c37a 100644 --- a/packages/modifiers/src/utilities/restrictToBoundingRect.ts +++ b/packages/modifiers/src/utilities/restrictToBoundingRect.ts @@ -1,10 +1,10 @@ -import type {ViewRect} from '@dnd-kit/core'; +import type {ClientRect} from '@dnd-kit/core'; import type {Transform} from '@dnd-kit/utilities'; export function restrictToBoundingRect( transform: Transform, - rect: ViewRect, - boundingRect: ViewRect + rect: ClientRect, + boundingRect: ClientRect ): Transform { const value = { ...transform, diff --git a/packages/modifiers/test/modifiers.test.tsx b/packages/modifiers/test/modifiers.test.tsx index 55ea4921..a0525a64 100644 --- a/packages/modifiers/test/modifiers.test.tsx +++ b/packages/modifiers/test/modifiers.test.tsx @@ -1,18 +1,16 @@ -import type {Modifier, ViewRect} from '@dnd-kit/core'; +import type {Modifier, ClientRect} from '@dnd-kit/core'; import type {FirstArgument, Transform} from '@dnd-kit/utilities'; import {restrictToHorizontalAxis, restrictToVerticalAxis} from '../src'; describe('@dnd-kit/modifiers', () => { - const defaultRect: ViewRect = { + const defaultRect: ClientRect = { left: 0, right: 0, top: 0, bottom: 0, width: 0, height: 0, - offsetLeft: 0, - offsetTop: 0, }; const defaultTransform: Transform = { x: 0, diff --git a/packages/sortable/src/components/SortableContext.tsx b/packages/sortable/src/components/SortableContext.tsx index 61a70d2b..73013db4 100644 --- a/packages/sortable/src/components/SortableContext.tsx +++ b/packages/sortable/src/components/SortableContext.tsx @@ -1,5 +1,5 @@ import React, {MutableRefObject, useEffect, useMemo, useRef} from 'react'; -import {useDndContext, LayoutRect, UniqueIdentifier} from '@dnd-kit/core'; +import {useDndContext, ClientRect, UniqueIdentifier} from '@dnd-kit/core'; import {useIsomorphicLayoutEffect, useUniqueId} from '@dnd-kit/utilities'; import type {SortingStrategy} from '../types'; @@ -22,7 +22,7 @@ interface ContextDescriptor { items: UniqueIdentifier[]; overIndex: number; useDragOverlay: boolean; - sortedRects: LayoutRect[]; + sortedRects: ClientRect[]; strategy: SortingStrategy; wasDragging: MutableRefObject; } diff --git a/packages/sortable/src/hooks/useSortable.ts b/packages/sortable/src/hooks/useSortable.ts index ccbfa2c0..93418357 100644 --- a/packages/sortable/src/hooks/useSortable.ts +++ b/packages/sortable/src/hooks/useSortable.ts @@ -91,7 +91,7 @@ export function useSortable({ const finalTransform = displaceItem ? dragSourceDisplacement ?? strategy({ - layoutRects: sortedRects, + rects: sortedRects, activeNodeRect, activeIndex, overIndex, diff --git a/packages/sortable/src/hooks/utilities/useDerivedTransform.ts b/packages/sortable/src/hooks/utilities/useDerivedTransform.ts index 704746e6..6ae113d5 100644 --- a/packages/sortable/src/hooks/utilities/useDerivedTransform.ts +++ b/packages/sortable/src/hooks/utilities/useDerivedTransform.ts @@ -1,9 +1,9 @@ import {useEffect, useRef, useState} from 'react'; -import {getBoundingClientRect, LayoutRect} from '@dnd-kit/core'; +import {getClientRect, ClientRect} from '@dnd-kit/core'; import {Transform, useIsomorphicLayoutEffect} from '@dnd-kit/utilities'; interface Arguments { - rect: React.MutableRefObject; + rect: React.MutableRefObject; disabled: boolean; index: number; node: React.MutableRefObject; @@ -24,11 +24,13 @@ export function useDerivedTransform({disabled, index, node, rect}: Arguments) { const initial = rect.current; if (initial) { - const current = getBoundingClientRect(node.current); + const current = getClientRect(node.current, { + ignoreTransform: true, + }); const delta = { - x: initial.offsetLeft - current.offsetLeft, - y: initial.offsetTop - current.offsetTop, + x: initial.left - current.left, + y: initial.top - current.top, scaleX: initial.width / current.width, scaleY: initial.height / current.height, }; diff --git a/packages/sortable/src/sensors/keyboard/sortableKeyboardCoordinates.ts b/packages/sortable/src/sensors/keyboard/sortableKeyboardCoordinates.ts index a752a95e..5dc1d27f 100644 --- a/packages/sortable/src/sensors/keyboard/sortableKeyboardCoordinates.ts +++ b/packages/sortable/src/sensors/keyboard/sortableKeyboardCoordinates.ts @@ -1,6 +1,5 @@ import { closestCorners, - getViewRect, getScrollableAncestors, KeyboardCode, DroppableContainer, @@ -16,12 +15,12 @@ const directions: string[] = [ export const sortableKeyboardCoordinates: KeyboardCoordinateGetter = ( event, - {context: {active, droppableContainers, translatedRect, scrollableAncestors}} + {context: {active, droppableContainers, collisionRect, scrollableAncestors}} ) => { if (directions.includes(event.code)) { event.preventDefault(); - if (!active || !translatedRect) { + if (!active || !collisionRect) { return; } @@ -32,39 +31,31 @@ export const sortableKeyboardCoordinates: KeyboardCoordinateGetter = ( return; } - const node = entry?.node.current; + const rect = entry?.rect.current; - if (!node) { + if (!rect) { return; } - const rect = getViewRect(node); - const container: DroppableContainer = { - ...entry, - rect: { - current: rect, - }, - }; - switch (event.code) { case KeyboardCode.Down: - if (translatedRect.top + translatedRect.height <= rect.top) { - filteredContainers.push(container); + if (collisionRect.top + collisionRect.height <= rect.top) { + filteredContainers.push(entry); } break; case KeyboardCode.Up: - if (translatedRect.top >= rect.top + rect.height) { - filteredContainers.push(container); + if (collisionRect.top >= rect.top + rect.height) { + filteredContainers.push(entry); } break; case KeyboardCode.Left: - if (translatedRect.left >= rect.left + rect.width) { - filteredContainers.push(container); + if (collisionRect.left >= rect.left + rect.width) { + filteredContainers.push(entry); } break; case KeyboardCode.Right: - if (translatedRect.left + translatedRect.width <= rect.left) { - filteredContainers.push(container); + if (collisionRect.left + collisionRect.width <= rect.left) { + filteredContainers.push(entry); } break; } @@ -72,27 +63,28 @@ export const sortableKeyboardCoordinates: KeyboardCoordinateGetter = ( const closestId = closestCorners({ active, - collisionRect: translatedRect, + collisionRect: collisionRect, droppableContainers: filteredContainers, }); if (closestId) { - const newNode = droppableContainers.get(closestId)?.node.current; + const newDroppable = droppableContainers.get(closestId); + const newNode = newDroppable?.node.current; + const newRect = newDroppable?.rect.current; - if (newNode) { + if (newNode && newRect) { const newScrollAncestors = getScrollableAncestors(newNode); const hasDifferentScrollAncestors = newScrollAncestors.some( (element, index) => scrollableAncestors[index] !== element ); - const newRect = getViewRect(newNode); const offset = hasDifferentScrollAncestors ? { x: 0, y: 0, } : { - x: translatedRect.width - newRect.width, - y: translatedRect.height - newRect.height, + x: collisionRect.width - newRect.width, + y: collisionRect.height - newRect.height, }; const newCoordinates = { x: newRect.left - offset.x, diff --git a/packages/sortable/src/strategies/horizontalListSorting.ts b/packages/sortable/src/strategies/horizontalListSorting.ts index 54ce733d..befc8962 100644 --- a/packages/sortable/src/strategies/horizontalListSorting.ts +++ b/packages/sortable/src/strategies/horizontalListSorting.ts @@ -1,4 +1,4 @@ -import type {LayoutRect} from '@dnd-kit/core'; +import type {ClientRect} from '@dnd-kit/core'; import type {SortingStrategy} from '../types'; // To-do: We should be calculating scale transformation @@ -8,22 +8,22 @@ const defaultScale = { }; export const horizontalListSortingStrategy: SortingStrategy = ({ - layoutRects, + rects, activeNodeRect: fallbackActiveRect, activeIndex, overIndex, index, }) => { - const activeNodeRect = layoutRects[activeIndex] ?? fallbackActiveRect; + const activeNodeRect = rects[activeIndex] ?? fallbackActiveRect; if (!activeNodeRect) { return null; } - const itemGap = getItemGap(layoutRects, index, activeIndex); + const itemGap = getItemGap(rects, index, activeIndex); if (index === activeIndex) { - const newIndexRect = layoutRects[overIndex]; + const newIndexRect = rects[overIndex]; if (!newIndexRect) { return null; @@ -32,10 +32,10 @@ export const horizontalListSortingStrategy: SortingStrategy = ({ return { x: activeIndex < overIndex - ? newIndexRect.offsetLeft + + ? newIndexRect.left + newIndexRect.width - - (activeNodeRect.offsetLeft + activeNodeRect.width) - : newIndexRect.offsetLeft - activeNodeRect.offsetLeft, + (activeNodeRect.left + activeNodeRect.width) + : newIndexRect.left - activeNodeRect.left, y: 0, ...defaultScale, }; @@ -64,14 +64,10 @@ export const horizontalListSortingStrategy: SortingStrategy = ({ }; }; -function getItemGap( - layoutRects: LayoutRect[], - index: number, - activeIndex: number -) { - const currentRect: LayoutRect | undefined = layoutRects[index]; - const previousRect: LayoutRect | undefined = layoutRects[index - 1]; - const nextRect: LayoutRect | undefined = layoutRects[index + 1]; +function getItemGap(rects: ClientRect[], index: number, activeIndex: number) { + const currentRect: ClientRect | undefined = rects[index]; + const previousRect: ClientRect | undefined = rects[index - 1]; + const nextRect: ClientRect | undefined = rects[index + 1]; if (!currentRect || (!previousRect && !nextRect)) { return 0; @@ -79,11 +75,11 @@ function getItemGap( if (activeIndex < index) { return previousRect - ? currentRect.offsetLeft - (previousRect.offsetLeft + previousRect.width) - : nextRect.offsetLeft - (currentRect.offsetLeft + currentRect.width); + ? currentRect.left - (previousRect.left + previousRect.width) + : nextRect.left - (currentRect.left + currentRect.width); } return nextRect - ? nextRect.offsetLeft - (currentRect.offsetLeft + currentRect.width) - : currentRect.offsetLeft - (previousRect.offsetLeft + previousRect.width); + ? nextRect.left - (currentRect.left + currentRect.width) + : currentRect.left - (previousRect.left + previousRect.width); } diff --git a/packages/sortable/src/strategies/rectSorting.ts b/packages/sortable/src/strategies/rectSorting.ts index fea0587c..cc4c194e 100644 --- a/packages/sortable/src/strategies/rectSorting.ts +++ b/packages/sortable/src/strategies/rectSorting.ts @@ -2,14 +2,14 @@ import {arrayMove} from '../utilities'; import type {SortingStrategy} from '../types'; export const rectSortingStrategy: SortingStrategy = ({ - layoutRects, + rects, activeIndex, overIndex, index, }) => { - const newRects = arrayMove(layoutRects, overIndex, activeIndex); + const newRects = arrayMove(rects, overIndex, activeIndex); - const oldRect = layoutRects[index]; + const oldRect = rects[index]; const newRect = newRects[index]; if (!newRect || !oldRect) { @@ -17,8 +17,8 @@ export const rectSortingStrategy: SortingStrategy = ({ } return { - x: newRect.offsetLeft - oldRect.offsetLeft, - y: newRect.offsetTop - oldRect.offsetTop, + x: newRect.left - oldRect.left, + y: newRect.top - oldRect.top, scaleX: newRect.width / oldRect.width, scaleY: newRect.height / oldRect.height, }; diff --git a/packages/sortable/src/strategies/rectSwapping.ts b/packages/sortable/src/strategies/rectSwapping.ts index 74b9f62e..27402712 100644 --- a/packages/sortable/src/strategies/rectSwapping.ts +++ b/packages/sortable/src/strategies/rectSwapping.ts @@ -3,20 +3,20 @@ import type {SortingStrategy} from '../types'; export const rectSwappingStrategy: SortingStrategy = ({ activeIndex, index, - layoutRects, + rects, overIndex, }) => { let oldRect; let newRect; if (index === activeIndex) { - oldRect = layoutRects[index]; - newRect = layoutRects[overIndex]; + oldRect = rects[index]; + newRect = rects[overIndex]; } if (index === overIndex) { - oldRect = layoutRects[index]; - newRect = layoutRects[activeIndex]; + oldRect = rects[index]; + newRect = rects[activeIndex]; } if (!newRect || !oldRect) { @@ -24,8 +24,8 @@ export const rectSwappingStrategy: SortingStrategy = ({ } return { - x: newRect.offsetLeft - oldRect.offsetLeft, - y: newRect.offsetTop - oldRect.offsetTop, + x: newRect.left - oldRect.left, + y: newRect.top - oldRect.top, scaleX: newRect.width / oldRect.width, scaleY: newRect.height / oldRect.height, }; diff --git a/packages/sortable/src/strategies/verticalListSorting.ts b/packages/sortable/src/strategies/verticalListSorting.ts index fd0d48a6..e4538647 100644 --- a/packages/sortable/src/strategies/verticalListSorting.ts +++ b/packages/sortable/src/strategies/verticalListSorting.ts @@ -1,4 +1,4 @@ -import type {LayoutRect} from '@dnd-kit/core'; +import type {ClientRect} from '@dnd-kit/core'; import type {SortingStrategy} from '../types'; // To-do: We should be calculating scale transformation @@ -11,17 +11,17 @@ export const verticalListSortingStrategy: SortingStrategy = ({ activeIndex, activeNodeRect: fallbackActiveRect, index, - layoutRects, + rects, overIndex, }) => { - const activeNodeRect = layoutRects[activeIndex] ?? fallbackActiveRect; + const activeNodeRect = rects[activeIndex] ?? fallbackActiveRect; if (!activeNodeRect) { return null; } if (index === activeIndex) { - const overIndexRect = layoutRects[overIndex]; + const overIndexRect = rects[overIndex]; if (!overIndexRect) { return null; @@ -31,15 +31,15 @@ export const verticalListSortingStrategy: SortingStrategy = ({ x: 0, y: activeIndex < overIndex - ? overIndexRect.offsetTop + + ? overIndexRect.top + overIndexRect.height - - (activeNodeRect.offsetTop + activeNodeRect.height) - : overIndexRect.offsetTop - activeNodeRect.offsetTop, + (activeNodeRect.top + activeNodeRect.height) + : overIndexRect.top - activeNodeRect.top, ...defaultScale, }; } - const itemGap = getItemGap(layoutRects, index, activeIndex); + const itemGap = getItemGap(rects, index, activeIndex); if (index > activeIndex && index <= overIndex) { return { @@ -65,13 +65,13 @@ export const verticalListSortingStrategy: SortingStrategy = ({ }; function getItemGap( - layoutRects: LayoutRect[], + clientRects: ClientRect[], index: number, activeIndex: number ) { - const currentRect: LayoutRect | undefined = layoutRects[index]; - const previousRect: LayoutRect | undefined = layoutRects[index - 1]; - const nextRect: LayoutRect | undefined = layoutRects[index + 1]; + const currentRect: ClientRect | undefined = clientRects[index]; + const previousRect: ClientRect | undefined = clientRects[index - 1]; + const nextRect: ClientRect | undefined = clientRects[index + 1]; if (!currentRect) { return 0; @@ -79,15 +79,15 @@ function getItemGap( if (activeIndex < index) { return previousRect - ? currentRect.offsetTop - (previousRect.offsetTop + previousRect.height) + ? currentRect.top - (previousRect.top + previousRect.height) : nextRect - ? nextRect.offsetTop - (currentRect.offsetTop + currentRect.height) + ? nextRect.top - (currentRect.top + currentRect.height) : 0; } return nextRect - ? nextRect.offsetTop - (currentRect.offsetTop + currentRect.height) + ? nextRect.top - (currentRect.top + currentRect.height) : previousRect - ? currentRect.offsetTop - (previousRect.offsetTop + previousRect.height) + ? currentRect.top - (previousRect.top + previousRect.height) : 0; } diff --git a/packages/sortable/src/types/index.ts b/packages/sortable/src/types/index.ts index 15ff01a2..5ea719df 100644 --- a/packages/sortable/src/types/index.ts +++ b/packages/sortable/src/types/index.ts @@ -1,10 +1,10 @@ -import type {LayoutRect, ViewRect} from '@dnd-kit/core'; +import type {ClientRect} from '@dnd-kit/core'; import type {Transform} from '@dnd-kit/utilities'; export type SortingStrategy = (args: { - activeNodeRect: ViewRect | null; + activeNodeRect: ClientRect | null; activeIndex: number; index: number; - layoutRects: LayoutRect[]; + rects: ClientRect[]; overIndex: number; }) => Transform | null; diff --git a/packages/sortable/src/utilities/getSortedRects.ts b/packages/sortable/src/utilities/getSortedRects.ts index c65c689f..4989c017 100644 --- a/packages/sortable/src/utilities/getSortedRects.ts +++ b/packages/sortable/src/utilities/getSortedRects.ts @@ -1,18 +1,18 @@ import type { - LayoutRect, + ClientRect, UniqueIdentifier, UseDndContextReturnValue, } from '@dnd-kit/core'; export function getSortedRects( items: UniqueIdentifier[], - layoutRects: UseDndContextReturnValue['droppableRects'] + rects: UseDndContextReturnValue['droppableRects'] ) { - return items.reduce((accumulator, id, index) => { - const layoutRect = layoutRects.get(id); + return items.reduce((accumulator, id, index) => { + const rect = rects.get(id); - if (layoutRect) { - accumulator[index] = layoutRect; + if (rect) { + accumulator[index] = rect; } return accumulator; diff --git a/packages/utilities/src/hooks/useNodeRef.ts b/packages/utilities/src/hooks/useNodeRef.ts index a29c8939..2f6f0461 100644 --- a/packages/utilities/src/hooks/useNodeRef.ts +++ b/packages/utilities/src/hooks/useNodeRef.ts @@ -1,10 +1,14 @@ import {useRef, useCallback} from 'react'; -export function useNodeRef() { +export function useNodeRef(onChange?: (element: HTMLElement | null) => void) { const node = useRef(null); - const setNodeRef = useCallback((element: HTMLElement | null) => { - node.current = element; - }, []); + const setNodeRef = useCallback( + (element: HTMLElement | null) => { + node.current = element; + onChange?.(element); + }, + [onChange] + ); return [node, setNodeRef] as const; } diff --git a/stories/2 - Presets/Sortable/1-Vertical.story.tsx b/stories/2 - Presets/Sortable/1-Vertical.story.tsx index 71633254..954786ab 100644 --- a/stories/2 - Presets/Sortable/1-Vertical.story.tsx +++ b/stories/2 - Presets/Sortable/1-Vertical.story.tsx @@ -163,3 +163,9 @@ export const RemovableItems = () => { /> ); }; + +export const TransformedContainer = () => { + return ( + + ); +}; diff --git a/stories/2 - Presets/Sortable/5-Virtualized.story.tsx b/stories/2 - Presets/Sortable/5-Virtualized.story.tsx index 7f806ba4..b32299d3 100644 --- a/stories/2 - Presets/Sortable/5-Virtualized.story.tsx +++ b/stories/2 - Presets/Sortable/5-Virtualized.story.tsx @@ -77,6 +77,7 @@ function Sortable({ className={styles.VirtualList} itemCount={items.length} itemSize={64} + stickyIndices={activeId ? [items.indexOf(activeId)] : undefined} renderItem={({index, style}) => { const id = items[index]; @@ -88,10 +89,10 @@ function Sortable({ handle={handle} wrapperStyle={() => ({ ...style, - opacity: id === activeId ? 0 : undefined, padding: 5, })} style={getItemStyles} + useDragOverlay /> ); }} diff --git a/stories/2 - Presets/Sortable/MultipleContainers.tsx b/stories/2 - Presets/Sortable/MultipleContainers.tsx index e06ffc1b..ce5776b5 100644 --- a/stories/2 - Presets/Sortable/MultipleContainers.tsx +++ b/stories/2 - Presets/Sortable/MultipleContainers.tsx @@ -314,8 +314,8 @@ export function MultipleContainers({ const isBelowOverItem = over && active.rect.current.translated && - active.rect.current.translated.offsetTop > - over.rect.offsetTop + over.rect.height; + active.rect.current.translated.top > + over.rect.top + over.rect.height; const modifier = isBelowOverItem ? 1 : 0; diff --git a/stories/2 - Presets/Sortable/Sortable.tsx b/stories/2 - Presets/Sortable/Sortable.tsx index 119e35df..acdd06c4 100644 --- a/stories/2 - Presets/Sortable/Sortable.tsx +++ b/stories/2 - Presets/Sortable/Sortable.tsx @@ -51,6 +51,7 @@ export interface Props { removable?: boolean; reorderItems?: typeof arrayMove; strategy?: SortingStrategy; + style?: React.CSSProperties; useDragOverlay?: boolean; getItemStyles?(args: { id: UniqueIdentifier; @@ -100,6 +101,7 @@ export function Sortable({ renderItem, reorderItems = arrayMove, strategy = rectSortingStrategy, + style, useDragOverlay = true, wrapperStyle = () => ({}), }: Props) { @@ -199,7 +201,7 @@ export function Sortable({ measuring={measuring} modifiers={modifiers} > - + {items.map((value, index) => ( diff --git a/stories/3 - Examples/FormElements/Switch/Switch.tsx b/stories/3 - Examples/FormElements/Switch/Switch.tsx index df347738..9a058a7c 100644 --- a/stories/3 - Examples/FormElements/Switch/Switch.tsx +++ b/stories/3 - Examples/FormElements/Switch/Switch.tsx @@ -3,7 +3,7 @@ import { DndContext, DragEndEvent, DragOverEvent, - getBoundingClientRect, + getClientRect, MeasuringConfiguration, PointerSensor, useSensor, @@ -32,7 +32,7 @@ export interface Props { const modifiers = [restrictToParentElement, restrictToHorizontalAxis]; const measuring: MeasuringConfiguration = { draggable: { - measure: getBoundingClientRect, + measure: getClientRect, }, }; diff --git a/stories/3 - Examples/Games/PlayingCards/PlayingCard/PlayingCard.module.css b/stories/3 - Examples/Games/PlayingCards/PlayingCard/PlayingCard.module.css index 85f3dcee..efbce21c 100644 --- a/stories/3 - Examples/Games/PlayingCards/PlayingCard/PlayingCard.module.css +++ b/stories/3 - Examples/Games/PlayingCards/PlayingCard/PlayingCard.module.css @@ -3,10 +3,9 @@ $box-shadow: 1px 1px 0 1px #f9f9fb, -1px 0 28px 0 rgba(34, 33, 81, 0.01), .Wrapper { position: relative; - width: 1px; + width: 140px; height: 180px; margin-bottom: -145px; - transform: translate3d(0, var(--translate-y, 0), 0); } .PlayingCard { @@ -22,9 +21,9 @@ $box-shadow: 1px 1px 0 1px #f9f9fb, -1px 0 28px 0 rgba(34, 33, 81, 0.01), font-family: 'Roboto Slab', Helvetica, sans-serif; user-select: none; transform-origin: 0 0; - transform: scale(var(--scale, 1)) translate3d(var(--translate-x, 0), 0, 0) + transform: scale(var(--scale, 1)) translate3d(var(--translate-x, 0), var(--translate-y, 0), 0) rotateX(60deg) rotateZ(33deg); - transition: transform 250ms ease; + transition: var(--transition, transform 250ms ease); &:hover:not(.pickedUp) { --translate-x: 5px; @@ -50,7 +49,8 @@ $box-shadow: 1px 1px 0 1px #f9f9fb, -1px 0 28px 0 rgba(34, 33, 81, 0.01), } &.pickedUp { - margin-left: 0; + --translate-x: 15px; + opacity: 0.95; animation: pop-transform 250ms cubic-bezier(0.18, 0.67, 0.6, 1.22); transition: box-shadow 250ms ease; @@ -99,12 +99,12 @@ sub { @keyframes pop-transform { 0% { - transform: scale(1) rotateX(60deg) rotateZ(33deg); + transform: scale(1) translate3d(var(--translate-x, 0), 0, 0) rotateX(60deg) rotateZ(33deg); box-shadow: 1px 1px 0 1px #f9f9fb, -1px 0 28px 0 rgba(34, 33, 81, 0.01), 28px 28px 28px 0 rgba(34, 33, 81, 0.25); } 100% { - transform: scale(1.075) rotateX(60deg) rotateZ(33deg); + transform: scale(1.075) translate3d(var(--translate-x, 0), 0, 0) rotateX(60deg) rotateZ(33deg); box-shadow: $box-shadow; } } diff --git a/stories/3 - Examples/Games/PlayingCards/PlayingCard/PlayingCard.tsx b/stories/3 - Examples/Games/PlayingCards/PlayingCard/PlayingCard.tsx index 9c480318..6494ef3f 100644 --- a/stories/3 - Examples/Games/PlayingCards/PlayingCard/PlayingCard.tsx +++ b/stories/3 - Examples/Games/PlayingCards/PlayingCard/PlayingCard.tsx @@ -45,8 +45,8 @@ export const PlayingCard = forwardRef( { '--translate-y': transform ? `${transform.y}px` : undefined, '--index': index, + '--transition': transition, zIndex: style?.zIndex, - transition, } as React.CSSProperties } > diff --git a/stories/3 - Examples/Tree/SortableTree.tsx b/stories/3 - Examples/Tree/SortableTree.tsx index ee9b02cd..f82ce12f 100644 --- a/stories/3 - Examples/Tree/SortableTree.tsx +++ b/stories/3 - Examples/Tree/SortableTree.tsx @@ -13,7 +13,6 @@ import { DragMoveEvent, DragEndEvent, DragOverEvent, - Measuring, MeasuringStrategy, DropAnimation, defaultDropAnimation, @@ -173,7 +172,6 @@ export function SortableTree({ ))} {createPortal( - + {activeId && activeItem ? ( setProperty(items, id, 'collapsed', (value) => { - console.log(value); return !value; }) ); diff --git a/stories/3 - Examples/Tree/components/TreeItem/TreeItem.module.css b/stories/3 - Examples/Tree/components/TreeItem/TreeItem.module.css index 67cacbb1..ea022e42 100644 --- a/stories/3 - Examples/Tree/components/TreeItem/TreeItem.module.css +++ b/stories/3 - Examples/Tree/components/TreeItem/TreeItem.module.css @@ -7,7 +7,8 @@ &.clone { display: inline-block; pointer-events: none; - padding: 5px; + padding: 0; + margin-left: 10px; .TreeItem { --vertical-padding: 5px; diff --git a/stories/3 - Examples/Tree/keyboardCoordinates.ts b/stories/3 - Examples/Tree/keyboardCoordinates.ts index 981ef1b7..76b4281d 100644 --- a/stories/3 - Examples/Tree/keyboardCoordinates.ts +++ b/stories/3 - Examples/Tree/keyboardCoordinates.ts @@ -1,9 +1,9 @@ import { closestCorners, - getViewRect, + getClientRect, KeyboardCode, - RectEntry, KeyboardCoordinateGetter, + DroppableContainer, } from '@dnd-kit/core'; import type {SensorContext} from './types'; @@ -24,18 +24,17 @@ export const sortableTreeKeyboardCoordinates: ( ) => KeyboardCoordinateGetter = (context, indentationWidth) => ( event, { - active, currentCoordinates, - context: {over, translatedRect, droppableContainers}, + context: {active, over, collisionRect, droppableContainers}, } ) => { if (directions.includes(event.code)) { - event.preventDefault(); - - if (!translatedRect) { + if (!active || !collisionRect) { return; } + event.preventDefault(); + const { current: {items, offset}, } = context; @@ -43,7 +42,7 @@ export const sortableTreeKeyboardCoordinates: ( if (horizontal.includes(event.code) && over?.id) { const {depth, maxDepth, minDepth} = getProjection( items, - active, + active.id, over.id, offset, indentationWidth @@ -71,58 +70,64 @@ export const sortableTreeKeyboardCoordinates: ( return undefined; } - const layoutRects: RectEntry[] = []; + const containers: DroppableContainer[] = []; const overRect = over?.id - ? droppableContainers[over.id]?.rect.current + ? droppableContainers.get(over.id)?.rect.current : undefined; - Object.entries(droppableContainers).forEach(([id, container]) => { - if (container?.disabled || !overRect) { - return; - } + if (overRect) { + droppableContainers.forEach((container) => { + if (container?.disabled) { + return; + } - const rect = container?.rect.current; + const rect = container?.rect.current; - if (!rect) { - return; - } + if (!rect) { + return; + } - switch (event.code) { - case KeyboardCode.Down: - if (overRect.offsetTop < rect.offsetTop) { - layoutRects.push([id, rect]); - } - break; - case KeyboardCode.Up: - if (overRect.offsetTop > rect.offsetTop) { - layoutRects.push([id, rect]); - } - break; - } - }); + switch (event.code) { + case KeyboardCode.Down: + if (overRect.top < rect.top) { + containers.push(container); + } + break; + case KeyboardCode.Up: + if (overRect.top > rect.top) { + containers.push(container); + } + break; + } + }); + } - const closestId = closestCorners(layoutRects, translatedRect); + const closestId = closestCorners({ + active, + collisionRect: collisionRect, + droppableContainers: containers, + }); if (closestId && over?.id) { - const newNode = droppableContainers[closestId]?.node.current; - const activeNodeRect = droppableContainers[active]?.rect.current; + const newNode = droppableContainers.get(closestId)?.node.current; + const activeNodeRect = droppableContainers.get(active.id)?.rect.current; if (newNode && activeNodeRect) { - const newRect = getViewRect(newNode); + const newRect = getClientRect(newNode, {ignoreTransform: true}); const newItem = items.find(({id}) => id === closestId); - const activeItem = items.find(({id}) => id === active); + const activeItem = items.find(({id}) => id === active.id); if (newItem && activeItem) { const {depth} = getProjection( items, - active, + active.id, closestId, (newItem.depth - activeItem.depth) * indentationWidth, indentationWidth ); const offset = - newRect.offsetTop > activeNodeRect.offsetTop + newRect.top > activeNodeRect.top ? Math.abs(activeNodeRect.height - newRect.height) : 0;