Skip to content

Commit

Permalink
Improve default sortableKeyboardCoordinates getter
Browse files Browse the repository at this point in the history
  • Loading branch information
clauderic committed May 19, 2022
1 parent 59ca82b commit 5811986
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/export-data-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@dnd-kit/core': patch
---

The `Data` and `DataRef` types are now exported by `@dnd-kit/core`.
8 changes: 8 additions & 0 deletions .changeset/sortable-keyboard-coordinates.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
'@dnd-kit/sortable': major
---

Changes to the default `sortableKeyboardCoordinates` KeyboardSensor coordinate getter.

#### Better handling of variable sizes

The default `sortableKeyboardCoordinates` function now has better handling of lists that have items of variable sizes. We recommend that consumers re-order lists `onDragOver` instead of `onDragEnd` when sorting lists of variable sizes via the keyboard for optimal compatibility.

#### Better handling of overlapping droppables

The default `sortableKeyboardCoordinates` function that is exported from the `@dnd-kit/sortable` package has been updated to better handle cases where the collision rectangle is overlapping droppable rectangles. For example, for `down` arrow key, the default function had logic that would only consider collisions against droppables that were below the `bottom` edge of the collision rect. This was problematic when the collision rect was overlapping droppable rects, because it meant that it's bottom edge was below the top edge of the droppable, and that resulted in that droppable being skipped.

```diff
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export type {

export type {
Active,
Data,
DataRef,
PublicContextDescriptor as DndContextDescriptor,
DraggableNode,
DroppableContainers,
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ export interface DraggableElement {
disabled: boolean;
}

export type Data = Record<string, any>;
type AnyData = Record<string, any>;

export type DataRef = MutableRefObject<Data | undefined>;
export type Data<T = AnyData> = T & AnyData;

export type DataRef<T = AnyData> = MutableRefObject<Data<T> | undefined>;

export interface DroppableContainer {
id: UniqueIdentifier;
Expand Down
6 changes: 4 additions & 2 deletions packages/sortable/src/hooks/useSortable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {
UseDraggableArguments,
UseDroppableArguments,
} from '@dnd-kit/core';
import type {Data} from '@dnd-kit/core';
import {CSS, isKeyboardEvent, useCombinedRefs} from '@dnd-kit/utilities';

import {Context} from '../components';
import type {SortingStrategy} from '../types';
import type {SortableData, SortingStrategy} from '../types';
import {isValidIndex} from '../utilities';
import {
defaultAnimateLayoutChanges,
Expand Down Expand Up @@ -56,7 +57,7 @@ export function useSortable({
strategy: globalStrategy,
} = useContext(Context);
const index = items.indexOf(id);
const data = useMemo(
const data = useMemo<SortableData & Data>(
() => ({sortable: {containerId, index, items}, ...customData}),
[containerId, customData, index, items]
);
Expand Down Expand Up @@ -168,6 +169,7 @@ export function useSortable({
active,
activeIndex,
attributes,
data,
rect,
index,
newIndex,
Expand Down
3 changes: 2 additions & 1 deletion packages/sortable/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export {
} from './strategies';
export {sortableKeyboardCoordinates} from './sensors';
export {arrayMove, arraySwap} from './utilities';
export type {SortingStrategy} from './types';
export {hasSortableData} from './types';
export type {SortableData, SortingStrategy} from './types';
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {
DroppableContainer,
KeyboardCoordinateGetter,
} from '@dnd-kit/core';
import {subtract} from '@dnd-kit/utilities';

import {hasSortableData} from '../../types';

const directions: string[] = [
KeyboardCode.Down,
Expand Down Expand Up @@ -41,7 +44,7 @@ export const sortableKeyboardCoordinates: KeyboardCoordinateGetter = (
return;
}

const rect = entry?.rect.current;
const rect = droppableRects.get(entry.id);

if (!rect) {
return;
Expand Down Expand Up @@ -85,33 +88,64 @@ export const sortableKeyboardCoordinates: KeyboardCoordinateGetter = (
}

if (closestId != null) {
const activeDroppable = droppableContainers.get(active.id);
const newDroppable = droppableContainers.get(closestId);
const newRect = newDroppable ? droppableRects.get(newDroppable.id) : null;
const newNode = newDroppable?.node.current;
const newRect = newDroppable?.rect.current;

if (newNode && newRect) {
if (newNode && newRect && activeDroppable && newDroppable) {
const newScrollAncestors = getScrollableAncestors(newNode);
const hasDifferentScrollAncestors = newScrollAncestors.some(
(element, index) => scrollableAncestors[index] !== element
);
const offset = hasDifferentScrollAncestors
? {
x: 0,
y: 0,
}
: {
x: collisionRect.width - newRect.width,
y: collisionRect.height - newRect.height,
};
const newCoordinates = {
x: newRect.left - offset.x,
y: newRect.top - offset.y,
const hasSameContainer = isSameContainer(activeDroppable, newDroppable);
const isAfterActive = isAfter(activeDroppable, newDroppable);
const offset =
hasDifferentScrollAncestors || !hasSameContainer
? {
x: 0,
y: 0,
}
: {
x: isAfterActive ? collisionRect.width - newRect.width : 0,
y: isAfterActive ? collisionRect.height - newRect.height : 0,
};
const rectCoordinates = {
x: newRect.left,
y: newRect.top,
};

const newCoordinates =
offset.x && offset.y
? rectCoordinates
: subtract(rectCoordinates, offset);

return newCoordinates;
}
}
}

return undefined;
};

function isSameContainer(a: DroppableContainer, b: DroppableContainer) {
if (!hasSortableData(a) || !hasSortableData(b)) {
return false;
}

return (
a.data.current.sortable.containerId === b.data.current.sortable.containerId
);
}

function isAfter(a: DroppableContainer, b: DroppableContainer) {
if (!hasSortableData(a) || !hasSortableData(b)) {
return false;
}

if (!isSameContainer(a, b)) {
return false;
}

return a.data.current.sortable.index < b.data.current.sortable.index;
}
9 changes: 9 additions & 0 deletions packages/sortable/src/types/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type {UniqueIdentifier} from '@dnd-kit/core';

export type SortableData = {
sortable: {
containerId: UniqueIdentifier;
items: UniqueIdentifier[];
index: number;
};
};
13 changes: 3 additions & 10 deletions packages/sortable/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
import type {ClientRect} from '@dnd-kit/core';
import type {Transform} from '@dnd-kit/utilities';

export type SortingStrategy = (args: {
activeNodeRect: ClientRect | null;
activeIndex: number;
index: number;
rects: ClientRect[];
overIndex: number;
}) => Transform | null;
export type {SortableData} from './data';
export type {SortingStrategy} from './strategies';
export {hasSortableData} from './type-guard';
10 changes: 10 additions & 0 deletions packages/sortable/src/types/strategies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type {ClientRect} from '@dnd-kit/core';
import type {Transform} from '@dnd-kit/utilities';

export type SortingStrategy = (args: {
activeNodeRect: ClientRect | null;
activeIndex: number;
index: number;
rects: ClientRect[];
overIndex: number;
}) => Transform | null;
26 changes: 26 additions & 0 deletions packages/sortable/src/types/type-guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type {Data, DroppableContainer, DraggableNode} from '@dnd-kit/core';

import type {SortableData} from './data';

export function hasSortableData<T extends DraggableNode | DroppableContainer>(
entry: T | null | undefined
): entry is T & {data: {current: Data<SortableData>}} {
if (!entry) {
return false;
}

const data = entry.data.current;

if (
data &&
'sortable' in data &&
typeof data.sortable === 'object' &&
'containerId' in data.sortable &&
'items' in data.sortable &&
'index' in data.sortable
) {
return true;
}

return false;
}

0 comments on commit 5811986

Please sign in to comment.