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.
Files changed (97) hide show
  1. package/README.md +14 -0
  2. package/dist/main/components/cursor.js +1 -2
  3. package/dist/main/components/default-container.js +31 -2
  4. package/dist/main/components/default-cursor.js +1 -1
  5. package/dist/main/components/default-drag-preview.d.ts +1 -1
  6. package/dist/main/components/default-drag-preview.js +1 -1
  7. package/dist/main/components/default-row.d.ts +1 -1
  8. package/dist/main/components/default-row.js +1 -1
  9. package/dist/main/components/list-outer-element.js +1 -1
  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.d.ts +2 -0
  16. package/dist/main/dnd/drag-hook.js +13 -4
  17. package/dist/main/dnd/drag-hook.test.d.ts +1 -0
  18. package/dist/main/dnd/drag-hook.test.js +19 -0
  19. package/dist/main/hooks/use-validated-props.js +1 -2
  20. package/dist/main/interfaces/node-api.js +4 -1
  21. package/dist/main/interfaces/tree-api.d.ts +27 -5
  22. package/dist/main/interfaces/tree-api.js +98 -14
  23. package/dist/main/interfaces/tree-api.test.js +31 -0
  24. package/dist/main/state/drag-slice.js +1 -2
  25. package/dist/main/types/dnd.d.ts +3 -1
  26. package/dist/main/types/state.d.ts +1 -1
  27. package/dist/main/types/tree-props.d.ts +4 -1
  28. package/dist/module/components/cursor.js +1 -2
  29. package/dist/module/components/default-container.js +32 -3
  30. package/dist/module/components/default-cursor.js +1 -1
  31. package/dist/module/components/default-drag-preview.d.ts +1 -1
  32. package/dist/module/components/default-drag-preview.js +1 -1
  33. package/dist/module/components/default-row.d.ts +1 -1
  34. package/dist/module/components/default-row.js +1 -1
  35. package/dist/module/components/list-outer-element.js +1 -1
  36. package/dist/module/components/provider.d.ts +1 -1
  37. package/dist/module/components/provider.js +4 -4
  38. package/dist/module/components/provider.test.js +71 -1
  39. package/dist/module/components/row-container.d.ts +1 -1
  40. package/dist/module/components/row-container.js +2 -3
  41. package/dist/module/dnd/compute-drop.js +1 -1
  42. package/dist/module/dnd/drag-hook.d.ts +2 -0
  43. package/dist/module/dnd/drag-hook.js +12 -4
  44. package/dist/module/dnd/drag-hook.test.d.ts +1 -0
  45. package/dist/module/dnd/drag-hook.test.js +17 -0
  46. package/dist/module/hooks/use-validated-props.js +1 -2
  47. package/dist/module/interfaces/node-api.js +4 -1
  48. package/dist/module/interfaces/tree-api.d.ts +27 -5
  49. package/dist/module/interfaces/tree-api.js +98 -14
  50. package/dist/module/interfaces/tree-api.test.js +31 -0
  51. package/dist/module/state/drag-slice.js +1 -2
  52. package/dist/module/types/dnd.d.ts +3 -1
  53. package/dist/module/types/state.d.ts +1 -1
  54. package/dist/module/types/tree-props.d.ts +4 -1
  55. package/package.json +27 -27
  56. package/src/components/cursor.tsx +1 -2
  57. package/src/components/default-container.tsx +40 -19
  58. package/src/components/default-cursor.tsx +1 -5
  59. package/src/components/default-drag-preview.tsx +3 -16
  60. package/src/components/default-node.tsx +0 -1
  61. package/src/components/default-row.tsx +2 -13
  62. package/src/components/drag-preview-container.tsx +1 -1
  63. package/src/components/list-inner-element.tsx +1 -1
  64. package/src/components/list-outer-element.tsx +2 -3
  65. package/src/components/provider.test.tsx +85 -9
  66. package/src/components/provider.tsx +8 -23
  67. package/src/components/row-container.tsx +4 -9
  68. package/src/components/tree.tsx +2 -6
  69. package/src/context.ts +2 -3
  70. package/src/data/create-index.ts +0 -1
  71. package/src/data/create-list.ts +1 -2
  72. package/src/data/create-root.ts +2 -9
  73. package/src/data/simple-tree.ts +5 -3
  74. package/src/dnd/compute-drop.ts +6 -15
  75. package/src/dnd/drag-hook.test.ts +22 -0
  76. package/src/dnd/drag-hook.ts +15 -6
  77. package/src/dnd/measure-hover.ts +2 -6
  78. package/src/dnd/outer-drop-hook.ts +1 -1
  79. package/src/hooks/use-fresh-node.ts +0 -1
  80. package/src/hooks/use-simple-tree.ts +2 -8
  81. package/src/hooks/use-validated-props.ts +4 -8
  82. package/src/interfaces/node-api.ts +2 -2
  83. package/src/interfaces/tree-api.test.ts +35 -0
  84. package/src/interfaces/tree-api.ts +103 -36
  85. package/src/state/dnd-slice.ts +1 -1
  86. package/src/state/drag-slice.ts +2 -5
  87. package/src/state/edit-slice.ts +1 -4
  88. package/src/state/focus-slice.ts +1 -1
  89. package/src/state/open-slice.ts +2 -5
  90. package/src/state/selection-slice.ts +2 -6
  91. package/src/types/dnd.ts +6 -1
  92. package/src/types/handlers.ts +1 -3
  93. package/src/types/renderers.ts +0 -1
  94. package/src/types/state.ts +1 -1
  95. package/src/types/tree-props.ts +15 -10
  96. package/src/types/utils.ts +2 -3
  97. package/src/utils.ts +5 -14
@@ -3,8 +3,7 @@ export function useValidatedProps(props) {
3
3
  if (props.initialData && props.data) {
4
4
  throw new Error(`React Arborist Tree => Provide either a data or initialData prop, but not both.`);
5
5
  }
6
- if (props.initialData &&
7
- (props.onCreate || props.onDelete || props.onMove || props.onRename)) {
6
+ if (props.initialData && (props.onCreate || props.onDelete || props.onMove || props.onRename)) {
8
7
  throw new Error(`React Arborist Tree => You passed the initialData prop along with a data handler.
9
8
  Use the data prop if you want to provide your own handlers.`);
10
9
  }
@@ -3,7 +3,10 @@ export class NodeApi {
3
3
  constructor(params) {
4
4
  this.handleClick = (e) => {
5
5
  if (e.metaKey && !this.tree.props.disableMultiSelection) {
6
- this.isSelected ? this.deselect() : this.selectMulti();
6
+ if (this.isSelected)
7
+ this.deselect();
8
+ else
9
+ this.selectMulti();
7
10
  }
8
11
  else if (e.shiftKey && !this.tree.props.disableMultiSelection) {
9
12
  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;
@@ -30,6 +30,46 @@ export class TreeApi {
30
30
  this.listEl = listEl;
31
31
  this.visibleStartIndex = 0;
32
32
  this.visibleStopIndex = 0;
33
+ /* Memoized prefix-sum of row heights; only used for variable heights. */
34
+ this.rowOffsets = null;
35
+ /**
36
+ * The height of the row at `index`, evaluating the `rowHeight` function if
37
+ * given. Falls back to the default height for an out-of-range index so this
38
+ * never feeds an invalid `0` to react-window's `itemSize`.
39
+ */
40
+ this.rowHeightAt = (index) => {
41
+ const rowHeight = this.props.rowHeight;
42
+ if (typeof rowHeight === "function") {
43
+ const node = this.at(index);
44
+ return node ? rowHeight(node) : this.rowHeight;
45
+ }
46
+ return rowHeight !== null && rowHeight !== void 0 ? rowHeight : 24;
47
+ };
48
+ /** The pixel offset of the top of the row at `index` from the top of the list. */
49
+ this.rowTopPosition = (index) => {
50
+ /* Fixed heights: O(1). */
51
+ if (typeof this.props.rowHeight !== "function") {
52
+ return index * this.rowHeight;
53
+ }
54
+ /* Variable heights: O(1) amortized via a memoized prefix sum. */
55
+ const offsets = this.getRowOffsets();
56
+ const clamped = Math.max(0, Math.min(index, offsets.length - 1));
57
+ return offsets[clamped];
58
+ };
59
+ /**
60
+ * Tell the underlying virtualized list to recompute row heights at and after
61
+ * `index`. Call this if a `rowHeight` function's output changes for reasons
62
+ * the tree can't observe (e.g. external state).
63
+ */
64
+ this.redrawList = (afterIndex = 0) => {
65
+ this.rowOffsets = null;
66
+ /* Only the VariableSizeList (function rowHeight) caches measurements; a
67
+ FixedSizeList has constant heights and nothing to recompute. */
68
+ const list = this.list.current;
69
+ if (list && "resetAfterIndex" in list) {
70
+ list.resetAfterIndex(Math.max(0, afterIndex));
71
+ }
72
+ };
33
73
  /* Changes here must also be made in update() */
34
74
  this.root = createRoot(this);
35
75
  this.visibleNodes = createList(this);
@@ -41,6 +81,18 @@ export class TreeApi {
41
81
  this.root = createRoot(this);
42
82
  this.visibleNodes = createList(this);
43
83
  this.idToIndex = createIndex(this.visibleNodes);
84
+ this.rowOffsets = null;
85
+ /* Variable-height mode renders a VariableSizeList, which caches item
86
+ measurements by index and never invalidates them on its own. When the
87
+ visible nodes change (insert/remove/reorder), those cached sizes belong
88
+ to the wrong rows, so drop them. Fixed-height mode renders a
89
+ FixedSizeList (no cache, nothing to reset). update() runs during render,
90
+ so pass shouldForceUpdate=false: the in-progress render repaints the list
91
+ and a forceUpdate here would warn about setting state mid-render. */
92
+ const list = this.list.current;
93
+ if (list && "resetAfterIndex" in list) {
94
+ list.resetAfterIndex(0, false);
95
+ }
44
96
  }
45
97
  /* Store helpers */
46
98
  dispatch(action) {
@@ -65,9 +117,24 @@ export class TreeApi {
65
117
  var _a;
66
118
  return (_a = this.props.indent) !== null && _a !== void 0 ? _a : 24;
67
119
  }
120
+ /**
121
+ * The fixed row height. When a `rowHeight` function is supplied for variable
122
+ * heights, this returns the default (24); use `rowHeightAt(index)` to get the
123
+ * height of a specific row.
124
+ */
68
125
  get rowHeight() {
69
- var _a;
70
- return (_a = this.props.rowHeight) !== null && _a !== void 0 ? _a : 24;
126
+ return typeof this.props.rowHeight === "number" ? this.props.rowHeight : 24;
127
+ }
128
+ /** Lazily-built prefix sum where offsets[i] is the top of row i. */
129
+ getRowOffsets() {
130
+ if (this.rowOffsets)
131
+ return this.rowOffsets;
132
+ const offsets = [0];
133
+ for (let i = 0; i < this.visibleNodes.length; i++) {
134
+ offsets.push(offsets[i] + this.rowHeightAt(i));
135
+ }
136
+ this.rowOffsets = offsets;
137
+ return offsets;
71
138
  }
72
139
  get overscanCount() {
73
140
  var _a;
@@ -169,9 +236,7 @@ export class TreeApi {
169
236
  create() {
170
237
  return __awaiter(this, arguments, void 0, function* (opts = {}) {
171
238
  var _a, _b;
172
- const parentId = opts.parentId === undefined
173
- ? utils.getInsertParentId(this)
174
- : opts.parentId;
239
+ const parentId = opts.parentId === undefined ? utils.getInsertParentId(this) : opts.parentId;
175
240
  const index = (_a = opts.index) !== null && _a !== void 0 ? _a : utils.getInsertIndex(this);
176
241
  const type = (_b = opts.type) !== null && _b !== void 0 ? _b : "leaf";
177
242
  const data = yield safeRun(this.props.onCreate, {
@@ -198,20 +263,26 @@ export class TreeApi {
198
263
  const idents = Array.isArray(node) ? node : [node];
199
264
  const ids = idents.map(identify);
200
265
  const nodes = ids.map((id) => this.get(id)).filter((n) => !!n);
266
+ /* Guard against Math.min(...[]) === Infinity when no ids resolve to nodes. */
267
+ const fromIndex = nodes.length ? Math.min(...nodes.map((n) => { var _a; return (_a = n.rowIndex) !== null && _a !== void 0 ? _a : 0; })) : 0;
201
268
  yield safeRun(this.props.onDelete, { nodes, ids });
269
+ this.redrawList(fromIndex);
202
270
  });
203
271
  }
204
272
  edit(node) {
273
+ var _a, _b;
205
274
  const id = identify(node);
206
275
  this.resolveEdit({ cancelled: true });
207
276
  this.scrollTo(id);
208
277
  this.dispatch(edit(id));
278
+ this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
209
279
  return new Promise((resolve) => {
210
280
  TreeApi.editPromise = resolve;
211
281
  });
212
282
  }
213
283
  submit(identity, value) {
214
284
  return __awaiter(this, void 0, void 0, function* () {
285
+ var _a, _b;
215
286
  if (!identity)
216
287
  return;
217
288
  const id = identify(identity);
@@ -222,12 +293,14 @@ export class TreeApi {
222
293
  });
223
294
  this.dispatch(edit(null));
224
295
  this.resolveEdit({ cancelled: false, value });
296
+ this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
225
297
  setTimeout(() => this.onFocus()); // Return focus to element;
226
298
  });
227
299
  }
228
300
  reset() {
229
301
  this.dispatch(edit(null));
230
302
  this.resolveEdit({ cancelled: true });
303
+ this.redrawList();
231
304
  setTimeout(() => this.onFocus()); // Return focus to element;
232
305
  }
233
306
  activate(id) {
@@ -407,9 +480,7 @@ export class TreeApi {
407
480
  return this.state.dnd.cursor.type === "highlight";
408
481
  }
409
482
  get dragNodes() {
410
- return this.state.dnd.dragIds
411
- .map((id) => this.get(id))
412
- .filter((n) => !!n);
483
+ return this.state.dnd.dragIds.map((id) => this.get(id)).filter((n) => !!n);
413
484
  }
414
485
  get dragNode() {
415
486
  return this.get(this.state.nodes.drag.id);
@@ -461,22 +532,28 @@ export class TreeApi {
461
532
  this.dispatch(dnd.cursor(cursor));
462
533
  }
463
534
  /* Visibility */
464
- open(identity) {
535
+ open(identity, redraw = true) {
536
+ var _a, _b;
465
537
  const id = identifyNull(identity);
466
538
  if (!id)
467
539
  return;
468
540
  if (this.isOpen(id))
469
541
  return;
470
542
  this.dispatch(visibility.open(id, this.isFiltered));
543
+ if (redraw)
544
+ this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
471
545
  safeRun(this.props.onToggle, id);
472
546
  }
473
- close(identity) {
547
+ close(identity, redraw = true) {
548
+ var _a, _b;
474
549
  const id = identifyNull(identity);
475
550
  if (!id)
476
551
  return;
477
552
  if (!this.isOpen(id))
478
553
  return;
479
554
  this.dispatch(visibility.close(id, this.isFiltered));
555
+ if (redraw)
556
+ this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
480
557
  safeRun(this.props.onToggle, id);
481
558
  }
482
559
  toggle(identity) {
@@ -492,9 +569,10 @@ export class TreeApi {
492
569
  const node = utils.dfs(this.root, id);
493
570
  let parent = node === null || node === void 0 ? void 0 : node.parent;
494
571
  while (parent) {
495
- this.open(parent.id);
572
+ this.open(parent.id, false);
496
573
  parent = parent.parent;
497
574
  }
575
+ this.redrawList();
498
576
  }
499
577
  openSiblings(node) {
500
578
  const parent = node.parent;
@@ -505,23 +583,29 @@ export class TreeApi {
505
583
  const isOpen = node.isOpen;
506
584
  for (let sibling of parent.children) {
507
585
  if (sibling.isInternal) {
508
- isOpen ? this.close(sibling.id) : this.open(sibling.id);
586
+ if (isOpen)
587
+ this.close(sibling.id, false);
588
+ else
589
+ this.open(sibling.id, false);
509
590
  }
510
591
  }
592
+ this.redrawList();
511
593
  this.scrollTo(this.focusedNode);
512
594
  }
513
595
  }
514
596
  openAll() {
515
597
  utils.walk(this.root, (node) => {
516
598
  if (node.isInternal)
517
- node.open();
599
+ this.open(node.id, false);
518
600
  });
601
+ this.redrawList();
519
602
  }
520
603
  closeAll() {
521
604
  utils.walk(this.root, (node) => {
522
605
  if (node.isInternal)
523
- node.close();
606
+ this.close(node.id, false);
524
607
  });
608
+ this.redrawList();
525
609
  }
526
610
  /* Scrolling */
527
611
  scrollTo(identity, align = "smart") {
@@ -10,3 +10,34 @@ test("tree.canDrop()", () => {
10
10
  expect(setupApi({ disableDrop: () => false }).canDrop()).toBe(true);
11
11
  expect(setupApi({ disableDrop: false }).canDrop()).toBe(true);
12
12
  });
13
+ const rowData = [{ id: "a" }, { id: "b" }, { id: "c" }];
14
+ test("rowHeight defaults to 24", () => {
15
+ const api = setupApi({});
16
+ expect(api.rowHeight).toBe(24);
17
+ expect(api.rowHeightAt(0)).toBe(24);
18
+ });
19
+ test("fixed numeric rowHeight", () => {
20
+ const api = setupApi({ data: rowData, rowHeight: 30 });
21
+ expect(api.rowHeight).toBe(30);
22
+ expect(api.rowHeightAt(0)).toBe(30);
23
+ expect(api.rowTopPosition(0)).toBe(0);
24
+ expect(api.rowTopPosition(2)).toBe(60);
25
+ expect(api.rowTopPosition(3)).toBe(90); // total list height
26
+ });
27
+ test("variable rowHeight function", () => {
28
+ const heights = { a: 10, b: 20, c: 40 };
29
+ const api = setupApi({
30
+ data: rowData,
31
+ rowHeight: (node) => heights[node.id],
32
+ });
33
+ // The back-compat getter falls back to the default for variable heights.
34
+ expect(api.rowHeight).toBe(24);
35
+ expect(api.rowHeightAt(0)).toBe(10);
36
+ expect(api.rowHeightAt(1)).toBe(20);
37
+ expect(api.rowTopPosition(0)).toBe(0);
38
+ expect(api.rowTopPosition(1)).toBe(10);
39
+ expect(api.rowTopPosition(2)).toBe(30);
40
+ expect(api.rowTopPosition(3)).toBe(70); // total list height
41
+ // Out-of-range index falls back to the default height, never an invalid 0.
42
+ expect(api.rowHeightAt(99)).toBe(24);
43
+ });
@@ -7,8 +7,7 @@ export function reducer(state = initialState().nodes.drag, action) {
7
7
  case "DND_DRAG_END":
8
8
  return Object.assign(Object.assign({}, state), { id: null, destinationParentId: null, destinationIndex: null, selectedIds: [] });
9
9
  case "DND_HOVERING":
10
- if (action.parentId !== state.destinationParentId ||
11
- action.index != state.destinationIndex) {
10
+ if (action.parentId !== state.destinationParentId || action.index != state.destinationIndex) {
12
11
  return Object.assign(Object.assign({}, state), { destinationParentId: action.parentId, destinationIndex: action.index });
13
12
  }
14
13
  else {
@@ -3,6 +3,8 @@ export type CursorLocation = {
3
3
  level: number | null;
4
4
  parentId: string | null;
5
5
  };
6
- export type DragItem = {
6
+ export type DragItem<T = any> = {
7
7
  id: string;
8
+ dragIds: string[];
9
+ data: T;
8
10
  };
@@ -1,2 +1,2 @@
1
1
  import { NodeApi } from "../interfaces/node-api";
2
- export type NodeState = typeof NodeApi.prototype["state"];
2
+ export type NodeState = (typeof NodeApi.prototype)["state"];
@@ -6,6 +6,8 @@ import { ListOnScrollProps, CommonProps as ReactWindowCommonProps } from "react-
6
6
  import { NodeApi } from "../interfaces/node-api";
7
7
  import { OpenMap } from "../state/open-slice";
8
8
  import { useDragDropManager, DndProviderProps } from "react-dnd";
9
+ /** Returns the height in pixels for a given node's row. */
10
+ export type RowHeightAccessor<T> = (node: NodeApi<T>) => number;
9
11
  export interface TreeProps<T> {
10
12
  data?: readonly T[];
11
13
  initialData?: readonly T[];
@@ -18,7 +20,7 @@ export interface TreeProps<T> {
18
20
  renderDragPreview?: ElementType<renderers.DragPreviewProps>;
19
21
  renderCursor?: ElementType<renderers.CursorProps>;
20
22
  renderContainer?: ElementType<{}>;
21
- rowHeight?: number;
23
+ rowHeight?: number | RowHeightAccessor<T>;
22
24
  overscanCount?: number;
23
25
  width?: number | string;
24
26
  height?: number;
@@ -57,6 +59,7 @@ export interface TreeProps<T> {
57
59
  backend: unknown;
58
60
  }>["backend"];
59
61
  dndManager?: ReturnType<typeof useDragDropManager>;
62
+ dragType?: string | ((node: NodeApi<T>) => string);
60
63
  outerElementType?: ReactWindowCommonProps["outerElementType"];
61
64
  innerElementType?: ReactWindowCommonProps["innerElementType"];
62
65
  }
package/package.json CHANGED
@@ -1,12 +1,33 @@
1
1
  {
2
2
  "name": "react-arborist",
3
- "version": "3.7.0",
3
+ "version": "3.9.0",
4
+ "keywords": [
5
+ "arborist",
6
+ "dnd",
7
+ "filterable",
8
+ "multiselection",
9
+ "react",
10
+ "react-arborist",
11
+ "tree",
12
+ "treeview",
13
+ "virtualized"
14
+ ],
15
+ "homepage": "https://react-arborist.netlify.app",
16
+ "bugs": "https://github.com/jameskerr/react-arborist/issues",
4
17
  "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/jameskerr/react-arborist.git"
21
+ },
5
22
  "source": "src/index.ts",
23
+ "files": [
24
+ "src",
25
+ "dist"
26
+ ],
27
+ "sideEffects": false,
6
28
  "main": "dist/main/index.js",
7
29
  "module": "dist/module/index.js",
8
30
  "types": "dist/module/index.d.ts",
9
- "sideEffects": false,
10
31
  "scripts": {
11
32
  "build:cjs": "tsc --outDir dist/main",
12
33
  "build:es": "tsc --outDir dist/module --module es2022 --moduleResolution node",
@@ -16,27 +37,6 @@
16
37
  "test": "jest",
17
38
  "watch": "yarn build:es --watch"
18
39
  },
19
- "files": [
20
- "src",
21
- "dist"
22
- ],
23
- "repository": {
24
- "type": "git",
25
- "url": "https://github.com/jameskerr/react-arborist.git"
26
- },
27
- "homepage": "https://react-arborist.netlify.app",
28
- "bugs": "https://github.com/jameskerr/react-arborist/issues",
29
- "keywords": [
30
- "react",
31
- "arborist",
32
- "react-arborist",
33
- "treeview",
34
- "tree",
35
- "vitualized",
36
- "dnd",
37
- "multiselection",
38
- "filterable"
39
- ],
40
40
  "dependencies": {
41
41
  "react-dnd": "^14.0.3",
42
42
  "react-dnd-html5-backend": "^14.0.3",
@@ -44,10 +44,6 @@
44
44
  "redux": "^5.0.0",
45
45
  "use-sync-external-store": "^1.2.0"
46
46
  },
47
- "peerDependencies": {
48
- "react": ">= 16.14",
49
- "react-dom": ">= 16.14"
50
- },
51
47
  "devDependencies": {
52
48
  "@testing-library/dom": "^9.3.0",
53
49
  "@testing-library/react": "^14.0.0",
@@ -64,5 +60,9 @@
64
60
  "rimraf": "^5.0.5",
65
61
  "ts-jest": "^29.1.1",
66
62
  "typescript": "^5.6.0"
63
+ },
64
+ "peerDependencies": {
65
+ "react": ">= 16.14",
66
+ "react-dom": ">= 16.14"
67
67
  }
68
68
  }
@@ -7,8 +7,7 @@ export function Cursor() {
7
7
  if (!cursor || cursor.type !== "line") return null;
8
8
  const indent = tree.indent;
9
9
  const top =
10
- tree.rowHeight * cursor.index +
11
- (tree.props.padding ?? tree.props.paddingTop ?? 0);
10
+ tree.rowTopPosition(cursor.index) + (tree.props.padding ?? tree.props.paddingTop ?? 0);
12
11
  const left = indent * cursor.level;
13
12
  const Cursor = tree.renderCursor;
14
13
  return <Cursor {...{ top, left, indent }} />;
@@ -1,4 +1,4 @@
1
- import { FixedSizeList } from "react-window";
1
+ import { FixedSizeList, VariableSizeList } from "react-window";
2
2
  import { useDataUpdates, useTreeApi } from "../context";
3
3
  import { focusNextElement, focusPrevElement } from "../utils";
4
4
  import { ListOuterElement } from "./list-outer-element";
@@ -216,24 +216,45 @@ export function DefaultContainer() {
216
216
  if (node) tree.focus(node.id);
217
217
  }}
218
218
  >
219
- {/* @ts-ignore */}
220
- <FixedSizeList
221
- className={tree.props.className}
222
- outerRef={tree.listEl}
223
- itemCount={tree.visibleNodes.length}
224
- height={tree.height}
225
- width={tree.width}
226
- itemSize={tree.rowHeight}
227
- overscanCount={tree.overscanCount}
228
- itemKey={(index) => tree.visibleNodes[index]?.id || index}
229
- outerElementType={tree.props.outerElementType ?? ListOuterElement}
230
- innerElementType={tree.props.innerElementType ?? ListInnerElement}
231
- onScroll={tree.props.onScroll}
232
- onItemsRendered={tree.onItemsRendered.bind(tree)}
233
- ref={tree.list}
234
- >
235
- {RowContainer}
236
- </FixedSizeList>
219
+ <List />
237
220
  </div>
238
221
  );
239
222
  }
223
+
224
+ /**
225
+ * Fixed-height trees (numeric rowHeight) render a FixedSizeList, preserving the
226
+ * original O(1) layout and avoiding VariableSizeList's measurement cache. Only
227
+ * the function form, which needs per-row heights, uses VariableSizeList.
228
+ */
229
+ function List() {
230
+ const tree = useTreeApi();
231
+ const commonProps = {
232
+ className: tree.props.className,
233
+ outerRef: tree.listEl,
234
+ itemCount: tree.visibleNodes.length,
235
+ height: tree.height,
236
+ width: tree.width,
237
+ overscanCount: tree.overscanCount,
238
+ itemKey: (index: number) => tree.visibleNodes[index]?.id || index,
239
+ outerElementType: tree.props.outerElementType ?? ListOuterElement,
240
+ innerElementType: tree.props.innerElementType ?? ListInnerElement,
241
+ onScroll: tree.props.onScroll,
242
+ onItemsRendered: tree.onItemsRendered.bind(tree),
243
+ };
244
+
245
+ if (typeof tree.props.rowHeight === "function") {
246
+ return (
247
+ // @ts-ignore
248
+ <VariableSizeList {...commonProps} itemSize={tree.rowHeightAt} ref={tree.list}>
249
+ {RowContainer}
250
+ </VariableSizeList>
251
+ );
252
+ }
253
+
254
+ return (
255
+ // @ts-ignore
256
+ <FixedSizeList {...commonProps} itemSize={tree.rowHeight} ref={tree.list}>
257
+ {RowContainer}
258
+ </FixedSizeList>
259
+ );
260
+ }
@@ -21,11 +21,7 @@ const circleStyle = {
21
21
  borderRadius: "50%",
22
22
  };
23
23
 
24
- export const DefaultCursor = React.memo(function DefaultCursor({
25
- top,
26
- left,
27
- indent,
28
- }: CursorProps) {
24
+ export const DefaultCursor = React.memo(function DefaultCursor({ top, left, indent }: CursorProps) {
29
25
  const style: CSSProperties = {
30
26
  position: "absolute",
31
27
  pointerEvents: "none",
@@ -2,7 +2,6 @@ import React, { CSSProperties, memo } from "react";
2
2
  import { XYCoord } from "react-dnd";
3
3
  import { useTreeApi } from "../context";
4
4
  import { DragPreviewProps } from "../types/renderers";
5
- import { IdObj } from "../types/utils";
6
5
 
7
6
  const layerStyles: CSSProperties = {
8
7
  position: "fixed",
@@ -26,13 +25,7 @@ const getCountStyle = (offset: XYCoord | null) => {
26
25
  return { transform: `translate(${x + 10}px, ${y + 10}px)` };
27
26
  };
28
27
 
29
- export function DefaultDragPreview({
30
- offset,
31
- mouse,
32
- id,
33
- dragIds,
34
- isDragging,
35
- }: DragPreviewProps) {
28
+ export function DefaultDragPreview({ offset, mouse, id, dragIds, isDragging }: DragPreviewProps) {
36
29
  return (
37
30
  <Overlay isDragging={isDragging}>
38
31
  <Position offset={offset}>
@@ -43,10 +36,7 @@ export function DefaultDragPreview({
43
36
  );
44
37
  }
45
38
 
46
- const Overlay = memo(function Overlay(props: {
47
- children: JSX.Element[];
48
- isDragging: boolean;
49
- }) {
39
+ const Overlay = memo(function Overlay(props: { children: JSX.Element[]; isDragging: boolean }) {
50
40
  if (!props.isDragging) return null;
51
41
  return <div style={layerStyles}>{props.children}</div>;
52
42
  });
@@ -70,10 +60,7 @@ function Count(props: { count: number; mouse: XYCoord | null }) {
70
60
  else return null;
71
61
  }
72
62
 
73
- const PreviewNode = memo(function PreviewNode<T>(props: {
74
- id: string | null;
75
- dragIds: string[];
76
- }) {
63
+ const PreviewNode = memo(function PreviewNode<T>(props: { id: string | null; dragIds: string[] }) {
77
64
  const tree = useTreeApi<T>();
78
65
  const node = tree.get(props.id);
79
66
  if (!node) return null;
@@ -1,6 +1,5 @@
1
1
  import React, { useEffect, useRef } from "react";
2
2
  import { NodeRendererProps } from "../types/renderers";
3
- import { IdObj } from "../types/utils";
4
3
 
5
4
  export function DefaultNode<T>(props: NodeRendererProps<T>) {
6
5
  return (
@@ -1,20 +1,9 @@
1
1
  import React from "react";
2
2
  import { RowRendererProps } from "../types/renderers";
3
- import { IdObj } from "../types/utils";
4
3
 
5
- export function DefaultRow<T>({
6
- node,
7
- attrs,
8
- innerRef,
9
- children,
10
- }: RowRendererProps<T>) {
4
+ export function DefaultRow<T>({ node, attrs, innerRef, children }: RowRendererProps<T>) {
11
5
  return (
12
- <div
13
- {...attrs}
14
- ref={innerRef}
15
- onFocus={(e) => e.stopPropagation()}
16
- onClick={node.handleClick}
17
- >
6
+ <div {...attrs} ref={innerRef} onFocus={(e) => e.stopPropagation()} onClick={node.handleClick}>
18
7
  {children}
19
8
  </div>
20
9
  );
@@ -1,5 +1,5 @@
1
1
  import { useDragLayer } from "react-dnd";
2
- import { useDndContext, useTreeApi } from "../context";
2
+ import { useTreeApi } from "../context";
3
3
  import { DefaultDragPreview } from "./default-drag-preview";
4
4
 
5
5
  export function DragPreviewContainer() {