react-arborist 3.7.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 +14 -0
- package/dist/main/components/cursor.js +1 -2
- package/dist/main/components/default-container.js +31 -2
- package/dist/main/components/default-cursor.js +1 -1
- package/dist/main/components/default-drag-preview.d.ts +1 -1
- package/dist/main/components/default-drag-preview.js +1 -1
- package/dist/main/components/default-row.d.ts +1 -1
- package/dist/main/components/default-row.js +1 -1
- package/dist/main/components/list-outer-element.js +1 -1
- package/dist/main/components/provider.d.ts +1 -1
- package/dist/main/components/provider.js +2 -2
- package/dist/main/components/provider.test.js +70 -0
- package/dist/main/components/row-container.d.ts +1 -1
- package/dist/main/components/row-container.js +2 -3
- package/dist/main/dnd/drag-hook.d.ts +2 -0
- package/dist/main/dnd/drag-hook.js +13 -4
- package/dist/main/dnd/drag-hook.test.d.ts +1 -0
- package/dist/main/dnd/drag-hook.test.js +19 -0
- package/dist/main/hooks/use-validated-props.js +1 -2
- package/dist/main/interfaces/node-api.js +4 -1
- package/dist/main/interfaces/tree-api.d.ts +27 -5
- package/dist/main/interfaces/tree-api.js +98 -14
- package/dist/main/interfaces/tree-api.test.js +31 -0
- package/dist/main/state/drag-slice.js +1 -2
- package/dist/main/types/dnd.d.ts +3 -1
- package/dist/main/types/state.d.ts +1 -1
- package/dist/main/types/tree-props.d.ts +4 -1
- package/dist/module/components/cursor.js +1 -2
- package/dist/module/components/default-container.js +32 -3
- package/dist/module/components/default-cursor.js +1 -1
- package/dist/module/components/default-drag-preview.d.ts +1 -1
- package/dist/module/components/default-drag-preview.js +1 -1
- package/dist/module/components/default-row.d.ts +1 -1
- package/dist/module/components/default-row.js +1 -1
- package/dist/module/components/list-outer-element.js +1 -1
- package/dist/module/components/provider.d.ts +1 -1
- package/dist/module/components/provider.js +4 -4
- package/dist/module/components/provider.test.js +71 -1
- package/dist/module/components/row-container.d.ts +1 -1
- package/dist/module/components/row-container.js +2 -3
- package/dist/module/dnd/compute-drop.js +1 -1
- package/dist/module/dnd/drag-hook.d.ts +2 -0
- package/dist/module/dnd/drag-hook.js +12 -4
- package/dist/module/dnd/drag-hook.test.d.ts +1 -0
- package/dist/module/dnd/drag-hook.test.js +17 -0
- package/dist/module/hooks/use-validated-props.js +1 -2
- package/dist/module/interfaces/node-api.js +4 -1
- package/dist/module/interfaces/tree-api.d.ts +27 -5
- package/dist/module/interfaces/tree-api.js +98 -14
- package/dist/module/interfaces/tree-api.test.js +31 -0
- package/dist/module/state/drag-slice.js +1 -2
- package/dist/module/types/dnd.d.ts +3 -1
- package/dist/module/types/state.d.ts +1 -1
- package/dist/module/types/tree-props.d.ts +4 -1
- package/package.json +27 -27
- package/src/components/cursor.tsx +1 -2
- package/src/components/default-container.tsx +40 -19
- package/src/components/default-cursor.tsx +1 -5
- package/src/components/default-drag-preview.tsx +3 -16
- package/src/components/default-node.tsx +0 -1
- package/src/components/default-row.tsx +2 -13
- package/src/components/drag-preview-container.tsx +1 -1
- package/src/components/list-inner-element.tsx +1 -1
- package/src/components/list-outer-element.tsx +2 -3
- package/src/components/provider.test.tsx +85 -9
- package/src/components/provider.tsx +8 -23
- package/src/components/row-container.tsx +4 -9
- package/src/components/tree.tsx +2 -6
- package/src/context.ts +2 -3
- package/src/data/create-index.ts +0 -1
- package/src/data/create-list.ts +1 -2
- package/src/data/create-root.ts +2 -9
- package/src/data/simple-tree.ts +5 -3
- package/src/dnd/compute-drop.ts +6 -15
- package/src/dnd/drag-hook.test.ts +22 -0
- package/src/dnd/drag-hook.ts +15 -6
- package/src/dnd/measure-hover.ts +2 -6
- package/src/dnd/outer-drop-hook.ts +1 -1
- package/src/hooks/use-fresh-node.ts +0 -1
- package/src/hooks/use-simple-tree.ts +2 -8
- package/src/hooks/use-validated-props.ts +4 -8
- package/src/interfaces/node-api.ts +2 -2
- package/src/interfaces/tree-api.test.ts +35 -0
- package/src/interfaces/tree-api.ts +103 -36
- package/src/state/dnd-slice.ts +1 -1
- package/src/state/drag-slice.ts +2 -5
- package/src/state/edit-slice.ts +1 -4
- package/src/state/focus-slice.ts +1 -1
- package/src/state/open-slice.ts +2 -5
- package/src/state/selection-slice.ts +2 -6
- package/src/types/dnd.ts +6 -1
- package/src/types/handlers.ts +1 -3
- package/src/types/renderers.ts +0 -1
- package/src/types/state.ts +1 -1
- package/src/types/tree-props.ts +15 -10
- package/src/types/utils.ts +2 -3
- package/src/utils.ts +5 -14
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { forwardRef } from "react";
|
|
2
2
|
import { useTreeApi } from "../context";
|
|
3
|
-
import { treeBlur } from "../state/focus-slice";
|
|
4
3
|
import { Cursor } from "./cursor";
|
|
5
4
|
|
|
6
5
|
export const ListOuterElement = forwardRef(function Outer(
|
|
7
6
|
props: React.HTMLProps<HTMLDivElement>,
|
|
8
|
-
ref
|
|
7
|
+
ref,
|
|
9
8
|
) {
|
|
10
9
|
const { children, ...rest } = props;
|
|
11
10
|
const tree = useTreeApi();
|
|
@@ -29,7 +28,7 @@ export const DropContainer = () => {
|
|
|
29
28
|
return (
|
|
30
29
|
<div
|
|
31
30
|
style={{
|
|
32
|
-
height: tree.visibleNodes.length
|
|
31
|
+
height: tree.rowTopPosition(tree.visibleNodes.length),
|
|
33
32
|
width: "100%",
|
|
34
33
|
position: "absolute",
|
|
35
34
|
left: "0",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createRef } from "react";
|
|
2
|
-
import { act, render } from "@testing-library/react";
|
|
2
|
+
import { act, render, screen } from "@testing-library/react";
|
|
3
|
+
import { FixedSizeList, VariableSizeList } from "react-window";
|
|
3
4
|
import { Tree } from "./tree";
|
|
4
5
|
import { TreeApi } from "../interfaces/tree-api";
|
|
6
|
+
import { NodeApi } from "../interfaces/node-api";
|
|
5
7
|
|
|
6
8
|
type Datum = { id: string; name: string; children?: Datum[] };
|
|
7
9
|
|
|
@@ -18,14 +20,7 @@ const data: Datum[] = [
|
|
|
18
20
|
|
|
19
21
|
test("imperative tree.update() props survive node toggles (#228)", () => {
|
|
20
22
|
const ref = createRef<TreeApi<Datum> | undefined>();
|
|
21
|
-
render(
|
|
22
|
-
<Tree<Datum>
|
|
23
|
-
data={data}
|
|
24
|
-
ref={ref}
|
|
25
|
-
rowHeight={24}
|
|
26
|
-
openByDefault={false}
|
|
27
|
-
/>
|
|
28
|
-
);
|
|
23
|
+
render(<Tree<Datum> data={data} ref={ref} rowHeight={24} openByDefault={false} />);
|
|
29
24
|
const api = ref.current!;
|
|
30
25
|
expect(api.rowHeight).toBe(24);
|
|
31
26
|
|
|
@@ -42,3 +37,84 @@ test("imperative tree.update() props survive node toggles (#228)", () => {
|
|
|
42
37
|
});
|
|
43
38
|
expect(api.rowHeight).toBe(48);
|
|
44
39
|
});
|
|
40
|
+
|
|
41
|
+
/* Backwards compatibility: switching FixedSizeList -> VariableSizeList must not
|
|
42
|
+
change layout for a numeric rowHeight. With openByDefault, all four nodes
|
|
43
|
+
(1 > 2, 3 > 4) are visible in DFS order. */
|
|
44
|
+
test("numeric rowHeight positions rows at index * height (#238 back-compat)", () => {
|
|
45
|
+
render(<Tree<Datum> data={data} rowHeight={24} openByDefault />);
|
|
46
|
+
const rows = screen.getAllByRole("treeitem");
|
|
47
|
+
expect(rows).toHaveLength(4);
|
|
48
|
+
rows.forEach((row, i) => {
|
|
49
|
+
expect(row.style.height).toBe("24px");
|
|
50
|
+
expect(row.style.top).toBe(`${i * 24}px`);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("function rowHeight gives each row its own height and cumulative top (#238)", () => {
|
|
55
|
+
const heights: Record<string, number> = { "1": 40, "2": 20, "3": 30, "4": 10 };
|
|
56
|
+
render(<Tree<Datum> data={data} rowHeight={(node) => heights[node.id]} openByDefault />);
|
|
57
|
+
const rows = screen.getAllByRole("treeitem");
|
|
58
|
+
expect(rows).toHaveLength(4);
|
|
59
|
+
const expected = [40, 20, 30, 10];
|
|
60
|
+
let top = 0;
|
|
61
|
+
rows.forEach((row, i) => {
|
|
62
|
+
expect(row.style.height).toBe(`${expected[i]}px`);
|
|
63
|
+
expect(row.style.top).toBe(`${top}px`);
|
|
64
|
+
top += expected[i];
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("mutations tell the list to recompute heights (#238)", () => {
|
|
69
|
+
const ref = createRef<TreeApi<Datum> | undefined>();
|
|
70
|
+
/* Only variable-height mode renders a VariableSizeList with a measurement
|
|
71
|
+
cache to recompute, so use a function rowHeight here. */
|
|
72
|
+
render(<Tree<Datum> data={data} ref={ref} rowHeight={() => 24} openByDefault />);
|
|
73
|
+
const api = ref.current!;
|
|
74
|
+
const reset = jest.spyOn(api.list.current as VariableSizeList, "resetAfterIndex");
|
|
75
|
+
|
|
76
|
+
act(() => api.close("1"));
|
|
77
|
+
expect(reset).toHaveBeenCalled();
|
|
78
|
+
|
|
79
|
+
reset.mockClear();
|
|
80
|
+
act(() => api.open("1"));
|
|
81
|
+
expect(reset).toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/* react-window caches measurements by index and never invalidates them itself.
|
|
85
|
+
When data changes via props in variable-height mode, those cached sizes belong
|
|
86
|
+
to the wrong rows, so update() must drop the cache. It runs during render, so
|
|
87
|
+
it uses the shouldForceUpdate=false variant. */
|
|
88
|
+
test("changing data in variable-height mode resets the list cache (#238)", () => {
|
|
89
|
+
const ref = createRef<TreeApi<Datum> | undefined>();
|
|
90
|
+
const rowHeight = (node: NodeApi<Datum>) => (node.isInternal ? 40 : 20);
|
|
91
|
+
const { rerender } = render(
|
|
92
|
+
<Tree<Datum> data={data} ref={ref} rowHeight={rowHeight} openByDefault />,
|
|
93
|
+
);
|
|
94
|
+
const reset = jest.spyOn(ref.current!.list.current as VariableSizeList, "resetAfterIndex");
|
|
95
|
+
|
|
96
|
+
const nextData: Datum[] = [{ id: "9", name: "fresh" }, ...data];
|
|
97
|
+
act(() => {
|
|
98
|
+
rerender(<Tree<Datum> data={nextData} ref={ref} rowHeight={rowHeight} openByDefault />);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(reset).toHaveBeenCalledWith(0, false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/* The numeric path must stay on FixedSizeList: it has constant item sizes, so
|
|
105
|
+
there is no measurement cache to go stale and none of VariableSizeList's
|
|
106
|
+
overhead. A FixedSizeList has no resetAfterIndex method at all. */
|
|
107
|
+
test("numeric rowHeight renders a cache-free FixedSizeList (#238)", () => {
|
|
108
|
+
const ref = createRef<TreeApi<Datum> | undefined>();
|
|
109
|
+
render(<Tree<Datum> data={data} ref={ref} rowHeight={24} openByDefault />);
|
|
110
|
+
const list = ref.current!.list.current!;
|
|
111
|
+
expect(list).toBeInstanceOf(FixedSizeList);
|
|
112
|
+
expect("resetAfterIndex" in list).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/* The function path uses VariableSizeList so per-row heights are possible. */
|
|
116
|
+
test("function rowHeight renders a VariableSizeList (#238)", () => {
|
|
117
|
+
const ref = createRef<TreeApi<Datum> | undefined>();
|
|
118
|
+
render(<Tree<Datum> data={data} ref={ref} rowHeight={() => 24} openByDefault />);
|
|
119
|
+
expect(ref.current!.list.current!).toBeInstanceOf(VariableSizeList);
|
|
120
|
+
});
|
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ReactNode,
|
|
3
|
-
useEffect,
|
|
4
|
-
useImperativeHandle,
|
|
5
|
-
useMemo,
|
|
6
|
-
useRef,
|
|
7
|
-
} from "react";
|
|
1
|
+
import { ReactNode, useEffect, useImperativeHandle, useMemo, useRef } from "react";
|
|
8
2
|
import { useSyncExternalStore } from "use-sync-external-store/shim";
|
|
9
|
-
import { FixedSizeList } from "react-window";
|
|
10
|
-
import {
|
|
11
|
-
DataUpdatesContext,
|
|
12
|
-
DndContext,
|
|
13
|
-
NodesContext,
|
|
14
|
-
TreeApiContext,
|
|
15
|
-
} from "../context";
|
|
3
|
+
import { FixedSizeList, VariableSizeList } from "react-window";
|
|
4
|
+
import { DataUpdatesContext, DndContext, NodesContext, TreeApiContext } from "../context";
|
|
16
5
|
import { TreeApi } from "../interfaces/tree-api";
|
|
17
6
|
import { initialState } from "../state/initial";
|
|
18
7
|
import { Actions, rootReducer, RootState } from "../state/root-reducer";
|
|
@@ -30,21 +19,17 @@ type Props<T> = {
|
|
|
30
19
|
|
|
31
20
|
const SERVER_STATE = initialState();
|
|
32
21
|
|
|
33
|
-
export function TreeProvider<T>({
|
|
34
|
-
|
|
35
|
-
imperativeHandle,
|
|
36
|
-
children,
|
|
37
|
-
}: Props<T>) {
|
|
38
|
-
const list = useRef<FixedSizeList | null>(null);
|
|
22
|
+
export function TreeProvider<T>({ treeProps, imperativeHandle, children }: Props<T>) {
|
|
23
|
+
const list = useRef<FixedSizeList | VariableSizeList | null>(null);
|
|
39
24
|
const listEl = useRef<HTMLDivElement | null>(null);
|
|
40
25
|
const store = useRef<Store<RootState, Actions>>(
|
|
41
26
|
// @ts-ignore
|
|
42
|
-
createStore(rootReducer, initialState(treeProps))
|
|
27
|
+
createStore(rootReducer, initialState(treeProps)),
|
|
43
28
|
);
|
|
44
29
|
const state = useSyncExternalStore<RootState>(
|
|
45
30
|
store.current.subscribe,
|
|
46
31
|
store.current.getState,
|
|
47
|
-
() => SERVER_STATE
|
|
32
|
+
() => SERVER_STATE,
|
|
48
33
|
);
|
|
49
34
|
|
|
50
35
|
/* The tree api object is stable. */
|
|
@@ -57,7 +42,7 @@ export function TreeProvider<T>({
|
|
|
57
42
|
useMemo(() => {
|
|
58
43
|
updateCount.current += 1;
|
|
59
44
|
api.update(treeProps);
|
|
60
|
-
},
|
|
45
|
+
}, Object.values(treeProps));
|
|
61
46
|
|
|
62
47
|
/* Rebuild visible nodes when open state changes, without clobbering
|
|
63
48
|
props set imperatively via api.update(). Bumping updateCount keeps
|
|
@@ -9,10 +9,7 @@ type Props = {
|
|
|
9
9
|
index: number;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export const RowContainer = React.memo(function RowContainer<T>({
|
|
13
|
-
index,
|
|
14
|
-
style,
|
|
15
|
-
}: Props) {
|
|
12
|
+
export const RowContainer = React.memo(function RowContainer<T>({ index, style }: Props) {
|
|
16
13
|
/* When will the <Row> will re-render.
|
|
17
14
|
*
|
|
18
15
|
* The row component is memo'd so it will only render
|
|
@@ -42,7 +39,7 @@ export const RowContainer = React.memo(function RowContainer<T>({
|
|
|
42
39
|
el.current = n;
|
|
43
40
|
dropRef(n);
|
|
44
41
|
},
|
|
45
|
-
[dropRef]
|
|
42
|
+
[dropRef],
|
|
46
43
|
);
|
|
47
44
|
|
|
48
45
|
const indent = tree.indent * node.level;
|
|
@@ -50,11 +47,9 @@ export const RowContainer = React.memo(function RowContainer<T>({
|
|
|
50
47
|
const rowStyle = useMemo(
|
|
51
48
|
() => ({
|
|
52
49
|
...style,
|
|
53
|
-
top:
|
|
54
|
-
parseFloat(style.top as string) +
|
|
55
|
-
(tree.props.padding ?? tree.props.paddingTop ?? 0),
|
|
50
|
+
top: parseFloat(style.top as string) + (tree.props.padding ?? tree.props.paddingTop ?? 0),
|
|
56
51
|
}),
|
|
57
|
-
[style, tree.props.padding, tree.props.paddingTop]
|
|
52
|
+
[style, tree.props.padding, tree.props.paddingTop],
|
|
58
53
|
);
|
|
59
54
|
const rowAttrs: React.HTMLAttributes<any> = {
|
|
60
55
|
role: "treeitem",
|
package/src/components/tree.tsx
CHANGED
|
@@ -5,13 +5,9 @@ import { OuterDrop } from "./outer-drop";
|
|
|
5
5
|
import { TreeContainer } from "./tree-container";
|
|
6
6
|
import { DragPreviewContainer } from "./drag-preview-container";
|
|
7
7
|
import { TreeProps } from "../types/tree-props";
|
|
8
|
-
import { IdObj } from "../types/utils";
|
|
9
8
|
import { useValidatedProps } from "../hooks/use-validated-props";
|
|
10
9
|
|
|
11
|
-
function TreeComponent<T>(
|
|
12
|
-
props: TreeProps<T>,
|
|
13
|
-
ref: React.Ref<TreeApi<T> | undefined>
|
|
14
|
-
) {
|
|
10
|
+
function TreeComponent<T>(props: TreeProps<T>, ref: React.Ref<TreeApi<T> | undefined>) {
|
|
15
11
|
const treeProps = useValidatedProps(props);
|
|
16
12
|
return (
|
|
17
13
|
<TreeProvider treeProps={treeProps} imperativeHandle={ref}>
|
|
@@ -24,5 +20,5 @@ function TreeComponent<T>(
|
|
|
24
20
|
}
|
|
25
21
|
|
|
26
22
|
export const Tree = forwardRef(TreeComponent) as <T>(
|
|
27
|
-
props: TreeProps<T> & { ref?: React.ForwardedRef<TreeApi<T> | undefined> }
|
|
23
|
+
props: TreeProps<T> & { ref?: React.ForwardedRef<TreeApi<T> | undefined> },
|
|
28
24
|
) => ReturnType<typeof TreeComponent>;
|
package/src/context.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import React, { createContext, useContext
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
|
2
2
|
import { TreeApi } from "./interfaces/tree-api";
|
|
3
3
|
import { RootState } from "./state/root-reducer";
|
|
4
|
-
import { IdObj } from "./types/utils";
|
|
5
4
|
|
|
6
5
|
export const TreeApiContext = createContext<TreeApi<any> | null>(null);
|
|
7
6
|
|
|
8
7
|
export function useTreeApi<T>() {
|
|
9
8
|
const value = useContext<TreeApi<T> | null>(
|
|
10
|
-
TreeApiContext as unknown as React.Context<TreeApi<T> | null
|
|
9
|
+
TreeApiContext as unknown as React.Context<TreeApi<T> | null>,
|
|
11
10
|
);
|
|
12
11
|
if (value === null) throw new Error("No Tree Api Provided");
|
|
13
12
|
return value;
|
package/src/data/create-index.ts
CHANGED
package/src/data/create-list.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { NodeApi } from "../interfaces/node-api";
|
|
2
2
|
import { TreeApi } from "../interfaces/tree-api";
|
|
3
|
-
import { IdObj } from "../types/utils";
|
|
4
3
|
|
|
5
4
|
export function createList<T>(tree: TreeApi<T>) {
|
|
6
5
|
if (tree.isFiltered) {
|
|
@@ -27,7 +26,7 @@ function flattenTree<T>(root: NodeApi<T>): NodeApi<T>[] {
|
|
|
27
26
|
|
|
28
27
|
function flattenAndFilterTree<T>(
|
|
29
28
|
root: NodeApi<T>,
|
|
30
|
-
isMatch: (n: NodeApi<T>) => boolean
|
|
29
|
+
isMatch: (n: NodeApi<T>) => boolean,
|
|
31
30
|
): NodeApi<T>[] {
|
|
32
31
|
const matches: Record<string, boolean> = {};
|
|
33
32
|
const list: NodeApi<T>[] = [];
|
package/src/data/create-root.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import { IdObj } from "../types/utils";
|
|
2
1
|
import { NodeApi } from "../interfaces/node-api";
|
|
3
2
|
import { TreeApi } from "../interfaces/tree-api";
|
|
4
3
|
|
|
5
4
|
export const ROOT_ID = "__REACT_ARBORIST_INTERNAL_ROOT__";
|
|
6
5
|
|
|
7
6
|
export function createRoot<T>(tree: TreeApi<T>): NodeApi<T> {
|
|
8
|
-
function visitSelfAndChildren(
|
|
9
|
-
data: T,
|
|
10
|
-
level: number,
|
|
11
|
-
parent: NodeApi<T> | null
|
|
12
|
-
) {
|
|
7
|
+
function visitSelfAndChildren(data: T, level: number, parent: NodeApi<T> | null) {
|
|
13
8
|
const id = tree.accessId(data);
|
|
14
9
|
const node = new NodeApi<T>({
|
|
15
10
|
tree,
|
|
@@ -23,9 +18,7 @@ export function createRoot<T>(tree: TreeApi<T>): NodeApi<T> {
|
|
|
23
18
|
});
|
|
24
19
|
const children = tree.accessChildren(data);
|
|
25
20
|
if (children) {
|
|
26
|
-
node.children = children.map((child: T) =>
|
|
27
|
-
visitSelfAndChildren(child, level + 1, node)
|
|
28
|
-
);
|
|
21
|
+
node.children = children.map((child: T) => visitSelfAndChildren(child, level + 1, node));
|
|
29
22
|
}
|
|
30
23
|
return node;
|
|
31
24
|
}
|
package/src/data/simple-tree.ts
CHANGED
|
@@ -56,15 +56,17 @@ function createRoot<T extends SimpleData>(data: T[]) {
|
|
|
56
56
|
|
|
57
57
|
function createNode<T extends SimpleData>(data: T, parent: SimpleNode<T>) {
|
|
58
58
|
const node = new SimpleNode<T>(data, parent);
|
|
59
|
-
if (data.children)
|
|
60
|
-
node.children = data.children.map((d) => createNode<T>(d as T, node));
|
|
59
|
+
if (data.children) node.children = data.children.map((d) => createNode<T>(d as T, node));
|
|
61
60
|
return node;
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
class SimpleNode<T extends SimpleData> {
|
|
65
64
|
id: string;
|
|
66
65
|
children?: SimpleNode<T>[];
|
|
67
|
-
constructor(
|
|
66
|
+
constructor(
|
|
67
|
+
public data: T,
|
|
68
|
+
public parent: SimpleNode<T> | null,
|
|
69
|
+
) {
|
|
68
70
|
this.id = data.id;
|
|
69
71
|
}
|
|
70
72
|
|
package/src/dnd/compute-drop.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { XYCoord } from "react-dnd";
|
|
2
2
|
import { NodeApi } from "../interfaces/node-api";
|
|
3
|
-
import {
|
|
4
|
-
bound,
|
|
5
|
-
indexOf,
|
|
6
|
-
isClosed,
|
|
7
|
-
isItem,
|
|
8
|
-
isOpenWithEmptyChildren,
|
|
9
|
-
} from "../utils";
|
|
3
|
+
import { bound, indexOf, isClosed, isItem, isOpenWithEmptyChildren } from "../utils";
|
|
10
4
|
import { DropResult } from "./drop-hook";
|
|
11
5
|
|
|
12
6
|
function measureHover(el: HTMLElement, offset: XYCoord) {
|
|
@@ -29,7 +23,7 @@ function getNodesAroundCursor(
|
|
|
29
23
|
node: NodeApi | null,
|
|
30
24
|
prev: NodeApi | null,
|
|
31
25
|
next: NodeApi | null,
|
|
32
|
-
hover: HoverData
|
|
26
|
+
hover: HoverData,
|
|
33
27
|
): [NodeApi | null, NodeApi | null] {
|
|
34
28
|
if (!node) {
|
|
35
29
|
// We're hovering over the empty part of the list, not over an item,
|
|
@@ -67,16 +61,13 @@ export type ComputedDrop = {
|
|
|
67
61
|
cursor: Cursor | null;
|
|
68
62
|
};
|
|
69
63
|
|
|
70
|
-
function dropAt(
|
|
71
|
-
parentId: string | undefined,
|
|
72
|
-
index: number | null
|
|
73
|
-
): DropResult {
|
|
64
|
+
function dropAt(parentId: string | undefined, index: number | null): DropResult {
|
|
74
65
|
return { parentId: parentId || null, index };
|
|
75
66
|
}
|
|
76
67
|
|
|
77
68
|
function lineCursor(index: number, level: number) {
|
|
78
69
|
return {
|
|
79
|
-
type: "line" as
|
|
70
|
+
type: "line" as const,
|
|
80
71
|
index,
|
|
81
72
|
level,
|
|
82
73
|
};
|
|
@@ -84,13 +75,13 @@ function lineCursor(index: number, level: number) {
|
|
|
84
75
|
|
|
85
76
|
function noCursor() {
|
|
86
77
|
return {
|
|
87
|
-
type: "none" as
|
|
78
|
+
type: "none" as const,
|
|
88
79
|
};
|
|
89
80
|
}
|
|
90
81
|
|
|
91
82
|
function highlightCursor(id: string) {
|
|
92
83
|
return {
|
|
93
|
-
type: "highlight" as
|
|
84
|
+
type: "highlight" as const,
|
|
94
85
|
id,
|
|
95
86
|
};
|
|
96
87
|
}
|
|
@@ -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
|
+
});
|
package/src/dnd/drag-hook.ts
CHANGED
|
@@ -4,28 +4,37 @@ 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
|
|
21
|
+
const [_, ref, preview] = useDrag<DragItem<T>, DropResult, void>(
|
|
14
22
|
() => ({
|
|
15
23
|
canDrag: () => node.isDraggable,
|
|
16
|
-
type:
|
|
24
|
+
type: dragTypeForNode(tree.props.dragType, node),
|
|
17
25
|
item: () => {
|
|
18
|
-
// This is fired once at the
|
|
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();
|
|
33
|
+
tree.redrawList();
|
|
25
34
|
tree.dispatch(dnd.dragEnd());
|
|
26
35
|
},
|
|
27
36
|
}),
|
|
28
|
-
[ids, node],
|
|
37
|
+
[ids, node, tree.props.dragType],
|
|
29
38
|
);
|
|
30
39
|
|
|
31
40
|
useEffect(() => {
|
package/src/dnd/measure-hover.ts
CHANGED
|
@@ -12,12 +12,8 @@ export function measureHover(el: HTMLElement, offset: XYCoord, indent: number) {
|
|
|
12
12
|
const inBottomHalf = !inTopHalf;
|
|
13
13
|
const pad = height / 4;
|
|
14
14
|
const inMiddle = y > pad && y < height - pad;
|
|
15
|
-
const maxLevel = Number(
|
|
16
|
-
|
|
17
|
-
);
|
|
18
|
-
const minLevel = Number(
|
|
19
|
-
inTopHalf ? el.dataset.level : nextEl ? nextEl.dataset.level : 0
|
|
20
|
-
);
|
|
15
|
+
const maxLevel = Number(inBottomHalf ? el.dataset.level : prevEl ? prevEl.dataset.level : 0);
|
|
16
|
+
const minLevel = Number(inTopHalf ? el.dataset.level : nextEl ? nextEl.dataset.level : 0);
|
|
21
17
|
const level = bound(Math.floor(x / indent), minLevel, maxLevel);
|
|
22
18
|
|
|
23
19
|
return { level, inTopHalf, inBottomHalf, inMiddle };
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState } from "react";
|
|
2
2
|
import { SimpleTree } from "../data/simple-tree";
|
|
3
|
-
import {
|
|
4
|
-
CreateHandler,
|
|
5
|
-
DeleteHandler,
|
|
6
|
-
MoveHandler,
|
|
7
|
-
RenameHandler,
|
|
8
|
-
} from "../types/handlers";
|
|
9
|
-
import { IdObj } from "../types/utils";
|
|
3
|
+
import { CreateHandler, DeleteHandler, MoveHandler, RenameHandler } from "../types/handlers";
|
|
10
4
|
|
|
11
5
|
export type SimpleTreeData = {
|
|
12
6
|
id: string;
|
|
@@ -22,7 +16,7 @@ export function useSimpleTree<T>(initialData: readonly T[]) {
|
|
|
22
16
|
() =>
|
|
23
17
|
new SimpleTree<// @ts-ignore
|
|
24
18
|
T>(data),
|
|
25
|
-
[data]
|
|
19
|
+
[data],
|
|
26
20
|
);
|
|
27
21
|
|
|
28
22
|
const onMove: MoveHandler<T> = (args: {
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import { TreeProps } from "../types/tree-props";
|
|
2
|
-
import {
|
|
3
|
-
import { SimpleTreeData, useSimpleTree } from "./use-simple-tree";
|
|
2
|
+
import { useSimpleTree } from "./use-simple-tree";
|
|
4
3
|
|
|
5
4
|
export function useValidatedProps<T>(props: TreeProps<T>): TreeProps<T> {
|
|
6
5
|
if (props.initialData && props.data) {
|
|
7
6
|
throw new Error(
|
|
8
|
-
`React Arborist Tree => Provide either a data or initialData prop, but not both
|
|
7
|
+
`React Arborist Tree => Provide either a data or initialData prop, but not both.`,
|
|
9
8
|
);
|
|
10
9
|
}
|
|
11
|
-
if (
|
|
12
|
-
props.initialData &&
|
|
13
|
-
(props.onCreate || props.onDelete || props.onMove || props.onRename)
|
|
14
|
-
) {
|
|
10
|
+
if (props.initialData && (props.onCreate || props.onDelete || props.onMove || props.onRename)) {
|
|
15
11
|
throw new Error(
|
|
16
12
|
`React Arborist Tree => You passed the initialData prop along with a data handler.
|
|
17
|
-
Use the data prop if you want to provide your own handlers
|
|
13
|
+
Use the data prop if you want to provide your own handlers.`,
|
|
18
14
|
);
|
|
19
15
|
}
|
|
20
16
|
if (props.initialData) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { TreeApi } from "./tree-api";
|
|
3
|
-
import { IdObj } from "../types/utils";
|
|
4
3
|
import { ROOT_ID } from "../data/create-root";
|
|
5
4
|
|
|
6
5
|
type Params<T> = {
|
|
@@ -202,7 +201,8 @@ export class NodeApi<T = any> {
|
|
|
202
201
|
|
|
203
202
|
handleClick = (e: React.MouseEvent) => {
|
|
204
203
|
if (e.metaKey && !this.tree.props.disableMultiSelection) {
|
|
205
|
-
this.isSelected
|
|
204
|
+
if (this.isSelected) this.deselect();
|
|
205
|
+
else this.selectMulti();
|
|
206
206
|
} else if (e.shiftKey && !this.tree.props.disableMultiSelection) {
|
|
207
207
|
this.selectContiguous();
|
|
208
208
|
} else {
|
|
@@ -13,3 +13,38 @@ test("tree.canDrop()", () => {
|
|
|
13
13
|
expect(setupApi({ disableDrop: () => false }).canDrop()).toBe(true);
|
|
14
14
|
expect(setupApi({ disableDrop: false }).canDrop()).toBe(true);
|
|
15
15
|
});
|
|
16
|
+
|
|
17
|
+
const rowData = [{ id: "a" }, { id: "b" }, { id: "c" }];
|
|
18
|
+
|
|
19
|
+
test("rowHeight defaults to 24", () => {
|
|
20
|
+
const api = setupApi({});
|
|
21
|
+
expect(api.rowHeight).toBe(24);
|
|
22
|
+
expect(api.rowHeightAt(0)).toBe(24);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("fixed numeric rowHeight", () => {
|
|
26
|
+
const api = setupApi({ data: rowData, rowHeight: 30 });
|
|
27
|
+
expect(api.rowHeight).toBe(30);
|
|
28
|
+
expect(api.rowHeightAt(0)).toBe(30);
|
|
29
|
+
expect(api.rowTopPosition(0)).toBe(0);
|
|
30
|
+
expect(api.rowTopPosition(2)).toBe(60);
|
|
31
|
+
expect(api.rowTopPosition(3)).toBe(90); // total list height
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("variable rowHeight function", () => {
|
|
35
|
+
const heights: Record<string, number> = { a: 10, b: 20, c: 40 };
|
|
36
|
+
const api = setupApi({
|
|
37
|
+
data: rowData,
|
|
38
|
+
rowHeight: (node) => heights[node.id],
|
|
39
|
+
});
|
|
40
|
+
// The back-compat getter falls back to the default for variable heights.
|
|
41
|
+
expect(api.rowHeight).toBe(24);
|
|
42
|
+
expect(api.rowHeightAt(0)).toBe(10);
|
|
43
|
+
expect(api.rowHeightAt(1)).toBe(20);
|
|
44
|
+
expect(api.rowTopPosition(0)).toBe(0);
|
|
45
|
+
expect(api.rowTopPosition(1)).toBe(10);
|
|
46
|
+
expect(api.rowTopPosition(2)).toBe(30);
|
|
47
|
+
expect(api.rowTopPosition(3)).toBe(70); // total list height
|
|
48
|
+
// Out-of-range index falls back to the default height, never an invalid 0.
|
|
49
|
+
expect(api.rowHeightAt(99)).toBe(24);
|
|
50
|
+
});
|