react-arborist 3.8.0 → 3.9.0

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/README.md CHANGED
@@ -351,9 +351,23 @@ interface TreeProps<T> {
351
351
  { backend: unknown }
352
352
  >["backend"];
353
353
  dndManager?: ReturnType<typeof useDragDropManager>;
354
+ dragType?: string | ((node: NodeApi<T>) => string);
354
355
  }
355
356
  ```
356
357
 
358
+ ### Dragging Nodes to External Drop Targets
359
+
360
+ The react-dnd drag item created for each row carries the dragged node's `data`, so a drop target rendered outside the tree can read it:
361
+
362
+ ```tsx
363
+ const [, drop] = useDrop(() => ({
364
+ accept: "NODE",
365
+ drop: (item) => console.log(item.data), // the dragged node's data
366
+ }));
367
+ ```
368
+
369
+ The tree and your external target must share one react-dnd backend. Wrap both in a single `DndProvider` and pass its manager to the tree via the `dndManager` prop. By default rows advertise the `"NODE"` item type; set the `dragType` prop (a fixed string, or a function of the node) to advertise a custom type instead. Note that the tree's own drop targets only accept `"NODE"`, so a row given a custom `dragType` is no longer reorderable within the tree.
370
+
357
371
  ## Row Component Props
358
372
 
359
373
  The _\<RowRenderer\>_ is responsible for attaching the drop ref, the row style (top, height) and the aria-attributes. The default should work fine for most use cases, but it can be replaced by your own component if you need. See the _renderRow_ prop in the _\<Tree\>_ component.
@@ -1,3 +1,5 @@
1
1
  import { ConnectDragSource } from "react-dnd";
2
2
  import { NodeApi } from "../interfaces/node-api";
3
+ import { TreeProps } from "../types/tree-props";
4
+ export declare function dragTypeForNode<T>(dragType: TreeProps<T>["dragType"], node: NodeApi<T>): string;
3
5
  export declare function useDragHook<T>(node: NodeApi<T>): ConnectDragSource;
@@ -1,29 +1,37 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dragTypeForNode = dragTypeForNode;
3
4
  exports.useDragHook = useDragHook;
4
5
  const react_1 = require("react");
5
6
  const react_dnd_1 = require("react-dnd");
6
7
  const react_dnd_html5_backend_1 = require("react-dnd-html5-backend");
7
8
  const context_1 = require("../context");
8
9
  const dnd_slice_1 = require("../state/dnd-slice");
10
+ /* The react-dnd item type a row's drag source broadcasts. The dragType prop
11
+ can be a fixed string or a per-node function; it defaults to "NODE". */
12
+ function dragTypeForNode(dragType, node) {
13
+ if (typeof dragType === "function")
14
+ return dragType(node);
15
+ return dragType !== null && dragType !== void 0 ? dragType : "NODE";
16
+ }
9
17
  function useDragHook(node) {
10
18
  const tree = (0, context_1.useTreeApi)();
11
19
  const ids = tree.selectedIds;
12
20
  const [_, ref, preview] = (0, react_dnd_1.useDrag)(() => ({
13
21
  canDrag: () => node.isDraggable,
14
- type: "NODE",
22
+ type: dragTypeForNode(tree.props.dragType, node),
15
23
  item: () => {
16
- // This is fired once at the begging of a drag operation
24
+ // This is fired once at the beginning of a drag operation
17
25
  const dragIds = tree.isSelected(node.id) ? Array.from(ids) : [node.id];
18
26
  tree.dispatch(dnd_slice_1.actions.dragStart(node.id, dragIds));
19
- return { id: node.id, dragIds };
27
+ return { id: node.id, dragIds, data: node.data };
20
28
  },
21
29
  end: () => {
22
30
  tree.hideCursor();
23
31
  tree.redrawList();
24
32
  tree.dispatch(dnd_slice_1.actions.dragEnd());
25
33
  },
26
- }), [ids, node]);
34
+ }), [ids, node, tree.props.dragType]);
27
35
  (0, react_1.useEffect)(() => {
28
36
  preview((0, react_dnd_html5_backend_1.getEmptyImage)());
29
37
  }, [preview]);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const drag_hook_1 = require("./drag-hook");
4
+ /* dragTypeForNode only reads node.data when dragType is a function, so a
5
+ minimal stub stands in for a real NodeApi. */
6
+ function nodeWith(data) {
7
+ return { data };
8
+ }
9
+ test("defaults to the internal NODE type when dragType is undefined", () => {
10
+ expect((0, drag_hook_1.dragTypeForNode)(undefined, nodeWith({ id: "a" }))).toBe("NODE");
11
+ });
12
+ test("uses a fixed string dragType for every node", () => {
13
+ expect((0, drag_hook_1.dragTypeForNode)("FILE", nodeWith({ id: "a" }))).toBe("FILE");
14
+ });
15
+ test("resolves a per-node dragType function against the node", () => {
16
+ const dragType = (node) => node.data.kind.toUpperCase();
17
+ expect((0, drag_hook_1.dragTypeForNode)(dragType, nodeWith({ kind: "folder" }))).toBe("FOLDER");
18
+ expect((0, drag_hook_1.dragTypeForNode)(dragType, nodeWith({ kind: "file" }))).toBe("FILE");
19
+ });
@@ -3,6 +3,8 @@ export type CursorLocation = {
3
3
  level: number | null;
4
4
  parentId: string | null;
5
5
  };
6
- export type DragItem = {
6
+ export type DragItem<T = any> = {
7
7
  id: string;
8
+ dragIds: string[];
9
+ data: T;
8
10
  };
@@ -59,6 +59,7 @@ export interface TreeProps<T> {
59
59
  backend: unknown;
60
60
  }>["backend"];
61
61
  dndManager?: ReturnType<typeof useDragDropManager>;
62
+ dragType?: string | ((node: NodeApi<T>) => string);
62
63
  outerElementType?: ReactWindowCommonProps["outerElementType"];
63
64
  innerElementType?: ReactWindowCommonProps["innerElementType"];
64
65
  }
@@ -1,3 +1,5 @@
1
1
  import { ConnectDragSource } from "react-dnd";
2
2
  import { NodeApi } from "../interfaces/node-api";
3
+ import { TreeProps } from "../types/tree-props";
4
+ export declare function dragTypeForNode<T>(dragType: TreeProps<T>["dragType"], node: NodeApi<T>): string;
3
5
  export declare function useDragHook<T>(node: NodeApi<T>): ConnectDragSource;
@@ -3,24 +3,31 @@ import { useDrag } from "react-dnd";
3
3
  import { getEmptyImage } from "react-dnd-html5-backend";
4
4
  import { useTreeApi } from "../context";
5
5
  import { actions as dnd } from "../state/dnd-slice";
6
+ /* The react-dnd item type a row's drag source broadcasts. The dragType prop
7
+ can be a fixed string or a per-node function; it defaults to "NODE". */
8
+ export function dragTypeForNode(dragType, node) {
9
+ if (typeof dragType === "function")
10
+ return dragType(node);
11
+ return dragType !== null && dragType !== void 0 ? dragType : "NODE";
12
+ }
6
13
  export function useDragHook(node) {
7
14
  const tree = useTreeApi();
8
15
  const ids = tree.selectedIds;
9
16
  const [_, ref, preview] = useDrag(() => ({
10
17
  canDrag: () => node.isDraggable,
11
- type: "NODE",
18
+ type: dragTypeForNode(tree.props.dragType, node),
12
19
  item: () => {
13
- // This is fired once at the begging of a drag operation
20
+ // This is fired once at the beginning of a drag operation
14
21
  const dragIds = tree.isSelected(node.id) ? Array.from(ids) : [node.id];
15
22
  tree.dispatch(dnd.dragStart(node.id, dragIds));
16
- return { id: node.id, dragIds };
23
+ return { id: node.id, dragIds, data: node.data };
17
24
  },
18
25
  end: () => {
19
26
  tree.hideCursor();
20
27
  tree.redrawList();
21
28
  tree.dispatch(dnd.dragEnd());
22
29
  },
23
- }), [ids, node]);
30
+ }), [ids, node, tree.props.dragType]);
24
31
  useEffect(() => {
25
32
  preview(getEmptyImage());
26
33
  }, [preview]);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { dragTypeForNode } from "./drag-hook";
2
+ /* dragTypeForNode only reads node.data when dragType is a function, so a
3
+ minimal stub stands in for a real NodeApi. */
4
+ function nodeWith(data) {
5
+ return { data };
6
+ }
7
+ test("defaults to the internal NODE type when dragType is undefined", () => {
8
+ expect(dragTypeForNode(undefined, nodeWith({ id: "a" }))).toBe("NODE");
9
+ });
10
+ test("uses a fixed string dragType for every node", () => {
11
+ expect(dragTypeForNode("FILE", nodeWith({ id: "a" }))).toBe("FILE");
12
+ });
13
+ test("resolves a per-node dragType function against the node", () => {
14
+ const dragType = (node) => node.data.kind.toUpperCase();
15
+ expect(dragTypeForNode(dragType, nodeWith({ kind: "folder" }))).toBe("FOLDER");
16
+ expect(dragTypeForNode(dragType, nodeWith({ kind: "file" }))).toBe("FILE");
17
+ });
@@ -3,6 +3,8 @@ export type CursorLocation = {
3
3
  level: number | null;
4
4
  parentId: string | null;
5
5
  };
6
- export type DragItem = {
6
+ export type DragItem<T = any> = {
7
7
  id: string;
8
+ dragIds: string[];
9
+ data: T;
8
10
  };
@@ -59,6 +59,7 @@ export interface TreeProps<T> {
59
59
  backend: unknown;
60
60
  }>["backend"];
61
61
  dndManager?: ReturnType<typeof useDragDropManager>;
62
+ dragType?: string | ((node: NodeApi<T>) => string);
62
63
  outerElementType?: ReactWindowCommonProps["outerElementType"];
63
64
  innerElementType?: ReactWindowCommonProps["innerElementType"];
64
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-arborist",
3
- "version": "3.8.0",
3
+ "version": "3.9.0",
4
4
  "keywords": [
5
5
  "arborist",
6
6
  "dnd",
@@ -0,0 +1,22 @@
1
+ import { NodeApi } from "../interfaces/node-api";
2
+ import { dragTypeForNode } from "./drag-hook";
3
+
4
+ /* dragTypeForNode only reads node.data when dragType is a function, so a
5
+ minimal stub stands in for a real NodeApi. */
6
+ function nodeWith<T>(data: T): NodeApi<T> {
7
+ return { data } as NodeApi<T>;
8
+ }
9
+
10
+ test("defaults to the internal NODE type when dragType is undefined", () => {
11
+ expect(dragTypeForNode(undefined, nodeWith({ id: "a" }))).toBe("NODE");
12
+ });
13
+
14
+ test("uses a fixed string dragType for every node", () => {
15
+ expect(dragTypeForNode("FILE", nodeWith({ id: "a" }))).toBe("FILE");
16
+ });
17
+
18
+ test("resolves a per-node dragType function against the node", () => {
19
+ const dragType = (node: NodeApi<{ kind: string }>) => node.data.kind.toUpperCase();
20
+ expect(dragTypeForNode(dragType, nodeWith({ kind: "folder" }))).toBe("FOLDER");
21
+ expect(dragTypeForNode(dragType, nodeWith({ kind: "file" }))).toBe("FILE");
22
+ });
@@ -4,21 +4,29 @@ import { getEmptyImage } from "react-dnd-html5-backend";
4
4
  import { useTreeApi } from "../context";
5
5
  import { NodeApi } from "../interfaces/node-api";
6
6
  import { DragItem } from "../types/dnd";
7
+ import { TreeProps } from "../types/tree-props";
7
8
  import { DropResult } from "./drop-hook";
8
9
  import { actions as dnd } from "../state/dnd-slice";
9
10
 
11
+ /* The react-dnd item type a row's drag source broadcasts. The dragType prop
12
+ can be a fixed string or a per-node function; it defaults to "NODE". */
13
+ export function dragTypeForNode<T>(dragType: TreeProps<T>["dragType"], node: NodeApi<T>): string {
14
+ if (typeof dragType === "function") return dragType(node);
15
+ return dragType ?? "NODE";
16
+ }
17
+
10
18
  export function useDragHook<T>(node: NodeApi<T>): ConnectDragSource {
11
- const tree = useTreeApi();
19
+ const tree = useTreeApi<T>();
12
20
  const ids = tree.selectedIds;
13
- const [_, ref, preview] = useDrag<DragItem, DropResult, void>(
21
+ const [_, ref, preview] = useDrag<DragItem<T>, DropResult, void>(
14
22
  () => ({
15
23
  canDrag: () => node.isDraggable,
16
- type: "NODE",
24
+ type: dragTypeForNode(tree.props.dragType, node),
17
25
  item: () => {
18
- // This is fired once at the begging of a drag operation
26
+ // This is fired once at the beginning of a drag operation
19
27
  const dragIds = tree.isSelected(node.id) ? Array.from(ids) : [node.id];
20
28
  tree.dispatch(dnd.dragStart(node.id, dragIds));
21
- return { id: node.id, dragIds };
29
+ return { id: node.id, dragIds, data: node.data };
22
30
  },
23
31
  end: () => {
24
32
  tree.hideCursor();
@@ -26,7 +34,7 @@ export function useDragHook<T>(node: NodeApi<T>): ConnectDragSource {
26
34
  tree.dispatch(dnd.dragEnd());
27
35
  },
28
36
  }),
29
- [ids, node],
37
+ [ids, node, tree.props.dragType],
30
38
  );
31
39
 
32
40
  useEffect(() => {
package/src/types/dnd.ts CHANGED
@@ -4,6 +4,11 @@ export type CursorLocation = {
4
4
  parentId: string | null;
5
5
  };
6
6
 
7
- export type DragItem = {
7
+ export type DragItem<T = any> = {
8
+ /* The id of the row the drag started on. */
8
9
  id: string;
10
+ /* Every node carried by the drag (the selection, or just `id`). */
11
+ dragIds: string[];
12
+ /* The dragged node's data, so external drop targets can read it. */
13
+ data: T;
9
14
  };
@@ -79,6 +79,15 @@ export interface TreeProps<T> {
79
79
  dndBackend?: Extract<DndProviderProps<unknown, unknown>, { backend: unknown }>["backend"];
80
80
  dndManager?: ReturnType<typeof useDragDropManager>;
81
81
 
82
+ /* The react-dnd item type each row's drag source advertises. Defaults to
83
+ "NODE". Set a custom value (or a per-node function) so rows can be dropped
84
+ onto external react-dnd targets that accept that type. The dragged node's
85
+ data is always exposed on the drag item, so an external target accepting
86
+ the default "NODE" type can read it without setting this. Note: the tree's
87
+ own drop targets only accept "NODE", so a row given a custom type is no
88
+ longer reorderable within the tree. */
89
+ dragType?: string | ((node: NodeApi<T>) => string);
90
+
82
91
  /* Custom react-window outer/inner elements */
83
92
  outerElementType?: ReactWindowCommonProps["outerElementType"];
84
93
  innerElementType?: ReactWindowCommonProps["innerElementType"];