Skip to content

Commit

Permalink
Add optimistic updates
Browse files Browse the repository at this point in the history
  • Loading branch information
clauderic committed Jun 3, 2024
1 parent bff07f8 commit ea2458f
Show file tree
Hide file tree
Showing 12 changed files with 449 additions and 117 deletions.
40 changes: 24 additions & 16 deletions apps/docs/stories/react/Sortable/MultipleLists/MultipleLists.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {useRef, useState} from 'react';
import type {PropsWithChildren} from 'react';
import {flushSync} from 'react-dom';
import {CollisionPriority} from '@dnd-kit/abstract';
import {DragDropProvider, useDragOperation} from '@dnd-kit/react';
import {useSortable} from '@dnd-kit/react/sortable';
Expand All @@ -17,7 +18,6 @@ import {
} from '../../components/index.js';
import {createRange} from '../../../utilities/createRange.js';
import {cloneDeep} from '../../../utilities/cloneDeep.js';
import {flushSync} from 'react-dom';

interface Props {
debug?: boolean;
Expand All @@ -28,9 +28,14 @@ interface Props {
vertical?: boolean;
}

export function MultipleLists(
{debug, defaultItems, grid, itemCount, scrollable, vertical}: Props
) {
export function MultipleLists({
debug,
defaultItems,
grid,
itemCount,
scrollable,
vertical,
}: Props) {
const [items, setItems] = useState(
defaultItems ?? {
A: createRange(itemCount).map((id) => `A${id}`),
Expand Down Expand Up @@ -143,11 +148,16 @@ const COLORS: Record<string, string> = {
D: '#ff3680',
};

function SortableItem(
{id, column, index, style, onRemove}: PropsWithChildren<SortableItemProps>
) {
function SortableItem({
id,
column,
index,
style,
onRemove,
}: PropsWithChildren<SortableItemProps>) {
const {handleRef, ref, isDragSource} = useSortable({
id,
group: column,
accept: 'item',
type: 'item',
feedback: 'clone',
Expand Down Expand Up @@ -182,15 +192,13 @@ interface SortableColumnProps {
scrollable?: boolean;
}

function SortableColumn(
{
children,
columns,
id,
index,
scrollable,
}: PropsWithChildren<SortableColumnProps>
) {
function SortableColumn({
children,
columns,
id,
index,
scrollable,
}: PropsWithChildren<SortableColumnProps>) {
const empty = !children;
const {source} = useDragOperation();
const {handleRef, isDragSource, ref} = useSortable({
Expand Down
80 changes: 39 additions & 41 deletions apps/docs/stories/react/Sortable/SortableExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
UniqueIdentifier,
} from '@dnd-kit/abstract';
import {FeedbackType, defaultPreset} from '@dnd-kit/dom';
import type {SortableTransition} from '@dnd-kit/dom/sortable';
import {type SortableTransition} from '@dnd-kit/dom/sortable';
import {DragDropProvider} from '@dnd-kit/react';
import {useSortable} from '@dnd-kit/react/sortable';
import {directionBiased} from '@dnd-kit/collision';
Expand All @@ -26,47 +26,46 @@ interface Props {
layout?: 'vertical' | 'horizontal' | 'grid';
transition?: SortableTransition;
itemCount?: number;
optimistic?: boolean;
collisionDetector?: CollisionDetector;
getItemStyle?(id: UniqueIdentifier, index: number): CSSProperties;
}

export function SortableExample(
{
debug,
itemCount = 15,
collisionDetector,
disabled,
dragHandle,
feedback,
layout = 'vertical',
modifiers,
transition,
getItemStyle,
}: Props
) {
export function SortableExample({
debug,
itemCount = 15,
collisionDetector,
disabled,
dragHandle,
feedback,
layout = 'vertical',
optimistic = true,
modifiers,
transition,
getItemStyle,
}: Props) {
const [items, setItems] = useState(createRange(itemCount));
const snapshot = useRef(cloneDeep(items));

return (
<DragDropProvider
plugins={debug ? [Debug, ...defaultPreset.plugins] : undefined}
modifiers={modifiers}
onDragStart={() => {
snapshot.current = cloneDeep(items);
}}
onDragOver={(event) => {
const {source, target} = event.operation;

if (!source || !target) {
return;
}
if (optimistic) return;

setItems((items) => move(items, source, target));
}}
onDragEnd={(event) => {
const {source, target} = event.operation;

if (event.canceled) {
setItems(snapshot.current);
return;
}

setItems((items) => move(items, source, target));
}}
>
<Wrapper layout={layout}>
Expand All @@ -79,6 +78,7 @@ export function SortableExample(
disabled={disabled?.includes(id)}
dragHandle={dragHandle}
feedback={feedback}
optimistic={optimistic}
transition={transition}
style={getItemStyle?.(id, index)}
/>
Expand All @@ -95,31 +95,31 @@ interface SortableProps {
disabled?: boolean;
dragHandle?: boolean;
feedback?: FeedbackType;
optimistic?: boolean;
transition?: SortableTransition;
style?: React.CSSProperties;
}

function SortableItem(
{
id,
index,
collisionDetector = directionBiased,
disabled,
dragHandle,
feedback,
transition,
style,
}: PropsWithChildren<SortableProps>
) {
function SortableItem({
id,
index,
collisionDetector = directionBiased,
disabled,
dragHandle,
feedback,
optimistic,
transition,
style,
}: PropsWithChildren<SortableProps>) {
const [element, setElement] = useState<Element | null>(null);
const handleRef = useRef<HTMLButtonElement | null>(null);

const {isDragSource} = useSortable({
id,
index,
element,
feedback,
transition,
optimistic,
handle: handleRef,
disabled,
collisionDetector,
Expand All @@ -137,12 +137,10 @@ function SortableItem(
);
}

function Wrapper(
{
layout,
children,
}: PropsWithChildren<{layout: 'vertical' | 'horizontal' | 'grid'}>
) {
function Wrapper({
layout,
children,
}: PropsWithChildren<{layout: 'vertical' | 'horizontal' | 'grid'}>) {
return <div style={getWrapperStyles(layout)}>{children}</div>;
}

Expand Down
21 changes: 17 additions & 4 deletions apps/docs/stories/react/Sortable/Vertical/Vertical.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,14 @@ export const VariableHeights: Story = {
name: 'Variable heights',
args: {
debug: false,
getItemStyle(id) {
const heights = {1: 100, 3: 150, 5: 200, 8: 100, 12: 150};
getItemStyle(id: number) {
const heights: Record<number, number> = {
1: 100,
3: 150,
5: 200,
8: 100,
12: 150,
};

return {
height: heights[id],
Expand All @@ -46,8 +52,15 @@ export const DynamicHeights: Story = {
name: 'Dynamic heights',
args: {
debug: false,
getItemStyle(_, index) {
const heights = {1: 100, 3: 150, 5: 200, 8: 100, 12: 150};
optimistic: false,
getItemStyle(_: number, index: number) {
const heights: Record<number, number> = {
1: 100,
3: 150,
5: 200,
8: 100,
12: 150,
};

return {
height: heights[index],
Expand Down
29 changes: 18 additions & 11 deletions packages/abstract/src/core/manager/dragOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface DragOperation<
U extends Droppable = Droppable,
> {
activatorEvent: Event | null;
canceled: boolean;
position: Position;
transform: Coordinates;
status: {
Expand Down Expand Up @@ -66,6 +67,7 @@ export function DragOperationManager<
initial: signal<Shape | null>(null),
current: signal<Shape | null>(null),
};
const canceled = signal<boolean>(false);
const position = new Position({x: 0, y: 0});
const activatorEvent = signal<Event | null>(null);
const sourceIdentifier = signal<UniqueIdentifier | null>(null);
Expand Down Expand Up @@ -104,6 +106,7 @@ export function DragOperationManager<
const currentShape = shape.current.peek();
const operation: Omit<DragOperation<T, U>, 'transform'> = {
activatorEvent: activatorEvent.peek(),
canceled: canceled.peek(),
source: source.peek(),
target: target.peek(),
status: {
Expand Down Expand Up @@ -132,6 +135,9 @@ export function DragOperationManager<
get activatorEvent() {
return activatorEvent.value;
},
get canceled() {
return canceled.value;
},
get source() {
return source.value;
},
Expand Down Expand Up @@ -219,6 +225,7 @@ export function DragOperationManager<
},
start({event, coordinates}: {event: Event; coordinates: Coordinates}) {
batch(() => {
canceled.value = false;
activatorEvent.value = event;
position.reset(coordinates);
});
Expand All @@ -235,15 +242,13 @@ export function DragOperationManager<
});
});
},
move(
{
by,
to,
cancelable = true,
}:
| {by: Coordinates; to?: undefined; cancelable?: boolean}
| {by?: undefined; to: Coordinates; cancelable?: boolean}
) {
move({
by,
to,
cancelable = true,
}:
| {by: Coordinates; to?: undefined; cancelable?: boolean}
| {by?: undefined; to: Coordinates; cancelable?: boolean}) {
if (!dragging.peek()) {
return;
}
Expand Down Expand Up @@ -280,7 +285,7 @@ export function DragOperationManager<
position.update(coordinates);
});
},
stop({canceled = false}: {canceled?: boolean} = {}) {
stop({canceled: isCanceled = false}: {canceled?: boolean} = {}) {
let promise: Promise<void> | undefined;
const suspend = () => {
const output = {
Expand All @@ -303,9 +308,11 @@ export function DragOperationManager<
});
};

canceled.value = isCanceled;

monitor.dispatch('dragend', {
operation: snapshot(operation),
canceled,
canceled: isCanceled,
suspend,
});

Expand Down
14 changes: 3 additions & 11 deletions packages/dom/src/core/entities/droppable/droppable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export class Droppable<T extends Data = Data> extends AbstractDroppable<T> {
const {dragOperation} = manager;

if (element && dragOperation.status.initialized) {
let timeout: NodeJS.Timeout | undefined;
const scrollableAncestor = getFirstScrollableAncestor(element);
const doc = getDocument(element);
const root =
Expand All @@ -63,20 +62,13 @@ export class Droppable<T extends Data = Data> extends AbstractDroppable<T> {
const intersectionObserver = new IntersectionObserver(
(entries) => {
const [entry] = entries.slice(-1);
const {width, height} = entry.boundingClientRect;

if (this.visible == null) {
this.visible = entry.isIntersecting;
if (!width && !height) {
return;
}

if (timeout) {
clearTimeout(timeout);
}

timeout = setTimeout(() => {
this.visible = entry.isIntersecting;
timeout = undefined;
}, 50);
this.visible = entry.isIntersecting;
},
{
root: root ?? doc,
Expand Down
Loading

0 comments on commit ea2458f

Please sign in to comment.