Skip to content

Commit 96b4a45

Browse files
committed
feat(DraggableStack): working dynamic DraggableStack (closes #23)
1 parent 9602f44 commit 96b4a45

File tree

12 files changed

+285
-133
lines changed

12 files changed

+285
-133
lines changed

example/src/pages/DraggableBasicExample.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
DndProvider,
33
DndProviderProps,
44
doesOverlapHorizontally,
5-
doesOverlapVertically,
65
Draggable,
76
DraggableProps,
87
Droppable,
@@ -26,11 +25,14 @@ export const DraggableBasicExample: FunctionComponent = () => {
2625
setCount(count => count + 1);
2726
};
2827

29-
const handleDragEnd: DndProviderProps['onDragEnd'] = ({active, over}) => {
28+
const handleDragEnd: DndProviderProps['onDragEnd'] = ({
29+
active: _active,
30+
over,
31+
}) => {
3032
'worklet';
3133
if (over) {
3234
console.log(`Current count is ${count}`);
33-
runOnJS(setCount)(2);
35+
runOnJS(onDragEnd)();
3436
}
3537
};
3638

@@ -44,11 +46,12 @@ export const DraggableBasicExample: FunctionComponent = () => {
4446
console.log('onFinalize');
4547
};
4648

47-
const shouldDropWorklet = (active, item) => {
49+
const shouldDropWorklet: DndProviderProps['shouldDropWorklet'] = (
50+
active,
51+
item,
52+
) => {
4853
'worklet';
4954
return doesOverlapHorizontally(active, item);
50-
console.log({active, item});
51-
return false;
5255
};
5356

5457
return (

example/src/pages/DraggableStackExample.tsx

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1-
import React, {useState, type FunctionComponent} from 'react';
2-
import {Button, StyleSheet, Text, View} from 'react-native';
31
import {
42
DndProvider,
5-
type ObjectWithId,
63
Draggable,
74
DraggableStack,
5+
UniqueIdentifier,
86
type DraggableStackProps,
7+
type ObjectWithId,
98
} from '@mgcrea/react-native-dnd/src';
9+
import React, {useCallback, useState, type FunctionComponent} from 'react';
10+
import {Button, StyleSheet, Text, View} from 'react-native';
11+
import {configureReanimatedLogger} from 'react-native-reanimated';
12+
13+
// This is the default configuration
14+
configureReanimatedLogger({
15+
// level: ReanimatedLogLevel.warn,
16+
strict: false, // Reanimated runs in strict mode by default
17+
});
1018

1119
const items = ['🤓', '🤖🤖', '👻👻👻', '👾👾👾👾'];
1220
const data = items.map((letter, index) => ({
@@ -15,15 +23,23 @@ const data = items.map((letter, index) => ({
1523
})) satisfies ObjectWithId[];
1624

1725
export const DraggableStackExample: FunctionComponent = () => {
18-
const onStackOrderChange: DraggableStackProps['onOrderChange'] = value => {
19-
console.log('onStackOrderChange', value);
20-
};
26+
const [items, setItems] = useState(data);
27+
const [fontSize, setFontSize] = useState(32);
28+
29+
const onStackOrderChange: DraggableStackProps['onOrderChange'] = useCallback(
30+
(order: UniqueIdentifier[]) => {
31+
console.log('onStackOrderChange', order);
32+
setTimeout(() => {
33+
// setItems(items => order.map(id => items.find(item => item.id === id)!));
34+
// setFontSize(fontSize => fontSize + 1);
35+
}, 1000);
36+
},
37+
[],
38+
);
2139
const onStackOrderUpdate: DraggableStackProps['onOrderUpdate'] = value => {
2240
console.log('onStackOrderUpdate', value);
2341
};
2442

25-
const [items, setItems] = useState(data);
26-
2743
return (
2844
<View style={styles.container}>
2945
<Text style={styles.title}>DraggableStack Example</Text>
@@ -39,33 +55,49 @@ export const DraggableStackExample: FunctionComponent = () => {
3955
key={letter.id}
4056
id={letter.id}
4157
style={[styles.draggable]}>
42-
<Text style={styles.text}>{letter.value}</Text>
58+
<Text style={[styles.text, {fontSize}]}>{letter.value}</Text>
4359
</Draggable>
4460
))}
4561
</DraggableStack>
4662
</DndProvider>
47-
<Button
48-
title="Add"
49-
onPress={() => {
50-
setItems(prevItems => {
51-
const randomIndex = 2; //Math.floor(Math.random() * prevItems.length);
52-
prevItems.splice(randomIndex, 0, {
53-
value: '🤪',
54-
id: `${prevItems.length}-🤪`,
63+
<View style={{flexDirection: 'row'}}>
64+
<Button
65+
title="Add"
66+
onPress={() => {
67+
setItems(prevItems => {
68+
const randomIndex = 2; //Math.floor(Math.random() * prevItems.length);
69+
prevItems.splice(randomIndex, 0, {
70+
value: '🤪',
71+
id: `${prevItems.length}-🤪`,
72+
});
73+
return prevItems.slice();
5574
});
56-
return prevItems.slice();
57-
});
58-
}}
59-
/>
60-
<Button
61-
title="Delete"
62-
onPress={() => {
63-
setItems(prevItems => {
64-
const randomIndex = Math.floor(Math.random() * prevItems.length);
65-
return prevItems.filter((_, index) => index !== randomIndex);
66-
});
67-
}}
68-
/>
75+
}}
76+
/>
77+
<Button
78+
title="Delete"
79+
onPress={() => {
80+
setItems(prevItems => {
81+
const randomIndex = Math.floor(Math.random() * prevItems.length);
82+
return prevItems.filter((_, index) => index !== randomIndex);
83+
});
84+
}}
85+
/>
86+
87+
<Button
88+
title="Plus"
89+
onPress={() => {
90+
setFontSize(prevFontSize => prevFontSize + 1);
91+
}}
92+
/>
93+
94+
<Button
95+
title="Minus"
96+
onPress={() => {
97+
setFontSize(prevFontSize => prevFontSize - 1);
98+
}}
99+
/>
100+
</View>
69101
</View>
70102
);
71103
};
@@ -93,7 +125,6 @@ const styles = StyleSheet.create({
93125
},
94126
draggable: {
95127
backgroundColor: 'seagreen',
96-
opacity: 0.5,
97128
height: 100,
98129
borderColor: 'rgba(0,0,0,0.2)',
99130
borderWidth: 1,

src/DndContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export type DraggableOptions = Record<UniqueIdentifier, DraggableItemOptions>;
1111
export type DroppableOptions = Record<UniqueIdentifier, ItemOptions>;
1212
export type Layouts = Record<UniqueIdentifier, SharedValue<LayoutRectangle>>;
1313
export type Offsets = Record<UniqueIdentifier, SharedPoint>;
14-
export type DraggableState = "resting" | "pending" | "dragging" | "dropping" | "acting";
14+
export type DraggableState = "resting" | "pending" | "dragging" | "dropping" | "acting" | "sleeping";
1515
export type DraggableStates = Record<UniqueIdentifier, SharedValue<DraggableState>>;
1616

1717
export type DndContextValue = {

src/components/Draggable.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const Draggable: FunctionComponent<PropsWithChildren<DraggableProps>> = (
4343
animatedStyleWorklet,
4444
...otherProps
4545
}) => {
46-
const { setNodeRef, setNodeLayout, offset, state } = useDraggable({
46+
const { animatedRef, setNodeLayout, offset, state } = useDraggable({
4747
id,
4848
data,
4949
disabled,
@@ -52,6 +52,7 @@ export const Draggable: FunctionComponent<PropsWithChildren<DraggableProps>> = (
5252
});
5353

5454
const animatedStyle = useAnimatedStyle(() => {
55+
const isSleeping = state.value === "sleeping"; // Should not animate if sleeping
5556
const isActive = state.value === "dragging";
5657
const isActing = state.value === "acting";
5758
const zIndex = isActive ? 999 : isActing ? 998 : 1;
@@ -61,15 +62,17 @@ export const Draggable: FunctionComponent<PropsWithChildren<DraggableProps>> = (
6162
transform: [
6263
{
6364
// translateX: offset.x.value,
64-
translateX: isActive
65-
? offset.x.value
66-
: withSpring(offset.x.value, { damping: 100, stiffness: 1000 }),
65+
translateX:
66+
isActive || isSleeping
67+
? offset.x.value
68+
: withSpring(offset.x.value, { damping: 100, stiffness: 1000 }),
6769
},
6870
{
6971
// translateY: offset.y.value,
70-
translateY: isActive
71-
? offset.y.value
72-
: withSpring(offset.y.value, { damping: 100, stiffness: 1000 }),
72+
translateY:
73+
isActive || isSleeping
74+
? offset.y.value
75+
: withSpring(offset.y.value, { damping: 100, stiffness: 1000 }),
7376
},
7477
],
7578
};
@@ -80,7 +83,7 @@ export const Draggable: FunctionComponent<PropsWithChildren<DraggableProps>> = (
8083
}, [id, state, activeOpacity]);
8184

8285
return (
83-
<Animated.View ref={setNodeRef} onLayout={setNodeLayout} style={[style, animatedStyle]} {...otherProps}>
86+
<Animated.View ref={animatedRef} onLayout={setNodeLayout} style={[style, animatedStyle]} {...otherProps}>
8487
{children}
8588
</Animated.View>
8689
);

src/features/sort/components/DraggableGrid.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, { Children, useMemo, type FunctionComponent, type PropsWithChildren } from "react";
1+
import React, { useMemo, type FunctionComponent, type PropsWithChildren } from "react";
22
import { View, type FlexStyle, type ViewProps } from "react-native";
3-
import type { UniqueIdentifier } from "../../../types";
3+
import { useChildrenIds } from "../../../hooks";
44
import { useDraggableGrid, type UseDraggableGridOptions } from "../hooks/useDraggableGrid";
55

66
export type DraggableGridProps = Pick<ViewProps, "style"> &
@@ -20,16 +20,7 @@ export const DraggableGrid: FunctionComponent<PropsWithChildren<DraggableGridPro
2020
size,
2121
style: styleProp,
2222
}) => {
23-
const initialOrder = useMemo(
24-
() =>
25-
Children.map(children, (child) => {
26-
if (React.isValidElement(child)) {
27-
return (child.props as { id?: UniqueIdentifier }).id;
28-
}
29-
return null;
30-
})?.filter(Boolean),
31-
[children],
32-
);
23+
const initialOrder = useChildrenIds(children);
3324

3425
const style = useMemo(
3526
() =>

src/features/sort/components/DraggableStack.tsx

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import React, { Children, useMemo, type FunctionComponent, type PropsWithChildren } from "react";
2-
import { View, type FlexStyle, type ViewProps } from "react-native";
3-
import type { UniqueIdentifier } from "../../../types";
1+
import React, { useEffect, useMemo, type FunctionComponent, type PropsWithChildren } from "react";
2+
import { type FlexStyle, type ViewProps } from "react-native";
3+
import Animated, { runOnUI } from "react-native-reanimated";
4+
import { useChildrenIds } from "../../../hooks";
45
import { useDraggableStack, type UseDraggableStackOptions } from "../hooks/useDraggableStack";
56

67
export type DraggableStackProps = Pick<ViewProps, "style"> &
@@ -18,16 +19,7 @@ export const DraggableStack: FunctionComponent<PropsWithChildren<DraggableStackP
1819
shouldSwapWorklet,
1920
style: styleProp,
2021
}) => {
21-
const initialOrder = useMemo(
22-
() =>
23-
Children.map(children, (child) => {
24-
if (React.isValidElement(child)) {
25-
return (child.props as { id?: UniqueIdentifier }).id;
26-
}
27-
return null;
28-
})?.filter(Boolean),
29-
[children],
30-
);
22+
const initialOrder = useChildrenIds(children);
3123

3224
const style = useMemo(
3325
() =>
@@ -43,7 +35,7 @@ export const DraggableStack: FunctionComponent<PropsWithChildren<DraggableStackP
4335

4436
const horizontal = ["row", "row-reverse"].includes(style.flexDirection);
4537

46-
useDraggableStack({
38+
const { refreshOffsets } = useDraggableStack({
4739
gap: style.gap,
4840
horizontal,
4941
initialOrder,
@@ -52,5 +44,10 @@ export const DraggableStack: FunctionComponent<PropsWithChildren<DraggableStackP
5244
shouldSwapWorklet,
5345
});
5446

55-
return <View style={style}>{children}</View>;
47+
useEffect(() => {
48+
// Refresh offsets when children change
49+
runOnUI(refreshOffsets)();
50+
}, [initialOrder, refreshOffsets]);
51+
52+
return <Animated.View style={style}>{children}</Animated.View>;
5653
};

src/features/sort/hooks/useDraggableSort.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export type ShouldSwapWorklet = (
1818
) => boolean;
1919

2020
export type UseDraggableSortOptions = {
21-
initialOrder?: UniqueIdentifier[];
21+
initialOrder: UniqueIdentifier[];
2222
horizontal?: boolean;
2323
onOrderChange?: (order: UniqueIdentifier[]) => void;
2424
onOrderUpdate?: (nextOrder: UniqueIdentifier[], prevOrder: UniqueIdentifier[]) => void;
@@ -27,7 +27,7 @@ export type UseDraggableSortOptions = {
2727

2828
export const useDraggableSort = ({
2929
horizontal = false,
30-
initialOrder = [],
30+
initialOrder,
3131
onOrderChange,
3232
onOrderUpdate,
3333
shouldSwapWorklet = doesOverlapOnAxis,
@@ -93,16 +93,24 @@ export const useDraggableSort = ({
9393
const layouts = draggableLayouts.get();
9494
const addedIds = next.filter((id) => !prev.includes(id));
9595
addedIds.forEach((id) => {
96-
const index = Object.entries(layouts)
97-
.sort(([, a], [, b]) => a.get()[horizontal ? "x" : "y"] - b.get()[horizontal ? "x" : "y"])
98-
.findIndex(([key]) => key === id);
96+
const positionEntries = Object.entries(layouts).map<[UniqueIdentifier, number]>(([key, layout]) => [
97+
key,
98+
layout.get()[horizontal ? "x" : "y"],
99+
]);
100+
positionEntries.sort((a, b) => a[1] - b[1]);
101+
const index = positionEntries.findIndex(([key]) => key === id);
99102
const nextOrder = draggableSortOrder.value.slice();
100103
nextOrder.splice(index, 0, id);
101104
// draggableLastOrder.value = draggableSortOrder.value.slice();
102105
draggableSortOrder.value = nextOrder;
103106
});
107+
108+
// Broadcast the order change to the parent component
109+
if (onOrderChange && (removedIds.length > 0 || addedIds.length > 0)) {
110+
runOnJS(onOrderChange)(draggableSortOrder.value);
111+
}
104112
},
105-
[],
113+
[onOrderChange],
106114
);
107115

108116
// Track active layout changes and update the placeholder index

0 commit comments

Comments
 (0)