react-arborist 3.7.0 → 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 (86) hide show
  1. package/dist/main/components/cursor.js +1 -2
  2. package/dist/main/components/default-container.js +31 -2
  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.js +1 -1
  9. package/dist/main/components/provider.d.ts +1 -1
  10. package/dist/main/components/provider.js +2 -2
  11. package/dist/main/components/provider.test.js +70 -0
  12. package/dist/main/components/row-container.d.ts +1 -1
  13. package/dist/main/components/row-container.js +2 -3
  14. package/dist/main/dnd/drag-hook.js +1 -0
  15. package/dist/main/hooks/use-validated-props.js +1 -2
  16. package/dist/main/interfaces/node-api.js +4 -1
  17. package/dist/main/interfaces/tree-api.d.ts +27 -5
  18. package/dist/main/interfaces/tree-api.js +98 -14
  19. package/dist/main/interfaces/tree-api.test.js +31 -0
  20. package/dist/main/state/drag-slice.js +1 -2
  21. package/dist/main/types/state.d.ts +1 -1
  22. package/dist/main/types/tree-props.d.ts +3 -1
  23. package/dist/module/components/cursor.js +1 -2
  24. package/dist/module/components/default-container.js +32 -3
  25. package/dist/module/components/default-cursor.js +1 -1
  26. package/dist/module/components/default-drag-preview.d.ts +1 -1
  27. package/dist/module/components/default-drag-preview.js +1 -1
  28. package/dist/module/components/default-row.d.ts +1 -1
  29. package/dist/module/components/default-row.js +1 -1
  30. package/dist/module/components/list-outer-element.js +1 -1
  31. package/dist/module/components/provider.d.ts +1 -1
  32. package/dist/module/components/provider.js +4 -4
  33. package/dist/module/components/provider.test.js +71 -1
  34. package/dist/module/components/row-container.d.ts +1 -1
  35. package/dist/module/components/row-container.js +2 -3
  36. package/dist/module/dnd/compute-drop.js +1 -1
  37. package/dist/module/dnd/drag-hook.js +1 -0
  38. package/dist/module/hooks/use-validated-props.js +1 -2
  39. package/dist/module/interfaces/node-api.js +4 -1
  40. package/dist/module/interfaces/tree-api.d.ts +27 -5
  41. package/dist/module/interfaces/tree-api.js +98 -14
  42. package/dist/module/interfaces/tree-api.test.js +31 -0
  43. package/dist/module/state/drag-slice.js +1 -2
  44. package/dist/module/types/state.d.ts +1 -1
  45. package/dist/module/types/tree-props.d.ts +3 -1
  46. package/package.json +27 -27
  47. package/src/components/cursor.tsx +1 -2
  48. package/src/components/default-container.tsx +40 -19
  49. package/src/components/default-cursor.tsx +1 -5
  50. package/src/components/default-drag-preview.tsx +3 -16
  51. package/src/components/default-node.tsx +0 -1
  52. package/src/components/default-row.tsx +2 -13
  53. package/src/components/drag-preview-container.tsx +1 -1
  54. package/src/components/list-inner-element.tsx +1 -1
  55. package/src/components/list-outer-element.tsx +2 -3
  56. package/src/components/provider.test.tsx +85 -9
  57. package/src/components/provider.tsx +8 -23
  58. package/src/components/row-container.tsx +4 -9
  59. package/src/components/tree.tsx +2 -6
  60. package/src/context.ts +2 -3
  61. package/src/data/create-index.ts +0 -1
  62. package/src/data/create-list.ts +1 -2
  63. package/src/data/create-root.ts +2 -9
  64. package/src/data/simple-tree.ts +5 -3
  65. package/src/dnd/compute-drop.ts +6 -15
  66. package/src/dnd/drag-hook.ts +1 -0
  67. package/src/dnd/measure-hover.ts +2 -6
  68. package/src/dnd/outer-drop-hook.ts +1 -1
  69. package/src/hooks/use-fresh-node.ts +0 -1
  70. package/src/hooks/use-simple-tree.ts +2 -8
  71. package/src/hooks/use-validated-props.ts +4 -8
  72. package/src/interfaces/node-api.ts +2 -2
  73. package/src/interfaces/tree-api.test.ts +35 -0
  74. package/src/interfaces/tree-api.ts +103 -36
  75. package/src/state/dnd-slice.ts +1 -1
  76. package/src/state/drag-slice.ts +2 -5
  77. package/src/state/edit-slice.ts +1 -4
  78. package/src/state/focus-slice.ts +1 -1
  79. package/src/state/open-slice.ts +2 -5
  80. package/src/state/selection-slice.ts +2 -6
  81. package/src/types/handlers.ts +1 -3
  82. package/src/types/renderers.ts +0 -1
  83. package/src/types/state.ts +1 -1
  84. package/src/types/tree-props.ts +6 -10
  85. package/src/types/utils.ts +2 -3
  86. package/src/utils.ts +5 -14
@@ -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 "line",
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 "none",
78
+ type: "none" as const,
88
79
  };
89
80
  }
90
81
 
91
82
  function highlightCursor(id: string) {
92
83
  return {
93
- type: "highlight" as "highlight",
84
+ type: "highlight" as const,
94
85
  id,
95
86
  };
96
87
  }
@@ -22,6 +22,7 @@ export function useDragHook<T>(node: NodeApi<T>): ConnectDragSource {
22
22
  },
23
23
  end: () => {
24
24
  tree.hideCursor();
25
+ tree.redrawList();
25
26
  tree.dispatch(dnd.dragEnd());
26
27
  },
27
28
  }),
@@ -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
- inBottomHalf ? el.dataset.level : prevEl ? prevEl.dataset.level : 0
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 };
@@ -37,7 +37,7 @@ export function useOuterDrop() {
37
37
  }
38
38
  },
39
39
  }),
40
- [tree]
40
+ [tree],
41
41
  );
42
42
 
43
43
  drop(tree.listEl);
@@ -1,6 +1,5 @@
1
1
  import { useMemo } from "react";
2
2
  import { useTreeApi } from "../context";
3
- import { IdObj } from "../types/utils";
4
3
 
5
4
  export function useFreshNode<T>(index: number) {
6
5
  const tree = useTreeApi<T>();
@@ -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 { IdObj } from "../types/utils";
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 ? this.deselect() : this.selectMulti();
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
+ });
@@ -2,7 +2,7 @@ import { EditResult } from "../types/handlers";
2
2
  import { BoolFunc, 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 * as utils from "../utils";
7
7
  import { DefaultCursor } from "../components/default-cursor";
8
8
  import { DefaultRow } from "../components/default-row";
@@ -30,12 +30,14 @@ export class TreeApi<T> {
30
30
  visibleStartIndex: number = 0;
31
31
  visibleStopIndex: number = 0;
32
32
  idToIndex: { [id: string]: number };
33
+ /* Memoized prefix-sum of row heights; only used for variable heights. */
34
+ private rowOffsets: number[] | null = null;
33
35
 
34
36
  constructor(
35
37
  public store: Store<RootState, Actions>,
36
38
  public props: TreeProps<T>,
37
- public list: MutableRefObject<FixedSizeList | null>,
38
- public listEl: MutableRefObject<HTMLDivElement | null>
39
+ public list: MutableRefObject<FixedSizeList | VariableSizeList | null>,
40
+ public listEl: MutableRefObject<HTMLDivElement | null>,
39
41
  ) {
40
42
  /* Changes here must also be made in update() */
41
43
  this.root = createRoot<T>(this);
@@ -49,6 +51,18 @@ export class TreeApi<T> {
49
51
  this.root = createRoot<T>(this);
50
52
  this.visibleNodes = createList<T>(this);
51
53
  this.idToIndex = createIndex(this.visibleNodes);
54
+ this.rowOffsets = null;
55
+ /* Variable-height mode renders a VariableSizeList, which caches item
56
+ measurements by index and never invalidates them on its own. When the
57
+ visible nodes change (insert/remove/reorder), those cached sizes belong
58
+ to the wrong rows, so drop them. Fixed-height mode renders a
59
+ FixedSizeList (no cache, nothing to reset). update() runs during render,
60
+ so pass shouldForceUpdate=false: the in-progress render repaints the list
61
+ and a forceUpdate here would warn about setting state mid-render. */
62
+ const list = this.list.current;
63
+ if (list && "resetAfterIndex" in list) {
64
+ list.resetAfterIndex(0, false);
65
+ }
52
66
  }
53
67
 
54
68
  /* Store helpers */
@@ -79,8 +93,65 @@ export class TreeApi<T> {
79
93
  return this.props.indent ?? 24;
80
94
  }
81
95
 
96
+ /**
97
+ * The fixed row height. When a `rowHeight` function is supplied for variable
98
+ * heights, this returns the default (24); use `rowHeightAt(index)` to get the
99
+ * height of a specific row.
100
+ */
82
101
  get rowHeight() {
83
- return this.props.rowHeight ?? 24;
102
+ return typeof this.props.rowHeight === "number" ? this.props.rowHeight : 24;
103
+ }
104
+
105
+ /**
106
+ * The height of the row at `index`, evaluating the `rowHeight` function if
107
+ * given. Falls back to the default height for an out-of-range index so this
108
+ * never feeds an invalid `0` to react-window's `itemSize`.
109
+ */
110
+ rowHeightAt = (index: number): number => {
111
+ const rowHeight = this.props.rowHeight;
112
+ if (typeof rowHeight === "function") {
113
+ const node = this.at(index);
114
+ return node ? rowHeight(node) : this.rowHeight;
115
+ }
116
+ return rowHeight ?? 24;
117
+ };
118
+
119
+ /** The pixel offset of the top of the row at `index` from the top of the list. */
120
+ rowTopPosition = (index: number): number => {
121
+ /* Fixed heights: O(1). */
122
+ if (typeof this.props.rowHeight !== "function") {
123
+ return index * this.rowHeight;
124
+ }
125
+ /* Variable heights: O(1) amortized via a memoized prefix sum. */
126
+ const offsets = this.getRowOffsets();
127
+ const clamped = Math.max(0, Math.min(index, offsets.length - 1));
128
+ return offsets[clamped];
129
+ };
130
+
131
+ /**
132
+ * Tell the underlying virtualized list to recompute row heights at and after
133
+ * `index`. Call this if a `rowHeight` function's output changes for reasons
134
+ * the tree can't observe (e.g. external state).
135
+ */
136
+ redrawList = (afterIndex: number = 0) => {
137
+ this.rowOffsets = null;
138
+ /* Only the VariableSizeList (function rowHeight) caches measurements; a
139
+ FixedSizeList has constant heights and nothing to recompute. */
140
+ const list = this.list.current;
141
+ if (list && "resetAfterIndex" in list) {
142
+ list.resetAfterIndex(Math.max(0, afterIndex));
143
+ }
144
+ };
145
+
146
+ /** Lazily-built prefix sum where offsets[i] is the top of row i. */
147
+ private getRowOffsets(): number[] {
148
+ if (this.rowOffsets) return this.rowOffsets;
149
+ const offsets: number[] = [0];
150
+ for (let i = 0; i < this.visibleNodes.length; i++) {
151
+ offsets.push(offsets[i] + this.rowHeightAt(i));
152
+ }
153
+ this.rowOffsets = offsets;
154
+ return offsets;
84
155
  }
85
156
 
86
157
  get overscanCount() {
@@ -95,9 +166,7 @@ export class TreeApi<T> {
95
166
  const match =
96
167
  this.props.searchMatch ??
97
168
  ((node, term) => {
98
- const string = JSON.stringify(
99
- Object.values(node.data as { [k: string]: unknown })
100
- );
169
+ const string = JSON.stringify(Object.values(node.data as { [k: string]: unknown }));
101
170
  return string.toLocaleLowerCase().includes(term.toLocaleLowerCase());
102
171
  });
103
172
  return (node: NodeApi<T>) => match(node, this.searchTerm);
@@ -113,7 +182,7 @@ export class TreeApi<T> {
113
182
  const id = utils.access<string>(data, get);
114
183
  if (!id)
115
184
  throw new Error(
116
- "Data must contain an 'id' property or props.idAccessor must return a string"
185
+ "Data must contain an 'id' property or props.idAccessor must return a string",
117
186
  );
118
187
  return id;
119
188
  }
@@ -150,8 +219,7 @@ export class TreeApi<T> {
150
219
 
151
220
  get(id: string | null): NodeApi<T> | null {
152
221
  if (!id) return null;
153
- if (id in this.idToIndex)
154
- return this.visibleNodes[this.idToIndex[id]] || null;
222
+ if (id in this.idToIndex) return this.visibleNodes[this.idToIndex[id]] || null;
155
223
  else return null;
156
224
  }
157
225
 
@@ -194,12 +262,9 @@ export class TreeApi<T> {
194
262
  type?: "internal" | "leaf";
195
263
  parentId?: null | string;
196
264
  index?: null | number;
197
- } = {}
265
+ } = {},
198
266
  ) {
199
- const parentId =
200
- opts.parentId === undefined
201
- ? utils.getInsertParentId(this)
202
- : opts.parentId;
267
+ const parentId = opts.parentId === undefined ? utils.getInsertParentId(this) : opts.parentId;
203
268
  const index = opts.index ?? utils.getInsertIndex(this);
204
269
  const type = opts.type ?? "leaf";
205
270
  const data = await safeRun(this.props.onCreate, {
@@ -224,7 +289,10 @@ export class TreeApi<T> {
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) => n.rowIndex ?? 0)) : 0;
227
294
  await safeRun(this.props.onDelete, { nodes, ids });
295
+ this.redrawList(fromIndex);
228
296
  }
229
297
 
230
298
  edit(node: string | IdObj): Promise<EditResult> {
@@ -232,6 +300,7 @@ export class TreeApi<T> {
232
300
  this.resolveEdit({ cancelled: true });
233
301
  this.scrollTo(id);
234
302
  this.dispatch(edit(id));
303
+ this.redrawList(this.get(id)?.rowIndex ?? 0);
235
304
  return new Promise((resolve) => {
236
305
  TreeApi.editPromise = resolve;
237
306
  });
@@ -247,12 +316,14 @@ export class TreeApi<T> {
247
316
  });
248
317
  this.dispatch(edit(null));
249
318
  this.resolveEdit({ cancelled: false, value });
319
+ this.redrawList(this.get(id)?.rowIndex ?? 0);
250
320
  setTimeout(() => this.onFocus()); // Return focus to element;
251
321
  }
252
322
 
253
323
  reset() {
254
324
  this.dispatch(edit(null));
255
325
  this.resolveEdit({ cancelled: true });
326
+ this.redrawList();
256
327
  setTimeout(() => this.onFocus()); // Return focus to element;
257
328
  }
258
329
 
@@ -389,9 +460,7 @@ export class TreeApi<T> {
389
460
  }
390
461
 
391
462
  selectAll() {
392
- const allSelectableNodes = this.filterSelectableNodes(
393
- Object.keys(this.idToIndex),
394
- );
463
+ const allSelectableNodes = this.filterSelectableNodes(Object.keys(this.idToIndex));
395
464
  this.setSelection({
396
465
  ids: allSelectableNodes,
397
466
  anchor: allSelectableNodes[0] ?? null,
@@ -408,11 +477,7 @@ export class TreeApi<T> {
408
477
  .filter((n): n is NodeApi<T> => !!n && n.isSelectable);
409
478
  }
410
479
 
411
- setSelection(args: {
412
- ids: (IdObj | string)[] | null;
413
- anchor: Identity;
414
- mostRecent: Identity;
415
- }) {
480
+ setSelection(args: { ids: (IdObj | string)[] | null; anchor: Identity; mostRecent: Identity }) {
416
481
  const ids = new Set(args.ids?.map(identify));
417
482
  const anchor = identifyNull(args.anchor);
418
483
  const mostRecent = identifyNull(args.mostRecent);
@@ -437,9 +502,7 @@ export class TreeApi<T> {
437
502
  }
438
503
 
439
504
  get dragNodes() {
440
- return this.state.dnd.dragIds
441
- .map((id) => this.get(id))
442
- .filter((n) => !!n) as NodeApi<T>[];
505
+ return this.state.dnd.dragIds.map((id) => this.get(id)).filter((n) => !!n) as NodeApi<T>[];
443
506
  }
444
507
 
445
508
  get dragNode() {
@@ -493,19 +556,21 @@ export class TreeApi<T> {
493
556
 
494
557
  /* Visibility */
495
558
 
496
- open(identity: Identity) {
559
+ open(identity: Identity, redraw: boolean = true) {
497
560
  const id = identifyNull(identity);
498
561
  if (!id) return;
499
562
  if (this.isOpen(id)) return;
500
563
  this.dispatch(visibility.open(id, this.isFiltered));
564
+ if (redraw) this.redrawList(this.get(id)?.rowIndex ?? 0);
501
565
  safeRun(this.props.onToggle, id);
502
566
  }
503
567
 
504
- close(identity: Identity) {
568
+ close(identity: Identity, redraw: boolean = true) {
505
569
  const id = identifyNull(identity);
506
570
  if (!id) return;
507
571
  if (!this.isOpen(id)) return;
508
572
  this.dispatch(visibility.close(id, this.isFiltered));
573
+ if (redraw) this.redrawList(this.get(id)?.rowIndex ?? 0);
509
574
  safeRun(this.props.onToggle, id);
510
575
  }
511
576
 
@@ -522,9 +587,10 @@ export class TreeApi<T> {
522
587
  let parent = node?.parent;
523
588
 
524
589
  while (parent) {
525
- this.open(parent.id);
590
+ this.open(parent.id, false);
526
591
  parent = parent.parent;
527
592
  }
593
+ this.redrawList();
528
594
  }
529
595
 
530
596
  openSiblings(node: NodeApi<T>) {
@@ -535,23 +601,27 @@ export class TreeApi<T> {
535
601
  const isOpen = node.isOpen;
536
602
  for (let sibling of parent.children) {
537
603
  if (sibling.isInternal) {
538
- isOpen ? this.close(sibling.id) : this.open(sibling.id);
604
+ if (isOpen) this.close(sibling.id, false);
605
+ else this.open(sibling.id, false);
539
606
  }
540
607
  }
608
+ this.redrawList();
541
609
  this.scrollTo(this.focusedNode);
542
610
  }
543
611
  }
544
612
 
545
613
  openAll() {
546
614
  utils.walk(this.root, (node) => {
547
- if (node.isInternal) node.open();
615
+ if (node.isInternal) this.open(node.id, false);
548
616
  });
617
+ this.redrawList();
549
618
  }
550
619
 
551
620
  closeAll() {
552
621
  utils.walk(this.root, (node) => {
553
- if (node.isInternal) node.close();
622
+ if (node.isInternal) this.close(node.id, false);
554
623
  });
624
+ this.redrawList();
555
625
  }
556
626
 
557
627
  /* Scrolling */
@@ -626,10 +696,7 @@ export class TreeApi<T> {
626
696
  return this.isActionPossible(data, this.props.disableSelect);
627
697
  }
628
698
 
629
- private isActionPossible(
630
- data: T,
631
- disabler: string | boolean | BoolFunc<T> = () => false,
632
- ) {
699
+ private isActionPossible(data: T, disabler: string | boolean | BoolFunc<T> = () => false) {
633
700
  return !utils.access(data, disabler);
634
701
  }
635
702
 
@@ -30,7 +30,7 @@ export const actions = {
30
30
  /* Reducer */
31
31
  export function reducer(
32
32
  state: DndState = initialState()["dnd"],
33
- action: ActionTypes<typeof actions>
33
+ action: ActionTypes<typeof actions>,
34
34
  ): DndState {
35
35
  switch (action.type) {
36
36
  case "DND_CURSOR":
@@ -15,7 +15,7 @@ export type DragSlice = {
15
15
 
16
16
  export function reducer(
17
17
  state: DragSlice = initialState().nodes.drag,
18
- action: ActionTypes<typeof dnd>
18
+ action: ActionTypes<typeof dnd>,
19
19
  ): DragSlice {
20
20
  switch (action.type) {
21
21
  case "DND_DRAG_START":
@@ -29,10 +29,7 @@ export function reducer(
29
29
  selectedIds: [],
30
30
  };
31
31
  case "DND_HOVERING":
32
- if (
33
- action.parentId !== state.destinationParentId ||
34
- action.index != state.destinationIndex
35
- ) {
32
+ if (action.parentId !== state.destinationParentId || action.index != state.destinationIndex) {
36
33
  return {
37
34
  ...state,
38
35
  destinationParentId: action.parentId,
@@ -7,10 +7,7 @@ export function edit(id: string | null) {
7
7
  }
8
8
 
9
9
  /* Reducer */
10
- export function reducer(
11
- state: EditState = { id: null },
12
- action: ReturnType<typeof edit>
13
- ) {
10
+ export function reducer(state: EditState = { id: null }, action: ReturnType<typeof edit>) {
14
11
  if (action.type === "EDIT") {
15
12
  return { ...state, id: action.id };
16
13
  } else {
@@ -16,7 +16,7 @@ export function treeBlur() {
16
16
 
17
17
  export function reducer(
18
18
  state: FocusState = { id: null, treeFocused: false },
19
- action: ReturnType<typeof focus> | ReturnType<typeof treeBlur>
19
+ action: ReturnType<typeof focus> | ReturnType<typeof treeBlur>,
20
20
  ) {
21
21
  if (action.type === "FOCUS") {
22
22
  return { ...state, id: action.id, treeFocused: true };
@@ -22,10 +22,7 @@ export const actions = {
22
22
 
23
23
  /* Reducer */
24
24
 
25
- function openMapReducer(
26
- state: OpenMap = {},
27
- action: ActionTypes<typeof actions>
28
- ) {
25
+ function openMapReducer(state: OpenMap = {}, action: ActionTypes<typeof actions>) {
29
26
  if (action.type === "VISIBILITY_OPEN") {
30
27
  return { ...state, [action.id]: true };
31
28
  } else if (action.type === "VISIBILITY_CLOSE") {
@@ -42,7 +39,7 @@ function openMapReducer(
42
39
 
43
40
  export function reducer(
44
41
  state: OpenSlice = { filtered: {}, unfiltered: {} },
45
- action: ActionTypes<typeof actions>
42
+ action: ActionTypes<typeof actions>,
46
43
  ): OpenSlice {
47
44
  if (!action.type.startsWith("VISIBILITY")) return state;
48
45
  if (action.filtered) {
@@ -28,11 +28,7 @@ export const actions = {
28
28
  ids: (Array.isArray(id) ? id : [id]).map(identify),
29
29
  }),
30
30
 
31
- set: (args: {
32
- ids: Set<string>;
33
- anchor: string | null;
34
- mostRecent: string | null;
35
- }) => ({
31
+ set: (args: { ids: Set<string>; anchor: string | null; mostRecent: string | null }) => ({
36
32
  type: "SELECTION_SET" as const,
37
33
  ...args,
38
34
  }),
@@ -51,7 +47,7 @@ export const actions = {
51
47
  /* Reducer */
52
48
  export function reducer(
53
49
  state: SelectionState = initialState()["nodes"]["selection"],
54
- action: ActionTypes<typeof actions>
50
+ action: ActionTypes<typeof actions>,
55
51
  ): SelectionState {
56
52
  const ids = state.ids;
57
53
  switch (action.type) {
@@ -27,6 +27,4 @@ export type DeleteHandler<T> = (args: {
27
27
  nodes: NodeApi<T>[];
28
28
  }) => void | Promise<void>;
29
29
 
30
- export type EditResult =
31
- | { cancelled: true }
32
- | { cancelled: false; value: string };
30
+ export type EditResult = { cancelled: true } | { cancelled: false; value: string };
@@ -1,5 +1,4 @@
1
1
  import { CSSProperties, HTMLAttributes, ReactElement } from "react";
2
- import { IdObj } from "./utils";
3
2
  import { NodeApi } from "../interfaces/node-api";
4
3
  import { TreeApi } from "../interfaces/tree-api";
5
4
  import { XYCoord } from "react-dnd";
@@ -1,3 +1,3 @@
1
1
  import { NodeApi } from "../interfaces/node-api";
2
2
 
3
- export type NodeState = typeof NodeApi.prototype["state"];
3
+ export type NodeState = (typeof NodeApi.prototype)["state"];
@@ -7,6 +7,9 @@ import { NodeApi } from "../interfaces/node-api";
7
7
  import { OpenMap } from "../state/open-slice";
8
8
  import { useDragDropManager, DndProviderProps } from "react-dnd";
9
9
 
10
+ /** Returns the height in pixels for a given node's row. */
11
+ export type RowHeightAccessor<T> = (node: NodeApi<T>) => number;
12
+
10
13
  export interface TreeProps<T> {
11
14
  /* Data Options */
12
15
  data?: readonly T[];
@@ -26,7 +29,7 @@ export interface TreeProps<T> {
26
29
  renderContainer?: ElementType<{}>;
27
30
 
28
31
  /* Sizes */
29
- rowHeight?: number;
32
+ rowHeight?: number | RowHeightAccessor<T>;
30
33
  overscanCount?: number;
31
34
  width?: number | string;
32
35
  height?: number;
@@ -47,11 +50,7 @@ export interface TreeProps<T> {
47
50
  disableDrop?:
48
51
  | string
49
52
  | boolean
50
- | ((args: {
51
- parentNode: NodeApi<T>;
52
- dragNodes: NodeApi<T>[];
53
- index: number;
54
- }) => boolean);
53
+ | ((args: { parentNode: NodeApi<T>; dragNodes: NodeApi<T>[]; index: number }) => boolean);
55
54
 
56
55
  /* Event Handlers */
57
56
  onActivate?: (node: NodeApi<T>) => void;
@@ -77,10 +76,7 @@ export interface TreeProps<T> {
77
76
  dndRootElement?: globalThis.Node | null;
78
77
  onClick?: MouseEventHandler;
79
78
  onContextMenu?: MouseEventHandler;
80
- dndBackend?: Extract<
81
- DndProviderProps<unknown, unknown>,
82
- { backend: unknown }
83
- >["backend"];
79
+ dndBackend?: Extract<DndProviderProps<unknown, unknown>, { backend: unknown }>["backend"];
84
80
  dndManager?: ReturnType<typeof useDragDropManager>;
85
81
 
86
82
  /* Custom react-window outer/inner elements */