@noya-app/noya-designsystem 0.1.54 → 0.1.56
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 +16 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +136 -44
- package/dist/index.d.ts +136 -44
- package/dist/index.js +1240 -802
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1484 -1054
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/validateDropIndicator.test.ts +35 -9
- package/src/components/Button.tsx +13 -0
- package/src/components/Dialog.tsx +3 -5
- package/src/components/Grid.tsx +3 -1
- package/src/components/GridView.tsx +1 -0
- package/src/components/InputField.tsx +10 -3
- package/src/components/ListView.tsx +5 -8
- package/src/components/Popover.tsx +68 -4
- package/src/components/SegmentedControl.tsx +41 -18
- package/src/components/SelectionToolbar.tsx +35 -22
- package/src/components/Toolbar.tsx +22 -12
- package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +3 -26
- package/src/components/sorting/DragRegistration.tsx +110 -0
- package/src/components/sorting/SharedDragProvider.tsx +307 -0
- package/src/components/sorting/Sortable.tsx +404 -0
- package/src/components/sorting/createSharedDrag.tsx +46 -0
- package/src/components/sorting/sorting.ts +180 -0
- package/src/contexts/DesignSystemConfiguration.tsx +8 -5
- package/src/contexts/OpenPortalsContext.tsx +67 -0
- package/src/index.css +2 -2
- package/src/index.tsx +5 -2
- package/src/utils/moveTreeItem.ts +4 -2
- package/src/components/Sortable.tsx +0 -468
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { DropdownChevronIcon } from "@noya-app/noya-icons";
|
|
2
1
|
import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
|
|
3
|
-
import React from "react";
|
|
2
|
+
import React, { ComponentProps } from "react";
|
|
4
3
|
|
|
5
4
|
import { memoGeneric } from "@noya-app/react-utils";
|
|
6
5
|
import { BaseToolbar } from "./BaseToolbar";
|
|
7
6
|
import { Button } from "./Button";
|
|
8
7
|
import { DividerVertical } from "./Divider";
|
|
9
8
|
import { DropdownMenu } from "./DropdownMenu";
|
|
10
|
-
import { renderIcon } from "./Icons";
|
|
11
9
|
import {
|
|
12
10
|
getKeyboardShortcutsForMenuItems,
|
|
13
11
|
isSelectableMenuItem,
|
|
@@ -15,7 +13,6 @@ import {
|
|
|
15
13
|
SelectableMenuItem,
|
|
16
14
|
SubMenuItem,
|
|
17
15
|
} from "./internal/Menu";
|
|
18
|
-
import { Spacer } from "./Spacer";
|
|
19
16
|
import { Tooltip } from "./Tooltip";
|
|
20
17
|
|
|
21
18
|
const iconButtonStyle = {
|
|
@@ -46,12 +43,13 @@ export const ToolbarMenuDropdown = memoGeneric(function ToolbarMenuDropdown<
|
|
|
46
43
|
}
|
|
47
44
|
}}
|
|
48
45
|
>
|
|
49
|
-
<Button
|
|
50
|
-
{item.
|
|
51
|
-
{item.
|
|
46
|
+
<Button
|
|
47
|
+
disabled={item.disabled}
|
|
48
|
+
active={item.checked || open}
|
|
49
|
+
icon={item.icon}
|
|
50
|
+
iconRight="DropdownChevronIcon"
|
|
51
|
+
>
|
|
52
52
|
{item.title}
|
|
53
|
-
<Spacer.Horizontal inline size={6} />
|
|
54
|
-
<DropdownChevronIcon />
|
|
55
53
|
</Button>
|
|
56
54
|
</DropdownMenu>
|
|
57
55
|
);
|
|
@@ -62,23 +60,26 @@ export const ToolbarMenuButton = memoGeneric(function ToolbarMenuButton<
|
|
|
62
60
|
>({
|
|
63
61
|
item,
|
|
64
62
|
onSelectMenuItem,
|
|
63
|
+
as,
|
|
65
64
|
}: {
|
|
66
65
|
item: SelectableMenuItem<T>;
|
|
67
66
|
onSelectMenuItem?: (value: T) => void;
|
|
67
|
+
as?: ComponentProps<typeof Button>["as"];
|
|
68
68
|
}) {
|
|
69
69
|
const content = (
|
|
70
70
|
<Button
|
|
71
|
+
as={as}
|
|
71
72
|
disabled={item.disabled}
|
|
72
73
|
active={item.checked}
|
|
74
|
+
icon={item.icon}
|
|
73
75
|
style={item.icon && !item.title ? iconButtonStyle : undefined}
|
|
76
|
+
{...(as === "a" && { href: item.value })}
|
|
74
77
|
onClick={() => {
|
|
75
78
|
if (onSelectMenuItem && item.value) {
|
|
76
79
|
onSelectMenuItem(item.value);
|
|
77
80
|
}
|
|
78
81
|
}}
|
|
79
82
|
>
|
|
80
|
-
{item.icon && renderIcon(item.icon)}
|
|
81
|
-
{item.title && item.icon && <Spacer.Horizontal inline size={6} />}
|
|
82
83
|
{item.title}
|
|
83
84
|
</Button>
|
|
84
85
|
);
|
|
@@ -97,9 +98,11 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
|
|
|
97
98
|
>({
|
|
98
99
|
item,
|
|
99
100
|
onSelectMenuItem,
|
|
101
|
+
buttonAs,
|
|
100
102
|
}: {
|
|
101
103
|
item: MenuItem<T>;
|
|
102
104
|
onSelectMenuItem?: (value: T) => void;
|
|
105
|
+
buttonAs?: ComponentProps<typeof Button>["as"];
|
|
103
106
|
}) {
|
|
104
107
|
if (item.type === "sectionHeader") return null;
|
|
105
108
|
|
|
@@ -109,7 +112,11 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
|
|
|
109
112
|
|
|
110
113
|
if (isSelectableMenuItem(item)) {
|
|
111
114
|
return (
|
|
112
|
-
<ToolbarMenuButton
|
|
115
|
+
<ToolbarMenuButton
|
|
116
|
+
item={item}
|
|
117
|
+
onSelectMenuItem={onSelectMenuItem}
|
|
118
|
+
as={buttonAs}
|
|
119
|
+
/>
|
|
113
120
|
);
|
|
114
121
|
}
|
|
115
122
|
|
|
@@ -121,9 +128,11 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
|
|
|
121
128
|
export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
|
|
122
129
|
items,
|
|
123
130
|
onSelectMenuItem,
|
|
131
|
+
buttonAs,
|
|
124
132
|
}: {
|
|
125
133
|
items: MenuItem<T>[];
|
|
126
134
|
onSelectMenuItem?: (value: T) => void;
|
|
135
|
+
buttonAs?: ComponentProps<typeof Button>["as"];
|
|
127
136
|
}) {
|
|
128
137
|
return (
|
|
129
138
|
<>
|
|
@@ -133,6 +142,7 @@ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
|
|
|
133
142
|
key={i}
|
|
134
143
|
item={item}
|
|
135
144
|
onSelectMenuItem={onSelectMenuItem}
|
|
145
|
+
buttonAs={buttonAs}
|
|
136
146
|
/>
|
|
137
147
|
);
|
|
138
148
|
})}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { cssVars } from "../../theme";
|
|
2
|
-
|
|
3
|
-
import { ChatBubbleWithDotsIcon } from "@noya-app/noya-icons";
|
|
4
1
|
import React from "react";
|
|
5
2
|
import { INPUT_HEIGHT } from "../../theme";
|
|
6
|
-
import { colorFromString } from "../../utils/colorFromString";
|
|
7
3
|
import { Avatar, AvatarProps, AvatarStack } from "../Avatar";
|
|
8
4
|
import Button from "../Button";
|
|
9
|
-
import { Spacer } from "../Spacer";
|
|
10
5
|
import { Small } from "../Text";
|
|
11
6
|
import { Tooltip } from "../Tooltip";
|
|
12
7
|
|
|
@@ -25,14 +20,12 @@ export const UserAvatar = ({ userId, name, image }: AvatarProps) => (
|
|
|
25
20
|
|
|
26
21
|
type ConnectedUsersMenuLayoutProps = {
|
|
27
22
|
renderUsers: () => React.ReactNode;
|
|
28
|
-
currentUserId?: string;
|
|
29
23
|
launchAIAssistant?: () => void;
|
|
30
24
|
isAssistantOpen?: boolean;
|
|
31
25
|
};
|
|
32
26
|
|
|
33
27
|
export const ConnectedUsersMenuLayout = ({
|
|
34
28
|
renderUsers,
|
|
35
|
-
currentUserId,
|
|
36
29
|
launchAIAssistant,
|
|
37
30
|
isAssistantOpen,
|
|
38
31
|
}: ConnectedUsersMenuLayoutProps) => {
|
|
@@ -41,29 +34,13 @@ export const ConnectedUsersMenuLayout = ({
|
|
|
41
34
|
<AvatarStack size={INPUT_HEIGHT} className="mr-1">
|
|
42
35
|
{renderUsers()}
|
|
43
36
|
</AvatarStack>
|
|
44
|
-
|
|
45
|
-
{/* AI Assistant Avatar */}
|
|
46
37
|
{launchAIAssistant && (
|
|
47
|
-
<Tooltip
|
|
48
|
-
content={
|
|
49
|
-
<div className="flex flex-col">
|
|
50
|
-
<Small>AI Assistant</Small>
|
|
51
|
-
</div>
|
|
52
|
-
}
|
|
53
|
-
>
|
|
38
|
+
<Tooltip content="AI Assistant">
|
|
54
39
|
<Button
|
|
55
|
-
|
|
40
|
+
active={isAssistantOpen}
|
|
56
41
|
onClick={launchAIAssistant}
|
|
57
|
-
|
|
58
|
-
backgroundColor: isAssistantOpen
|
|
59
|
-
? colorFromString(currentUserId ?? "")
|
|
60
|
-
: undefined,
|
|
61
|
-
}}
|
|
42
|
+
icon="ChatBubbleWithDotsIcon"
|
|
62
43
|
>
|
|
63
|
-
<ChatBubbleWithDotsIcon
|
|
64
|
-
color={isAssistantOpen ? undefined : cssVars.colors.icon}
|
|
65
|
-
/>
|
|
66
|
-
<Spacer.Horizontal inline size={6} />
|
|
67
44
|
AI
|
|
68
45
|
</Button>
|
|
69
46
|
</Tooltip>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AcceptsDrop,
|
|
3
|
+
MoveDragItemHandler,
|
|
4
|
+
RelativeDropPosition,
|
|
5
|
+
} from "./sorting";
|
|
6
|
+
|
|
7
|
+
import { Active, Over, UniqueIdentifier } from "@dnd-kit/core";
|
|
8
|
+
import { AcceptsFromList } from "./sorting";
|
|
9
|
+
|
|
10
|
+
import { Point } from "@noya-app/noya-geometry";
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { DragItem } from "./sorting";
|
|
13
|
+
|
|
14
|
+
export type GetDropIndicatorParameters = {
|
|
15
|
+
absolutePosition: Point;
|
|
16
|
+
active: Active;
|
|
17
|
+
over: Over;
|
|
18
|
+
sourceListId: string;
|
|
19
|
+
targetListId: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface RegisteredList {
|
|
23
|
+
id: string;
|
|
24
|
+
keys: UniqueIdentifier[];
|
|
25
|
+
onMoveItem: MoveDragItemHandler;
|
|
26
|
+
renderOverlay?: (id: UniqueIdentifier) => React.ReactNode;
|
|
27
|
+
acceptsFromList: AcceptsFromList;
|
|
28
|
+
acceptsDrop: AcceptsDrop;
|
|
29
|
+
getDropIndicator: (
|
|
30
|
+
parameters: GetDropIndicatorParameters
|
|
31
|
+
) => RelativeDropPosition | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DragRegistrationContextType {
|
|
35
|
+
registerList: (list: RegisteredList) => void;
|
|
36
|
+
unregisterList: (listId: string) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const DragRegistrationContext =
|
|
40
|
+
React.createContext<DragRegistrationContextType | null>(null);
|
|
41
|
+
|
|
42
|
+
export function useDragRegistration(
|
|
43
|
+
context: React.Context<DragRegistrationContextType | null>
|
|
44
|
+
): DragRegistrationContextType {
|
|
45
|
+
const value = React.useContext(context);
|
|
46
|
+
|
|
47
|
+
if (!value) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"useDragRegistration must be used within a DragRegistrationProvider"
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function useDragRegistrationManager() {
|
|
57
|
+
const [registeredLists, setRegisteredLists] = React.useState<
|
|
58
|
+
Map<string, RegisteredList>
|
|
59
|
+
>(new Map());
|
|
60
|
+
|
|
61
|
+
const registerList = React.useCallback((list: RegisteredList) => {
|
|
62
|
+
setRegisteredLists((prev) => new Map(prev).set(list.id, list));
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const unregisterList = React.useCallback((listId: string) => {
|
|
66
|
+
setRegisteredLists((prev) => {
|
|
67
|
+
const newMap = new Map(prev);
|
|
68
|
+
newMap.delete(listId);
|
|
69
|
+
return newMap;
|
|
70
|
+
});
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
return { registerList, unregisterList, registeredLists };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type ActiveDragContextType = {
|
|
77
|
+
activeItem: DragItem | null;
|
|
78
|
+
delta: Point;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const ActiveDragContext =
|
|
82
|
+
React.createContext<ActiveDragContextType | null>(null);
|
|
83
|
+
|
|
84
|
+
export function useActiveDrag(
|
|
85
|
+
context: React.Context<ActiveDragContextType | null>
|
|
86
|
+
): ActiveDragContextType {
|
|
87
|
+
const value = React.useContext(context);
|
|
88
|
+
|
|
89
|
+
if (!value) {
|
|
90
|
+
throw new Error("useActiveDrag must be used within a ActiveDragProvider");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* We use this context to automatically create a drag context if not exists.
|
|
98
|
+
* We do this separately from the contexts that actually store data since for those
|
|
99
|
+
* we want to allow a custom context to be passed and not get overwritten by nested contexts.
|
|
100
|
+
*/
|
|
101
|
+
export const AnyDragContext = React.createContext<boolean>(false);
|
|
102
|
+
|
|
103
|
+
export function useAnyDragContext() {
|
|
104
|
+
return React.useContext(AnyDragContext);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type SharedDragProviderContexts = {
|
|
108
|
+
dragRegistrationContext: React.Context<DragRegistrationContextType | null>;
|
|
109
|
+
activeDragContext: React.Context<ActiveDragContextType | null>;
|
|
110
|
+
};
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closestCenter,
|
|
3
|
+
CollisionDetection,
|
|
4
|
+
DndContext,
|
|
5
|
+
DragEndEvent,
|
|
6
|
+
DragMoveEvent,
|
|
7
|
+
DragOverlay,
|
|
8
|
+
DragStartEvent,
|
|
9
|
+
PointerSensor,
|
|
10
|
+
pointerWithin,
|
|
11
|
+
useSensor,
|
|
12
|
+
useSensors,
|
|
13
|
+
} from "@dnd-kit/core";
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
import { useMemo } from "react";
|
|
16
|
+
import { createPortal } from "react-dom";
|
|
17
|
+
import {
|
|
18
|
+
ActiveDragContext,
|
|
19
|
+
ActiveDragContextType,
|
|
20
|
+
AnyDragContext,
|
|
21
|
+
DragRegistrationContext,
|
|
22
|
+
DragRegistrationContextType,
|
|
23
|
+
SharedDragProviderContexts,
|
|
24
|
+
useDragRegistrationManager,
|
|
25
|
+
} from "./DragRegistration";
|
|
26
|
+
import { DragItem } from "./sorting";
|
|
27
|
+
|
|
28
|
+
type SharedDragProviderProps = {
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
contexts?: SharedDragProviderContexts;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function SharedDragProvider({
|
|
34
|
+
children,
|
|
35
|
+
contexts = {
|
|
36
|
+
dragRegistrationContext: DragRegistrationContext,
|
|
37
|
+
activeDragContext: ActiveDragContext,
|
|
38
|
+
},
|
|
39
|
+
}: SharedDragProviderProps) {
|
|
40
|
+
const sensors = useSensors(
|
|
41
|
+
useSensor(PointerSensor, {
|
|
42
|
+
activationConstraint: {
|
|
43
|
+
distance: 4,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const mounted = useMounted();
|
|
49
|
+
const [activeItem, setActiveItem] = React.useState<DragItem | null>(null);
|
|
50
|
+
const [delta, setDelta] = React.useState({ x: 0, y: 0 });
|
|
51
|
+
const { registerList, unregisterList, registeredLists } =
|
|
52
|
+
useDragRegistrationManager();
|
|
53
|
+
|
|
54
|
+
const activatorEventRef = React.useRef<PointerEvent | null>(null);
|
|
55
|
+
|
|
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
|
+
const handleDragStart = React.useCallback(
|
|
72
|
+
(event: DragStartEvent) => {
|
|
73
|
+
// Capture the activator event for position calculations
|
|
74
|
+
activatorEventRef.current = event.activatorEvent as PointerEvent;
|
|
75
|
+
|
|
76
|
+
// Find which list contains this item
|
|
77
|
+
for (const [listId, list] of registeredLists) {
|
|
78
|
+
const itemIndex = list.keys.findIndex((key) => key === event.active.id);
|
|
79
|
+
|
|
80
|
+
if (itemIndex >= 0) {
|
|
81
|
+
const dragItem: DragItem = {
|
|
82
|
+
id: event.active.id,
|
|
83
|
+
listId,
|
|
84
|
+
index: itemIndex,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
setActiveItem(dragItem);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
[registeredLists]
|
|
93
|
+
);
|
|
94
|
+
|
|
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) => {
|
|
101
|
+
const { active, over } = event;
|
|
102
|
+
const activatorEventPoint = activatorEventRef.current
|
|
103
|
+
? {
|
|
104
|
+
x: activatorEventRef.current.clientX,
|
|
105
|
+
y: activatorEventRef.current.clientY,
|
|
106
|
+
}
|
|
107
|
+
: null;
|
|
108
|
+
|
|
109
|
+
setActiveItem(null);
|
|
110
|
+
activatorEventRef.current = null;
|
|
111
|
+
|
|
112
|
+
if (!activeItem || !over || active.id === over.id) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Find target list and item
|
|
117
|
+
let targetListId: string | null = null;
|
|
118
|
+
let targetIndex: number = -1;
|
|
119
|
+
|
|
120
|
+
for (const [listId, list] of registeredLists) {
|
|
121
|
+
const itemIndex = list.keys.findIndex((key) => key === over.id);
|
|
122
|
+
|
|
123
|
+
if (itemIndex >= 0) {
|
|
124
|
+
targetListId = listId;
|
|
125
|
+
targetIndex = itemIndex;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
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
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!targetListId || !acceptsListDrop(activeItem.listId, targetListId)) {
|
|
138
|
+
setActiveItem(null);
|
|
139
|
+
activatorEventRef.current = null;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const targetList = registeredLists.get(targetListId);
|
|
144
|
+
const sourceList = registeredLists.get(activeItem.listId);
|
|
145
|
+
|
|
146
|
+
if (!targetList || !sourceList) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
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
|
+
}
|
|
200
|
+
},
|
|
201
|
+
[activeItem, registeredLists, acceptsListDrop, delta]
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const contextValue: DragRegistrationContextType = React.useMemo(
|
|
205
|
+
() => ({
|
|
206
|
+
activeItem,
|
|
207
|
+
delta,
|
|
208
|
+
registerList,
|
|
209
|
+
unregisterList,
|
|
210
|
+
}),
|
|
211
|
+
[activeItem, delta, registerList, unregisterList]
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const activeList = activeItem ? registeredLists.get(activeItem.listId) : null;
|
|
215
|
+
const activeDragContextValue: ActiveDragContextType = React.useMemo(
|
|
216
|
+
() => ({ activeItem, delta }),
|
|
217
|
+
[activeItem, delta]
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const collisionDetection = useMemo(() => {
|
|
221
|
+
return getItemFirstCollisionDetection(Array.from(registeredLists.keys()));
|
|
222
|
+
}, [registeredLists]);
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<AnyDragContext.Provider value={true}>
|
|
226
|
+
<contexts.dragRegistrationContext.Provider value={contextValue}>
|
|
227
|
+
<contexts.activeDragContext.Provider value={activeDragContextValue}>
|
|
228
|
+
<DndContext
|
|
229
|
+
sensors={sensors}
|
|
230
|
+
collisionDetection={collisionDetection}
|
|
231
|
+
onDragStart={handleDragStart}
|
|
232
|
+
onDragMove={handleDragMove}
|
|
233
|
+
onDragEnd={handleDragEnd}
|
|
234
|
+
>
|
|
235
|
+
{children}
|
|
236
|
+
{mounted &&
|
|
237
|
+
activeList?.renderOverlay &&
|
|
238
|
+
activeItem &&
|
|
239
|
+
createPortal(
|
|
240
|
+
<DragOverlay dropAnimation={null}>
|
|
241
|
+
{activeList.renderOverlay(activeItem.id)}
|
|
242
|
+
</DragOverlay>,
|
|
243
|
+
document.body
|
|
244
|
+
)}
|
|
245
|
+
</DndContext>
|
|
246
|
+
</contexts.activeDragContext.Provider>
|
|
247
|
+
</contexts.dragRegistrationContext.Provider>
|
|
248
|
+
</AnyDragContext.Provider>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function useMounted() {
|
|
253
|
+
const [mounted, setMounted] = React.useState(false);
|
|
254
|
+
|
|
255
|
+
React.useEffect(() => {
|
|
256
|
+
setMounted(true);
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
return mounted;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function withDragProvider<T extends object>(
|
|
263
|
+
Component: React.ComponentType<T>
|
|
264
|
+
) {
|
|
265
|
+
return function WithDragProvider(props: T) {
|
|
266
|
+
const anyDragContext = React.useContext(AnyDragContext);
|
|
267
|
+
|
|
268
|
+
if (!anyDragContext) {
|
|
269
|
+
return (
|
|
270
|
+
<SharedDragProvider>
|
|
271
|
+
<Component {...props} />
|
|
272
|
+
</SharedDragProvider>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return <Component {...props} />;
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Custom collision detection that prioritizes items over containers
|
|
281
|
+
export const getItemFirstCollisionDetection =
|
|
282
|
+
(registeredListIds: string[]): CollisionDetection =>
|
|
283
|
+
(parameters) => {
|
|
284
|
+
const pointerCollisions = pointerWithin(parameters);
|
|
285
|
+
|
|
286
|
+
if (pointerCollisions.length > 0) {
|
|
287
|
+
const itemCollisions = pointerCollisions.filter((collision) => {
|
|
288
|
+
for (const listId of registeredListIds) {
|
|
289
|
+
if (collision.id === listId) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return true;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (itemCollisions.length > 0) {
|
|
298
|
+
return itemCollisions;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Fall back to pointer-based detection (includes containers)
|
|
303
|
+
// This handles empty lists and edge cases
|
|
304
|
+
return pointerCollisions.length > 0
|
|
305
|
+
? pointerCollisions
|
|
306
|
+
: closestCenter(parameters);
|
|
307
|
+
};
|