@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.
@@ -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 };