react-arborist 3.6.1 → 3.8.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.
Files changed (93) hide show
  1. package/dist/main/components/cursor.js +1 -2
  2. package/dist/main/components/default-container.js +31 -1
  3. package/dist/main/components/default-cursor.js +1 -1
  4. package/dist/main/components/default-drag-preview.d.ts +1 -1
  5. package/dist/main/components/default-drag-preview.js +1 -1
  6. package/dist/main/components/default-row.d.ts +1 -1
  7. package/dist/main/components/default-row.js +1 -1
  8. package/dist/main/components/list-outer-element.d.ts +1 -0
  9. package/dist/main/components/list-outer-element.js +4 -3
  10. package/dist/main/components/provider.d.ts +1 -1
  11. package/dist/main/components/provider.js +2 -2
  12. package/dist/main/components/provider.test.js +70 -0
  13. package/dist/main/components/row-container.d.ts +1 -1
  14. package/dist/main/components/row-container.js +2 -3
  15. package/dist/main/dnd/drag-hook.js +1 -0
  16. package/dist/main/hooks/use-validated-props.js +1 -2
  17. package/dist/main/index.d.ts +3 -0
  18. package/dist/main/index.js +7 -1
  19. package/dist/main/interfaces/node-api.js +4 -1
  20. package/dist/main/interfaces/tree-api.d.ts +27 -5
  21. package/dist/main/interfaces/tree-api.js +98 -14
  22. package/dist/main/interfaces/tree-api.test.js +31 -0
  23. package/dist/main/state/drag-slice.js +1 -2
  24. package/dist/main/types/state.d.ts +1 -1
  25. package/dist/main/types/tree-props.d.ts +6 -2
  26. package/dist/module/components/cursor.js +1 -2
  27. package/dist/module/components/default-container.js +32 -2
  28. package/dist/module/components/default-cursor.js +1 -1
  29. package/dist/module/components/default-drag-preview.d.ts +1 -1
  30. package/dist/module/components/default-drag-preview.js +1 -1
  31. package/dist/module/components/default-row.d.ts +1 -1
  32. package/dist/module/components/default-row.js +1 -1
  33. package/dist/module/components/list-outer-element.d.ts +1 -0
  34. package/dist/module/components/list-outer-element.js +2 -2
  35. package/dist/module/components/provider.d.ts +1 -1
  36. package/dist/module/components/provider.js +4 -4
  37. package/dist/module/components/provider.test.js +71 -1
  38. package/dist/module/components/row-container.d.ts +1 -1
  39. package/dist/module/components/row-container.js +2 -3
  40. package/dist/module/dnd/compute-drop.js +1 -1
  41. package/dist/module/dnd/drag-hook.js +1 -0
  42. package/dist/module/hooks/use-validated-props.js +1 -2
  43. package/dist/module/index.d.ts +3 -0
  44. package/dist/module/index.js +3 -0
  45. package/dist/module/interfaces/node-api.js +4 -1
  46. package/dist/module/interfaces/tree-api.d.ts +27 -5
  47. package/dist/module/interfaces/tree-api.js +98 -14
  48. package/dist/module/interfaces/tree-api.test.js +31 -0
  49. package/dist/module/state/drag-slice.js +1 -2
  50. package/dist/module/types/state.d.ts +1 -1
  51. package/dist/module/types/tree-props.d.ts +6 -2
  52. package/package.json +27 -27
  53. package/src/components/cursor.tsx +1 -2
  54. package/src/components/default-container.tsx +40 -19
  55. package/src/components/default-cursor.tsx +1 -5
  56. package/src/components/default-drag-preview.tsx +3 -16
  57. package/src/components/default-node.tsx +0 -1
  58. package/src/components/default-row.tsx +2 -13
  59. package/src/components/drag-preview-container.tsx +1 -1
  60. package/src/components/list-inner-element.tsx +1 -1
  61. package/src/components/list-outer-element.tsx +3 -4
  62. package/src/components/provider.test.tsx +85 -9
  63. package/src/components/provider.tsx +8 -23
  64. package/src/components/row-container.tsx +4 -9
  65. package/src/components/tree.tsx +2 -6
  66. package/src/context.ts +2 -3
  67. package/src/data/create-index.ts +0 -1
  68. package/src/data/create-list.ts +1 -2
  69. package/src/data/create-root.ts +2 -9
  70. package/src/data/simple-tree.ts +5 -3
  71. package/src/dnd/compute-drop.ts +6 -15
  72. package/src/dnd/drag-hook.ts +1 -0
  73. package/src/dnd/measure-hover.ts +2 -6
  74. package/src/dnd/outer-drop-hook.ts +1 -1
  75. package/src/hooks/use-fresh-node.ts +0 -1
  76. package/src/hooks/use-simple-tree.ts +2 -8
  77. package/src/hooks/use-validated-props.ts +4 -8
  78. package/src/index.ts +3 -0
  79. package/src/interfaces/node-api.ts +2 -2
  80. package/src/interfaces/tree-api.test.ts +35 -0
  81. package/src/interfaces/tree-api.ts +103 -36
  82. package/src/state/dnd-slice.ts +1 -1
  83. package/src/state/drag-slice.ts +2 -5
  84. package/src/state/edit-slice.ts +1 -4
  85. package/src/state/focus-slice.ts +1 -1
  86. package/src/state/open-slice.ts +2 -5
  87. package/src/state/selection-slice.ts +2 -6
  88. package/src/types/handlers.ts +1 -3
  89. package/src/types/renderers.ts +0 -1
  90. package/src/types/state.ts +1 -1
  91. package/src/types/tree-props.ts +11 -11
  92. package/src/types/utils.ts +2 -3
  93. package/src/utils.ts +5 -14
@@ -11,8 +11,7 @@ function Cursor() {
11
11
  if (!cursor || cursor.type !== "line")
12
12
  return null;
13
13
  const indent = tree.indent;
14
- const top = tree.rowHeight * cursor.index +
15
- ((_b = (_a = tree.props.padding) !== null && _a !== void 0 ? _a : tree.props.paddingTop) !== null && _b !== void 0 ? _b : 0);
14
+ const top = tree.rowTopPosition(cursor.index) + ((_b = (_a = tree.props.padding) !== null && _a !== void 0 ? _a : tree.props.paddingTop) !== null && _b !== void 0 ? _b : 0);
16
15
  const left = indent * cursor.level;
17
16
  const Cursor = tree.renderCursor;
18
17
  return (0, jsx_runtime_1.jsx)(Cursor, { top, left, indent });
@@ -233,5 +233,35 @@ function DefaultContainer() {
233
233
  });
234
234
  if (node)
235
235
  tree.focus(node.id);
236
- }, children: (0, jsx_runtime_1.jsx)(react_window_1.FixedSizeList, { className: tree.props.className, outerRef: tree.listEl, itemCount: tree.visibleNodes.length, height: tree.height, width: tree.width, itemSize: tree.rowHeight, overscanCount: tree.overscanCount, itemKey: (index) => { var _a; return ((_a = tree.visibleNodes[index]) === null || _a === void 0 ? void 0 : _a.id) || index; }, outerElementType: list_outer_element_1.ListOuterElement, innerElementType: list_inner_element_1.ListInnerElement, onScroll: tree.props.onScroll, onItemsRendered: tree.onItemsRendered.bind(tree), ref: tree.list, children: row_container_1.RowContainer }) }));
236
+ }, children: (0, jsx_runtime_1.jsx)(List, {}) }));
237
+ }
238
+ /**
239
+ * Fixed-height trees (numeric rowHeight) render a FixedSizeList, preserving the
240
+ * original O(1) layout and avoiding VariableSizeList's measurement cache. Only
241
+ * the function form, which needs per-row heights, uses VariableSizeList.
242
+ */
243
+ function List() {
244
+ var _a, _b;
245
+ const tree = (0, context_1.useTreeApi)();
246
+ const commonProps = {
247
+ className: tree.props.className,
248
+ outerRef: tree.listEl,
249
+ itemCount: tree.visibleNodes.length,
250
+ height: tree.height,
251
+ width: tree.width,
252
+ overscanCount: tree.overscanCount,
253
+ itemKey: (index) => { var _a; return ((_a = tree.visibleNodes[index]) === null || _a === void 0 ? void 0 : _a.id) || index; },
254
+ outerElementType: (_a = tree.props.outerElementType) !== null && _a !== void 0 ? _a : list_outer_element_1.ListOuterElement,
255
+ innerElementType: (_b = tree.props.innerElementType) !== null && _b !== void 0 ? _b : list_inner_element_1.ListInnerElement,
256
+ onScroll: tree.props.onScroll,
257
+ onItemsRendered: tree.onItemsRendered.bind(tree),
258
+ };
259
+ if (typeof tree.props.rowHeight === "function") {
260
+ return (
261
+ // @ts-ignore
262
+ (0, jsx_runtime_1.jsx)(react_window_1.VariableSizeList, Object.assign({}, commonProps, { itemSize: tree.rowHeightAt, ref: tree.list, children: row_container_1.RowContainer })));
263
+ }
264
+ return (
265
+ // @ts-ignore
266
+ (0, jsx_runtime_1.jsx)(react_window_1.FixedSizeList, Object.assign({}, commonProps, { itemSize: tree.rowHeight, ref: tree.list, children: row_container_1.RowContainer })));
237
267
  }
@@ -23,7 +23,7 @@ const circleStyle = {
23
23
  boxShadow: "0 0 0 3px #4B91E2",
24
24
  borderRadius: "50%",
25
25
  };
26
- exports.DefaultCursor = react_1.default.memo(function DefaultCursor({ top, left, indent, }) {
26
+ exports.DefaultCursor = react_1.default.memo(function DefaultCursor({ top, left, indent }) {
27
27
  const style = {
28
28
  position: "absolute",
29
29
  pointerEvents: "none",
@@ -1,2 +1,2 @@
1
1
  import { DragPreviewProps } from "../types/renderers";
2
- export declare function DefaultDragPreview({ offset, mouse, id, dragIds, isDragging, }: DragPreviewProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function DefaultDragPreview({ offset, mouse, id, dragIds, isDragging }: DragPreviewProps): import("react/jsx-runtime").JSX.Element;
@@ -25,7 +25,7 @@ const getCountStyle = (offset) => {
25
25
  const { x, y } = offset;
26
26
  return { transform: `translate(${x + 10}px, ${y + 10}px)` };
27
27
  };
28
- function DefaultDragPreview({ offset, mouse, id, dragIds, isDragging, }) {
28
+ function DefaultDragPreview({ offset, mouse, id, dragIds, isDragging }) {
29
29
  return ((0, jsx_runtime_1.jsxs)(Overlay, { isDragging: isDragging, children: [(0, jsx_runtime_1.jsx)(Position, { offset: offset, children: (0, jsx_runtime_1.jsx)(PreviewNode, { id: id, dragIds: dragIds }) }), (0, jsx_runtime_1.jsx)(Count, { mouse: mouse, count: dragIds.length })] }));
30
30
  }
31
31
  const Overlay = (0, react_1.memo)(function Overlay(props) {
@@ -1,2 +1,2 @@
1
1
  import { RowRendererProps } from "../types/renderers";
2
- export declare function DefaultRow<T>({ node, attrs, innerRef, children, }: RowRendererProps<T>): import("react/jsx-runtime").JSX.Element;
2
+ export declare function DefaultRow<T>({ node, attrs, innerRef, children }: RowRendererProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -2,6 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DefaultRow = DefaultRow;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
- function DefaultRow({ node, attrs, innerRef, children, }) {
5
+ function DefaultRow({ node, attrs, innerRef, children }) {
6
6
  return ((0, jsx_runtime_1.jsx)("div", Object.assign({}, attrs, { ref: innerRef, onFocus: (e) => e.stopPropagation(), onClick: node.handleClick, children: children })));
7
7
  }
@@ -1 +1,2 @@
1
1
  export declare const ListOuterElement: import("react").ForwardRefExoticComponent<Omit<import("react").HTMLProps<HTMLDivElement>, "ref"> & import("react").RefAttributes<unknown>>;
2
+ export declare const DropContainer: () => import("react/jsx-runtime").JSX.Element;
@@ -11,7 +11,7 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  return t;
12
12
  };
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.ListOuterElement = void 0;
14
+ exports.DropContainer = exports.ListOuterElement = void 0;
15
15
  const jsx_runtime_1 = require("react/jsx-runtime");
16
16
  const react_1 = require("react");
17
17
  const context_1 = require("../context");
@@ -24,15 +24,16 @@ exports.ListOuterElement = (0, react_1.forwardRef)(function Outer(props, ref) {
24
24
  ref: ref }, rest, { onClick: (e) => {
25
25
  if (e.currentTarget === e.target)
26
26
  tree.deselectAll();
27
- }, children: [(0, jsx_runtime_1.jsx)(DropContainer, {}), children] })));
27
+ }, children: [(0, jsx_runtime_1.jsx)(exports.DropContainer, {}), children] })));
28
28
  });
29
29
  const DropContainer = () => {
30
30
  const tree = (0, context_1.useTreeApi)();
31
31
  return ((0, jsx_runtime_1.jsx)("div", { style: {
32
- height: tree.visibleNodes.length * tree.rowHeight,
32
+ height: tree.rowTopPosition(tree.visibleNodes.length),
33
33
  width: "100%",
34
34
  position: "absolute",
35
35
  left: "0",
36
36
  right: "0",
37
37
  }, children: (0, jsx_runtime_1.jsx)(cursor_1.Cursor, {}) }));
38
38
  };
39
+ exports.DropContainer = DropContainer;
@@ -6,5 +6,5 @@ type Props<T> = {
6
6
  imperativeHandle: React.Ref<TreeApi<T> | undefined>;
7
7
  children: ReactNode;
8
8
  };
9
- export declare function TreeProvider<T>({ treeProps, imperativeHandle, children, }: Props<T>): import("react/jsx-runtime").JSX.Element;
9
+ export declare function TreeProvider<T>({ treeProps, imperativeHandle, children }: Props<T>): import("react/jsx-runtime").JSX.Element;
10
10
  export {};
@@ -13,7 +13,7 @@ const react_dnd_1 = require("react-dnd");
13
13
  const redux_1 = require("redux");
14
14
  const open_slice_1 = require("../state/open-slice");
15
15
  const SERVER_STATE = (0, initial_1.initialState)();
16
- function TreeProvider({ treeProps, imperativeHandle, children, }) {
16
+ function TreeProvider({ treeProps, imperativeHandle, children }) {
17
17
  const list = (0, react_1.useRef)(null);
18
18
  const listEl = (0, react_1.useRef)(null);
19
19
  const store = (0, react_1.useRef)(
@@ -29,7 +29,7 @@ function TreeProvider({ treeProps, imperativeHandle, children, }) {
29
29
  (0, react_1.useMemo)(() => {
30
30
  updateCount.current += 1;
31
31
  api.update(treeProps);
32
- }, [...Object.values(treeProps)]);
32
+ }, Object.values(treeProps));
33
33
  /* Rebuild visible nodes when open state changes, without clobbering
34
34
  props set imperatively via api.update(). Bumping updateCount keeps
35
35
  DataUpdates consumers (e.g. DefaultContainer) in sync. */
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const jsx_runtime_1 = require("react/jsx-runtime");
4
4
  const react_1 = require("react");
5
5
  const react_2 = require("@testing-library/react");
6
+ const react_window_1 = require("react-window");
6
7
  const tree_1 = require("./tree");
7
8
  const data = [
8
9
  {
@@ -31,3 +32,72 @@ test("imperative tree.update() props survive node toggles (#228)", () => {
31
32
  });
32
33
  expect(api.rowHeight).toBe(48);
33
34
  });
35
+ /* Backwards compatibility: switching FixedSizeList -> VariableSizeList must not
36
+ change layout for a numeric rowHeight. With openByDefault, all four nodes
37
+ (1 > 2, 3 > 4) are visible in DFS order. */
38
+ test("numeric rowHeight positions rows at index * height (#238 back-compat)", () => {
39
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, rowHeight: 24, openByDefault: true }));
40
+ const rows = react_2.screen.getAllByRole("treeitem");
41
+ expect(rows).toHaveLength(4);
42
+ rows.forEach((row, i) => {
43
+ expect(row.style.height).toBe("24px");
44
+ expect(row.style.top).toBe(`${i * 24}px`);
45
+ });
46
+ });
47
+ test("function rowHeight gives each row its own height and cumulative top (#238)", () => {
48
+ const heights = { "1": 40, "2": 20, "3": 30, "4": 10 };
49
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, rowHeight: (node) => heights[node.id], openByDefault: true }));
50
+ const rows = react_2.screen.getAllByRole("treeitem");
51
+ expect(rows).toHaveLength(4);
52
+ const expected = [40, 20, 30, 10];
53
+ let top = 0;
54
+ rows.forEach((row, i) => {
55
+ expect(row.style.height).toBe(`${expected[i]}px`);
56
+ expect(row.style.top).toBe(`${top}px`);
57
+ top += expected[i];
58
+ });
59
+ });
60
+ test("mutations tell the list to recompute heights (#238)", () => {
61
+ const ref = (0, react_1.createRef)();
62
+ /* Only variable-height mode renders a VariableSizeList with a measurement
63
+ cache to recompute, so use a function rowHeight here. */
64
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, ref: ref, rowHeight: () => 24, openByDefault: true }));
65
+ const api = ref.current;
66
+ const reset = jest.spyOn(api.list.current, "resetAfterIndex");
67
+ (0, react_2.act)(() => api.close("1"));
68
+ expect(reset).toHaveBeenCalled();
69
+ reset.mockClear();
70
+ (0, react_2.act)(() => api.open("1"));
71
+ expect(reset).toHaveBeenCalled();
72
+ });
73
+ /* react-window caches measurements by index and never invalidates them itself.
74
+ When data changes via props in variable-height mode, those cached sizes belong
75
+ to the wrong rows, so update() must drop the cache. It runs during render, so
76
+ it uses the shouldForceUpdate=false variant. */
77
+ test("changing data in variable-height mode resets the list cache (#238)", () => {
78
+ const ref = (0, react_1.createRef)();
79
+ const rowHeight = (node) => (node.isInternal ? 40 : 20);
80
+ const { rerender } = (0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, ref: ref, rowHeight: rowHeight, openByDefault: true }));
81
+ const reset = jest.spyOn(ref.current.list.current, "resetAfterIndex");
82
+ const nextData = [{ id: "9", name: "fresh" }, ...data];
83
+ (0, react_2.act)(() => {
84
+ rerender((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: nextData, ref: ref, rowHeight: rowHeight, openByDefault: true }));
85
+ });
86
+ expect(reset).toHaveBeenCalledWith(0, false);
87
+ });
88
+ /* The numeric path must stay on FixedSizeList: it has constant item sizes, so
89
+ there is no measurement cache to go stale and none of VariableSizeList's
90
+ overhead. A FixedSizeList has no resetAfterIndex method at all. */
91
+ test("numeric rowHeight renders a cache-free FixedSizeList (#238)", () => {
92
+ const ref = (0, react_1.createRef)();
93
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, ref: ref, rowHeight: 24, openByDefault: true }));
94
+ const list = ref.current.list.current;
95
+ expect(list).toBeInstanceOf(react_window_1.FixedSizeList);
96
+ expect("resetAfterIndex" in list).toBe(false);
97
+ });
98
+ /* The function path uses VariableSizeList so per-row heights are possible. */
99
+ test("function rowHeight renders a VariableSizeList (#238)", () => {
100
+ const ref = (0, react_1.createRef)();
101
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, ref: ref, rowHeight: () => 24, openByDefault: true }));
102
+ expect(ref.current.list.current).toBeInstanceOf(react_window_1.VariableSizeList);
103
+ });
@@ -3,5 +3,5 @@ type Props = {
3
3
  style: React.CSSProperties;
4
4
  index: number;
5
5
  };
6
- export declare const RowContainer: React.MemoExoticComponent<(<T>({ index, style, }: Props) => import("react/jsx-runtime").JSX.Element)>;
6
+ export declare const RowContainer: React.MemoExoticComponent<(<T>({ index, style }: Props) => import("react/jsx-runtime").JSX.Element)>;
7
7
  export {};
@@ -30,7 +30,7 @@ const context_1 = require("../context");
30
30
  const drag_hook_1 = require("../dnd/drag-hook");
31
31
  const drop_hook_1 = require("../dnd/drop-hook");
32
32
  const use_fresh_node_1 = require("../hooks/use-fresh-node");
33
- exports.RowContainer = react_1.default.memo(function RowContainer({ index, style, }) {
33
+ exports.RowContainer = react_1.default.memo(function RowContainer({ index, style }) {
34
34
  /* When will the <Row> will re-render.
35
35
  *
36
36
  * The row component is memo'd so it will only render
@@ -61,8 +61,7 @@ exports.RowContainer = react_1.default.memo(function RowContainer({ index, style
61
61
  const nodeStyle = (0, react_1.useMemo)(() => ({ paddingLeft: indent }), [indent]);
62
62
  const rowStyle = (0, react_1.useMemo)(() => {
63
63
  var _a, _b;
64
- return (Object.assign(Object.assign({}, style), { top: parseFloat(style.top) +
65
- ((_b = (_a = tree.props.padding) !== null && _a !== void 0 ? _a : tree.props.paddingTop) !== null && _b !== void 0 ? _b : 0) }));
64
+ return (Object.assign(Object.assign({}, style), { top: parseFloat(style.top) + ((_b = (_a = tree.props.padding) !== null && _a !== void 0 ? _a : tree.props.paddingTop) !== null && _b !== void 0 ? _b : 0) }));
66
65
  }, [style, tree.props.padding, tree.props.paddingTop]);
67
66
  const rowAttrs = {
68
67
  role: "treeitem",
@@ -20,6 +20,7 @@ function useDragHook(node) {
20
20
  },
21
21
  end: () => {
22
22
  tree.hideCursor();
23
+ tree.redrawList();
23
24
  tree.dispatch(dnd_slice_1.actions.dragEnd());
24
25
  },
25
26
  }), [ids, node]);
@@ -6,8 +6,7 @@ function useValidatedProps(props) {
6
6
  if (props.initialData && props.data) {
7
7
  throw new Error(`React Arborist Tree => Provide either a data or initialData prop, but not both.`);
8
8
  }
9
- if (props.initialData &&
10
- (props.onCreate || props.onDelete || props.onMove || props.onRename)) {
9
+ if (props.initialData && (props.onCreate || props.onDelete || props.onMove || props.onRename)) {
11
10
  throw new Error(`React Arborist Tree => You passed the initialData prop along with a data handler.
12
11
  Use the data prop if you want to provide your own handlers.`);
13
12
  }
@@ -1,4 +1,7 @@
1
1
  export { Tree } from "./components/tree";
2
+ export { DropContainer } from "./components/list-outer-element";
3
+ export { ListOuterElement } from "./components/list-outer-element";
4
+ export { ListInnerElement } from "./components/list-inner-element";
2
5
  export * from "./types/handlers";
3
6
  export * from "./types/renderers";
4
7
  export * from "./types/state";
@@ -14,10 +14,16 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.getTreeLinePrefix = exports.Tree = void 0;
17
+ exports.getTreeLinePrefix = exports.ListInnerElement = exports.ListOuterElement = exports.DropContainer = exports.Tree = void 0;
18
18
  /* The Public Api */
19
19
  var tree_1 = require("./components/tree");
20
20
  Object.defineProperty(exports, "Tree", { enumerable: true, get: function () { return tree_1.Tree; } });
21
+ var list_outer_element_1 = require("./components/list-outer-element");
22
+ Object.defineProperty(exports, "DropContainer", { enumerable: true, get: function () { return list_outer_element_1.DropContainer; } });
23
+ var list_outer_element_2 = require("./components/list-outer-element");
24
+ Object.defineProperty(exports, "ListOuterElement", { enumerable: true, get: function () { return list_outer_element_2.ListOuterElement; } });
25
+ var list_inner_element_1 = require("./components/list-inner-element");
26
+ Object.defineProperty(exports, "ListInnerElement", { enumerable: true, get: function () { return list_inner_element_1.ListInnerElement; } });
21
27
  __exportStar(require("./types/handlers"), exports);
22
28
  __exportStar(require("./types/renderers"), exports);
23
29
  __exportStar(require("./types/state"), exports);
@@ -6,7 +6,10 @@ class NodeApi {
6
6
  constructor(params) {
7
7
  this.handleClick = (e) => {
8
8
  if (e.metaKey && !this.tree.props.disableMultiSelection) {
9
- this.isSelected ? this.deselect() : this.selectMulti();
9
+ if (this.isSelected)
10
+ this.deselect();
11
+ else
12
+ this.selectMulti();
10
13
  }
11
14
  else if (e.shiftKey && !this.tree.props.disableMultiSelection) {
12
15
  this.selectContiguous();
@@ -2,7 +2,7 @@ import { EditResult } from "../types/handlers";
2
2
  import { Identity, IdObj } from "../types/utils";
3
3
  import { TreeProps } from "../types/tree-props";
4
4
  import { MutableRefObject } from "react";
5
- import { Align, FixedSizeList, ListOnItemsRenderedProps } from "react-window";
5
+ import { Align, FixedSizeList, ListOnItemsRenderedProps, VariableSizeList } from "react-window";
6
6
  import { DefaultRow } from "../components/default-row";
7
7
  import { DefaultNode } from "../components/default-node";
8
8
  import { NodeApi } from "./node-api";
@@ -14,7 +14,7 @@ import { Store } from "redux";
14
14
  export declare class TreeApi<T> {
15
15
  store: Store<RootState, Actions>;
16
16
  props: TreeProps<T>;
17
- list: MutableRefObject<FixedSizeList | null>;
17
+ list: MutableRefObject<FixedSizeList | VariableSizeList | null>;
18
18
  listEl: MutableRefObject<HTMLDivElement | null>;
19
19
  static editPromise: null | ((args: EditResult) => void);
20
20
  root: NodeApi<T>;
@@ -24,7 +24,8 @@ export declare class TreeApi<T> {
24
24
  idToIndex: {
25
25
  [id: string]: number;
26
26
  };
27
- constructor(store: Store<RootState, Actions>, props: TreeProps<T>, list: MutableRefObject<FixedSizeList | null>, listEl: MutableRefObject<HTMLDivElement | null>);
27
+ private rowOffsets;
28
+ constructor(store: Store<RootState, Actions>, props: TreeProps<T>, list: MutableRefObject<FixedSizeList | VariableSizeList | null>, listEl: MutableRefObject<HTMLDivElement | null>);
28
29
  update(props: TreeProps<T>): void;
29
30
  dispatch(action: Actions): {
30
31
  type: "FOCUS";
@@ -121,7 +122,28 @@ export declare class TreeApi<T> {
121
122
  get width(): string | number;
122
123
  get height(): number;
123
124
  get indent(): number;
125
+ /**
126
+ * The fixed row height. When a `rowHeight` function is supplied for variable
127
+ * heights, this returns the default (24); use `rowHeightAt(index)` to get the
128
+ * height of a specific row.
129
+ */
124
130
  get rowHeight(): number;
131
+ /**
132
+ * The height of the row at `index`, evaluating the `rowHeight` function if
133
+ * given. Falls back to the default height for an out-of-range index so this
134
+ * never feeds an invalid `0` to react-window's `itemSize`.
135
+ */
136
+ rowHeightAt: (index: number) => number;
137
+ /** The pixel offset of the top of the row at `index` from the top of the list. */
138
+ rowTopPosition: (index: number) => number;
139
+ /**
140
+ * Tell the underlying virtualized list to recompute row heights at and after
141
+ * `index`. Call this if a `rowHeight` function's output changes for reasons
142
+ * the tree can't observe (e.g. external state).
143
+ */
144
+ redrawList: (afterIndex?: number) => void;
145
+ /** Lazily-built prefix sum where offsets[i] is the top of row i. */
146
+ private getRowOffsets;
125
147
  get overscanCount(): number;
126
148
  get searchTerm(): string;
127
149
  get matchFn(): (node: NodeApi<T>) => boolean;
@@ -185,8 +207,8 @@ export declare class TreeApi<T> {
185
207
  canDrop(): boolean;
186
208
  hideCursor(): void;
187
209
  showCursor(cursor: Cursor): void;
188
- open(identity: Identity): void;
189
- close(identity: Identity): void;
210
+ open(identity: Identity, redraw?: boolean): void;
211
+ close(identity: Identity, redraw?: boolean): void;
190
212
  toggle(identity: Identity): void;
191
213
  openParents(identity: Identity): void;
192
214
  openSiblings(node: NodeApi<T>): void;
@@ -56,6 +56,46 @@ class TreeApi {
56
56
  this.listEl = listEl;
57
57
  this.visibleStartIndex = 0;
58
58
  this.visibleStopIndex = 0;
59
+ /* Memoized prefix-sum of row heights; only used for variable heights. */
60
+ this.rowOffsets = null;
61
+ /**
62
+ * The height of the row at `index`, evaluating the `rowHeight` function if
63
+ * given. Falls back to the default height for an out-of-range index so this
64
+ * never feeds an invalid `0` to react-window's `itemSize`.
65
+ */
66
+ this.rowHeightAt = (index) => {
67
+ const rowHeight = this.props.rowHeight;
68
+ if (typeof rowHeight === "function") {
69
+ const node = this.at(index);
70
+ return node ? rowHeight(node) : this.rowHeight;
71
+ }
72
+ return rowHeight !== null && rowHeight !== void 0 ? rowHeight : 24;
73
+ };
74
+ /** The pixel offset of the top of the row at `index` from the top of the list. */
75
+ this.rowTopPosition = (index) => {
76
+ /* Fixed heights: O(1). */
77
+ if (typeof this.props.rowHeight !== "function") {
78
+ return index * this.rowHeight;
79
+ }
80
+ /* Variable heights: O(1) amortized via a memoized prefix sum. */
81
+ const offsets = this.getRowOffsets();
82
+ const clamped = Math.max(0, Math.min(index, offsets.length - 1));
83
+ return offsets[clamped];
84
+ };
85
+ /**
86
+ * Tell the underlying virtualized list to recompute row heights at and after
87
+ * `index`. Call this if a `rowHeight` function's output changes for reasons
88
+ * the tree can't observe (e.g. external state).
89
+ */
90
+ this.redrawList = (afterIndex = 0) => {
91
+ this.rowOffsets = null;
92
+ /* Only the VariableSizeList (function rowHeight) caches measurements; a
93
+ FixedSizeList has constant heights and nothing to recompute. */
94
+ const list = this.list.current;
95
+ if (list && "resetAfterIndex" in list) {
96
+ list.resetAfterIndex(Math.max(0, afterIndex));
97
+ }
98
+ };
59
99
  /* Changes here must also be made in update() */
60
100
  this.root = (0, create_root_1.createRoot)(this);
61
101
  this.visibleNodes = (0, create_list_1.createList)(this);
@@ -67,6 +107,18 @@ class TreeApi {
67
107
  this.root = (0, create_root_1.createRoot)(this);
68
108
  this.visibleNodes = (0, create_list_1.createList)(this);
69
109
  this.idToIndex = (0, create_index_1.createIndex)(this.visibleNodes);
110
+ this.rowOffsets = null;
111
+ /* Variable-height mode renders a VariableSizeList, which caches item
112
+ measurements by index and never invalidates them on its own. When the
113
+ visible nodes change (insert/remove/reorder), those cached sizes belong
114
+ to the wrong rows, so drop them. Fixed-height mode renders a
115
+ FixedSizeList (no cache, nothing to reset). update() runs during render,
116
+ so pass shouldForceUpdate=false: the in-progress render repaints the list
117
+ and a forceUpdate here would warn about setting state mid-render. */
118
+ const list = this.list.current;
119
+ if (list && "resetAfterIndex" in list) {
120
+ list.resetAfterIndex(0, false);
121
+ }
70
122
  }
71
123
  /* Store helpers */
72
124
  dispatch(action) {
@@ -91,9 +143,24 @@ class TreeApi {
91
143
  var _a;
92
144
  return (_a = this.props.indent) !== null && _a !== void 0 ? _a : 24;
93
145
  }
146
+ /**
147
+ * The fixed row height. When a `rowHeight` function is supplied for variable
148
+ * heights, this returns the default (24); use `rowHeightAt(index)` to get the
149
+ * height of a specific row.
150
+ */
94
151
  get rowHeight() {
95
- var _a;
96
- return (_a = this.props.rowHeight) !== null && _a !== void 0 ? _a : 24;
152
+ return typeof this.props.rowHeight === "number" ? this.props.rowHeight : 24;
153
+ }
154
+ /** Lazily-built prefix sum where offsets[i] is the top of row i. */
155
+ getRowOffsets() {
156
+ if (this.rowOffsets)
157
+ return this.rowOffsets;
158
+ const offsets = [0];
159
+ for (let i = 0; i < this.visibleNodes.length; i++) {
160
+ offsets.push(offsets[i] + this.rowHeightAt(i));
161
+ }
162
+ this.rowOffsets = offsets;
163
+ return offsets;
97
164
  }
98
165
  get overscanCount() {
99
166
  var _a;
@@ -195,9 +262,7 @@ class TreeApi {
195
262
  create() {
196
263
  return __awaiter(this, arguments, void 0, function* (opts = {}) {
197
264
  var _a, _b;
198
- const parentId = opts.parentId === undefined
199
- ? utils.getInsertParentId(this)
200
- : opts.parentId;
265
+ const parentId = opts.parentId === undefined ? utils.getInsertParentId(this) : opts.parentId;
201
266
  const index = (_a = opts.index) !== null && _a !== void 0 ? _a : utils.getInsertIndex(this);
202
267
  const type = (_b = opts.type) !== null && _b !== void 0 ? _b : "leaf";
203
268
  const data = yield safeRun(this.props.onCreate, {
@@ -224,20 +289,26 @@ class TreeApi {
224
289
  const idents = Array.isArray(node) ? node : [node];
225
290
  const ids = idents.map(identify);
226
291
  const nodes = ids.map((id) => this.get(id)).filter((n) => !!n);
292
+ /* Guard against Math.min(...[]) === Infinity when no ids resolve to nodes. */
293
+ const fromIndex = nodes.length ? Math.min(...nodes.map((n) => { var _a; return (_a = n.rowIndex) !== null && _a !== void 0 ? _a : 0; })) : 0;
227
294
  yield safeRun(this.props.onDelete, { nodes, ids });
295
+ this.redrawList(fromIndex);
228
296
  });
229
297
  }
230
298
  edit(node) {
299
+ var _a, _b;
231
300
  const id = identify(node);
232
301
  this.resolveEdit({ cancelled: true });
233
302
  this.scrollTo(id);
234
303
  this.dispatch((0, edit_slice_1.edit)(id));
304
+ this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
235
305
  return new Promise((resolve) => {
236
306
  TreeApi.editPromise = resolve;
237
307
  });
238
308
  }
239
309
  submit(identity, value) {
240
310
  return __awaiter(this, void 0, void 0, function* () {
311
+ var _a, _b;
241
312
  if (!identity)
242
313
  return;
243
314
  const id = identify(identity);
@@ -248,12 +319,14 @@ class TreeApi {
248
319
  });
249
320
  this.dispatch((0, edit_slice_1.edit)(null));
250
321
  this.resolveEdit({ cancelled: false, value });
322
+ this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
251
323
  setTimeout(() => this.onFocus()); // Return focus to element;
252
324
  });
253
325
  }
254
326
  reset() {
255
327
  this.dispatch((0, edit_slice_1.edit)(null));
256
328
  this.resolveEdit({ cancelled: true });
329
+ this.redrawList();
257
330
  setTimeout(() => this.onFocus()); // Return focus to element;
258
331
  }
259
332
  activate(id) {
@@ -433,9 +506,7 @@ class TreeApi {
433
506
  return this.state.dnd.cursor.type === "highlight";
434
507
  }
435
508
  get dragNodes() {
436
- return this.state.dnd.dragIds
437
- .map((id) => this.get(id))
438
- .filter((n) => !!n);
509
+ return this.state.dnd.dragIds.map((id) => this.get(id)).filter((n) => !!n);
439
510
  }
440
511
  get dragNode() {
441
512
  return this.get(this.state.nodes.drag.id);
@@ -487,22 +558,28 @@ class TreeApi {
487
558
  this.dispatch(dnd_slice_1.actions.cursor(cursor));
488
559
  }
489
560
  /* Visibility */
490
- open(identity) {
561
+ open(identity, redraw = true) {
562
+ var _a, _b;
491
563
  const id = identifyNull(identity);
492
564
  if (!id)
493
565
  return;
494
566
  if (this.isOpen(id))
495
567
  return;
496
568
  this.dispatch(open_slice_1.actions.open(id, this.isFiltered));
569
+ if (redraw)
570
+ this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
497
571
  safeRun(this.props.onToggle, id);
498
572
  }
499
- close(identity) {
573
+ close(identity, redraw = true) {
574
+ var _a, _b;
500
575
  const id = identifyNull(identity);
501
576
  if (!id)
502
577
  return;
503
578
  if (!this.isOpen(id))
504
579
  return;
505
580
  this.dispatch(open_slice_1.actions.close(id, this.isFiltered));
581
+ if (redraw)
582
+ this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
506
583
  safeRun(this.props.onToggle, id);
507
584
  }
508
585
  toggle(identity) {
@@ -518,9 +595,10 @@ class TreeApi {
518
595
  const node = utils.dfs(this.root, id);
519
596
  let parent = node === null || node === void 0 ? void 0 : node.parent;
520
597
  while (parent) {
521
- this.open(parent.id);
598
+ this.open(parent.id, false);
522
599
  parent = parent.parent;
523
600
  }
601
+ this.redrawList();
524
602
  }
525
603
  openSiblings(node) {
526
604
  const parent = node.parent;
@@ -531,23 +609,29 @@ class TreeApi {
531
609
  const isOpen = node.isOpen;
532
610
  for (let sibling of parent.children) {
533
611
  if (sibling.isInternal) {
534
- isOpen ? this.close(sibling.id) : this.open(sibling.id);
612
+ if (isOpen)
613
+ this.close(sibling.id, false);
614
+ else
615
+ this.open(sibling.id, false);
535
616
  }
536
617
  }
618
+ this.redrawList();
537
619
  this.scrollTo(this.focusedNode);
538
620
  }
539
621
  }
540
622
  openAll() {
541
623
  utils.walk(this.root, (node) => {
542
624
  if (node.isInternal)
543
- node.open();
625
+ this.open(node.id, false);
544
626
  });
627
+ this.redrawList();
545
628
  }
546
629
  closeAll() {
547
630
  utils.walk(this.root, (node) => {
548
631
  if (node.isInternal)
549
- node.close();
632
+ this.close(node.id, false);
550
633
  });
634
+ this.redrawList();
551
635
  }
552
636
  /* Scrolling */
553
637
  scrollTo(identity, align = "smart") {
@@ -12,3 +12,34 @@ test("tree.canDrop()", () => {
12
12
  expect(setupApi({ disableDrop: () => false }).canDrop()).toBe(true);
13
13
  expect(setupApi({ disableDrop: false }).canDrop()).toBe(true);
14
14
  });
15
+ const rowData = [{ id: "a" }, { id: "b" }, { id: "c" }];
16
+ test("rowHeight defaults to 24", () => {
17
+ const api = setupApi({});
18
+ expect(api.rowHeight).toBe(24);
19
+ expect(api.rowHeightAt(0)).toBe(24);
20
+ });
21
+ test("fixed numeric rowHeight", () => {
22
+ const api = setupApi({ data: rowData, rowHeight: 30 });
23
+ expect(api.rowHeight).toBe(30);
24
+ expect(api.rowHeightAt(0)).toBe(30);
25
+ expect(api.rowTopPosition(0)).toBe(0);
26
+ expect(api.rowTopPosition(2)).toBe(60);
27
+ expect(api.rowTopPosition(3)).toBe(90); // total list height
28
+ });
29
+ test("variable rowHeight function", () => {
30
+ const heights = { a: 10, b: 20, c: 40 };
31
+ const api = setupApi({
32
+ data: rowData,
33
+ rowHeight: (node) => heights[node.id],
34
+ });
35
+ // The back-compat getter falls back to the default for variable heights.
36
+ expect(api.rowHeight).toBe(24);
37
+ expect(api.rowHeightAt(0)).toBe(10);
38
+ expect(api.rowHeightAt(1)).toBe(20);
39
+ expect(api.rowTopPosition(0)).toBe(0);
40
+ expect(api.rowTopPosition(1)).toBe(10);
41
+ expect(api.rowTopPosition(2)).toBe(30);
42
+ expect(api.rowTopPosition(3)).toBe(70); // total list height
43
+ // Out-of-range index falls back to the default height, never an invalid 0.
44
+ expect(api.rowHeightAt(99)).toBe(24);
45
+ });