@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-designsystem",
3
- "version": "0.1.56",
3
+ "version": "0.1.57",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -8,6 +8,7 @@ import {
8
8
  ListViewRowProps,
9
9
  } from "../components/ListView";
10
10
  import { List } from "./List";
11
+ import { SharedDragProps } from "./sorting/DragRegistration";
11
12
 
12
13
  export type CollectionViewType = "grid" | "list";
13
14
 
@@ -93,6 +94,8 @@ export interface CollectionProps<T, M extends string = string> {
93
94
  /** @default false */
94
95
  sortable?: boolean;
95
96
  dragIndicatorStyle?: ListViewRowProps<M>["dragIndicatorStyle"];
97
+ sharedDragProps?: SharedDragProps;
98
+ sortableId?: string;
96
99
  }
97
100
 
98
101
  export const Collection = forwardRefGeneric(function Collection<
@@ -64,6 +64,8 @@ export const List = memoGeneric(
64
64
  getPlaceholder,
65
65
  onClickItem,
66
66
  dragIndicatorStyle,
67
+ sortableId,
68
+ sharedDragProps,
67
69
  } = props;
68
70
 
69
71
  const [internalHoveredId, setHoveredId] = useState<string>();
@@ -133,6 +135,8 @@ export const List = memoGeneric(
133
135
  onMoveItem={onMoveItem}
134
136
  renderEmptyState={renderEmptyState}
135
137
  getDropTargetParentIndex={getDropTargetParentIndex}
138
+ sharedDragProps={sharedDragProps}
139
+ sortableId={sortableId}
136
140
  {...dropTargetProps}
137
141
  renderItem={(
138
142
  item: T,
@@ -31,6 +31,7 @@ import { ContextMenu } from "./ContextMenu";
31
31
  import { InputField } from "./InputField";
32
32
  import { MenuItem } from "./internal/Menu";
33
33
  import { ScrollArea } from "./ScrollArea";
34
+ import { SharedDragProps } from "./sorting/DragRegistration";
34
35
  import { Sortable } from "./sorting/Sortable";
35
36
  import {
36
37
  DropValidator,
@@ -793,6 +794,8 @@ export type ListViewRootProps = {
793
794
  onDrop?: React.DragEventHandler<HTMLDivElement>;
794
795
  renderEmptyState?: () => ReactNode;
795
796
  getDropTargetParentIndex?: SortableItemContextProps["getDropTargetParentIndex"];
797
+ sharedDragProps?: SharedDragProps;
798
+ sortableId?: string;
796
799
  };
797
800
 
798
801
  const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
@@ -828,6 +831,8 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
828
831
  onDrop,
829
832
  renderEmptyState,
830
833
  getDropTargetParentIndex,
834
+ sharedDragProps,
835
+ sortableId,
831
836
  }: RenderProps<T> & ListViewRootProps,
832
837
  forwardedRef: ForwardedRef<IVirtualizedList>
833
838
  ) {
@@ -1000,11 +1005,13 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
1000
1005
  const withSortable = (children: ReactNode) =>
1001
1006
  sortable ? (
1002
1007
  <Sortable.Root
1008
+ id={sortableId}
1003
1009
  onMoveItem={onMoveItem}
1004
1010
  keys={ids}
1005
1011
  renderOverlay={renderOverlay}
1006
1012
  acceptsDrop={acceptsDrop}
1007
1013
  getDropTargetParentIndex={getDropTargetParentIndex}
1014
+ sharedDragProps={sharedDragProps}
1008
1015
  >
1009
1016
  {children}
1010
1017
  </Sortable.Root>
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  AcceptsDrop,
3
+ DragState,
3
4
  MoveDragItemHandler,
4
5
  RelativeDropPosition,
5
6
  } from "./sorting";
@@ -9,7 +10,6 @@ import { AcceptsFromList } from "./sorting";
9
10
 
10
11
  import { Point } from "@noya-app/noya-geometry";
11
12
  import React from "react";
12
- import { DragItem } from "./sorting";
13
13
 
14
14
  export type GetDropIndicatorParameters = {
15
15
  absolutePosition: Point;
@@ -73,10 +73,7 @@ export function useDragRegistrationManager() {
73
73
  return { registerList, unregisterList, registeredLists };
74
74
  }
75
75
 
76
- export type ActiveDragContextType = {
77
- activeItem: DragItem | null;
78
- delta: Point;
79
- };
76
+ export type ActiveDragContextType = DragState;
80
77
 
81
78
  export const ActiveDragContext =
82
79
  React.createContext<ActiveDragContextType | null>(null);
@@ -104,7 +101,7 @@ export function useAnyDragContext() {
104
101
  return React.useContext(AnyDragContext);
105
102
  }
106
103
 
107
- export type SharedDragProviderContexts = {
104
+ export type SharedDragProps = {
108
105
  dragRegistrationContext: React.Context<DragRegistrationContextType | null>;
109
106
  activeDragContext: React.Context<ActiveDragContextType | null>;
110
107
  };
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Active,
2
3
  closestCenter,
3
4
  CollisionDetection,
4
5
  DndContext,
@@ -6,11 +7,13 @@ import {
6
7
  DragMoveEvent,
7
8
  DragOverlay,
8
9
  DragStartEvent,
10
+ Over,
9
11
  PointerSensor,
10
12
  pointerWithin,
11
13
  useSensor,
12
14
  useSensors,
13
15
  } from "@dnd-kit/core";
16
+ import { Point } from "@noya-app/noya-geometry";
14
17
  import * as React from "react";
15
18
  import { useMemo } from "react";
16
19
  import { createPortal } from "react-dom";
@@ -20,19 +23,20 @@ import {
20
23
  AnyDragContext,
21
24
  DragRegistrationContext,
22
25
  DragRegistrationContextType,
23
- SharedDragProviderContexts,
26
+ RegisteredList,
27
+ SharedDragProps,
24
28
  useDragRegistrationManager,
25
29
  } from "./DragRegistration";
26
- import { DragItem } from "./sorting";
30
+ import { DragItem, DragState, RelativeDropPosition } from "./sorting";
27
31
 
28
32
  type SharedDragProviderProps = {
29
33
  children: React.ReactNode;
30
- contexts?: SharedDragProviderContexts;
34
+ sharedDragProps?: SharedDragProps;
31
35
  };
32
36
 
33
37
  export function SharedDragProvider({
34
38
  children,
35
- contexts = {
39
+ sharedDragProps = {
36
40
  dragRegistrationContext: DragRegistrationContext,
37
41
  activeDragContext: ActiveDragContext,
38
42
  },
@@ -46,28 +50,17 @@ export function SharedDragProvider({
46
50
  );
47
51
 
48
52
  const mounted = useMounted();
49
- const [activeItem, setActiveItem] = React.useState<DragItem | null>(null);
50
- const [delta, setDelta] = React.useState({ x: 0, y: 0 });
53
+ const [dragState, setDragState] = React.useState<DragState>({
54
+ source: null,
55
+ target: null,
56
+ indicator: null,
57
+ });
58
+
51
59
  const { registerList, unregisterList, registeredLists } =
52
60
  useDragRegistrationManager();
53
61
 
54
62
  const activatorEventRef = React.useRef<PointerEvent | null>(null);
55
63
 
56
- const acceptsListDrop = React.useCallback(
57
- (sourceListId: string, targetListId: string) => {
58
- const sourceList = registeredLists.get(sourceListId);
59
- const targetList = registeredLists.get(targetListId);
60
-
61
- if (!sourceList || !targetList) return false;
62
-
63
- return targetList.acceptsFromList({
64
- sourceListId,
65
- targetListId,
66
- });
67
- },
68
- [registeredLists]
69
- );
70
-
71
64
  const handleDragStart = React.useCallback(
72
65
  (event: DragStartEvent) => {
73
66
  // Capture the activator event for position calculations
@@ -79,12 +72,16 @@ export function SharedDragProvider({
79
72
 
80
73
  if (itemIndex >= 0) {
81
74
  const dragItem: DragItem = {
82
- id: event.active.id,
75
+ itemId: event.active.id,
83
76
  listId,
84
77
  index: itemIndex,
85
78
  };
86
79
 
87
- setActiveItem(dragItem);
80
+ setDragState({
81
+ source: dragItem,
82
+ target: null,
83
+ indicator: null,
84
+ });
88
85
  break;
89
86
  }
90
87
  }
@@ -92,129 +89,90 @@ export function SharedDragProvider({
92
89
  [registeredLists]
93
90
  );
94
91
 
95
- const handleDragMove = React.useCallback((event: DragMoveEvent) => {
96
- setDelta({ x: event.delta.x, y: event.delta.y });
97
- }, []);
98
-
99
- const handleDragEnd = React.useCallback(
100
- (event: DragEndEvent) => {
92
+ const handleDragMove = React.useCallback(
93
+ (event: DragMoveEvent) => {
101
94
  const { active, over } = event;
102
- const activatorEventPoint = activatorEventRef.current
103
- ? {
104
- x: activatorEventRef.current.clientX,
105
- y: activatorEventRef.current.clientY,
106
- }
107
- : null;
108
95
 
109
- setActiveItem(null);
110
- activatorEventRef.current = null;
111
-
112
- if (!activeItem || !over || active.id === over.id) {
113
- return;
114
- }
96
+ if (!activatorEventRef.current || !dragState.source) return;
97
+
98
+ const dropTarget = findDropTarget({
99
+ activationPoint: {
100
+ x: activatorEventRef.current.clientX,
101
+ y: activatorEventRef.current.clientY,
102
+ },
103
+ delta: { x: event.delta.x, y: event.delta.y },
104
+ over,
105
+ active,
106
+ source: dragState.source,
107
+ registeredLists,
108
+ });
115
109
 
116
- // Find target list and item
117
- let targetListId: string | null = null;
118
- let targetIndex: number = -1;
110
+ setDragState({
111
+ source: dragState.source,
112
+ target: dropTarget?.target ?? null,
113
+ indicator: dropTarget?.indicator ?? null,
114
+ });
115
+ },
116
+ [registeredLists, dragState.source]
117
+ );
119
118
 
120
- for (const [listId, list] of registeredLists) {
121
- const itemIndex = list.keys.findIndex((key) => key === over.id);
119
+ const handleDragEnd = React.useCallback(
120
+ (event: DragEndEvent) => {
121
+ const { active, over } = event;
122
122
 
123
- if (itemIndex >= 0) {
124
- targetListId = listId;
125
- targetIndex = itemIndex;
126
- break;
127
- }
123
+ if (!activatorEventRef.current || !dragState.source) return;
124
+
125
+ const dropTarget = findDropTarget({
126
+ activationPoint: {
127
+ x: activatorEventRef.current.clientX,
128
+ y: activatorEventRef.current.clientY,
129
+ },
130
+ delta: { x: event.delta.x, y: event.delta.y },
131
+ over,
132
+ active,
133
+ source: dragState.source,
134
+ registeredLists,
135
+ });
128
136
 
129
- // Also check if dropping on the list container itself
130
- if (over.id === listId) {
131
- targetListId = listId;
132
- targetIndex = list.keys.length; // Append to end
133
- break;
134
- }
135
- }
137
+ setDragState({
138
+ source: null,
139
+ target: null,
140
+ indicator: null,
141
+ });
142
+ activatorEventRef.current = null;
136
143
 
137
- if (!targetListId || !acceptsListDrop(activeItem.listId, targetListId)) {
138
- setActiveItem(null);
139
- activatorEventRef.current = null;
140
- return;
141
- }
144
+ if (!dropTarget) return;
142
145
 
143
- const targetList = registeredLists.get(targetListId);
144
- const sourceList = registeredLists.get(activeItem.listId);
146
+ const sourceList = registeredLists.get(dropTarget.source.listId);
147
+ const targetList = registeredLists.get(dropTarget.target.listId);
145
148
 
146
- if (!targetList || !sourceList) {
147
- return;
148
- }
149
+ if (!sourceList || !targetList) return;
149
150
 
150
- // Handle drops into items within the list
151
- if (targetIndex < targetList.keys.length && activatorEventPoint) {
152
- // Calculate absolute cursor position
153
- const eventX = activatorEventPoint.x;
154
- const eventY = activatorEventPoint.y;
155
- const currentX = eventX + delta.x;
156
- const currentY = eventY + delta.y;
157
- const absolutePosition = { x: currentX, y: currentY };
158
-
159
- const indicator = targetList.getDropIndicator({
160
- absolutePosition,
161
- active,
162
- over,
163
- sourceListId: activeItem.listId,
164
- targetListId,
165
- });
166
-
167
- if (!indicator) return;
168
-
169
- // Notify the source list to handle the move
170
- sourceList.onMoveItem(
171
- activeItem.index,
172
- targetIndex,
173
- indicator,
174
- activeItem.listId,
175
- targetListId
176
- );
177
- }
178
- // Handle drops into empty lists or at the end of lists
179
- else if (targetIndex >= targetList.keys.length) {
180
- // For empty lists or drops at the end, we don't need detailed position validation
181
- // Just check if the drop is acceptable with a "below" position
182
- const canDrop = targetList.acceptsDrop(
183
- activeItem.index,
184
- Math.max(0, targetList.keys.length - 1), // Use last valid index or 0 for empty lists
185
- "below",
186
- activeItem.listId,
187
- targetListId
188
- );
189
-
190
- if (canDrop) {
191
- sourceList.onMoveItem(
192
- activeItem.index,
193
- targetIndex,
194
- "below",
195
- activeItem.listId,
196
- targetListId
197
- );
198
- }
199
- }
151
+ sourceList.onMoveItem(
152
+ dropTarget.source.index,
153
+ dropTarget.target.index,
154
+ dropTarget.indicator,
155
+ dropTarget.source.listId,
156
+ dropTarget.target.listId
157
+ );
200
158
  },
201
- [activeItem, registeredLists, acceptsListDrop, delta]
159
+ [registeredLists, dragState.source]
202
160
  );
203
161
 
204
162
  const contextValue: DragRegistrationContextType = React.useMemo(
205
163
  () => ({
206
- activeItem,
207
- delta,
208
164
  registerList,
209
165
  unregisterList,
210
166
  }),
211
- [activeItem, delta, registerList, unregisterList]
167
+ [registerList, unregisterList]
212
168
  );
213
169
 
214
- const activeList = activeItem ? registeredLists.get(activeItem.listId) : null;
170
+ const activeList = dragState.source
171
+ ? registeredLists.get(dragState.source.listId)
172
+ : null;
215
173
  const activeDragContextValue: ActiveDragContextType = React.useMemo(
216
- () => ({ activeItem, delta }),
217
- [activeItem, delta]
174
+ () => dragState,
175
+ [dragState]
218
176
  );
219
177
 
220
178
  const collisionDetection = useMemo(() => {
@@ -223,8 +181,10 @@ export function SharedDragProvider({
223
181
 
224
182
  return (
225
183
  <AnyDragContext.Provider value={true}>
226
- <contexts.dragRegistrationContext.Provider value={contextValue}>
227
- <contexts.activeDragContext.Provider value={activeDragContextValue}>
184
+ <sharedDragProps.dragRegistrationContext.Provider value={contextValue}>
185
+ <sharedDragProps.activeDragContext.Provider
186
+ value={activeDragContextValue}
187
+ >
228
188
  <DndContext
229
189
  sensors={sensors}
230
190
  collisionDetection={collisionDetection}
@@ -235,16 +195,16 @@ export function SharedDragProvider({
235
195
  {children}
236
196
  {mounted &&
237
197
  activeList?.renderOverlay &&
238
- activeItem &&
198
+ dragState.source &&
239
199
  createPortal(
240
200
  <DragOverlay dropAnimation={null}>
241
- {activeList.renderOverlay(activeItem.id)}
201
+ {activeList.renderOverlay(dragState.source.itemId)}
242
202
  </DragOverlay>,
243
203
  document.body
244
204
  )}
245
205
  </DndContext>
246
- </contexts.activeDragContext.Provider>
247
- </contexts.dragRegistrationContext.Provider>
206
+ </sharedDragProps.activeDragContext.Provider>
207
+ </sharedDragProps.dragRegistrationContext.Provider>
248
208
  </AnyDragContext.Provider>
249
209
  );
250
210
  }
@@ -305,3 +265,112 @@ export const getItemFirstCollisionDetection =
305
265
  ? pointerCollisions
306
266
  : closestCenter(parameters);
307
267
  };
268
+
269
+ function findDropTarget({
270
+ activationPoint,
271
+ delta,
272
+ over,
273
+ active,
274
+ source,
275
+ registeredLists,
276
+ }: {
277
+ activationPoint: Point;
278
+ delta: Point;
279
+ over: Over | null;
280
+ active: Active;
281
+ source: DragItem;
282
+ registeredLists: Map<string, RegisteredList>;
283
+ }):
284
+ | {
285
+ source: DragItem;
286
+ target: DragItem;
287
+ indicator: RelativeDropPosition;
288
+ }
289
+ | undefined {
290
+ if (!over || active.id === over.id) return;
291
+
292
+ // Find target list and item
293
+ let targetListId: string | null = null;
294
+ let targetIndex: number = -1;
295
+
296
+ for (const [listId, list] of registeredLists) {
297
+ const itemIndex = list.keys.findIndex((key) => key === over.id);
298
+
299
+ if (itemIndex >= 0) {
300
+ targetListId = listId;
301
+ targetIndex = itemIndex;
302
+ break;
303
+ }
304
+
305
+ // Also check if dropping on the list container itself
306
+ if (over.id === listId) {
307
+ targetListId = listId;
308
+ targetIndex = list.keys.length; // Append to end
309
+ break;
310
+ }
311
+ }
312
+
313
+ const targetList =
314
+ targetListId !== null ? registeredLists.get(targetListId) : null;
315
+ const sourceList = registeredLists.get(source.listId);
316
+
317
+ if (
318
+ !targetList ||
319
+ !sourceList ||
320
+ !targetList.acceptsFromList({
321
+ sourceListId: sourceList.id,
322
+ targetListId: targetList.id,
323
+ })
324
+ ) {
325
+ return;
326
+ }
327
+
328
+ // Dropping on the list container itself
329
+ if (over.id === targetList.id) {
330
+ const canDrop = targetList.acceptsDrop(
331
+ source.index,
332
+ Math.max(0, targetList.keys.length - 1), // Use last valid index or 0 for empty lists
333
+ "below",
334
+ sourceList.id,
335
+ targetList.id
336
+ );
337
+
338
+ if (canDrop) {
339
+ return {
340
+ source,
341
+ target: {
342
+ itemId: over.id,
343
+ listId: targetList.id,
344
+ index: targetIndex,
345
+ },
346
+ indicator: "below",
347
+ };
348
+ }
349
+ }
350
+
351
+ const eventX = activationPoint.x;
352
+ const eventY = activationPoint.y;
353
+ const currentX = eventX + delta.x;
354
+ const currentY = eventY + delta.y;
355
+ const absolutePosition = { x: currentX, y: currentY };
356
+
357
+ const indicator = targetList.getDropIndicator({
358
+ absolutePosition,
359
+ active,
360
+ over,
361
+ sourceListId: sourceList.id,
362
+ targetListId: targetList.id,
363
+ });
364
+
365
+ if (indicator) {
366
+ return {
367
+ source,
368
+ target: {
369
+ itemId: over.id,
370
+ listId: targetList.id,
371
+ index: targetIndex,
372
+ },
373
+ indicator,
374
+ };
375
+ }
376
+ }