@noya-app/noya-designsystem 0.1.56 → 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,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,60 @@ 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;
102
-
103
- // For cross-list drops, use combined keys like MultiSortable does
104
- const validationKeys = isCrossContainer ? [active.id, ...keys] : keys;
105
-
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;
110
-
111
- const parentIndex =
112
- overIndex === -1 ? undefined : getDropTargetParentIndex?.(overIndex);
67
+ const activeDrag = useActiveDrag(activeDragContext);
68
+ const { source, target } = activeDrag;
69
+
70
+ if (!source || !target) return;
71
+
72
+ const indicator =
73
+ target.listId === listId && target.itemId === itemId
74
+ ? activeDrag.indicator
75
+ : undefined;
76
+
77
+ const targetParentIndex =
78
+ target.index === -1 ? undefined : getDropTargetParentIndex?.(target.index);
113
79
 
114
80
  const isValidParent =
115
- parentIndex === undefined || parentIndex === -1
81
+ targetParentIndex === undefined || targetParentIndex === -1
116
82
  ? false
117
- : acceptsDrop(activeIndex, parentIndex, "inside", listId, listId);
83
+ : acceptsDrop(
84
+ source.index,
85
+ targetParentIndex,
86
+ "inside",
87
+ source.listId,
88
+ target.listId
89
+ );
118
90
 
119
91
  const overIdAcceptsDrop =
120
- overIndex === -1
92
+ target.index === -1
121
93
  ? undefined
122
- : acceptsDrop(activeIndex, overIndex, "inside", listId, listId);
94
+ : acceptsDrop(
95
+ source.index,
96
+ target.index,
97
+ "inside",
98
+ source.listId,
99
+ target.listId
100
+ );
123
101
 
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;
102
+ const thisItemIsTargetParent = targetParentIndex === index;
138
103
 
139
104
  const showDragInsideIndicatorOnParent =
140
- !overIdAcceptsDrop && isValidParent && parentIndex === index;
105
+ !overIdAcceptsDrop && isValidParent && thisItemIsTargetParent;
141
106
 
142
- return (
143
- relativeDropPosition ??
144
- (showDragInsideIndicatorOnParent ? "inside" : undefined)
145
- );
107
+ return indicator ?? (showDragInsideIndicatorOnParent ? "inside" : undefined);
146
108
  }
147
109
 
148
110
  interface RootProps {
@@ -155,7 +117,7 @@ interface RootProps {
155
117
  acceptsFromList?: AcceptsFromList;
156
118
  acceptsDrop?: DropValidator;
157
119
  getDropTargetParentIndex?: (index: number) => number | undefined;
158
- sharedDragProviderContexts?: SharedDragProviderContexts;
120
+ sharedDragProps?: SharedDragProps;
159
121
  containerRef?: React.RefObject<HTMLElement>;
160
122
  }
161
123
 
@@ -168,11 +130,15 @@ function SortableItem<TElement extends HTMLElement>({
168
130
  disabled?: boolean;
169
131
  children: (props: ItemChildrenProps<TElement>) => React.ReactNode;
170
132
  }) {
171
- const { keys, acceptsDrop, axis, listId, getDropTargetParentIndex } =
133
+ const { acceptsDrop, listId, getDropTargetParentIndex } =
172
134
  React.useContext(SortableItemContext);
173
135
 
174
- const { attributes, listeners, setNodeRef, isDragging, index, overIndex } =
175
- useSortable({ id, disabled });
136
+ const dragItemKey = createDragItemKey(listId, id);
137
+
138
+ const { attributes, listeners, setNodeRef, isDragging, index } = useSortable({
139
+ id: dragItemKey,
140
+ disabled,
141
+ });
176
142
 
177
143
  const ref = React.useCallback(
178
144
  (node: TElement | null) => setNodeRef(node),
@@ -180,15 +146,10 @@ function SortableItem<TElement extends HTMLElement>({
180
146
  );
181
147
 
182
148
  const relativeDropPosition = useSortableDropIndicator({
183
- id,
184
149
  index,
185
- isDragging,
186
- overIndex,
187
- keys,
188
- acceptsDrop,
189
- axis,
150
+ itemId: dragItemKey,
190
151
  listId,
191
- activeIndex: index,
152
+ acceptsDrop,
192
153
  getDropTargetParentIndex,
193
154
  });
194
155
 
@@ -205,7 +166,7 @@ function SortableItem<TElement extends HTMLElement>({
205
166
 
206
167
  function SortableRoot_({
207
168
  id: idProp,
208
- keys,
169
+ keys: keysProp,
209
170
  onMoveItem,
210
171
  renderOverlay,
211
172
  acceptsFromList,
@@ -213,15 +174,17 @@ function SortableRoot_({
213
174
  axis = "y",
214
175
  children,
215
176
  getDropTargetParentIndex,
216
- sharedDragProviderContexts,
177
+ sharedDragProps,
217
178
  containerRef,
218
179
  }: RootProps) {
219
180
  const defaultId = React.useId();
220
181
  const id = idProp ?? defaultId;
182
+ const keys = React.useMemo(() => {
183
+ return keysProp.map((key) => createDragItemKey(id, key));
184
+ }, [keysProp, id]);
221
185
 
222
186
  const { registerList, unregisterList } = useDragRegistration(
223
- sharedDragProviderContexts?.dragRegistrationContext ??
224
- DragRegistrationContext
187
+ sharedDragProps?.dragRegistrationContext ?? DragRegistrationContext
225
188
  );
226
189
 
227
190
  const { setNodeRef: setDroppableRef } = useDroppable({
@@ -244,9 +207,7 @@ function SortableRoot_({
244
207
  const index = keys.findIndex((key) => key === id);
245
208
  return renderOverlay?.(index) ?? null;
246
209
  },
247
- acceptsFromList:
248
- acceptsFromList ??
249
- (({ sourceListId, targetListId }) => sourceListId === targetListId),
210
+ acceptsFromList: acceptsFromList ?? (() => true),
250
211
  acceptsDrop,
251
212
  getDropIndicator: (parameters) => {
252
213
  const { absolutePosition, active, over, sourceListId, targetListId } =
@@ -294,20 +255,12 @@ function SortableRoot_({
294
255
  () => ({
295
256
  keys,
296
257
  acceptsDrop,
297
- axis,
298
258
  listId: id,
299
259
  getDropTargetParentIndex,
300
260
  activeDragContext:
301
- sharedDragProviderContexts?.activeDragContext ?? ActiveDragContext,
261
+ sharedDragProps?.activeDragContext ?? ActiveDragContext,
302
262
  }),
303
- [
304
- keys,
305
- acceptsDrop,
306
- axis,
307
- id,
308
- getDropTargetParentIndex,
309
- sharedDragProviderContexts,
310
- ]
263
+ [keys, acceptsDrop, id, getDropTargetParentIndex, sharedDragProps]
311
264
  );
312
265
 
313
266
  // 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
  }
@@ -36,16 +36,16 @@ export type AcceptsFromList = ({
36
36
  }) => boolean;
37
37
 
38
38
  export interface DragItem {
39
- id: UniqueIdentifier;
39
+ itemId: UniqueIdentifier;
40
40
  listId: string;
41
41
  index: number;
42
42
  }
43
43
 
44
- export interface DropTarget {
45
- id: UniqueIdentifier;
46
- listId: string;
47
- index: number;
48
- }
44
+ export type DragState = {
45
+ source: DragItem | null;
46
+ target: DragItem | null;
47
+ indicator: RelativeDropPosition | null;
48
+ };
49
49
 
50
50
  export const normalizeListTargetIndex = (
51
51
  index: number,
@@ -142,6 +142,19 @@ export function validateDropIndicator({
142
142
  offsetStart
143
143
  );
144
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
+
145
158
  // Drop above or below if possible
146
159
  const acceptedHalf = acceptsDrop(
147
160
  activeIndex,
@@ -170,6 +183,16 @@ function isContainedInMiddleThird(
170
183
  );
171
184
  }
172
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
+
173
196
  function getContainingHalf(
174
197
  elementStart: number,
175
198
  elementSize: number,
@@ -178,3 +201,15 @@ function getContainingHalf(
178
201
  if (offsetStart < elementStart + elementSize / 2) return "above";
179
202
  return "below";
180
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
+ }