@noya-app/noya-designsystem 0.1.55 → 0.1.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +14 -0
- package/dist/index.d.mts +113 -36
- package/dist/index.d.ts +113 -36
- package/dist/index.js +1042 -665
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +995 -625
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/validateDropIndicator.test.ts +35 -9
- package/src/components/Collection.tsx +3 -0
- package/src/components/List.tsx +4 -0
- package/src/components/ListView.tsx +12 -8
- package/src/components/Toolbar.tsx +15 -2
- package/src/components/sorting/DragRegistration.tsx +107 -0
- package/src/components/sorting/SharedDragProvider.tsx +376 -0
- package/src/components/sorting/Sortable.tsx +357 -0
- package/src/components/sorting/createSharedDrag.tsx +41 -0
- package/src/components/sorting/sorting.ts +215 -0
- package/src/index.tsx +4 -2
- package/src/utils/moveTreeItem.ts +4 -2
- package/src/components/Sortable.tsx +0 -468
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
ActiveDragContextType,
|
|
4
|
+
DragRegistrationContextType,
|
|
5
|
+
SharedDragProps,
|
|
6
|
+
} from "./DragRegistration";
|
|
7
|
+
import { SharedDragProvider } from "./SharedDragProvider";
|
|
8
|
+
import { Sortable, SortableProps } from "./Sortable";
|
|
9
|
+
|
|
10
|
+
export function createSharedDrag(): {
|
|
11
|
+
Provider: typeof SharedDragProvider;
|
|
12
|
+
Sortable: <TItem, TElement extends HTMLElement>(
|
|
13
|
+
props: SortableProps<TItem, TElement>
|
|
14
|
+
) => React.ReactNode;
|
|
15
|
+
props: SharedDragProps;
|
|
16
|
+
} {
|
|
17
|
+
const dragRegistrationContext =
|
|
18
|
+
React.createContext<DragRegistrationContextType | null>(null);
|
|
19
|
+
const activeDragContext = React.createContext<ActiveDragContextType | null>(
|
|
20
|
+
null
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const sharedDragProps: SharedDragProps = {
|
|
24
|
+
dragRegistrationContext,
|
|
25
|
+
activeDragContext,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const Provider = ({ children }: { children: React.ReactNode }) => (
|
|
29
|
+
<SharedDragProvider sharedDragProps={sharedDragProps}>
|
|
30
|
+
{children}
|
|
31
|
+
</SharedDragProvider>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
Provider,
|
|
36
|
+
Sortable: <TItem, TElement extends HTMLElement>(
|
|
37
|
+
props: SortableProps<TItem, TElement>
|
|
38
|
+
) => <Sortable sharedDragProps={sharedDragProps} {...props} />,
|
|
39
|
+
props: sharedDragProps,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { UniqueIdentifier } from "@dnd-kit/core";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
export type RelativeDropPosition = "above" | "below" | "inside";
|
|
5
|
+
|
|
6
|
+
export type DropValidator = (
|
|
7
|
+
sourceIndex: number,
|
|
8
|
+
targetIndex: number,
|
|
9
|
+
position: RelativeDropPosition,
|
|
10
|
+
sourceListId: string,
|
|
11
|
+
targetListId: string
|
|
12
|
+
) => boolean;
|
|
13
|
+
|
|
14
|
+
export type AcceptsDrop = (
|
|
15
|
+
sourceIndex: number,
|
|
16
|
+
targetIndex: number,
|
|
17
|
+
position: RelativeDropPosition,
|
|
18
|
+
sourceListId: string,
|
|
19
|
+
targetListId: string
|
|
20
|
+
) => boolean;
|
|
21
|
+
|
|
22
|
+
export type MoveDragItemHandler = (
|
|
23
|
+
sourceIndex: number,
|
|
24
|
+
targetIndex: number,
|
|
25
|
+
position: RelativeDropPosition,
|
|
26
|
+
sourceListId: string,
|
|
27
|
+
targetListId: string
|
|
28
|
+
) => void;
|
|
29
|
+
|
|
30
|
+
export type AcceptsFromList = ({
|
|
31
|
+
sourceListId,
|
|
32
|
+
targetListId,
|
|
33
|
+
}: {
|
|
34
|
+
sourceListId: string;
|
|
35
|
+
targetListId: string;
|
|
36
|
+
}) => boolean;
|
|
37
|
+
|
|
38
|
+
export interface DragItem {
|
|
39
|
+
itemId: UniqueIdentifier;
|
|
40
|
+
listId: string;
|
|
41
|
+
index: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type DragState = {
|
|
45
|
+
source: DragItem | null;
|
|
46
|
+
target: DragItem | null;
|
|
47
|
+
indicator: RelativeDropPosition | null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const normalizeListTargetIndex = (
|
|
51
|
+
index: number,
|
|
52
|
+
position: "above" | "below"
|
|
53
|
+
): number => {
|
|
54
|
+
return position === "above" ? index : index + 1;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const defaultAcceptsDrop: DropValidator = (
|
|
58
|
+
sourceIndex,
|
|
59
|
+
targetIndex,
|
|
60
|
+
position
|
|
61
|
+
) => {
|
|
62
|
+
if (position === "inside") return false;
|
|
63
|
+
|
|
64
|
+
const normalized = normalizeListTargetIndex(targetIndex, position);
|
|
65
|
+
|
|
66
|
+
if (sourceIndex === normalized || sourceIndex + 1 === normalized) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return true;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type SortableItemContextProps = {
|
|
74
|
+
keys: UniqueIdentifier[];
|
|
75
|
+
acceptsDrop: DropValidator;
|
|
76
|
+
axis: "x" | "y";
|
|
77
|
+
position: { x: number; y: number };
|
|
78
|
+
setActivatorEvent: (event: PointerEvent) => void;
|
|
79
|
+
getDropTargetParentIndex?: (index: number) => number | undefined;
|
|
80
|
+
listId: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const SortableItemContext =
|
|
84
|
+
React.createContext<SortableItemContextProps>({
|
|
85
|
+
keys: [],
|
|
86
|
+
acceptsDrop: defaultAcceptsDrop,
|
|
87
|
+
axis: "y",
|
|
88
|
+
position: { x: 0, y: 0 },
|
|
89
|
+
setActivatorEvent: () => {},
|
|
90
|
+
getDropTargetParentIndex: undefined,
|
|
91
|
+
listId: "",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export type ValidateDropIndicatorParams = {
|
|
95
|
+
acceptsDrop: DropValidator; // Function that validates if a drop is allowed
|
|
96
|
+
keys: UniqueIdentifier[]; // Array of all sortable item IDs
|
|
97
|
+
activeId: UniqueIdentifier; // ID of item being dragged
|
|
98
|
+
overId: UniqueIdentifier; // ID of item being dragged over
|
|
99
|
+
offsetStart: number; // Current drag position (x or y coordinate)
|
|
100
|
+
elementStart: number; // Start position of target element (left or top)
|
|
101
|
+
elementSize: number; // Size of target element (width or height)
|
|
102
|
+
sourceListId: string; // ID of the list the item is being dragged from
|
|
103
|
+
targetListId: string; // ID of the list the item is being dragged to
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export function validateDropIndicator({
|
|
107
|
+
acceptsDrop,
|
|
108
|
+
keys,
|
|
109
|
+
activeId,
|
|
110
|
+
overId,
|
|
111
|
+
offsetStart,
|
|
112
|
+
elementStart,
|
|
113
|
+
elementSize,
|
|
114
|
+
sourceListId,
|
|
115
|
+
targetListId,
|
|
116
|
+
}: ValidateDropIndicatorParams): RelativeDropPosition | undefined {
|
|
117
|
+
const activeIndex = keys.findIndex((id) => id === activeId); // index of item being dragged
|
|
118
|
+
const overIndex = keys.findIndex((id) => id === overId); // index of item being dragged over
|
|
119
|
+
|
|
120
|
+
if (overIndex === -1) return undefined;
|
|
121
|
+
if (activeIndex === -1 && sourceListId === targetListId) return undefined;
|
|
122
|
+
|
|
123
|
+
const acceptsDropInside = acceptsDrop(
|
|
124
|
+
activeIndex,
|
|
125
|
+
overIndex,
|
|
126
|
+
"inside",
|
|
127
|
+
sourceListId,
|
|
128
|
+
targetListId
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Drop into the middle third if possible
|
|
132
|
+
if (
|
|
133
|
+
isContainedInMiddleThird(elementStart, elementSize, offsetStart) &&
|
|
134
|
+
acceptsDropInside
|
|
135
|
+
) {
|
|
136
|
+
return "inside";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const containingHalf = getContainingHalf(
|
|
140
|
+
elementStart,
|
|
141
|
+
elementSize,
|
|
142
|
+
offsetStart
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// For moving within the same list, we are more strict about the drop position.
|
|
146
|
+
// The source item must actually be contained in the target element.
|
|
147
|
+
// Otherwise the user may unintentionally drop the item into target,
|
|
148
|
+
// since it’s difficult to select no target at all.
|
|
149
|
+
// When dropping into a different list, the user definitely wants the item within that list,
|
|
150
|
+
// and it also handles the empty list case.
|
|
151
|
+
if (
|
|
152
|
+
!isContained(elementStart, elementSize, offsetStart) &&
|
|
153
|
+
sourceListId === targetListId
|
|
154
|
+
) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Drop above or below if possible
|
|
159
|
+
const acceptedHalf = acceptsDrop(
|
|
160
|
+
activeIndex,
|
|
161
|
+
overIndex,
|
|
162
|
+
containingHalf,
|
|
163
|
+
sourceListId,
|
|
164
|
+
targetListId
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return acceptedHalf
|
|
168
|
+
? containingHalf
|
|
169
|
+
: // Lastly, check if inside but not middle third
|
|
170
|
+
acceptsDropInside
|
|
171
|
+
? "inside"
|
|
172
|
+
: undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isContainedInMiddleThird(
|
|
176
|
+
elementStart: number,
|
|
177
|
+
elementSize: number,
|
|
178
|
+
offsetStart: number
|
|
179
|
+
): boolean {
|
|
180
|
+
return (
|
|
181
|
+
offsetStart >= elementStart + elementSize / 3 &&
|
|
182
|
+
offsetStart <= elementStart + (elementSize * 2) / 3
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isContained(
|
|
187
|
+
elementStart: number,
|
|
188
|
+
elementSize: number,
|
|
189
|
+
offsetStart: number
|
|
190
|
+
): boolean {
|
|
191
|
+
return (
|
|
192
|
+
offsetStart >= elementStart && offsetStart <= elementStart + elementSize
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getContainingHalf(
|
|
197
|
+
elementStart: number,
|
|
198
|
+
elementSize: number,
|
|
199
|
+
offsetStart: number
|
|
200
|
+
): "above" | "below" {
|
|
201
|
+
if (offsetStart < elementStart + elementSize / 2) return "above";
|
|
202
|
+
return "below";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function createDragItemKey(listId: string, itemId: UniqueIdentifier) {
|
|
206
|
+
return `${listId}-~-${itemId}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function parseDragItemKey(key: string): {
|
|
210
|
+
listId: string;
|
|
211
|
+
itemId: UniqueIdentifier;
|
|
212
|
+
} {
|
|
213
|
+
const [listId, itemId] = key.split("-~-");
|
|
214
|
+
return { listId, itemId };
|
|
215
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -60,8 +60,10 @@ export * from "./components/SegmentedControl";
|
|
|
60
60
|
export * from "./components/SelectionToolbar";
|
|
61
61
|
export * from "./components/SelectMenu";
|
|
62
62
|
export * from "./components/Slider";
|
|
63
|
-
export * from "./components/
|
|
64
|
-
export
|
|
63
|
+
export * from "./components/sorting/createSharedDrag";
|
|
64
|
+
export * from "./components/sorting/SharedDragProvider";
|
|
65
|
+
export * from "./components/sorting/Sortable";
|
|
66
|
+
export * from "./components/sorting/sorting";
|
|
65
67
|
export * from "./components/Spacer";
|
|
66
68
|
export * from "./components/Switch";
|
|
67
69
|
export * from "./components/Text";
|
|
@@ -2,7 +2,7 @@ import { isDeepEqual } from "@noya-app/noya-utils";
|
|
|
2
2
|
import {
|
|
3
3
|
defaultAcceptsDrop,
|
|
4
4
|
RelativeDropPosition,
|
|
5
|
-
} from "../components/
|
|
5
|
+
} from "../components/sorting/sorting";
|
|
6
6
|
|
|
7
7
|
type MoveOptions = {
|
|
8
8
|
paths: number[][];
|
|
@@ -91,7 +91,9 @@ export function acceptsDrop({
|
|
|
91
91
|
return defaultAcceptsDrop(
|
|
92
92
|
sourceIndexPath.at(-1)!,
|
|
93
93
|
targetIndexPath.at(-1)!,
|
|
94
|
-
position
|
|
94
|
+
position,
|
|
95
|
+
"",
|
|
96
|
+
""
|
|
95
97
|
);
|
|
96
98
|
}
|
|
97
99
|
|