@noya-app/noya-designsystem 0.1.56 → 0.1.58

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,4 +1,4 @@
1
- import { UniqueIdentifier, useDndContext, useDroppable } from "@dnd-kit/core";
1
+ import { UniqueIdentifier, useDroppable } from "@dnd-kit/core";
2
2
  import {
3
3
  horizontalListSortingStrategy,
4
4
  SortableContext,
@@ -11,13 +11,14 @@ import {
11
11
  ActiveDragContextType,
12
12
  DragRegistrationContext,
13
13
  RegisteredList,
14
- SharedDragProviderContexts,
14
+ SharedDragProps,
15
15
  useActiveDrag,
16
16
  useDragRegistration,
17
17
  } from "./DragRegistration";
18
18
  import { withDragProvider } from "./SharedDragProvider";
19
19
  import {
20
20
  AcceptsFromList,
21
+ createDragItemKey,
21
22
  defaultAcceptsDrop,
22
23
  DropValidator,
23
24
  MoveDragItemHandler,
@@ -35,18 +36,14 @@ type ItemChildrenProps<T> = {
35
36
 
36
37
  // Context for drop indicators within a sortable list
37
38
  interface SortableItemContextProps {
38
- keys: UniqueIdentifier[];
39
39
  acceptsDrop: DropValidator;
40
- axis: "x" | "y";
41
40
  listId: string;
42
41
  getDropTargetParentIndex?: (index: number) => number | undefined;
43
42
  activeDragContext: React.Context<ActiveDragContextType | null>;
44
43
  }
45
44
 
46
45
  const SortableItemContext = React.createContext<SortableItemContextProps>({
47
- keys: [],
48
46
  acceptsDrop: defaultAcceptsDrop,
49
- axis: "y",
50
47
  listId: "",
51
48
  getDropTargetParentIndex: undefined,
52
49
  activeDragContext: ActiveDragContext,
@@ -54,95 +51,59 @@ const SortableItemContext = React.createContext<SortableItemContextProps>({
54
51
 
55
52
  // Custom hook for drop indicators in sortable lists
56
53
  function useSortableDropIndicator({
57
- id,
58
54
  index,
59
- isDragging,
60
- overIndex,
61
- activeIndex,
62
- keys,
63
- acceptsDrop,
64
- axis,
55
+ itemId,
65
56
  listId,
57
+ acceptsDrop,
66
58
  getDropTargetParentIndex,
67
59
  }: {
68
- id: UniqueIdentifier;
69
60
  index: number;
70
- isDragging: boolean;
71
- overIndex: number;
72
- activeIndex: number;
73
- keys: UniqueIdentifier[];
74
- acceptsDrop: DropValidator;
75
- axis: "x" | "y";
61
+ itemId: UniqueIdentifier;
76
62
  listId: string;
63
+ acceptsDrop: DropValidator;
77
64
  getDropTargetParentIndex?: (index: number) => number | undefined;
78
65
  }): RelativeDropPosition | undefined {
79
- const { active, over, activatorEvent } = useDndContext();
80
66
  const { activeDragContext } = React.useContext(SortableItemContext);
81
- const { activeItem, delta: position } = useActiveDrag(activeDragContext);
82
-
83
- if (
84
- index < 0 ||
85
- isDragging ||
86
- !active ||
87
- !over ||
88
- !activeItem ||
89
- !(
90
- // Same-list drop: check normal overIndex matching
91
- (
92
- (activeItem.listId === listId && index === overIndex) ||
93
- // Cross-list drop: check if we're hovering over this specific item
94
- (activeItem.listId !== listId && over.id === id)
95
- )
96
- )
97
- ) {
98
- return undefined;
99
- }
100
-
101
- const isCrossContainer = activeItem.listId !== listId;
67
+ const activeDrag = useActiveDrag(activeDragContext);
68
+ const { source, target } = activeDrag;
102
69
 
103
- // For cross-list drops, use combined keys like MultiSortable does
104
- const validationKeys = isCrossContainer ? [active.id, ...keys] : keys;
70
+ if (!source || !target) return;
105
71
 
106
- const eventX = (activatorEvent as PointerEvent | null)?.clientX ?? 0;
107
- const eventY = (activatorEvent as PointerEvent | null)?.clientY ?? 0;
108
- const offsetLeft = eventX + position.x;
109
- const offsetTop = eventY + position.y;
72
+ if (target.listId === listId && target.itemId === itemId) {
73
+ return activeDrag.indicator ?? undefined;
74
+ }
110
75
 
111
- const parentIndex =
112
- overIndex === -1 ? undefined : getDropTargetParentIndex?.(overIndex);
76
+ const targetParentIndex =
77
+ target.index === -1 ? undefined : getDropTargetParentIndex?.(target.index);
113
78
 
114
79
  const isValidParent =
115
- parentIndex === undefined || parentIndex === -1
80
+ targetParentIndex === undefined || targetParentIndex === -1
116
81
  ? false
117
- : acceptsDrop(activeIndex, parentIndex, "inside", listId, listId);
82
+ : acceptsDrop({
83
+ sourceIndex: source.index,
84
+ targetIndex: targetParentIndex,
85
+ position: "inside",
86
+ sourceListId: source.listId,
87
+ targetListId: target.listId,
88
+ });
118
89
 
119
90
  const overIdAcceptsDrop =
120
- overIndex === -1
91
+ target.index === -1
121
92
  ? undefined
122
- : acceptsDrop(activeIndex, overIndex, "inside", listId, listId);
93
+ : acceptsDrop({
94
+ sourceIndex: source.index,
95
+ targetIndex: target.index,
96
+ position: "inside",
97
+ sourceListId: source.listId,
98
+ targetListId: target.listId,
99
+ });
123
100
 
124
- const relativeDropPosition =
125
- index >= 0 && index === overIndex && !isDragging && active && over
126
- ? validateDropIndicator({
127
- acceptsDrop,
128
- keys: validationKeys,
129
- activeId: active.id,
130
- overId: over.id,
131
- offsetStart: axis === "x" ? offsetLeft : offsetTop,
132
- elementStart: axis === "x" ? over.rect.left : over.rect.top,
133
- elementSize: axis === "x" ? over.rect.width : over.rect.height,
134
- sourceListId: listId,
135
- targetListId: listId,
136
- })
137
- : undefined;
101
+ const thisItemIsTargetParent = targetParentIndex === index;
138
102
 
139
- const showDragInsideIndicatorOnParent =
140
- !overIdAcceptsDrop && isValidParent && parentIndex === index;
103
+ const showIndicatorOnParent =
104
+ !overIdAcceptsDrop && isValidParent && thisItemIsTargetParent;
141
105
 
142
- return (
143
- relativeDropPosition ??
144
- (showDragInsideIndicatorOnParent ? "inside" : undefined)
145
- );
106
+ return showIndicatorOnParent ? "inside" : undefined;
146
107
  }
147
108
 
148
109
  interface RootProps {
@@ -155,7 +116,7 @@ interface RootProps {
155
116
  acceptsFromList?: AcceptsFromList;
156
117
  acceptsDrop?: DropValidator;
157
118
  getDropTargetParentIndex?: (index: number) => number | undefined;
158
- sharedDragProviderContexts?: SharedDragProviderContexts;
119
+ sharedDragProps?: SharedDragProps;
159
120
  containerRef?: React.RefObject<HTMLElement>;
160
121
  }
161
122
 
@@ -168,11 +129,15 @@ function SortableItem<TElement extends HTMLElement>({
168
129
  disabled?: boolean;
169
130
  children: (props: ItemChildrenProps<TElement>) => React.ReactNode;
170
131
  }) {
171
- const { keys, acceptsDrop, axis, listId, getDropTargetParentIndex } =
132
+ const { acceptsDrop, listId, getDropTargetParentIndex } =
172
133
  React.useContext(SortableItemContext);
173
134
 
174
- const { attributes, listeners, setNodeRef, isDragging, index, overIndex } =
175
- useSortable({ id, disabled });
135
+ const dragItemKey = createDragItemKey(listId, id);
136
+
137
+ const { attributes, listeners, setNodeRef, isDragging, index } = useSortable({
138
+ id: dragItemKey,
139
+ disabled,
140
+ });
176
141
 
177
142
  const ref = React.useCallback(
178
143
  (node: TElement | null) => setNodeRef(node),
@@ -180,15 +145,10 @@ function SortableItem<TElement extends HTMLElement>({
180
145
  );
181
146
 
182
147
  const relativeDropPosition = useSortableDropIndicator({
183
- id,
184
148
  index,
185
- isDragging,
186
- overIndex,
187
- keys,
188
- acceptsDrop,
189
- axis,
149
+ itemId: dragItemKey,
190
150
  listId,
191
- activeIndex: index,
151
+ acceptsDrop,
192
152
  getDropTargetParentIndex,
193
153
  });
194
154
 
@@ -205,7 +165,7 @@ function SortableItem<TElement extends HTMLElement>({
205
165
 
206
166
  function SortableRoot_({
207
167
  id: idProp,
208
- keys,
168
+ keys: keysProp,
209
169
  onMoveItem,
210
170
  renderOverlay,
211
171
  acceptsFromList,
@@ -213,25 +173,21 @@ function SortableRoot_({
213
173
  axis = "y",
214
174
  children,
215
175
  getDropTargetParentIndex,
216
- sharedDragProviderContexts,
176
+ sharedDragProps,
217
177
  containerRef,
218
178
  }: RootProps) {
219
179
  const defaultId = React.useId();
220
180
  const id = idProp ?? defaultId;
181
+ const keys = React.useMemo(() => {
182
+ return keysProp.map((key) => createDragItemKey(id, key));
183
+ }, [keysProp, id]);
221
184
 
222
185
  const { registerList, unregisterList } = useDragRegistration(
223
- sharedDragProviderContexts?.dragRegistrationContext ??
224
- DragRegistrationContext
186
+ sharedDragProps?.dragRegistrationContext ?? DragRegistrationContext
225
187
  );
226
188
 
227
189
  const { setNodeRef: setDroppableRef } = useDroppable({
228
190
  id,
229
- // Make the container droppable only when the list is empty
230
- // This avoids conflicts with individual sortable items.
231
- // This isn't strictly necessary since we also use a custom collision detection
232
- // that prioritizes items over containers, but it helps with dropping into
233
- // the best position (either above or below, rather than always below).
234
- disabled: keys.length > 0,
235
191
  });
236
192
 
237
193
  // Register this list with the global drag system
@@ -244,9 +200,7 @@ function SortableRoot_({
244
200
  const index = keys.findIndex((key) => key === id);
245
201
  return renderOverlay?.(index) ?? null;
246
202
  },
247
- acceptsFromList:
248
- acceptsFromList ??
249
- (({ sourceListId, targetListId }) => sourceListId === targetListId),
203
+ acceptsFromList: acceptsFromList ?? (() => true),
250
204
  acceptsDrop,
251
205
  getDropIndicator: (parameters) => {
252
206
  const { absolutePosition, active, over, sourceListId, targetListId } =
@@ -294,20 +248,12 @@ function SortableRoot_({
294
248
  () => ({
295
249
  keys,
296
250
  acceptsDrop,
297
- axis,
298
251
  listId: id,
299
252
  getDropTargetParentIndex,
300
253
  activeDragContext:
301
- sharedDragProviderContexts?.activeDragContext ?? ActiveDragContext,
254
+ sharedDragProps?.activeDragContext ?? ActiveDragContext,
302
255
  }),
303
- [
304
- keys,
305
- acceptsDrop,
306
- axis,
307
- id,
308
- getDropTargetParentIndex,
309
- sharedDragProviderContexts,
310
- ]
256
+ [keys, acceptsDrop, id, getDropTargetParentIndex, sharedDragProps]
311
257
  );
312
258
 
313
259
  // Set the droppable ref after every render.
@@ -2,17 +2,17 @@ import * as React from "react";
2
2
  import {
3
3
  ActiveDragContextType,
4
4
  DragRegistrationContextType,
5
- SharedDragProviderContexts,
5
+ SharedDragProps,
6
6
  } from "./DragRegistration";
7
7
  import { SharedDragProvider } from "./SharedDragProvider";
8
8
  import { Sortable, SortableProps } from "./Sortable";
9
9
 
10
10
  export function createSharedDrag(): {
11
11
  Provider: typeof SharedDragProvider;
12
- props: SharedDragProviderContexts;
13
12
  Sortable: <TItem, TElement extends HTMLElement>(
14
13
  props: SortableProps<TItem, TElement>
15
14
  ) => React.ReactNode;
15
+ props: SharedDragProps;
16
16
  } {
17
17
  const dragRegistrationContext =
18
18
  React.createContext<DragRegistrationContextType | null>(null);
@@ -20,27 +20,22 @@ export function createSharedDrag(): {
20
20
  null
21
21
  );
22
22
 
23
+ const sharedDragProps: SharedDragProps = {
24
+ dragRegistrationContext,
25
+ activeDragContext,
26
+ };
27
+
23
28
  const Provider = ({ children }: { children: React.ReactNode }) => (
24
- <SharedDragProvider
25
- contexts={{
26
- dragRegistrationContext,
27
- activeDragContext,
28
- }}
29
- >
29
+ <SharedDragProvider sharedDragProps={sharedDragProps}>
30
30
  {children}
31
31
  </SharedDragProvider>
32
32
  );
33
33
 
34
- const dragProps = {
35
- dragRegistrationContext,
36
- activeDragContext,
37
- };
38
-
39
34
  return {
40
35
  Provider,
41
36
  Sortable: <TItem, TElement extends HTMLElement>(
42
37
  props: SortableProps<TItem, TElement>
43
- ) => <Sortable sharedDragProviderContexts={dragProps} {...props} />,
44
- props: dragProps,
38
+ ) => <Sortable sharedDragProps={sharedDragProps} {...props} />,
39
+ props: sharedDragProps,
45
40
  };
46
41
  }
@@ -3,29 +3,19 @@ import * as React from "react";
3
3
 
4
4
  export type RelativeDropPosition = "above" | "below" | "inside";
5
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;
6
+ export type MoveDragItemParameters = {
7
+ sourceIndex: number;
8
+ sourceListId: string;
9
+ targetIndex: number;
10
+ targetListId: string;
11
+ position: RelativeDropPosition;
12
+ };
13
+
14
+ export type DropValidator = (parameters: MoveDragItemParameters) => boolean;
15
+
16
+ export type AcceptsDrop = (parameters: MoveDragItemParameters) => boolean;
17
+
18
+ export type MoveDragItemHandler = (parameters: MoveDragItemParameters) => void;
29
19
 
30
20
  export type AcceptsFromList = ({
31
21
  sourceListId,
@@ -36,16 +26,16 @@ export type AcceptsFromList = ({
36
26
  }) => boolean;
37
27
 
38
28
  export interface DragItem {
39
- id: UniqueIdentifier;
29
+ itemId: UniqueIdentifier;
40
30
  listId: string;
41
31
  index: number;
42
32
  }
43
33
 
44
- export interface DropTarget {
45
- id: UniqueIdentifier;
46
- listId: string;
47
- index: number;
48
- }
34
+ export type DragState = {
35
+ source: DragItem | null;
36
+ target: DragItem | null;
37
+ indicator: RelativeDropPosition | null;
38
+ };
49
39
 
50
40
  export const normalizeListTargetIndex = (
51
41
  index: number,
@@ -54,11 +44,11 @@ export const normalizeListTargetIndex = (
54
44
  return position === "above" ? index : index + 1;
55
45
  };
56
46
 
57
- export const defaultAcceptsDrop: DropValidator = (
47
+ export const defaultAcceptsDrop: DropValidator = ({
58
48
  sourceIndex,
59
49
  targetIndex,
60
- position
61
- ) => {
50
+ position,
51
+ }) => {
62
52
  if (position === "inside") return false;
63
53
 
64
54
  const normalized = normalizeListTargetIndex(targetIndex, position);
@@ -120,13 +110,13 @@ export function validateDropIndicator({
120
110
  if (overIndex === -1) return undefined;
121
111
  if (activeIndex === -1 && sourceListId === targetListId) return undefined;
122
112
 
123
- const acceptsDropInside = acceptsDrop(
124
- activeIndex,
125
- overIndex,
126
- "inside",
113
+ const acceptsDropInside = acceptsDrop({
114
+ sourceIndex: activeIndex,
115
+ targetIndex: overIndex,
116
+ position: "inside",
127
117
  sourceListId,
128
- targetListId
129
- );
118
+ targetListId,
119
+ });
130
120
 
131
121
  // Drop into the middle third if possible
132
122
  if (
@@ -143,13 +133,13 @@ export function validateDropIndicator({
143
133
  );
144
134
 
145
135
  // Drop above or below if possible
146
- const acceptedHalf = acceptsDrop(
147
- activeIndex,
148
- overIndex,
149
- containingHalf,
136
+ const acceptedHalf = acceptsDrop({
137
+ sourceIndex: activeIndex,
138
+ targetIndex: overIndex,
139
+ position: containingHalf,
150
140
  sourceListId,
151
- targetListId
152
- );
141
+ targetListId,
142
+ });
153
143
 
154
144
  return acceptedHalf
155
145
  ? containingHalf
@@ -178,3 +168,17 @@ function getContainingHalf(
178
168
  if (offsetStart < elementStart + elementSize / 2) return "above";
179
169
  return "below";
180
170
  }
171
+
172
+ export const dragItemKeySeparator = "-~-";
173
+
174
+ export function createDragItemKey(listId: string, itemId: UniqueIdentifier) {
175
+ return `${listId}${dragItemKeySeparator}${itemId}`;
176
+ }
177
+
178
+ export function parseDragItemKey(key: string): {
179
+ listId: string;
180
+ itemId: UniqueIdentifier;
181
+ } {
182
+ const [listId, itemId] = key.split(dragItemKeySeparator);
183
+ return { listId, itemId };
184
+ }
@@ -88,13 +88,13 @@ export function acceptsDrop({
88
88
  return false;
89
89
  }
90
90
 
91
- return defaultAcceptsDrop(
92
- sourceIndexPath.at(-1)!,
93
- targetIndexPath.at(-1)!,
91
+ return defaultAcceptsDrop({
92
+ sourceIndex: sourceIndexPath.at(-1)!,
93
+ targetIndex: targetIndexPath.at(-1)!,
94
94
  position,
95
- "",
96
- ""
97
- );
95
+ sourceListId: "",
96
+ targetListId: "",
97
+ });
98
98
  }
99
99
 
100
100
  return true;