@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
|
@@ -1,468 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
closestCenter,
|
|
3
|
-
DndContext,
|
|
4
|
-
DragEndEvent,
|
|
5
|
-
DragMoveEvent,
|
|
6
|
-
DragOverlay,
|
|
7
|
-
DragStartEvent,
|
|
8
|
-
PointerSensor,
|
|
9
|
-
UniqueIdentifier,
|
|
10
|
-
useDndContext,
|
|
11
|
-
useSensor,
|
|
12
|
-
useSensors,
|
|
13
|
-
} from "@dnd-kit/core";
|
|
14
|
-
import {
|
|
15
|
-
horizontalListSortingStrategy,
|
|
16
|
-
SortableContext,
|
|
17
|
-
useSortable,
|
|
18
|
-
verticalListSortingStrategy,
|
|
19
|
-
} from "@dnd-kit/sortable";
|
|
20
|
-
import { memoGeneric } from "@noya-app/react-utils";
|
|
21
|
-
import * as React from "react";
|
|
22
|
-
import { memo } from "react";
|
|
23
|
-
import { createPortal } from "react-dom";
|
|
24
|
-
|
|
25
|
-
export type RelativeDropPosition = "above" | "below" | "inside";
|
|
26
|
-
|
|
27
|
-
export type DropValidator = (
|
|
28
|
-
sourceIndex: number,
|
|
29
|
-
targetIndex: number,
|
|
30
|
-
position: RelativeDropPosition
|
|
31
|
-
) => boolean;
|
|
32
|
-
|
|
33
|
-
export const normalizeListTargetIndex = (
|
|
34
|
-
index: number,
|
|
35
|
-
position: "above" | "below"
|
|
36
|
-
): number => {
|
|
37
|
-
return position === "above" ? index : index + 1;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const defaultAcceptsDrop: DropValidator = (
|
|
41
|
-
sourceIndex,
|
|
42
|
-
targetIndex,
|
|
43
|
-
position
|
|
44
|
-
) => {
|
|
45
|
-
if (position === "inside") return false;
|
|
46
|
-
|
|
47
|
-
const normalized = normalizeListTargetIndex(targetIndex, position);
|
|
48
|
-
|
|
49
|
-
if (sourceIndex === normalized || sourceIndex + 1 === normalized) {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return true;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export type SortableItemContextProps = {
|
|
57
|
-
keys: UniqueIdentifier[];
|
|
58
|
-
acceptsDrop: DropValidator;
|
|
59
|
-
axis: "x" | "y";
|
|
60
|
-
position: { x: number; y: number };
|
|
61
|
-
setActivatorEvent: (event: PointerEvent) => void;
|
|
62
|
-
getDropTargetParentIndex?: (index: number) => number | undefined;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const SortableItemContext = React.createContext<SortableItemContextProps>({
|
|
66
|
-
keys: [],
|
|
67
|
-
acceptsDrop: defaultAcceptsDrop,
|
|
68
|
-
axis: "y",
|
|
69
|
-
position: { x: 0, y: 0 },
|
|
70
|
-
setActivatorEvent: () => {},
|
|
71
|
-
getDropTargetParentIndex: undefined,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
type ValidateDropIndicatorParams = {
|
|
75
|
-
acceptsDrop: DropValidator; // Function that validates if a drop is allowed
|
|
76
|
-
keys: UniqueIdentifier[]; // Array of all sortable item IDs
|
|
77
|
-
activeId: UniqueIdentifier; // ID of item being dragged
|
|
78
|
-
overId: UniqueIdentifier; // ID of item being dragged over
|
|
79
|
-
offsetStart: number; // Current drag position (x or y coordinate)
|
|
80
|
-
elementStart: number; // Start position of target element (left or top)
|
|
81
|
-
elementSize: number; // Size of target element (width or height)
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export function validateDropIndicator({
|
|
85
|
-
acceptsDrop,
|
|
86
|
-
keys,
|
|
87
|
-
activeId,
|
|
88
|
-
overId,
|
|
89
|
-
offsetStart,
|
|
90
|
-
elementStart,
|
|
91
|
-
elementSize,
|
|
92
|
-
}: ValidateDropIndicatorParams): RelativeDropPosition | undefined {
|
|
93
|
-
const activeIndex = keys.findIndex((id) => id === activeId); // index of item being dragged
|
|
94
|
-
const overIndex = keys.findIndex((id) => id === overId); // index of item being dragged over
|
|
95
|
-
|
|
96
|
-
if (activeIndex === -1 || overIndex === -1) return undefined;
|
|
97
|
-
|
|
98
|
-
const acceptsDropInside = acceptsDrop(activeIndex, overIndex, "inside");
|
|
99
|
-
|
|
100
|
-
// Drop into the middle third if possible
|
|
101
|
-
if (
|
|
102
|
-
isContainedInMiddleThird(elementStart, elementSize, offsetStart) &&
|
|
103
|
-
acceptsDropInside
|
|
104
|
-
) {
|
|
105
|
-
return "inside";
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const containingHalf = getContainingHalf(
|
|
109
|
-
elementStart,
|
|
110
|
-
elementSize,
|
|
111
|
-
offsetStart
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
// Drop above or below if possible
|
|
115
|
-
const acceptedHalf = acceptsDrop(activeIndex, overIndex, containingHalf);
|
|
116
|
-
|
|
117
|
-
return acceptedHalf
|
|
118
|
-
? containingHalf
|
|
119
|
-
: // Lastly, check if inside but not middle third
|
|
120
|
-
acceptsDropInside
|
|
121
|
-
? "inside"
|
|
122
|
-
: undefined;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function isContainedInMiddleThird(
|
|
126
|
-
elementStart: number,
|
|
127
|
-
elementSize: number,
|
|
128
|
-
offsetStart: number
|
|
129
|
-
): boolean {
|
|
130
|
-
return (
|
|
131
|
-
offsetStart >= elementStart + elementSize / 3 &&
|
|
132
|
-
offsetStart <= elementStart + (elementSize * 2) / 3
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function getContainingHalf(
|
|
137
|
-
elementStart: number,
|
|
138
|
-
elementSize: number,
|
|
139
|
-
offsetStart: number
|
|
140
|
-
): "above" | "below" {
|
|
141
|
-
if (offsetStart < elementStart + elementSize / 2) return "above";
|
|
142
|
-
return "below";
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/* ----------------------------------------------------------------------------
|
|
146
|
-
* Item
|
|
147
|
-
* ------------------------------------------------------------------------- */
|
|
148
|
-
|
|
149
|
-
type UseSortableReturnType = ReturnType<typeof useSortable>;
|
|
150
|
-
|
|
151
|
-
type ItemChildrenProps<T> = {
|
|
152
|
-
ref: React.Ref<T>;
|
|
153
|
-
relativeDropPosition?: RelativeDropPosition;
|
|
154
|
-
style?: React.CSSProperties;
|
|
155
|
-
} & UseSortableReturnType["attributes"];
|
|
156
|
-
|
|
157
|
-
interface ItemProps<T> {
|
|
158
|
-
id: UniqueIdentifier;
|
|
159
|
-
disabled?: boolean;
|
|
160
|
-
children: (props: ItemChildrenProps<T>) => JSX.Element;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function SortableItem<T extends HTMLElement>({
|
|
164
|
-
id,
|
|
165
|
-
disabled,
|
|
166
|
-
children,
|
|
167
|
-
}: ItemProps<T>) {
|
|
168
|
-
const {
|
|
169
|
-
keys,
|
|
170
|
-
position,
|
|
171
|
-
acceptsDrop,
|
|
172
|
-
setActivatorEvent,
|
|
173
|
-
axis,
|
|
174
|
-
getDropTargetParentIndex,
|
|
175
|
-
} = React.useContext(SortableItemContext);
|
|
176
|
-
const sortable = useSortable({ id, disabled });
|
|
177
|
-
const { activatorEvent } = useDndContext();
|
|
178
|
-
|
|
179
|
-
const {
|
|
180
|
-
active,
|
|
181
|
-
attributes,
|
|
182
|
-
listeners,
|
|
183
|
-
setNodeRef,
|
|
184
|
-
isDragging,
|
|
185
|
-
index,
|
|
186
|
-
activeIndex,
|
|
187
|
-
overIndex,
|
|
188
|
-
over,
|
|
189
|
-
} = sortable;
|
|
190
|
-
|
|
191
|
-
if (activatorEvent) {
|
|
192
|
-
setActivatorEvent(activatorEvent as PointerEvent);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const eventX = (activatorEvent as PointerEvent | null)?.clientX ?? 0;
|
|
196
|
-
const eventY = (activatorEvent as PointerEvent | null)?.clientY ?? 0;
|
|
197
|
-
const offsetLeft = eventX + position.x;
|
|
198
|
-
const offsetTop = eventY + position.y;
|
|
199
|
-
|
|
200
|
-
const ref = React.useCallback((node: T) => setNodeRef(node), [setNodeRef]);
|
|
201
|
-
const parentIndex =
|
|
202
|
-
overIndex === -1 ? undefined : getDropTargetParentIndex?.(overIndex);
|
|
203
|
-
const isValidParent =
|
|
204
|
-
parentIndex === undefined || parentIndex === -1
|
|
205
|
-
? false
|
|
206
|
-
: acceptsDrop(activeIndex, parentIndex, "inside");
|
|
207
|
-
const overIdAcceptsDrop =
|
|
208
|
-
overIndex === -1
|
|
209
|
-
? undefined
|
|
210
|
-
: acceptsDrop(activeIndex, overIndex, "inside");
|
|
211
|
-
|
|
212
|
-
const relativeDropPosition =
|
|
213
|
-
index >= 0 && index === overIndex && !isDragging && active && over
|
|
214
|
-
? validateDropIndicator({
|
|
215
|
-
acceptsDrop,
|
|
216
|
-
keys,
|
|
217
|
-
activeId: active.id,
|
|
218
|
-
overId: over.id,
|
|
219
|
-
offsetStart: axis === "x" ? offsetLeft : offsetTop,
|
|
220
|
-
elementStart: axis === "x" ? over.rect.left : over.rect.top,
|
|
221
|
-
elementSize: axis === "x" ? over.rect.width : over.rect.height,
|
|
222
|
-
})
|
|
223
|
-
: undefined;
|
|
224
|
-
|
|
225
|
-
const showDragInsideIndicatorOnParent =
|
|
226
|
-
!overIdAcceptsDrop && isValidParent && parentIndex === index;
|
|
227
|
-
|
|
228
|
-
return children({
|
|
229
|
-
ref,
|
|
230
|
-
...attributes,
|
|
231
|
-
...listeners,
|
|
232
|
-
relativeDropPosition:
|
|
233
|
-
relativeDropPosition ??
|
|
234
|
-
(showDragInsideIndicatorOnParent ? "inside" : undefined),
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/* ----------------------------------------------------------------------------
|
|
239
|
-
* Root
|
|
240
|
-
* ------------------------------------------------------------------------- */
|
|
241
|
-
|
|
242
|
-
interface RootProps {
|
|
243
|
-
keys: UniqueIdentifier[];
|
|
244
|
-
children: React.ReactNode;
|
|
245
|
-
renderOverlay?: (index: number) => React.ReactNode;
|
|
246
|
-
onMoveItem?: (
|
|
247
|
-
sourceIndex: number,
|
|
248
|
-
targetIndex: number,
|
|
249
|
-
position: RelativeDropPosition
|
|
250
|
-
) => void;
|
|
251
|
-
acceptsDrop?: SortableItemContextProps["acceptsDrop"];
|
|
252
|
-
axis?: SortableItemContextProps["axis"];
|
|
253
|
-
getDropTargetParentIndex?: SortableItemContextProps["getDropTargetParentIndex"];
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function SortableRoot({
|
|
257
|
-
keys,
|
|
258
|
-
children,
|
|
259
|
-
onMoveItem,
|
|
260
|
-
renderOverlay,
|
|
261
|
-
acceptsDrop = defaultAcceptsDrop,
|
|
262
|
-
axis = "y",
|
|
263
|
-
getDropTargetParentIndex,
|
|
264
|
-
}: RootProps) {
|
|
265
|
-
const sensors = useSensors(
|
|
266
|
-
useSensor(PointerSensor, {
|
|
267
|
-
activationConstraint: {
|
|
268
|
-
distance: 4,
|
|
269
|
-
},
|
|
270
|
-
})
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
const [activeIndex, setActiveIndex] = React.useState<number | undefined>();
|
|
274
|
-
const activatorEvent = React.useRef<PointerEvent | null>(null);
|
|
275
|
-
const [position, setPosition] = React.useState<{ x: number; y: number }>({
|
|
276
|
-
x: 0,
|
|
277
|
-
y: 0,
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
const setActivatorEvent = React.useCallback((event: PointerEvent) => {
|
|
281
|
-
activatorEvent.current = event;
|
|
282
|
-
}, []);
|
|
283
|
-
|
|
284
|
-
const handleDragStart = React.useCallback(
|
|
285
|
-
(event: DragStartEvent) => {
|
|
286
|
-
const index = keys.findIndex((id) => id === event.active.id);
|
|
287
|
-
setActiveIndex(index);
|
|
288
|
-
},
|
|
289
|
-
[keys]
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
const handleDragMove = React.useCallback((event: DragMoveEvent) => {
|
|
293
|
-
setPosition({ ...event.delta });
|
|
294
|
-
}, []);
|
|
295
|
-
|
|
296
|
-
const handleDragEnd = React.useCallback(
|
|
297
|
-
(event: DragEndEvent) => {
|
|
298
|
-
const { active, over } = event;
|
|
299
|
-
|
|
300
|
-
setActiveIndex(undefined);
|
|
301
|
-
if (over && active.id !== over.id) {
|
|
302
|
-
const oldIndex = keys.findIndex((id) => id === active.id);
|
|
303
|
-
const newIndex = keys.findIndex((id) => id === over.id);
|
|
304
|
-
|
|
305
|
-
if (oldIndex === -1 || newIndex === -1) return;
|
|
306
|
-
|
|
307
|
-
const eventX = activatorEvent.current?.clientX ?? 0;
|
|
308
|
-
const eventY = activatorEvent.current?.clientY ?? 0;
|
|
309
|
-
const offsetLeft = eventX + position.x;
|
|
310
|
-
const offsetTop = eventY + position.y;
|
|
311
|
-
|
|
312
|
-
const indicator = validateDropIndicator({
|
|
313
|
-
acceptsDrop,
|
|
314
|
-
keys,
|
|
315
|
-
activeId: active.id,
|
|
316
|
-
overId: over.id,
|
|
317
|
-
offsetStart: axis === "x" ? offsetLeft : offsetTop,
|
|
318
|
-
elementStart: axis === "x" ? over.rect.left : over.rect.top,
|
|
319
|
-
elementSize: axis === "x" ? over.rect.width : over.rect.height,
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
if (!indicator) return;
|
|
323
|
-
|
|
324
|
-
onMoveItem?.(oldIndex, newIndex, indicator);
|
|
325
|
-
}
|
|
326
|
-
},
|
|
327
|
-
[acceptsDrop, axis, keys, onMoveItem, position.x, position.y]
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
const [mounted, setMounted] = React.useState(false);
|
|
331
|
-
|
|
332
|
-
React.useEffect(() => {
|
|
333
|
-
setMounted(true);
|
|
334
|
-
}, []);
|
|
335
|
-
|
|
336
|
-
return (
|
|
337
|
-
<SortableItemContext.Provider
|
|
338
|
-
value={React.useMemo(
|
|
339
|
-
(): SortableItemContextProps => ({
|
|
340
|
-
keys,
|
|
341
|
-
acceptsDrop,
|
|
342
|
-
position,
|
|
343
|
-
setActivatorEvent,
|
|
344
|
-
axis,
|
|
345
|
-
getDropTargetParentIndex,
|
|
346
|
-
}),
|
|
347
|
-
[
|
|
348
|
-
acceptsDrop,
|
|
349
|
-
axis,
|
|
350
|
-
getDropTargetParentIndex,
|
|
351
|
-
keys,
|
|
352
|
-
position,
|
|
353
|
-
setActivatorEvent,
|
|
354
|
-
]
|
|
355
|
-
)}
|
|
356
|
-
>
|
|
357
|
-
<DndContext
|
|
358
|
-
sensors={sensors}
|
|
359
|
-
collisionDetection={closestCenter}
|
|
360
|
-
onDragStart={handleDragStart}
|
|
361
|
-
onDragMove={handleDragMove}
|
|
362
|
-
onDragEnd={handleDragEnd}
|
|
363
|
-
>
|
|
364
|
-
<SortableContext
|
|
365
|
-
items={keys}
|
|
366
|
-
strategy={
|
|
367
|
-
axis === "x"
|
|
368
|
-
? horizontalListSortingStrategy
|
|
369
|
-
: verticalListSortingStrategy
|
|
370
|
-
}
|
|
371
|
-
>
|
|
372
|
-
{children}
|
|
373
|
-
</SortableContext>
|
|
374
|
-
{mounted &&
|
|
375
|
-
renderOverlay &&
|
|
376
|
-
createPortal(
|
|
377
|
-
<DragOverlay dropAnimation={null}>
|
|
378
|
-
{activeIndex !== undefined &&
|
|
379
|
-
activeIndex >= 0 &&
|
|
380
|
-
renderOverlay(activeIndex)}
|
|
381
|
-
</DragOverlay>,
|
|
382
|
-
document.body
|
|
383
|
-
)}
|
|
384
|
-
</DndContext>
|
|
385
|
-
</SortableItemContext.Provider>
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Create a map of all subcomponents
|
|
390
|
-
const SortableSubcomponents = {
|
|
391
|
-
Root: memo(SortableRoot),
|
|
392
|
-
Item: memoGeneric(SortableItem),
|
|
393
|
-
} as const;
|
|
394
|
-
|
|
395
|
-
interface SortableProps<T, TElement extends HTMLElement>
|
|
396
|
-
extends Omit<RootProps, "children" | "keys" | "renderOverlay"> {
|
|
397
|
-
items: T[];
|
|
398
|
-
keyExtractor: (item: T) => UniqueIdentifier;
|
|
399
|
-
shouldRenderOverlay?: boolean;
|
|
400
|
-
renderItem: (
|
|
401
|
-
item: T,
|
|
402
|
-
props: ItemChildrenProps<TElement>,
|
|
403
|
-
{ isOverlay }: { isOverlay: boolean }
|
|
404
|
-
) => JSX.Element;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const SortableComponent = memoGeneric(function Sortable<
|
|
408
|
-
TItem,
|
|
409
|
-
TElement extends HTMLElement,
|
|
410
|
-
>(props: SortableProps<TItem, TElement>) {
|
|
411
|
-
const {
|
|
412
|
-
renderItem,
|
|
413
|
-
items,
|
|
414
|
-
keyExtractor,
|
|
415
|
-
shouldRenderOverlay = true,
|
|
416
|
-
...rest
|
|
417
|
-
} = props;
|
|
418
|
-
|
|
419
|
-
const keys = React.useMemo(
|
|
420
|
-
() => items.map(keyExtractor),
|
|
421
|
-
[items, keyExtractor]
|
|
422
|
-
);
|
|
423
|
-
|
|
424
|
-
const renderOverlay = React.useCallback(
|
|
425
|
-
(index: number) => {
|
|
426
|
-
return renderItem(
|
|
427
|
-
items[index],
|
|
428
|
-
{
|
|
429
|
-
ref: () => {},
|
|
430
|
-
style: {},
|
|
431
|
-
tabIndex: -1,
|
|
432
|
-
"aria-disabled": true,
|
|
433
|
-
"aria-pressed": false,
|
|
434
|
-
"aria-roledescription": "Item being dragged",
|
|
435
|
-
"aria-describedby": "Item being dragged",
|
|
436
|
-
role: "button",
|
|
437
|
-
},
|
|
438
|
-
{ isOverlay: true }
|
|
439
|
-
);
|
|
440
|
-
},
|
|
441
|
-
[renderItem, items]
|
|
442
|
-
);
|
|
443
|
-
|
|
444
|
-
return (
|
|
445
|
-
<SortableSubcomponents.Root
|
|
446
|
-
{...rest}
|
|
447
|
-
keys={keys}
|
|
448
|
-
renderOverlay={shouldRenderOverlay ? renderOverlay : undefined}
|
|
449
|
-
>
|
|
450
|
-
{keys.map((key, index) => (
|
|
451
|
-
<SortableSubcomponents.Item key={key} id={key}>
|
|
452
|
-
{(props) => {
|
|
453
|
-
return renderItem(
|
|
454
|
-
items[index],
|
|
455
|
-
props as ItemChildrenProps<TElement>,
|
|
456
|
-
{ isOverlay: false }
|
|
457
|
-
);
|
|
458
|
-
}}
|
|
459
|
-
</SortableSubcomponents.Item>
|
|
460
|
-
))}
|
|
461
|
-
</SortableSubcomponents.Root>
|
|
462
|
-
);
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
// Export both the namespace and the component
|
|
466
|
-
const Sortable = Object.assign(SortableComponent, SortableSubcomponents);
|
|
467
|
-
|
|
468
|
-
export { Sortable };
|