react-arborist 3.5.0 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/main/components/provider.js +8 -1
- package/dist/main/components/provider.test.d.ts +1 -0
- package/dist/main/components/provider.test.js +33 -0
- package/dist/main/interfaces/node-api.d.ts +1 -0
- package/dist/main/interfaces/node-api.js +3 -0
- package/dist/main/interfaces/tree-api.d.ts +10 -7
- package/dist/main/interfaces/tree-api.js +40 -20
- package/dist/main/types/tree-props.d.ts +1 -0
- package/dist/module/components/provider.js +8 -1
- package/dist/module/components/provider.test.d.ts +1 -0
- package/dist/module/components/provider.test.js +31 -0
- package/dist/module/interfaces/node-api.d.ts +1 -0
- package/dist/module/interfaces/node-api.js +3 -0
- package/dist/module/interfaces/tree-api.d.ts +10 -7
- package/dist/module/interfaces/tree-api.js +40 -20
- package/dist/module/types/tree-props.d.ts +1 -0
- package/package.json +7 -1
- package/src/components/provider.test.tsx +44 -0
- package/src/components/provider.tsx +9 -1
- package/src/interfaces/node-api.ts +4 -0
- package/src/interfaces/tree-api.ts +54 -26
- package/src/types/tree-props.ts +1 -0
package/README.md
CHANGED
|
@@ -310,6 +310,7 @@ interface TreeProps<T> {
|
|
|
310
310
|
openByDefault?: boolean;
|
|
311
311
|
selectionFollowsFocus?: boolean;
|
|
312
312
|
disableMultiSelection?: boolean;
|
|
313
|
+
disableSelect?: string | boolean | BoolFunc<T>;
|
|
313
314
|
disableEdit?: string | boolean | BoolFunc<T>;
|
|
314
315
|
disableDrag?: string | boolean | BoolFunc<T>;
|
|
315
316
|
disableDrop?:
|
|
@@ -29,7 +29,14 @@ 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
|
+
/* Rebuild visible nodes when open state changes, without clobbering
|
|
34
|
+
props set imperatively via api.update(). Bumping updateCount keeps
|
|
35
|
+
DataUpdates consumers (e.g. DefaultContainer) in sync. */
|
|
36
|
+
(0, react_1.useMemo)(() => {
|
|
37
|
+
updateCount.current += 1;
|
|
38
|
+
api.update(api.props);
|
|
39
|
+
}, [state.nodes.open]);
|
|
33
40
|
/* Expose the tree api */
|
|
34
41
|
(0, react_1.useImperativeHandle)(imperativeHandle, () => api);
|
|
35
42
|
/* Change selection based on props */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const react_2 = require("@testing-library/react");
|
|
6
|
+
const tree_1 = require("./tree");
|
|
7
|
+
const data = [
|
|
8
|
+
{
|
|
9
|
+
id: "1",
|
|
10
|
+
name: "root",
|
|
11
|
+
children: [
|
|
12
|
+
{ id: "2", name: "a" },
|
|
13
|
+
{ id: "3", name: "b", children: [{ id: "4", name: "c" }] },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
test("imperative tree.update() props survive node toggles (#228)", () => {
|
|
18
|
+
const ref = (0, react_1.createRef)();
|
|
19
|
+
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, ref: ref, rowHeight: 24, openByDefault: false }));
|
|
20
|
+
const api = ref.current;
|
|
21
|
+
expect(api.rowHeight).toBe(24);
|
|
22
|
+
(0, react_2.act)(() => {
|
|
23
|
+
api.update(Object.assign(Object.assign({}, api.props), { rowHeight: 48 }));
|
|
24
|
+
});
|
|
25
|
+
expect(api.rowHeight).toBe(48);
|
|
26
|
+
/* Opening a node dispatches a redux action that changes state.nodes.open.
|
|
27
|
+
Before #337, the open-state effect re-ran api.update(treeProps), reverting
|
|
28
|
+
rowHeight to 24. */
|
|
29
|
+
(0, react_2.act)(() => {
|
|
30
|
+
api.open("1");
|
|
31
|
+
});
|
|
32
|
+
expect(api.rowHeight).toBe(48);
|
|
33
|
+
});
|
|
@@ -136,7 +136,7 @@ export declare class TreeApi<T> {
|
|
|
136
136
|
get(id: string | null): NodeApi<T> | null;
|
|
137
137
|
at(index: number): NodeApi<T> | null;
|
|
138
138
|
nodesBetween(startId: string | null, endId: string | null): NodeApi<T>[];
|
|
139
|
-
indexOf(id:
|
|
139
|
+
indexOf(id: Identity): number | null;
|
|
140
140
|
get editingId(): string | null;
|
|
141
141
|
createInternal(): Promise<void>;
|
|
142
142
|
createLeaf(): Promise<void>;
|
|
@@ -145,11 +145,11 @@ export declare class TreeApi<T> {
|
|
|
145
145
|
parentId?: null | string;
|
|
146
146
|
index?: null | number;
|
|
147
147
|
}): Promise<void>;
|
|
148
|
-
delete(node:
|
|
148
|
+
delete(node: Identity | string[] | IdObj[]): Promise<void>;
|
|
149
149
|
edit(node: string | IdObj): Promise<EditResult>;
|
|
150
150
|
submit(identity: Identity, value: string): Promise<void>;
|
|
151
151
|
reset(): void;
|
|
152
|
-
activate(id:
|
|
152
|
+
activate(id: Identity): void;
|
|
153
153
|
private resolveEdit;
|
|
154
154
|
get selectedIds(): Set<string>;
|
|
155
155
|
get selectedNodes(): NodeApi<T>[];
|
|
@@ -170,10 +170,11 @@ export declare class TreeApi<T> {
|
|
|
170
170
|
selectContiguous(identity: Identity): void;
|
|
171
171
|
deselectAll(): void;
|
|
172
172
|
selectAll(): void;
|
|
173
|
+
private filterSelectableNodes;
|
|
173
174
|
setSelection(args: {
|
|
174
175
|
ids: (IdObj | string)[] | null;
|
|
175
|
-
anchor:
|
|
176
|
-
mostRecent:
|
|
176
|
+
anchor: Identity;
|
|
177
|
+
mostRecent: Identity;
|
|
177
178
|
}): void;
|
|
178
179
|
get cursorParentId(): string | null;
|
|
179
180
|
get cursorOverFolder(): boolean;
|
|
@@ -202,10 +203,12 @@ export declare class TreeApi<T> {
|
|
|
202
203
|
isOpen(id?: string): boolean;
|
|
203
204
|
isEditable(data: T): boolean;
|
|
204
205
|
isDraggable(data: T): boolean;
|
|
205
|
-
|
|
206
|
+
isSelectable(data: T): boolean;
|
|
207
|
+
private isActionPossible;
|
|
208
|
+
isDragging(node: Identity): boolean;
|
|
206
209
|
isFocused(id: string): boolean;
|
|
207
210
|
isMatch(node: NodeApi<T>): boolean;
|
|
208
|
-
willReceiveDrop(node:
|
|
211
|
+
willReceiveDrop(node: Identity): boolean;
|
|
209
212
|
onFocus(): void;
|
|
210
213
|
onBlur(): void;
|
|
211
214
|
onItemsRendered(args: ListOnItemsRenderedProps): void;
|
|
@@ -327,20 +327,24 @@ class TreeApi {
|
|
|
327
327
|
this.focus(this.at(index));
|
|
328
328
|
}
|
|
329
329
|
select(node, opts = {}) {
|
|
330
|
+
var _a;
|
|
330
331
|
if (!node)
|
|
331
332
|
return;
|
|
332
333
|
const changeFocus = opts.focus !== false;
|
|
333
334
|
const id = identify(node);
|
|
334
335
|
if (changeFocus)
|
|
335
336
|
this.dispatch((0, focus_slice_1.focus)(id));
|
|
336
|
-
this.
|
|
337
|
-
|
|
338
|
-
|
|
337
|
+
if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
|
|
338
|
+
this.setSelection({
|
|
339
|
+
ids: [id],
|
|
340
|
+
anchor: id,
|
|
341
|
+
mostRecent: id,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
339
344
|
this.scrollTo(id, opts.align);
|
|
340
345
|
if (this.focusedNode && changeFocus) {
|
|
341
346
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
342
347
|
}
|
|
343
|
-
safeRun(this.props.onSelect, this.selectedNodes);
|
|
344
348
|
}
|
|
345
349
|
deselect(node) {
|
|
346
350
|
if (!node)
|
|
@@ -356,9 +360,11 @@ class TreeApi {
|
|
|
356
360
|
const changeFocus = opts.focus !== false;
|
|
357
361
|
if (changeFocus)
|
|
358
362
|
this.dispatch((0, focus_slice_1.focus)(node.id));
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
363
|
+
if (node.isSelectable) {
|
|
364
|
+
this.dispatch(selection_slice_1.actions.add(node.id));
|
|
365
|
+
this.dispatch(selection_slice_1.actions.anchor(node.id));
|
|
366
|
+
this.dispatch(selection_slice_1.actions.mostRecent(node.id));
|
|
367
|
+
}
|
|
362
368
|
this.scrollTo(node, opts.align);
|
|
363
369
|
if (this.focusedNode && changeFocus) {
|
|
364
370
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
@@ -366,14 +372,18 @@ class TreeApi {
|
|
|
366
372
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
367
373
|
}
|
|
368
374
|
selectContiguous(identity) {
|
|
375
|
+
var _a;
|
|
369
376
|
if (!identity)
|
|
370
377
|
return;
|
|
371
378
|
const id = identify(identity);
|
|
372
|
-
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
373
379
|
this.dispatch((0, focus_slice_1.focus)(id));
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
380
|
+
if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
|
|
381
|
+
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
382
|
+
const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, identifyNull(id)));
|
|
383
|
+
this.dispatch(selection_slice_1.actions.remove(this.nodesBetween(anchor, mostRecent)));
|
|
384
|
+
this.dispatch(selection_slice_1.actions.add(selectableNodes));
|
|
385
|
+
this.dispatch(selection_slice_1.actions.mostRecent(id));
|
|
386
|
+
}
|
|
377
387
|
this.scrollTo(id);
|
|
378
388
|
if (this.focusedNode)
|
|
379
389
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
@@ -384,17 +394,23 @@ class TreeApi {
|
|
|
384
394
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
385
395
|
}
|
|
386
396
|
selectAll() {
|
|
387
|
-
var _a;
|
|
397
|
+
var _a, _b, _c;
|
|
398
|
+
const allSelectableNodes = this.filterSelectableNodes(Object.keys(this.idToIndex));
|
|
388
399
|
this.setSelection({
|
|
389
|
-
ids:
|
|
390
|
-
anchor:
|
|
391
|
-
mostRecent:
|
|
400
|
+
ids: allSelectableNodes,
|
|
401
|
+
anchor: (_a = allSelectableNodes[0]) !== null && _a !== void 0 ? _a : null,
|
|
402
|
+
mostRecent: (_b = allSelectableNodes[allSelectableNodes.length - 1]) !== null && _b !== void 0 ? _b : null,
|
|
392
403
|
});
|
|
393
|
-
this.dispatch((0, focus_slice_1.focus)((
|
|
404
|
+
this.dispatch((0, focus_slice_1.focus)((_c = this.lastNode) === null || _c === void 0 ? void 0 : _c.id));
|
|
394
405
|
if (this.focusedNode)
|
|
395
406
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
396
407
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
397
408
|
}
|
|
409
|
+
filterSelectableNodes(nodes) {
|
|
410
|
+
return nodes
|
|
411
|
+
.map((n) => this.get(identify(n)))
|
|
412
|
+
.filter((n) => !!n && n.isSelectable);
|
|
413
|
+
}
|
|
398
414
|
setSelection(args) {
|
|
399
415
|
var _a;
|
|
400
416
|
const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map(identify));
|
|
@@ -592,12 +608,16 @@ class TreeApi {
|
|
|
592
608
|
}
|
|
593
609
|
}
|
|
594
610
|
isEditable(data) {
|
|
595
|
-
|
|
596
|
-
return !utils.access(data, check);
|
|
611
|
+
return this.isActionPossible(data, this.props.disableEdit);
|
|
597
612
|
}
|
|
598
613
|
isDraggable(data) {
|
|
599
|
-
|
|
600
|
-
|
|
614
|
+
return this.isActionPossible(data, this.props.disableDrag);
|
|
615
|
+
}
|
|
616
|
+
isSelectable(data) {
|
|
617
|
+
return this.isActionPossible(data, this.props.disableSelect);
|
|
618
|
+
}
|
|
619
|
+
isActionPossible(data, disabler = () => false) {
|
|
620
|
+
return !utils.access(data, disabler);
|
|
601
621
|
}
|
|
602
622
|
isDragging(node) {
|
|
603
623
|
const id = identifyNull(node);
|
|
@@ -31,6 +31,7 @@ export interface TreeProps<T> {
|
|
|
31
31
|
openByDefault?: boolean;
|
|
32
32
|
selectionFollowsFocus?: boolean;
|
|
33
33
|
disableMultiSelection?: boolean;
|
|
34
|
+
disableSelect?: string | boolean | BoolFunc<T>;
|
|
34
35
|
disableEdit?: string | boolean | BoolFunc<T>;
|
|
35
36
|
disableDrag?: string | boolean | BoolFunc<T>;
|
|
36
37
|
disableDrop?: string | boolean | ((args: {
|
|
@@ -26,7 +26,14 @@ export function TreeProvider({ treeProps, imperativeHandle, children, }) {
|
|
|
26
26
|
useMemo(() => {
|
|
27
27
|
updateCount.current += 1;
|
|
28
28
|
api.update(treeProps);
|
|
29
|
-
}, [...Object.values(treeProps)
|
|
29
|
+
}, [...Object.values(treeProps)]);
|
|
30
|
+
/* Rebuild visible nodes when open state changes, without clobbering
|
|
31
|
+
props set imperatively via api.update(). Bumping updateCount keeps
|
|
32
|
+
DataUpdates consumers (e.g. DefaultContainer) in sync. */
|
|
33
|
+
useMemo(() => {
|
|
34
|
+
updateCount.current += 1;
|
|
35
|
+
api.update(api.props);
|
|
36
|
+
}, [state.nodes.open]);
|
|
30
37
|
/* Expose the tree api */
|
|
31
38
|
useImperativeHandle(imperativeHandle, () => api);
|
|
32
39
|
/* Change selection based on props */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createRef } from "react";
|
|
3
|
+
import { act, render } from "@testing-library/react";
|
|
4
|
+
import { Tree } from "./tree";
|
|
5
|
+
const data = [
|
|
6
|
+
{
|
|
7
|
+
id: "1",
|
|
8
|
+
name: "root",
|
|
9
|
+
children: [
|
|
10
|
+
{ id: "2", name: "a" },
|
|
11
|
+
{ id: "3", name: "b", children: [{ id: "4", name: "c" }] },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
test("imperative tree.update() props survive node toggles (#228)", () => {
|
|
16
|
+
const ref = createRef();
|
|
17
|
+
render(_jsx(Tree, { data: data, ref: ref, rowHeight: 24, openByDefault: false }));
|
|
18
|
+
const api = ref.current;
|
|
19
|
+
expect(api.rowHeight).toBe(24);
|
|
20
|
+
act(() => {
|
|
21
|
+
api.update(Object.assign(Object.assign({}, api.props), { rowHeight: 48 }));
|
|
22
|
+
});
|
|
23
|
+
expect(api.rowHeight).toBe(48);
|
|
24
|
+
/* Opening a node dispatches a redux action that changes state.nodes.open.
|
|
25
|
+
Before #337, the open-state effect re-ran api.update(treeProps), reverting
|
|
26
|
+
rowHeight to 24. */
|
|
27
|
+
act(() => {
|
|
28
|
+
api.open("1");
|
|
29
|
+
});
|
|
30
|
+
expect(api.rowHeight).toBe(48);
|
|
31
|
+
});
|
|
@@ -136,7 +136,7 @@ export declare class TreeApi<T> {
|
|
|
136
136
|
get(id: string | null): NodeApi<T> | null;
|
|
137
137
|
at(index: number): NodeApi<T> | null;
|
|
138
138
|
nodesBetween(startId: string | null, endId: string | null): NodeApi<T>[];
|
|
139
|
-
indexOf(id:
|
|
139
|
+
indexOf(id: Identity): number | null;
|
|
140
140
|
get editingId(): string | null;
|
|
141
141
|
createInternal(): Promise<void>;
|
|
142
142
|
createLeaf(): Promise<void>;
|
|
@@ -145,11 +145,11 @@ export declare class TreeApi<T> {
|
|
|
145
145
|
parentId?: null | string;
|
|
146
146
|
index?: null | number;
|
|
147
147
|
}): Promise<void>;
|
|
148
|
-
delete(node:
|
|
148
|
+
delete(node: Identity | string[] | IdObj[]): Promise<void>;
|
|
149
149
|
edit(node: string | IdObj): Promise<EditResult>;
|
|
150
150
|
submit(identity: Identity, value: string): Promise<void>;
|
|
151
151
|
reset(): void;
|
|
152
|
-
activate(id:
|
|
152
|
+
activate(id: Identity): void;
|
|
153
153
|
private resolveEdit;
|
|
154
154
|
get selectedIds(): Set<string>;
|
|
155
155
|
get selectedNodes(): NodeApi<T>[];
|
|
@@ -170,10 +170,11 @@ export declare class TreeApi<T> {
|
|
|
170
170
|
selectContiguous(identity: Identity): void;
|
|
171
171
|
deselectAll(): void;
|
|
172
172
|
selectAll(): void;
|
|
173
|
+
private filterSelectableNodes;
|
|
173
174
|
setSelection(args: {
|
|
174
175
|
ids: (IdObj | string)[] | null;
|
|
175
|
-
anchor:
|
|
176
|
-
mostRecent:
|
|
176
|
+
anchor: Identity;
|
|
177
|
+
mostRecent: Identity;
|
|
177
178
|
}): void;
|
|
178
179
|
get cursorParentId(): string | null;
|
|
179
180
|
get cursorOverFolder(): boolean;
|
|
@@ -202,10 +203,12 @@ export declare class TreeApi<T> {
|
|
|
202
203
|
isOpen(id?: string): boolean;
|
|
203
204
|
isEditable(data: T): boolean;
|
|
204
205
|
isDraggable(data: T): boolean;
|
|
205
|
-
|
|
206
|
+
isSelectable(data: T): boolean;
|
|
207
|
+
private isActionPossible;
|
|
208
|
+
isDragging(node: Identity): boolean;
|
|
206
209
|
isFocused(id: string): boolean;
|
|
207
210
|
isMatch(node: NodeApi<T>): boolean;
|
|
208
|
-
willReceiveDrop(node:
|
|
211
|
+
willReceiveDrop(node: Identity): boolean;
|
|
209
212
|
onFocus(): void;
|
|
210
213
|
onBlur(): void;
|
|
211
214
|
onItemsRendered(args: ListOnItemsRenderedProps): void;
|
|
@@ -301,20 +301,24 @@ export class TreeApi {
|
|
|
301
301
|
this.focus(this.at(index));
|
|
302
302
|
}
|
|
303
303
|
select(node, opts = {}) {
|
|
304
|
+
var _a;
|
|
304
305
|
if (!node)
|
|
305
306
|
return;
|
|
306
307
|
const changeFocus = opts.focus !== false;
|
|
307
308
|
const id = identify(node);
|
|
308
309
|
if (changeFocus)
|
|
309
310
|
this.dispatch(focus(id));
|
|
310
|
-
this.
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
|
|
312
|
+
this.setSelection({
|
|
313
|
+
ids: [id],
|
|
314
|
+
anchor: id,
|
|
315
|
+
mostRecent: id,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
313
318
|
this.scrollTo(id, opts.align);
|
|
314
319
|
if (this.focusedNode && changeFocus) {
|
|
315
320
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
316
321
|
}
|
|
317
|
-
safeRun(this.props.onSelect, this.selectedNodes);
|
|
318
322
|
}
|
|
319
323
|
deselect(node) {
|
|
320
324
|
if (!node)
|
|
@@ -330,9 +334,11 @@ export class TreeApi {
|
|
|
330
334
|
const changeFocus = opts.focus !== false;
|
|
331
335
|
if (changeFocus)
|
|
332
336
|
this.dispatch(focus(node.id));
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
337
|
+
if (node.isSelectable) {
|
|
338
|
+
this.dispatch(selection.add(node.id));
|
|
339
|
+
this.dispatch(selection.anchor(node.id));
|
|
340
|
+
this.dispatch(selection.mostRecent(node.id));
|
|
341
|
+
}
|
|
336
342
|
this.scrollTo(node, opts.align);
|
|
337
343
|
if (this.focusedNode && changeFocus) {
|
|
338
344
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
@@ -340,14 +346,18 @@ export class TreeApi {
|
|
|
340
346
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
341
347
|
}
|
|
342
348
|
selectContiguous(identity) {
|
|
349
|
+
var _a;
|
|
343
350
|
if (!identity)
|
|
344
351
|
return;
|
|
345
352
|
const id = identify(identity);
|
|
346
|
-
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
347
353
|
this.dispatch(focus(id));
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
354
|
+
if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
|
|
355
|
+
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
356
|
+
const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, identifyNull(id)));
|
|
357
|
+
this.dispatch(selection.remove(this.nodesBetween(anchor, mostRecent)));
|
|
358
|
+
this.dispatch(selection.add(selectableNodes));
|
|
359
|
+
this.dispatch(selection.mostRecent(id));
|
|
360
|
+
}
|
|
351
361
|
this.scrollTo(id);
|
|
352
362
|
if (this.focusedNode)
|
|
353
363
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
@@ -358,17 +368,23 @@ export class TreeApi {
|
|
|
358
368
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
359
369
|
}
|
|
360
370
|
selectAll() {
|
|
361
|
-
var _a;
|
|
371
|
+
var _a, _b, _c;
|
|
372
|
+
const allSelectableNodes = this.filterSelectableNodes(Object.keys(this.idToIndex));
|
|
362
373
|
this.setSelection({
|
|
363
|
-
ids:
|
|
364
|
-
anchor:
|
|
365
|
-
mostRecent:
|
|
374
|
+
ids: allSelectableNodes,
|
|
375
|
+
anchor: (_a = allSelectableNodes[0]) !== null && _a !== void 0 ? _a : null,
|
|
376
|
+
mostRecent: (_b = allSelectableNodes[allSelectableNodes.length - 1]) !== null && _b !== void 0 ? _b : null,
|
|
366
377
|
});
|
|
367
|
-
this.dispatch(focus((
|
|
378
|
+
this.dispatch(focus((_c = this.lastNode) === null || _c === void 0 ? void 0 : _c.id));
|
|
368
379
|
if (this.focusedNode)
|
|
369
380
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
370
381
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
371
382
|
}
|
|
383
|
+
filterSelectableNodes(nodes) {
|
|
384
|
+
return nodes
|
|
385
|
+
.map((n) => this.get(identify(n)))
|
|
386
|
+
.filter((n) => !!n && n.isSelectable);
|
|
387
|
+
}
|
|
372
388
|
setSelection(args) {
|
|
373
389
|
var _a;
|
|
374
390
|
const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map(identify));
|
|
@@ -566,12 +582,16 @@ export class TreeApi {
|
|
|
566
582
|
}
|
|
567
583
|
}
|
|
568
584
|
isEditable(data) {
|
|
569
|
-
|
|
570
|
-
return !utils.access(data, check);
|
|
585
|
+
return this.isActionPossible(data, this.props.disableEdit);
|
|
571
586
|
}
|
|
572
587
|
isDraggable(data) {
|
|
573
|
-
|
|
574
|
-
|
|
588
|
+
return this.isActionPossible(data, this.props.disableDrag);
|
|
589
|
+
}
|
|
590
|
+
isSelectable(data) {
|
|
591
|
+
return this.isActionPossible(data, this.props.disableSelect);
|
|
592
|
+
}
|
|
593
|
+
isActionPossible(data, disabler = () => false) {
|
|
594
|
+
return !utils.access(data, disabler);
|
|
575
595
|
}
|
|
576
596
|
isDragging(node) {
|
|
577
597
|
const id = identifyNull(node);
|
|
@@ -31,6 +31,7 @@ export interface TreeProps<T> {
|
|
|
31
31
|
openByDefault?: boolean;
|
|
32
32
|
selectionFollowsFocus?: boolean;
|
|
33
33
|
disableMultiSelection?: boolean;
|
|
34
|
+
disableSelect?: string | boolean | BoolFunc<T>;
|
|
34
35
|
disableEdit?: string | boolean | BoolFunc<T>;
|
|
35
36
|
disableDrag?: string | boolean | BoolFunc<T>;
|
|
36
37
|
disableDrop?: string | boolean | ((args: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-arborist",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"source": "src/index.ts",
|
|
6
6
|
"main": "dist/main/index.js",
|
|
@@ -49,12 +49,18 @@
|
|
|
49
49
|
"react-dom": ">= 16.14"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
+
"@testing-library/dom": "^9.3.0",
|
|
53
|
+
"@testing-library/react": "^14.0.0",
|
|
52
54
|
"@types/jest": "^29.5.11",
|
|
53
55
|
"@types/react": "^18.2.43",
|
|
56
|
+
"@types/react-dom": "^18.2.0",
|
|
54
57
|
"@types/react-window": "^1.8.8",
|
|
55
58
|
"@types/use-sync-external-store": "^0.0.6",
|
|
56
59
|
"jest": "^29.7.0",
|
|
60
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
57
61
|
"npm-run-all": "^4.1.5",
|
|
62
|
+
"react": "^18.2.0",
|
|
63
|
+
"react-dom": "^18.2.0",
|
|
58
64
|
"rimraf": "^5.0.5",
|
|
59
65
|
"ts-jest": "^29.1.1",
|
|
60
66
|
"typescript": "^5.6.0"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { act, render } from "@testing-library/react";
|
|
3
|
+
import { Tree } from "./tree";
|
|
4
|
+
import { TreeApi } from "../interfaces/tree-api";
|
|
5
|
+
|
|
6
|
+
type Datum = { id: string; name: string; children?: Datum[] };
|
|
7
|
+
|
|
8
|
+
const data: Datum[] = [
|
|
9
|
+
{
|
|
10
|
+
id: "1",
|
|
11
|
+
name: "root",
|
|
12
|
+
children: [
|
|
13
|
+
{ id: "2", name: "a" },
|
|
14
|
+
{ id: "3", name: "b", children: [{ id: "4", name: "c" }] },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
test("imperative tree.update() props survive node toggles (#228)", () => {
|
|
20
|
+
const ref = createRef<TreeApi<Datum> | undefined>();
|
|
21
|
+
render(
|
|
22
|
+
<Tree<Datum>
|
|
23
|
+
data={data}
|
|
24
|
+
ref={ref}
|
|
25
|
+
rowHeight={24}
|
|
26
|
+
openByDefault={false}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
const api = ref.current!;
|
|
30
|
+
expect(api.rowHeight).toBe(24);
|
|
31
|
+
|
|
32
|
+
act(() => {
|
|
33
|
+
api.update({ ...api.props, rowHeight: 48 });
|
|
34
|
+
});
|
|
35
|
+
expect(api.rowHeight).toBe(48);
|
|
36
|
+
|
|
37
|
+
/* Opening a node dispatches a redux action that changes state.nodes.open.
|
|
38
|
+
Before #337, the open-state effect re-ran api.update(treeProps), reverting
|
|
39
|
+
rowHeight to 24. */
|
|
40
|
+
act(() => {
|
|
41
|
+
api.open("1");
|
|
42
|
+
});
|
|
43
|
+
expect(api.rowHeight).toBe(48);
|
|
44
|
+
});
|
|
@@ -57,7 +57,15 @@ export function TreeProvider<T>({
|
|
|
57
57
|
useMemo(() => {
|
|
58
58
|
updateCount.current += 1;
|
|
59
59
|
api.update(treeProps);
|
|
60
|
-
}, [...Object.values(treeProps)
|
|
60
|
+
}, [...Object.values(treeProps)]);
|
|
61
|
+
|
|
62
|
+
/* Rebuild visible nodes when open state changes, without clobbering
|
|
63
|
+
props set imperatively via api.update(). Bumping updateCount keeps
|
|
64
|
+
DataUpdates consumers (e.g. DefaultContainer) in sync. */
|
|
65
|
+
useMemo(() => {
|
|
66
|
+
updateCount.current += 1;
|
|
67
|
+
api.update(api.props);
|
|
68
|
+
}, [state.nodes.open]);
|
|
61
69
|
|
|
62
70
|
/* Expose the tree api */
|
|
63
71
|
useImperativeHandle(imperativeHandle, () => api);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EditResult } from "../types/handlers";
|
|
2
|
-
import { Identity, IdObj } from "../types/utils";
|
|
2
|
+
import { BoolFunc, Identity, IdObj } from "../types/utils";
|
|
3
3
|
import { TreeProps } from "../types/tree-props";
|
|
4
4
|
import { MutableRefObject } from "react";
|
|
5
5
|
import { Align, FixedSizeList, ListOnItemsRenderedProps } from "react-window";
|
|
@@ -169,7 +169,7 @@ export class TreeApi<T> {
|
|
|
169
169
|
return this.visibleNodes.slice(start, end + 1);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
indexOf(id:
|
|
172
|
+
indexOf(id: Identity) {
|
|
173
173
|
const key = utils.identifyNull(id);
|
|
174
174
|
if (!key) return null;
|
|
175
175
|
return this.idToIndex[key];
|
|
@@ -219,7 +219,7 @@ export class TreeApi<T> {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
async delete(node:
|
|
222
|
+
async delete(node: Identity | string[] | IdObj[]) {
|
|
223
223
|
if (!node) return;
|
|
224
224
|
const idents = Array.isArray(node) ? node : [node];
|
|
225
225
|
const ids = idents.map(identify);
|
|
@@ -256,7 +256,7 @@ export class TreeApi<T> {
|
|
|
256
256
|
setTimeout(() => this.onFocus()); // Return focus to element;
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
activate(id:
|
|
259
|
+
activate(id: Identity) {
|
|
260
260
|
const node = this.get(identifyNull(id));
|
|
261
261
|
if (!node) return;
|
|
262
262
|
safeRun(this.props.onActivate, node);
|
|
@@ -328,14 +328,17 @@ export class TreeApi<T> {
|
|
|
328
328
|
const changeFocus = opts.focus !== false;
|
|
329
329
|
const id = identify(node);
|
|
330
330
|
if (changeFocus) this.dispatch(focus(id));
|
|
331
|
-
this.
|
|
332
|
-
|
|
333
|
-
|
|
331
|
+
if (this.get(id)?.isSelectable) {
|
|
332
|
+
this.setSelection({
|
|
333
|
+
ids: [id],
|
|
334
|
+
anchor: id,
|
|
335
|
+
mostRecent: id,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
334
338
|
this.scrollTo(id, opts.align);
|
|
335
339
|
if (this.focusedNode && changeFocus) {
|
|
336
340
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
337
341
|
}
|
|
338
|
-
safeRun(this.props.onSelect, this.selectedNodes);
|
|
339
342
|
}
|
|
340
343
|
|
|
341
344
|
deselect(node: Identity) {
|
|
@@ -350,9 +353,11 @@ export class TreeApi<T> {
|
|
|
350
353
|
if (!node) return;
|
|
351
354
|
const changeFocus = opts.focus !== false;
|
|
352
355
|
if (changeFocus) this.dispatch(focus(node.id));
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
+
if (node.isSelectable) {
|
|
357
|
+
this.dispatch(selection.add(node.id));
|
|
358
|
+
this.dispatch(selection.anchor(node.id));
|
|
359
|
+
this.dispatch(selection.mostRecent(node.id));
|
|
360
|
+
}
|
|
356
361
|
this.scrollTo(node, opts.align);
|
|
357
362
|
if (this.focusedNode && changeFocus) {
|
|
358
363
|
safeRun(this.props.onFocus, this.focusedNode);
|
|
@@ -363,11 +368,16 @@ export class TreeApi<T> {
|
|
|
363
368
|
selectContiguous(identity: Identity) {
|
|
364
369
|
if (!identity) return;
|
|
365
370
|
const id = identify(identity);
|
|
366
|
-
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
367
371
|
this.dispatch(focus(id));
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
372
|
+
if (this.get(id)?.isSelectable) {
|
|
373
|
+
const { anchor, mostRecent } = this.state.nodes.selection;
|
|
374
|
+
const selectableNodes = this.filterSelectableNodes(
|
|
375
|
+
this.nodesBetween(anchor, identifyNull(id)),
|
|
376
|
+
);
|
|
377
|
+
this.dispatch(selection.remove(this.nodesBetween(anchor, mostRecent)));
|
|
378
|
+
this.dispatch(selection.add(selectableNodes));
|
|
379
|
+
this.dispatch(selection.mostRecent(id));
|
|
380
|
+
}
|
|
371
381
|
this.scrollTo(id);
|
|
372
382
|
if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
|
|
373
383
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
@@ -379,20 +389,29 @@ export class TreeApi<T> {
|
|
|
379
389
|
}
|
|
380
390
|
|
|
381
391
|
selectAll() {
|
|
392
|
+
const allSelectableNodes = this.filterSelectableNodes(
|
|
393
|
+
Object.keys(this.idToIndex),
|
|
394
|
+
);
|
|
382
395
|
this.setSelection({
|
|
383
|
-
ids:
|
|
384
|
-
anchor:
|
|
385
|
-
mostRecent:
|
|
396
|
+
ids: allSelectableNodes,
|
|
397
|
+
anchor: allSelectableNodes[0] ?? null,
|
|
398
|
+
mostRecent: allSelectableNodes[allSelectableNodes.length - 1] ?? null,
|
|
386
399
|
});
|
|
387
400
|
this.dispatch(focus(this.lastNode?.id));
|
|
388
401
|
if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
|
|
389
402
|
safeRun(this.props.onSelect, this.selectedNodes);
|
|
390
403
|
}
|
|
391
404
|
|
|
405
|
+
private filterSelectableNodes(nodes: (IdObj | string)[]) {
|
|
406
|
+
return nodes
|
|
407
|
+
.map((n) => this.get(identify(n)))
|
|
408
|
+
.filter((n): n is NodeApi<T> => !!n && n.isSelectable);
|
|
409
|
+
}
|
|
410
|
+
|
|
392
411
|
setSelection(args: {
|
|
393
412
|
ids: (IdObj | string)[] | null;
|
|
394
|
-
anchor:
|
|
395
|
-
mostRecent:
|
|
413
|
+
anchor: Identity;
|
|
414
|
+
mostRecent: Identity;
|
|
396
415
|
}) {
|
|
397
416
|
const ids = new Set(args.ids?.map(identify));
|
|
398
417
|
const anchor = identifyNull(args.anchor);
|
|
@@ -596,16 +615,25 @@ export class TreeApi<T> {
|
|
|
596
615
|
}
|
|
597
616
|
|
|
598
617
|
isEditable(data: T) {
|
|
599
|
-
|
|
600
|
-
return !utils.access(data, check);
|
|
618
|
+
return this.isActionPossible(data, this.props.disableEdit);
|
|
601
619
|
}
|
|
602
620
|
|
|
603
621
|
isDraggable(data: T) {
|
|
604
|
-
|
|
605
|
-
|
|
622
|
+
return this.isActionPossible(data, this.props.disableDrag);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
isSelectable(data: T) {
|
|
626
|
+
return this.isActionPossible(data, this.props.disableSelect);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private isActionPossible(
|
|
630
|
+
data: T,
|
|
631
|
+
disabler: string | boolean | BoolFunc<T> = () => false,
|
|
632
|
+
) {
|
|
633
|
+
return !utils.access(data, disabler);
|
|
606
634
|
}
|
|
607
635
|
|
|
608
|
-
isDragging(node:
|
|
636
|
+
isDragging(node: Identity) {
|
|
609
637
|
const id = identifyNull(node);
|
|
610
638
|
if (!id) return false;
|
|
611
639
|
return this.state.nodes.drag.id === id;
|
|
@@ -619,7 +647,7 @@ export class TreeApi<T> {
|
|
|
619
647
|
return this.matchFn(node);
|
|
620
648
|
}
|
|
621
649
|
|
|
622
|
-
willReceiveDrop(node:
|
|
650
|
+
willReceiveDrop(node: Identity) {
|
|
623
651
|
const id = identifyNull(node);
|
|
624
652
|
if (!id) return false;
|
|
625
653
|
const { destinationParentId, destinationIndex } = this.state.nodes.drag;
|
package/src/types/tree-props.ts
CHANGED
|
@@ -41,6 +41,7 @@ export interface TreeProps<T> {
|
|
|
41
41
|
openByDefault?: boolean;
|
|
42
42
|
selectionFollowsFocus?: boolean;
|
|
43
43
|
disableMultiSelection?: boolean;
|
|
44
|
+
disableSelect?: string | boolean | BoolFunc<T>;
|
|
44
45
|
disableEdit?: string | boolean | BoolFunc<T>;
|
|
45
46
|
disableDrag?: string | boolean | BoolFunc<T>;
|
|
46
47
|
disableDrop?:
|